DEV Community

Anjan Roy
Anjan Roy

Posted on

Making of transferZ (Part: 1/n)

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.

  1. Receive
  2. 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
}

Client Server running on a Desktop

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 πŸ˜‰.

Discussion (1)

Collapse
temirfe profile image
Temirbek

How devices locate each other? I installed on two android phones and they don't see each other, both on same wi-fi