Dart for Raw TCP Applications Is Easy

For most modern applications we do our network transactions using HTTP level. HTTP is riding at the top of the OSI Stack . Usually underneath that, several steps down the stack, at the so-called “Network Layer” is the TCP/IP protocol. There are times when you need to be able to do direct Network Layer calls. Because Dart comes with built-in support for networking for its HTTP needs it also comes with, and exposes to programmers the Network Layer sockets as well. Below is my tutorial on how to set up a very simple bi-direction TCP/IP client/server system using Dart.

The OSI Stack as shown with cats in stacked bins

The Server

Let’s start with the server code. A TCP server at its simplest form connects to a specific network port on the machine that it is running on. It spends its life listening for new connections and perhaps even responding to them. For our simple example we will just listen for streams of data, convert them to text, print them out and respond to the client that connected to us with a “Received” response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import 'dart:convert';
import 'dart:io';

void main(List<String> arguments) async {
  print('TCP Server');
  final server = await ServerSocket.bind('0.0.0.0', 9001);
  server.listen((socket) {
    var receivedCount = 0;
    socket.listen((eventBytes) {
      final result = utf8.decode(eventBytes);
      print('From client = $result');
      socket.add(utf8.encode('Received $receivedCount'));
      receivedCount++;
    });
  });
}

Line #4 is the command to bind to the 9001 port on the system’s default network interface. This is an asynchronous operation so the await key is critical. Once we have that we have the server start listening for new sockets using the server.listen method, block starting at Line #7. Whenever a new client connects this will call our function and pass in the socket parameter. For this simplified example all we are going to do with the socket is listen for bytes. Once the underlying network system says “I have enough bytes so I want you to process them” we will assuming it is an encoded UTF-8 string that needs decoding. We then print that string to the command line and return a similarly encoded string back to the sending client.

The Client

The client side looks similar in many ways:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import 'dart:convert';
import 'dart:io';

void main(List<String> arguments) async {
  print('TCP Client');
  final socket = await Socket.connect('127.0.0.1', 9001);
  socket.listen((eventBytes) {
    final result = utf8.decode(eventBytes);
    print('From server = $result');
  });

  socket.add(utf8.encode('Hello!'));

  await Future.delayed(Duration(seconds: 2));
  await socket.flush();
  socket.destroy();
  print('DONE');
}

Rather than “binding” to a socket to just sit there and listen to connections as a server does we want to “connect” to a server that should already be listening to the socket. Since we are running all of this on the same server we are going to connect to “localhost” which is short-handed as “127.0.0.1” in IPv4 address terms. Like the bind method this is an asynchronous call so the await call is critical. Once we have a connection to the server we set up our own listener. This behaves identically to the server’s listener. The listen method returns immediately so immediately afterward, on Line #12 we send our “Hello” message to the server. Recall that the server is expecting a UTF-8 Encoded string’s bytes. This conforms with that expectation.

Line #14 is a simple delay to wait for any traffic back, which should have happened nearly instantaneously anyway, before flushing the socket (Line #15) and then destryoing it (Line #16). There is a close method on the socket but this only shuts down the sending of data direction. If you use this method the app will sit there and continue to listen to traffic from the server, ultimately timing out if it hears nothing from the server after a long enough period of time. The destroy method on the other hand severs the connections in both directions immediately.

Conclusion

The basics of working with raw TCP connections in Dart is very straight forward. There are a lot of utilities that help doing things like converting to/from raw byte streams that further makes it helpful. Although I’m documenting this it should be clear that this is not a good idea for most applications. There are lots of intricacies to implementing things at this level. For example, adding data to the socket is not discretizing it into “packets”. That is not an explicit construct at the TCP layer. It is more like a stream of bytes. The server side has to handle all of that logic. Therefore, for example, calling socket.add several times in a row doesn’t create three discrete “listening” events server side but instead the TCP stack bundled them up into one large byte stream which is processed all at once server side. Still, there are many applications where one has to be working at this low level. For those applications Dart makes it very simple.