Wednesday, January 20, 2016

Driving Cars with Websockets


Isolates look unlikely to serve as a teachable implementation for the remote proxy pattern in Dart. They are relatively simple, but still remain a tad too cumbersome. Last night's exploration of simple streams shows promise, begging the question of how another stream might work—websockets.

I borrow (OK, steal verbatim) the example websocket server from the Dart on the server example:
#!/usr/bin/env dart

import 'dart:async';
import 'dart:io';

handleMsg(msg) {
  print('Message received: $msg');
}

main() {
  runZoned(() async {
    var server = await HttpServer.bind('localhost', 4040);
    await for (var req in server) {
      if (req.uri.path == '/ws') {
        // Upgrade a HttpRequest to a WebSocket connection.
        var socket = await WebSocketTransformer.upgrade(req);
        socket.listen(handleMsg);
      };
    }
  },
  onError: (e) => print("An error occurred: $e"));
}
I will add my proxy pattern code shortly. For now, I just want to ensure that it works. I save this as bin/server.dart, chmod 755 and start it up with ./bin/server.dart. Nothing crashes, so I assume that I am good to go.

In my existing proxy pattern script, I add the appropriate websocket client code:
#!/usr/bin/env dart

import 'dart:async';
import 'dart:io';

main() async {
  var socket = await WebSocket.connect('ws://localhost:4040/ws');
  socket.add('Hello, World!');
  // ...
}
That should connect to my running websocket server and add a message to the websocket stream to which the server is currently listening. When I run this client script, I see the following message from the server:
$ ./bin/server.dart
Message received: Hello, World!
Nice! Web sockets were never all that hard in Dart, but they are getting close to trivial.

So what is it going to take to convert my ProxyCar from streams (which were converted last night from isolates) to websockets? Blood sacrifice? The answer is surprisingly little.

In my client code, I continue to open the websocket, pass that to a ProxyCar, then perform some remote car operations:
main() async {
  var socket = await WebSocket.connect('ws://localhost:4040/ws');

  ProxyCar car = new ProxyCar(socket);

  print("Attempting to drive remote car...");
  await car.drive();
  print("Car is ${car.state}");

  print("--");

  print("Attempting to stop remote car...");
  await car.stop();
  print("Car is ${await car.state}");
}
The ProxyCar is responsible for listening to this websocket for state responses from the real car. I establish that listener in the constructor:
class ProxyCar implements AsyncAuto {
  Stream _socket, _broadcast;
  String _state;

  ProxyCar(this._socket) {
    _broadcast = _socket.asBroadcastStream();
    _broadcast.listen((message) {
      _state = message;
    });
  }
  // ...
}
I get a broadcast version of the websocket so that I can listen to it in multiple locations. Here, I listen for state updates. When I send action messages to the real car, I also listen to the stream for confirmation that the message was received:
class ProxyCar implements AsyncAuto {
  Stream _socket, _broadcast;
  String _state;
  // ...
  String get state => _state;
  Future drive() => _send('drive');
  Future stop()  => _send('stop');

  Future _send(message) {
    _socket.add(message);
    return _broadcast.first;
  }
}
I may reconsider that at some point, just for ease of discussion. For now, I leave it as-is.

The server is using the same library. Instead of the ProxyCar instance, it works with the real subject in this pattern: an AsynCar instance:
      // ...
      if (req.uri.path == '/ws') {
        // Upgrade a HttpRequest to a WebSocket connection.
        var socket = await WebSocketTransformer.upgrade(req);
        new AsyncCar(socket);
      };
      // ...
Like the ProxyCar, the AsyncCar implements the AsyncAuto interface:
// Subject
abstract class AsyncAuto implements Automobile {
  String get state;
  Future drive();
  Future stop();
}
If anything, the AsyncCar real subject is even simpler than the proxy:
class AsyncCar implements AsyncAuto {
  Stream _socket;
  Car _car;

  AsyncCar(this._socket) {
    _car = new Car();

    _socket.listen((message) {
      print("[AsyncCar] received: $message");
      if (message == 'drive') _car.drive();
      if (message == 'stop')  _car.stop();
      _socket.add(state);
    });
  }

  String get state => _car.state;
  Future drive() => new Future((){ _car.drive(); });
  Future stop()  => new Future((){ _car.stop(); });
}
It adapts a synchronous Car, which it manipulates in response to messages that it receives from the socket. If the websocket message is 'drive', then the car instance is sent the drive message. If the websocket see 'stop', stop() is invoked on car.

And that actually works. Running the client code results in:
$ ./bin/drive.dart
Attempting to drive remote car...
Car is driving
--
Attempting to stop remote car...
Car is idle
Checking the server output, I see:
$ ./bin/server.dart
[AsyncCar] received: drive
[AsyncCar] received: stop
So there you have it. I can drive a car over websockets with Dart. And it was pretty darn easy!


Day #70

No comments:

Post a Comment