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 😉.