DEV Community

jtenner
jtenner

Posted on

A TCP Server Example in AssemblyScript

In node.js it's actually pretty easy to set up a TCP server. The following is a node TypeScript example that should look relatively familiar to node.js developers.

const server = net.createServer((socket: net.Socket) => {
  console.log(`Connected: ${socket.remoteAddress}:${socket.remotePort}`);
  socket.on("data", (data: Buffer) => {
    socket.write(data); // echo the data back
  });

  socket.write(Buffer.from("Hello world!"));
  socket.on("error", (error) => console.error(err));
});

server.listen(PORT, '127.0.0.1');

However, because (for the purposes of today's example) we want to do our heavy lifting in AssemblyScript, communicating with WebAssembly is going to be the goal of today's exercise.

First, let's uniquely identify individual connections in WebAssembly by modifying the TCP server to effectively host and enumerate the connections.

// Let's create a map of connections to write to
const connMap = new Map<number, net.Socket>();

let socketID = 0;

const wasm = instantiateBuffer<any>(wasmOutput, {
  socket: {
    // this method will write to the socket later
    write(id: number, pointer: number, length: number): 1 | 0 /* bool */ {
      // When converting an `i32` to an unsigned value, always use `>>> 0`
      id >>>= 0; // unsigned id
      pointer >>>= 0; // unsigned pointer
      length >>>= 0; // unsigned length
      const socket = connMap.get(id)!; // Get the socket
      // write the bytes 
      return socket.write(wasm.U8.slice(pointer, pointer + length))
        ? 1  // write was successful 
        : 0; // buffer was queued
    },
  },
});

The write function will allow us to communicate to the TCP socket later. The next step will be to create the server and map the socket to a unique identifier. It might be better to pool the connection ids, but the following works as a small example.

const server = net.createServer((socket: net.Socket) => {
  const id = socketID++;
  connMap.set(id, socket);
  wasm.onConnection(id);
  console.log(`Connected: ${socket.remoteAddress}:${socket.remotePort}`);
  socket.on("data", (data: Buffer) => {
    // Let's push the data into wasm as an ArrayBuffer (id: 0).
    let pointer = wasm.__alloc(data.length, 0); 
    // copy the buffer data to wasm.U8 at the allocation location
    data.copy(wasm.U8, pointer);
    // call a WebAssembly function (retains + releases the data automatically)
    wasm.onData(id, pointer);
  });
  socket.on("error", (error) => {
    // notify WebAssembly the socket errored
    console.error(error);
    wasm.onError(id); 
  });
  socket.on("close", () => {
    // close the socket
    connMap.delete(id);
    wasm.onClose(id);
  });
});

This is a very minimal setup but covers our needs for hosting a JavaScript TCP server for a WebAssembly module. Now we need to create the AssemblyScript module. For those who are inexperienced with AssemblyScript, you can install AssemblyScript with the following commands.

npm install --save-dev AssemblyScript/assemblyscript
npx asinit .

Now we will write a few lines of AssemblyScript to export and import a few WebAssembly functions, which if you have been following the example are:

export function onConnection(id: i32): void;
export function onData(id: i32, buffer: ArrayBuffer): void;
export function onError(id: i32): void;
export function onClose(id: i32): void;

On the WebAssembly side, we can create a connection map to link the connection id with a Connection reference.

// assembly/index.ts
import { Connection } from "./tcp/Connection";

// map each id to a new Connection object
let connections = new Map<u32, Connection>();

export function onConnection(id: u32): void {
  let session = new Connection();
  session.id = id;
  connections.set(id, session);
}

export function onClose(id: u32): void {
  connections.delete(id); // delete the connection
}

export function onData(id: u32, data: ArrayBuffer): void {
  let session = connections.get(id);
  session.onData(data);
}

export function onError(id: u32): void {
  // noOp
}

Now there are only two pieces of the puzzle left to fill in. We need to create our Connection class, and write back the received data to the hosted socket.

// assembly/tcp/Connection.ts
import { Socket } from "../socket";

export class Connection {
  id: i32 = 0;

  onData(data: ArrayBuffer): void {
    Socket.write(this.id, changetype<usize>(data), data.byteLength);
  }
}

The changetype<usize>(data) expression might look unfamiliar, but we are simply dereferencing the ArrayBuffer and using it as a pointer to write some data back to the socket.

Finally, we need to create a namespace for the imported write() function. We can use a @external function decorator as a compiler directive to reference the Socket.write function in a very specific way.

// assembly/socket/index.ts
export declare namespace Socket {
  // @ts-ignore: Compiler directive (link external host function)
  @external("Socket", "write")
  export function write(id: i32, pointer: usize, byteLength: i32): bool;
}

This namespace will host our Socket.write function. Yes. This is not valid TypeScript and reports an error in your vscode ide. This is because AssemblyScript is not exactly a subset TypeScript. However, being able to control how functions get linked like this is very useful!

Finally, we can spin up a TCP connection and emit "Hello world!\r\n" from the socket to watch the text appear in our console, echoed from the server.

Please check out this github repo for an example of how to get started:

tcp-socket-example

The tcp server example is located in ./src/index.ts. To start the server, use the npm start command. The server will compile the assemblyscript module automatically and bootstrap the module for you when the script starts.

Feel free to comment and ask questions below! This example could be made clearer, and I would love feedback on how to help others get started.

Best wishes,
@jtenner

Top comments (1)

Collapse
 
m_kunc profile image
Martin Kunc

@jtenner, is the github repo available please ?