Using TCP and UDP in Deno

Mayank C
Tech Tonic

--

Deno’s core runtime comes with support for TCP and UDP. The underlying TCP connection is already being used by HTTP and WebSocket. While, the UDP handling is still under the unstable umbrella, and is expected to get vetted very soon (perhaps from Deno 2.0).

For a quick background, TCP requires a prior connection establishment before data could be exchanged. TCP has reliability and security built-in. UDP is a stateless protocol and there is no connection required before sending data. There are no guarantees for message delivery either. But, UDP is good for quick exchange as there is no prior connection overhead.

As expected, Deno makes it very easy to write clients and servers in TCP, and peers in UDP. In this article, we’ll go over the core runtime’s APIs for handling TCP and UDP. At the end, we’ll also build a chatting app in both the protocols (though, UDP is not suitable for it).

UDP

UDP is a connection-less protocol. A message can be sent to the peer without establishing a connection. Deno’s core runtime has a listenDatagram function (under the unstable umbrella) that can be used to create a UDP listener. The listener (aka DatagramConn) implements an AsyncIterator, so the incoming requests can be looped upon. Alternatively, there is an async receive function that can be used with promises.

Listen

The listenDatagram function can be used to start listening on a given port number. The other input is the type of socket: UDP or unixpacket. We’ll focus only on UDP (as this article is about TCP and UDP).

const l=Deno.listenDatagram({port: 10000, transport: "udp"});

To get the local listening address, use l.addr :

const l=Deno.listenDatagram({port: 10000, transport: "udp"});
l.addr;
//{ transport: "udp", hostname: "127.0.0.1", port: 10000 }

The listenDatagram function returns a DatagramConn object that provides a way to get incoming requests, send data, and close the socket. Let’s go over them in detail.

AsyncIterable

The DatagramConn i.e. listener implements an AsyncIterator, therefore it can be looped upon for incoming requests:

const l=Deno.listenDatagram({port: 10000, transport: "udp"});
for await(const r of l)
r;

Each incoming request is a tuple of the received data and the peer address. The peer address would be useful in replying.

request: <[Uint8Array, Addr]>

Receive

The receive function can be used to get the incoming requests using promises. The return type of receive function is the same as the AsyncIterator i.e. it contains data and the peer address.

const l=Deno.listenDatagram({port: 10000, transport: "udp"});
while(1) {
const r=await l.receive();
r; //[Uint8Array, Addr]
}

Send

The send function can be used to send data to a peer. As there is no prior connection to the peer, the send function needs peer address as well.

const peerAddr:Deno.NetAddr={transport: "udp", hostname: "127.0.0.1", port: 10000};
await l.send(new TextEncoder().encode('abcd'), peerAddr);

Close

The close function closes the listener:

l.close();

That’s all about handling UDP! It’s very simple. Note that the DatagramConn object doesn’t implement Reader/Writer interfaces (not yet). This means that a DatagramConncan’t be used in place of Readers and writers. We’ll see a full example later.

TCP

TCP is a connection oriented protocol i.e. a prior connection is required before data could be exchanged. Deno’s core runtime comes with listen/listenTls functions to create a TCP server, and connect/connectTls functions to create a TCP client. These functions return a Conn object that implements Reader , Writer , and Closer interfaces. The listener additionally implements AsyncIterable. Unlike UDP, Conn doesn’t have receive and send functions. Rather, it uses read and write .

Listen

The listen function is used to create a TCP server. At minimum, port number is required. Hostname and transport can be provided optionally.

const l=Deno.listen({port: 10000});

To create a secure socket, listenTls can be used. At minimum, listenTls needs port number, server certificate, and key file.

const l=Deno.listenTls({ port: 10000,
hostname: 'localhost',
certFile: './l.crt',
keyFile: './l.key'});

Both listen and listenTls returns a Conn object for every incoming connection.

Once a server starts listening, the conn object can be iterated to process all the incoming connections.

AsyncIterator

Just like in other places like HTTP, UDP, etc., a TCP conn object returned by the listen function also implements the AsyncIterator. Therefore, it can be looped upon.

const l=Deno.listen({port: 10000});
for await(const r of l) {
r.localAddr;
r.remoteAddr;
}
//localAddr = { transport: "tcp", hostname: "127.0.0.1", port: 10000 }
//remoteAddr = { transport: "tcp", hostname: "127.0.0.1", port: 57667 }

Connect

The connect function is used to create a TCP client. This function creates a connection between client and server. Once a successful connection is established, any data can be sent from any side.

const c=await Deno.connect({port: 10000});

Deno.connect returns the same conn object to the client, but this one doesn’t implement AsyncIterator.

The connectTls function is used to create a secure TCP client. In addition to port, connectTls can take optional server certificate.

const c=await Deno.connectTls({port: 10000, hostname: 'localhost', certFile: './s.crt'});

The local and remote address can be printed in the same way they were printed on the server:

