Using the ThreadServer Class

Introduction

One of the gems in Haxe's standard library is the ThreadServer class. It can be used to easily create a multithreaded server where each thread handles many sockets, but all message handling code is executed by a worker thread so it doesn't have to be threadsafe. This tutorial will demonstrate how it is used.

The ThreadServer class is customized by defining data types for holding client and message data and replacing some of its methods. Methods are replaced by either subclassing ThreadServer and overriding them, or dynamically rebinding them. In this example I'll take the former approach.

Data Types

There are two data types that must be defined. One contains client information, the other is for messages. In the example below the client is a typedef that just contains an id, and the message is a typedef that just contains a string. Note that in another implementation these data types could be classes or even basic types.

Methods

There are several methods that must be replaced and a few that have default implementations.

The clientConnected method is called when a new connection is accepted by the server. It should be used to create a client object. In the example below a client is created and assigned a random id. clientDisconnected can be used to clean up client data when a connection closes.

The readClientMessage method is called when new data arrives at a socket. Its job is to create messages from a buffer of data read from the socket. It makes no assumptions about the format of the data. In this example we're assuming that each incoming message will be text that ends in a period.

The clientMessage method is called when a message is created by readClientMessage. The server's message handling code goes here. The example just prints out the incoming message.

Example

Server code:

// ServerExample.hx
import neko.Lib;
import sys.net.Socket;
import neko.net.ThreadServer;
import haxe.io.Bytes;

typedef Client = {
  var id : Int;
}

typedef Message = {
  var str : String;
}

class ServerExample extends ThreadServer<Client, Message>
{
  // create a Client
  override function clientConnected( s : Socket ) : Client
  {
    var num = Std.random(100);
    Lib.println("client " + num + " is " + s.peer());
    return { id: num };
  }

  override function clientDisconnected( c : Client )
  {
    Lib.println("client " + Std.string(c.id) + " disconnected");
  }

  override function readClientMessage(c:Client, buf:Bytes, pos:Int, len:Int)
  {
    // find out if there's a full message, and if so, how long it is.
    var complete = false;
    var cpos = pos;
    while (cpos < (pos+len) && !complete)
    {
     //check for a period/full stop (i.e.:  "." ) to signify a complete message 
      complete = (buf.get(cpos) == 46);
      cpos++;
    }

    // no full message
    if( !complete ) return null;

    // got a full message, return it
    var msg:String = buf.readString(pos, cpos-pos);
    return {msg: {str: msg}, bytes: cpos-pos};
  }

  override function clientMessage( c : Client, msg : Message )
  {
    Lib.println(c.id + " sent: " + msg.str);
  }

  public static function main()
  {
      var server = new ServerExample();
      server.run("localhost", 1234);
  }
}

The test client opens a connection to the server and sends a bunch of messages then closes it.

Client code:

// ClientExample.hx
import neko.Lib;
import neko.Sys;
import sys.net.Host;
import sys.net.Socket;

class ClientExample
{
  public static function main()
  {
      Lib.println("opening connection");
      var sock = new Socket();
      sock.connect(new Host("localhost"), 1234);

      Lib.println("sending messages");
      sock.write("this is a test.");            Sys.sleep(.1);
      sock.write("this is another test.");      Sys.sleep(.1);
      sock.write("this is a third test.");      Sys.sleep(.1);
      sock.write("this is the last test.");

      sock.close();
      Lib.println("client done");
  }
}

Compile with:

haxe -neko server.n -main ServerExample
haxe -neko client.n -main ClientExample

Start the server in one terminal with:

neko server.n

Start the client in another terminal with:

neko client.n

Server output should be:

client 57 is { host => 127.0.0.1, port => 35343 }
57 sent: this is a test.
57 sent: this is another test.
57 sent: this is a third test.
57 sent: this is the last test.
client 57 disconnected

Client output should be:

opening connection
sending messages
client done

Alternatively you can use Netcat to test the server. Netcat is a TCP/UDP tool that allows you to manually connect to a port in order to send and receive raw text. Most Linux distros have netcat preinstalled (using the alias "nc"). It can also be installed on Windows, OSX, Linux manually.

Start the ThreadServer above then connect via netcat

nc -v localhost 1234

Upon successful connection, you can type anything into the console and followed by the Enter key to send raw text to the server. To stop netcat, hit Ctrl+C

version #19866, modified 2013-12-19 20:45:38 by jmschrack