JavaScript Socket Programming Examples

Let’s do socket-level programming in JavaScript. Node, of course.

Unit Goals

To gain proficiency in writing client-server applications in JavaScript at the socket level.

Overview

We will look at several TCP applications, written completely from scratch in JavaScript. The net module supports asynchronous programming with a nice stream-based interface over operating system level sockets. (There is IPC support in that module, too, but here we only care about TCP applications.)

Highlights from the module:

Events?

Yes, events, not threads. JavaScript programming is going to look very, very different than the Java and Python programming we saw earlier. Different in a good way. I think you’ll like this new way. A lot.

A Stateless Server and a Client

This is a fairly simple server. Whenever a client connects, a callback is fired that sends the current datetime to the client.

dateserver.js
import net from "net"

// A use-once date server. Clients get the date on connection and that's it!
const server = net.createServer((socket) => {
  socket.end(`${new Date()}\n`)
})

server.listen(59090)

Run like this:

$ node dateserver.js

Leave it running then move to another console window.

Things to note:

You can do a quick check with nc (in another window of course):

$ nc 127.0.0.1 59090
Sun Oct 23 2022 15:09:16 GMT-0700 (Pacific Daylight Time)
$ nc 127.0.0.1 59090
Sun Oct 23 2022 15:09:17 GMT-0700 (Pacific Daylight Time)

Of course, learning how to write your own clients is important! Here is a trivial client. It connects, prints the datetime it gets from the server, then exits. Remember, the server automatically closes the connection itself, so our client does not have to explicitly close.

dateclient.js
// A client for the date server.
//
// Example usage:
//
//   node dateclient.js 10.0.1.40

import net from "net"

const client = new net.Socket()
client.connect({ port: 59090, host: process.argv[2] ?? "localhost" })
client.on("data", (data) => {
  console.log(data.toString("utf-8"))
})

Things to note here:

$ node dateclient.js
Sun Oct 23 2022 15:44:57 GMT-0700 (Pacific Daylight Time)
Exercise: Experiment with the socket’s setEncoding method, which will allow you to avoid explicit encoding on every socket read.

Two-Way Communication

Our next example server is more sophisticated; it accepts data from a client. A client sends lines of text to the server and the server sends back the lines capitalized. JavaScript is naturally asynchronous, so multiple clients are automatically handled concurrently. Each client has its own connection which stays open for as long it wants to run its capitalization session.

The server does more logging than usual, so you can see what is going on while experimenting:

capitalizeserver.js
import net from "net"

const server = net.createServer((socket) => {
  console.log("Connection from", socket.remoteAddress, "port", socket.remotePort)

  socket.on("data", (buffer) => {
    console.log("Request from", socket.remoteAddress, "port", socket.remotePort)
    socket.write(`${buffer.toString("utf-8").toUpperCase()}\n`)
  })
  socket.on("end", () => {
    console.log("Closed", socket.remoteAddress, "port", socket.remotePort)
  })
})

server.maxConnections = 20
server.listen(59898)

Run like this:

$ node dateserver.js

Because Node is event-driven, servers by default run “in a loop.” So they are agnostic to whether a client will be sending just one string or sending many. The server just keeps reading until the client closes the connection. If you are running nc on the command line for your test client, you can send line after line until you explicitly stop with whatever your operating system stream closing operation is (e.g. Ctrl+D on Macs).

$ nc localhost 59898
yeet
YEET
Seems t'be workin'
SEEMS T'BE WORKIN'
Привет, мир
ПРИВЕТ, МИР

Not surprisingly, command line redirection works:

$ nc localhost 59898 < dateserver.js
IMPORT NET FROM "NET"

// A USE-ONCE DATE SERVER. CLIENTS GET THE DATE ON CONNECTION AND THAT'S IT!
CONST SERVER = NET.CREATESERVER((SOCKET) => {
  SOCKET.END(`${NEW DATE()}\N`)
})

SERVER.LISTEN(59090)

If you like, you can totally copy the behavior of nc by writing your own client:

capitalizeclient.js
// A client for the capitalization server. After connecting, every line
// sent to the server will come back capitalized.
//
// Use interactively:
//
//   node capitalizeclient.js 10.0.1.40
//
// Or pipe in a file to be capitalized:
//
//   node capitalizeclient.js 10.0.1.40 < myfile