c.localAddr;
//{ transport: "tcp", hostname: "::1", port: 60344 }
c.remoteAddr
{ transport: "tcp", hostname: "::1", port: 10000 }

Read

The read function can be used to block for data to arrive. TCP is a stream oriented protocol, therefore there is no well-defined start and finish. The read function can be used to read the buffered data for that TCP connection.

The read function has the same implementation as Deno.Reader. It reads up to array.length bytes from the socket. It returns the actual number of bytes that got read. For example, if an array of length 1000 is passed, the actual read bytes might only be 10.

const b=new Uint8Array(100);
const n=await r.read(b);
//n = 4
//b= Uint8Array(100) [97, 98, 99, 100, 0, 0, 0, 0, 0, 0 ...]

There are only 4 useful bytes in the array. The array needs to be sliced before it is used.

Write

The write function writes data on the socket that’d be sent to the other party. The write function follows the same Deno.Writer interface. It takes a Uint8Array as input and writes it completely on the socket. It also returns the number of actual bytes written (usually that’d be the same as the length of the array).

const d=new TextEncoder().encode('abcd');
await r.write(d);

Close

The close function closes the socket connection. Both client and server can use the close function.

r.close();

UDP chat

Now that we’ve gone through the basics of both types of sockets, let’s go over a complete chat example. First, we’ll develop a UDP chat. Both the peers have the same collection of messages including an exit message. The messages would be randomly chosen and sent to the peer. Whenever the exit message gets chosen, the program would exit. The socket couldn’t be closed because there wasn’t a connection at all. There are two parties in this example: A and B. B is acting like a client, therefore it initiates the first message. If none act as initiator, both would keep waiting.

The code to choose random messages is common for both TCP and UDP:

const messages=['hi', 'hello', 'hey', 'how are you?', 'how can i help you?', 'sure', 'good', 'thanks', 'exit'];function getRandomMessage() {
const i=Math.floor(Math.random() * messages.length);
return messages[i];
}

Here is the code for party A and party B.

//Party Aconst l=Deno.listenDatagram({port: 10000, transport: "udp", hostname: "127.0.0.1"});
for await(const r of l) {
console.log('B >> ', new TextDecoder().decode(r[0]));
const msg=getRandomMessage();
console.log('A >> ', msg);
if(msg === 'exit')
Deno.exit(1);
await l.send(new TextEncoder().encode(msg), r[1]);
}

//Party B
const l=Deno.listenDatagram({port: 10001, transport: "udp", hostname: "127.0.0.1"});
await l.send(new TextEncoder().encode(getRandomMessage()), {port: 10000, transport: "udp", hostname: "127.0.0.1"}); //B acts as initiator
for await(const r of l) {
const msg=getRandomMessage();
console.log('B >> ', msg);
if(msg === 'exit')
Deno.exit(1);
await l.send(new TextEncoder().encode(msg), r[1]);
}

Here is the output of a sample run:

B >>  hello
A >> how are you?
B >> sure
A >> hello
B >> sure
A >> hey
B >> hi
A >> how are you?
B >> hello
A >> thanks
B >> how are you?
A >> hi
B >> how can i help you?
A >> how are you?
B >> hi
A >> hey
B >> exit

TCP chat

Now that we’ve gone through the UDP chat, let’s convert it to TCP chat. This time, a connection needs to be initiated from client using connect function. Party A is a server, and will wait for incoming connections. Party B is a client, and will initiate a message as soon as a connection is established. The send and receive functions used in the UDP chat program will be replaced by write and read respectively.

//SERVER (A)const l=Deno.listen({port: 10000, transport: "tcp", hostname: "127.0.0.1"});
const c=await l.accept();
while(1) {
let buf=new Uint8Array(50);
const n=await c.read(buf) || 0;
buf=buf.slice(0, n);
console.log('B >> ', new TextDecoder().decode(buf));
const msg=getRandomMessage();
console.log('A >> ', msg);
if(msg === 'exit') {
c.close();
break;
}
await c.write(new TextEncoder().encode(msg));
}
//CLIENT (B)const c=await Deno.connect({port: 10000, transport: "tcp", hostname: "127.0.0.1"});
await c.write(new TextEncoder().encode(getRandomMessage()));
while(1) {
let buf=new Uint8Array(50);
const n=await c.read(buf) || 0;
buf=buf.slice(0, n);
console.log('B >> ', new TextDecoder().decode(buf));
const msg=getRandomMessage();
console.log('A >> ', msg);
if(msg === 'exit') {
c.close();
break;
}
await c.write(new TextEncoder().encode(msg));
}

Here is the output of a test run:

B >>  hello
A >> sure
B >> how are you?
A >> thanks
B >> hello
A >> thanks
B >> how are you?
A >> hey
B >> hey
A >> good
B >> hello
A >> how can i help you?
B >> how are you?
A >> exit

--

--