Here we begin ...
idea
Building a flutter application for transferring files wirelessly, in between your devices, mostly written with Dart Language, can be really great.
info
transferZ is an opensource project, find whole source code here. Feel free to contribute by submitting a PR.
Now, you can download and test transferZ on your device.
Today we gonna implement Client-Server portion of this Flutter application.
And at end of this series, you'll have a working file transfer app.
Wanna do so ?
Yes
Alright follow me 😉
model
A device can act in either of these two modes at a time.
- Receive
- Send
In first case, device will behave like a Client and in case of later one, it'll simply be a Server.
When in Receive mode, first Client fetches a list of files, which are ready to be shared by Server.
Then it iterates through that list of files and fetches them one by one, while Server keeps listening for incoming requests and serves them, if it's from an allowed PEER.
Yes, it's this much simple 😄.
init
Let's first write some code to make our device act like a Client.
Receive Mode
Following import statements will fetch required Dart classes for us.
import 'dart:io';
import 'dart:convert' show json, utf8;
import 'dart:async';
A class, named Client will incorporate all Client functionalities, to be shown in Receive Mode.
class Client {
String _peerIP; // this the server IP, where we'll connect
int _peerPort; // port on which server listens
HttpClient _httpClient;
Client(this._peerIP, this._peerPort) {
_httpClient = HttpClient(); // gets initialized in constructor block
}
}
Now add a method, named connect in our Client class.
// method GETs a certain path, and returns a HttpClientRequest object, which is to be closed later, to send that request to server.
Future<HttpClientRequest> connect(String path) async => await _httpClient
.get(this._peerIP, this._peerPort, path)
.catchError((e) => null);
As we modeled our Client, to be first fetching a list of files, which are ready to be shared by Server, we'll require a method, named fetchFileNames, in Client class.
Future<List<String>> fetchFileNames(HttpClientRequest req) async {
// a map, stores JSON response from server
var fileNames = <String, List<String>>{};
// Completer class helps us to return a Future from this method and when Completer completes, result is send back.
var completer = Completer<List<String>>();
await req.close().then((HttpClientResponse resp) {
if (resp.statusCode == 200)
// in case of success, listen for data passed through Stream
resp.listen(
(data) => fileNames = serializeJSON(utf8.decode(data)), // decode response and then serialize it into Map<String, List<String>>
onDone: () => completer.complete(fileNames['files']), // extract list of file names and send that back
onError: (e) => completer.complete(<String>[]), // send a blank list
cancelOnError: true,
);
else
// if HttpStatus other than 200, simply return a blank list
resp.listen(
(data) {},
onDone: () => completer.complete(<String>[]),
onError: (e) => completer.complete(<String>[]),
cancelOnError: true,
);
});
return completer.future;
}
Closing HttpClientRequest object, which was received from previous connect method, gives us a HttpClientResponse object, which is used for reading response from Server.
Now we need to define another method serializeJSON, which will help us to convert JSON response into Map < String, List< String > >.
Map<String, List<String>> serializeJSON(String data) =>
return Map<String, dynamic>.from(json.decode(data))
.map((key, val) => MapEntry(key, List<String>.from(val)));
What do we have now ?
A list of Strings, which are file names to be fetched from Server.
Let's fetch those files from Server. Add a new method fetchFile in Client class.
Future<bool> fetchFile(HttpClientRequest req, String targetPath) async {
var completer = Completer<bool>(); // returns a future of boolean type, to denote success or failure
await req.close().then((HttpClientResponse resp) {
if (resp.statusCode == 200)
File(targetPath).openWrite(mode: FileMode.write).addStream(resp).then(
(val) => completer.complete(true),
onError: (e) => completer.complete(false),
); // file opened in write mode, and added response Stream into file.
else
resp.listen(
(data) {},
onDone: () => completer.complete(false),
onError: (e) => completer.complete(false),
cancelOnError: true,
); // in case of error, simply return false to denote failure
});
return completer.future;
}
If file is properly fetched, method returns true, else false is sent back
And a last method, named disconnect for closing HttpClient connection.
disconnect() =>
// never force close a http connection, by passing force value attribute true.
_httpClient.close();
And we've completed declaring client 😎.
Send Mode
Back to Server. Let's first define a class, named Server.
import 'dart:io';
import 'dart:convert' show json;
class Server {
String _host; // IP where on which server listens
int _port; // port on which server listens for incoming connections
List<String> _filteredPeers; // list of IP, which are permitted to request files
List<String> _files; // files ready to be shared
ServerStatusCallBack _serverStatusCallBack; // Server status callback, lets user know, what's happening
bool isStopped = true; // server state denoter
Server(this._host, this._port, this._filteredPeers, this._files,
this._serverStatusCallBack); // constructor defined
For controlling Server, we need to declare another variable with in this class.
HttpServer _httpServer;
Let's start listening for incoming connections.
initServer() async {
await HttpServer.bind(this._host, this._port).then((HttpServer server) {
// binds server on specified IP and port
_httpServer = server; // initializes _httpServer, which will be useful for controlling server functionalities
isStopped = false; // server state changed
_serverStatusCallBack.generalUpdate('Server started'); // lets user know about it, using callback function
}, onError: (e) {
isStopped = true;
_serverStatusCallBack.generalUpdate('Failed to start Server');
});
// listens for incoming request in asynchronous fashion
await for (HttpRequest request in _httpServer) {
handleRequest(request); // handles incoming request
}
}
For handling incoming connection, add a method, named handleRequest in our Server class.
handleRequest(HttpRequest request) {
// first checks whether device allowed to request file or not
if (isPeerAllowed(request.connectionInfo.remoteAddress.address)) {
if (request.method == 'GET')
handleGETRequest(request); // only GET is permitted
else
request.response
..statusCode = HttpStatus.methodNotAllowed
..headers.contentType = ContentType.json
..write(json.encode(
<String, int>{'status': 'GET method only'}))
..close().then(
(val) => _serverStatusCallBack.updateServerStatus({
request.connectionInfo.remoteAddress.host:
'GET method only'}), onError: (e) {
_serverStatusCallBack.updateServerStatus({
request.connectionInfo.remoteAddress.host:
'Transfer Error'
});
}); // otherwise let client know about it by sending a JSON response, where HTTP statusCode is set as HttpStatus.methodNotAllowed
} else
request.response
..statusCode = HttpStatus.forbidden
..headers.contentType = ContentType.json
..write(
json.encode(<String, int>{'status': 'Access denied'}))
..close().then(
(val) =>
_serverStatusCallBack.generalUpdate('Access Denied'),
onError: (e) {
_serverStatusCallBack.generalUpdate('Transfer Error');
}); // if client is not permitted to access
}
Checking if Peer can be granted access, is done in isPeerAllowed.
bool isPeerAllowed(String remoteAddress) =>
this._filteredPeers.contains(remoteAddress); // _filteredPeers was supplied during instantiating this class
Time to handle filtered GET requests, with handleGETRequest.
handleGETRequest(HttpRequest getRequest) {
if (getRequest.uri.path == '/') {
// client hits at `http://hostAddress:port/`, for fetching list of files to be shared
getRequest.response
..statusCode = HttpStatus.ok // statusCode 200
..headers.contentType = ContentType.json // JSON response sent
..write(json.encode(<String, List<String>>{"files": this._files})) // response to be processed by client for converting JSON string back to Map<String, List<String>>
..close().then(
(val) => _serverStatusCallBack.updateServerStatus({
getRequest.connectionInfo.remoteAddress.host:
'Accessible file list shared'
}), onError: (e) {
_serverStatusCallBack.updateServerStatus({
getRequest.connectionInfo.remoteAddress.host:
'Transfer Error'
}); // in case of error
});
} else {
if (this._files.contains(getRequest.uri.path)) {
// if client hits at `http://hostIP:port/filePath`, first it's checked whether this file is supposed to be shared or not
String remote = getRequest.connectionInfo.remoteAddress.host;
getRequest.response.statusCode = HttpStatus.ok;
_serverStatusCallBack.updateServerStatus({
getRequest.connectionInfo.remoteAddress.host:
'File fetch in Progress'
});
// then file is opened and added into response Stream
getRequest.response
.addStream(File(getRequest.uri.path).openRead())
.then(
(val) {
getRequest.response.close(); // close connection
_serverStatusCallBack
.updateServerStatus({remote: 'File fetched'});
},
onError: (e) {
_serverStatusCallBack
.updateServerStatus({remote: 'Transfer Error'}); // in case of error
},
);
}
}
}
And last but not least, a way to stop Server.
stopServer() {
isStopped = true;
_httpServer?.close(force: true);
_serverStatusCallBack.generalUpdate('Server stopped');
}
And this is all about Server 😎.
Now define an abstract class ServerStatusCallBack, to facilitate callback functionality. This abstract class needs to be implemented, from where we plan to start, control and stop Server.
abstract class ServerStatusCallBack {
updateServerStatus(Map<String, String> msg); // a certain peer specific message
generalUpdate(String msg); // general message about current server state
}
run
Time to run our client.
Receive Mode
Currently I'm going to run client simply from a command line app, which is to be updated in upcoming articles, when we reach UI context in flutter.
main() {
var client = Client('x.x.x.x', 8000); // x.x.x.x -> server IP and 8000 is port number
// feel free to change port number, try keeping it >1024
client.connect('/').then((HttpClientRequest req) {
if (req != null) // checks whether server is down or not
client.fetchFileNames(req).then((List<String> targetFiles) { // first fetch list of files by sending a request in `/` path
targetFiles.forEach((path) { // fetch files, one by one
client.connect(path).then((HttpClientRequest req) { // request for a file, with that file's name as request path
if (req != null) { // if server finds request eligible
print('fetching ${getTargetFilePath(path)}');
client
.fetchFile(req, getTargetFilePath(path)) // fetch file
.then((bool isSuccess) { // after it's done, check for transfer status
print(isSuccess
? 'Successfully downloaded file'
: 'Failed to download file');
if (targetFiles.last == path) {
print('complete');
client.disconnect(); // disconnect client, when we find that all files has been successfully fetched
}
});
} else
print('Connection Failed');
});
});
if (targetFiles.isEmpty) {
print('incomplete');
client.disconnect(); // there might be a situation when client not permitted to access files, then we get an empty list of files, it's handled here
}
});
else
print('Connection Failed'); // couldn't connect to server, may be down
});
}
// returns path where client will store fetched files
String getTargetFilePath(String path) =>
'/path-to-target-directory-to-store-fetched-file/${path.split('/').last}';
Send Mode
In case of Server too, for time being, we'll keep it command line executable.
Let's first define a class ServerDriver, which implements ServerStatusCallBack, gets server status updates.
class ServerDriver extends ServerStatusCallBack {
Server server;
@override
updateServerStatus(Map<String, String> msg) =>
msg.forEach((key, val) =>
print('$key -- $val')); // peer specific message, where key of Map<String, String> is peerIP
@override
generalUpdate(String msg) => print(msg); // general status update about server
init() {
server = Server(
'0.0.0.0',
8000,
<String>['192.168.1.103', '192.168.1.102'], // these are allowed peers( clients ), can request for fetching files
<String>[
'/path-to-file/image.png',
'/path-to-file/spidy_love.zip',
],
this); // this -- because ServerStatusCallBack is implemented in this class
}
start() {
server.initServer(); // starts receiving incoming requests
}
}
And finally main() function, which creates an instance of ServerDriver, starts server.
main() {
var serverDriver = ServerDriver();
serverDriver.init(); // binds server in a speficied address and port
serverDriver.start(); // starts accepting incoming requests
}
Remember, this client-server program works in Local Area Network( LAN ). Interested in making it work in WAN, give it a try 👍.
next
Currently we've two working programs, a client and a server, written fully in Dart Language, which will be eventually put into a Flutter Application transferZ.
In next article of this series, we'll build Peer Discovery portion of transferZ.
In the mean time fork this repo, and try playing around.
You may consider following me on Twitter and GitHub, for more announcements.
See you soon 😉.
Top comments (1)
How devices locate each other? I installed on two android phones and they don't see each other, both on same wi-fi