import net from "net"
import readline from "readline"

const client = new net.Socket()
client.connect(59898, process.argv[2] ?? "localhost", () => {
  console.log("Connected to server")
})
client.on("data", (data) => {
  console.log(data.toString("utf-8"))
})

const reader = readline.createInterface({ input: process.stdin })
reader.on("line", (line) => {
  client.write(`${line}\n`)
})
reader.on("close", () => {
  client.end()
})
$ node capitalizeclient.js
yeet
YEET
Seems t'be workin'
SEEMS T'BE WORKIN'
Привет, мир
ПРИВЕТ, МИР

Command line redirection works here too.

If you want a client that sends just one line, then the client needs to destroy its socket after it receives and processes the data it receives:

onetimecapitalizeclient.js
// A use-once client for the capitalization server.
//
// Usage:
//
//   node onetimecapitalizeclient.js 10.0.1.40 'string to capitalize'

import net from "net"

const client = new net.Socket()
client.connect({ port: 59898 }, process.argv[2] ?? "localhost", () => {
  client.write(`${process.argv[3]}\r\n`)
})
client.on("data", (data) => {
  console.log(`Server says: ${data.toString("utf-8")}`)
  client.destroy()
})
$ node onetimecapitalizeclient.js localhost "I think it works"
Server says: I THINK IT WORKS
Exercise: How many loops do you see in the server or either of the clients? Why is this?

A Tic-Tac-Toe Game Server

Here is the server for a two-player game. It listens for two clients to connect: the first is Player X and the second is Player O. The client and server send simple string messages back and forth to each other; messages correspond to the Tic Tac Toe protocol, which I made up for this example.

Here is the classic TCP server:

tictactoeserver.js
// A server for Tic-tac toe games.
//
// The first two client connections become X and O for the first game; the next
// two connections face off in the second game, and so on. Games run concurrently.
//
// The games use TTTP, the "Tic Tac Toe Protocol" which I just made up:
//
// Client -> Server
//     MOVE <n>
//     QUIT
//
// Server -> Client
//     WELCOME <char>
//     VALID_MOVE
//     OTHER_PLAYER_MOVED <n>
//     OTHER_PLAYER_LEFT
//     VICTORY
//     DEFEAT
//     TIE
//     MESSAGE <text>
//
// The cells are numbered top-to-bottom, left-to-right, as 0..8.

import net from "net"

let game = null

net
  .createServer((socket) => {
    console.log("Connection from", socket.remoteAddress, "port", socket.remotePort)
    if (game === null) {
      game = new Game()
      game.playerX = new Player(game, socket, "X")
    } else {
      game.playerO = new Player(game, socket, "O")
      game = null
    }
  })
  .listen(58901, () => {
    console.log("Tic Tac Toe Server is Running")
  })

class Game {
  // A board has nine squares. Each square is either unowned or it is owned by a
  // player. So we use a simple array of player references. If null, the corresponding
  // square is unowned, otherwise the array cell stores a reference to the player that
  // owns it.
  constructor() {
    this.board = Array(9).fill(null)
  }

  hasWinner() {
    const b = this.board
    const wins = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ]
    return wins.some(([x, y, z]) => b[x] !== null && b[x] === b[y] && b[y] === b[z])
  }

  boardFilledUp() {
    return this.board.every((square) => square !== null)
  }

  move(location, player) {
    if (player !== this.currentPlayer) {
      throw new Error("Not your turn")
    } else if (!player.opponent) {
      throw new Error("You don’t have an opponent yet")
    } else if (this.board[location] !== null) {
      throw new Error("Cell already occupied")
    }
    this.board[location] = this.currentPlayer
    this.currentPlayer = this.currentPlayer.opponent
  }
}

class Player {
  constructor(game, socket, mark) {
    Object.assign(this, { game, socket, mark })
    this.send(`WELCOME ${mark}`)
    if (mark === "X") {
      game.currentPlayer = this
      this.send("MESSAGE Waiting for opponent to connect")
    } else {
      this.opponent = game.playerX
      this.opponent.opponent = this
      this.opponent.send("MESSAGE Your move")
    }

    socket.on("data", (buffer) => {
      const command = buffer.toString("utf-8").trim()
      if (command === "QUIT") {
        socket.destroy()
      } else if (/^MOVE \d+$/.test(command)) {
        const location = Number(command.substring(5))
        try {
          game.move(location, this)
          this.send("VALID_MOVE")
          this.opponent.send(`OPPONENT_MOVED ${location}`)
          if (this.game.hasWinner()) {
            this.send("VICTORY")
            this.opponent.send("DEFEAT")
          } else if (this.game.boardFilledUp()) {
            ;[this, this.opponent].forEach((p) => p.send("TIE"))
          }
        } catch (e) {
          this.send(`MESSAGE ${e.message}`)
        }
      }
    })

    socket.on("close", () => {
      try {
        this.opponent.send("OTHER_PLAYER_LEFT")
      } catch (e) {}
    })
  }

  send(message) {
    this.socket.write(`${message}\n`)
  }
}

We can use the Java Tic Tac Toe client we wrote earlier to communicate with this server (yay for mixed language programming!), or if you feel old school, play the game with nc.

What about a graphical JavaScript client?

Well, now. Where are JavaScript GUIs generally hosted? Web browsers! But the thing is, web browsers have quite a few security restrictions. They can’t just connect to arbitrary ports on arbitrary machines. If you want JavaScript clients to run in a browser, you have to deal with these browser restrictions. So we won’t be writing JavaScript clients here; we’ll have to learn about HTTP and web sockets.

Here’s a run with nc:

$ nc localhost 58901
WELCOME X
MESSAGE Waiting for opponent to connect
MESSAGE Your move
MOVE 3
VALID_MOVE
OPPONENT_MOVED 1
MOVE 2
VALID_MOVE
OPPONENT_MOVED 8
MOVE 5
VALID_MOVE
OPPONENT_MOVED 4
MOVE 7
VALID_MOVE
OPPONENT_MOVED 0
DEFEAT
$ nc localhost 58901
WELCOME O
OPPONENT_MOVED 3
MOVE 1
VALID_MOVE
OPPONENT_MOVED 2
MOVE 8
VALID_MOVE
OPPONENT_MOVED 5
MOVE 4
VALID_MOVE
OPPONENT_MOVED 7
MOVE 0
VALID_MOVE
VICTORY

But do try the server with the Java client, too.

A Chat Server

Now let’s do a JavaScript implementation of the chat server we wrote earlier in Java.

We have think about this differently in JavaScript than we did in Java. The Java implementation was synchronous, so each user chat thread is implemented with two sequential code blocks:

getUserName();
dialog();
Exercise: Review the Java implementation (see the run method of the Handler class).

This will not work in JavaScript.

Instead, we have to explicity keep track of application “state”: either we are in the “waiting for name” state, in which data from the client are candidate names, or we are in the “dialog” state, in which client data is a message for the chat. As there are only two states, we can simply record whether a name yet exists, using an optional string value:

chatserver.js
import net from "net"

let usernames = new Set()
let clients = new Set()

function broadcastMessage(socket, message) {
  for (const client of clients) {
    if (client !== socket) {
      client.write(`${message}\n`)
    }
  }
}

function tryAcceptName(socket, name) {
  if (usernames.has(name)) {
    socket.write("Username already taken. Please try again:\n")
    return null
  }
  usernames.add(name)
  clients.add(socket)
  broadcastMessage(socket, `${name} has joined the chat!`)
  return name
}

function handleClientLeaving(socket, name) {
  console.log(`${name} disconnected`)
  usernames.delete(name)
  clients.delete(socket)
  broadcastMessage(socket, `${name} has left the chat!`)
}

net
  .createServer((socket) => {
    let name = null
    console.log(`Connection from ${socket.remoteAddress} port ${socket.remotePort}`)
    socket.write("Welcome to the chat! Please enter your name:\n")
    socket.on("data", (buffer) => {
      const message = buffer.toString("utf-8").trim()
      if (!name) {
        name = tryAcceptName(socket, message)
      } else {
        broadcastMessage(socket, `${name}: ${message}`)
      }
    })
    socket.on("end", () => handleClientLeaving(socket, name))
  })
  .listen(59001, () => {
    console.log("Chat Server is Running")
  })

The server can be tested with nc or the Java client we saw earlier.

Summary

We’ve covered:

  • The Node.js net module
  • One-way communication between client and server
  • Two-way communication between client and server
  • Events
  • JavaScript network-based multiplayer game servers
  • Why a JavaScript chat server will look different from a Java one