jsnetexamples 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: function net.createServer function net.connect class net.Server Events: listening, connection, error, close Properties: maxConnections Methods: listen, listening, address, getConnections, ref, unref class net.Socket Events: lookup, connect, ready, data, drain, timeout, error, end, close Properties: connecting, pending, destroyed, localAddress, localPort, remoteAddress, remotePort, remoteFamily, bufferSize, bytesRead, bytesWritten Methods: connect, address, write, end, pause, resume, ref, unref, setEncoding, setKeepAlive, setNoDelay, destroy 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 const net = require('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);
Things to note: Node allows you to send either a string or a Buffer object. If you send a string, Node will use the socket’s current encoding setting to encode the string into bytes. Node’s callback architecture “loops by default.” To make our use-once server, you have to explicitly end the connection; an easy way to do this is to use end instead of write. By the way, a one-liner is possible...
require('net').createServer(s => s.end(`${new Date()}\n`)).listen(59090);
but, please, no. Here is a trivial client. It connects, prints the datetime it gets from the server, then exits. dateclient.js // A client for the date server.
//
// Example usage:
//
// node dateclient.js 10.0.1.40
const net = require('net');
const client = new net.Socket();
client.connect({ port: 59090, host: process.argv[2] });
client.on('data', (data) => {
console.log(data.toString('utf-8'));
});
Things to note here: By default, the data you receive from a socket is a Buffer object, so it must be encoded to obtain the string. Because Node networking is event-driven, clients “run in a loop” by default—so to make it a use-once client, the client would have to explicitly call client.destroy() or submit data with client.end(), or we can let the server do the close. (Note how this differs from the Java and Python servers we saw earlier, where use-once was the default and we had to write an explicit loop to make multiple requests.) Exercise: Experiment with the socket’s setEncoding method, which will allow you to avoid explicit encoding on every socket read. Don’t forget you can always test your servers with nc, though learning how to write your own clients is important.
$ nc localhost 59090
Sun Feb 17 2019 10:12:15 GMT-0800 (Pacific Standard Time)
Two-Way Communication This 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 const net = require('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);
Without the logging, we can get a one-liner:
require('net').createServer(s => {
s.on('data', buffer => s.write(`${buffer.toString('utf-8').toUpperCase()}\n`));
}).listen(59898);
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. That depends on how we write the client. If the client wants to send 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'
const net = require('net');
const client = new net.Socket();
client.connect({ port: 59898 }, process.argv[2], () => {
client.write(`${process.argv[3]}\r\n`);
});
client.on('data', (data) => {
console.log(`Server says: ${data.toString('utf-8')}`);
client.destroy();
});
Now here’s a client that can send multiple lines. It will read lines from standard input until there are no more lines left: 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
const net = require('net');
const readline = require('readline');
const client = new net.Socket();
client.connect(59898, process.argv[2], () => {
console.log('Connected to server');
});
client.on('data', (data) => {
console.log(data.toString('utf-8'));
});
const rl = readline.createInterface({ input: process.stdin });
rl.on('line', (line) => {
client.write(`${line}\n`);
});
rl.on('close', () => {
client.end();
});
Exercise: How many loops do you see in the server or either of the clients? Why is this? This script uses a line reader from the standard Node library, which we tie to standard input. So we can use it either interactively or by command line redirection. If used interactively, exit with Ctrl+D or Ctrl+C:
$ node capitalizeclient.js
yeet
YEET
Seems t'be workin'
SEEMS T'BE WORKIN'
Привет, мир
ПРИВЕТ, МИР
Guess what? This is exactly how nc works!
$ nc localhost 59898
yeet
YEET
Seems t'be workin'
SEEMS T'BE WORKIN'
Привет, мир
ПРИВЕТ, МИР
You can also use command line redirection with the date client:
$ node newcapitalizeclient.js localhost < dateclient.js
Exercise: Try it. A Network Tic-Tac-Toe Game 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
// QUIT
//
// Server -> Client
// WELCOME
// VALID_MOVE
// OTHER_PLAYER_MOVED
// OTHER_PLAYER_LEFT
// VICTORY
// DEFEAT
// TIE
// MESSAGE
//
// The cells are numbered top-to-bottom, left-to-right, as 0..8.
const net = require('net');
(() => {
// When null, we are waiting for the first player to connect, after which we will
// create a new game. After the second player connects, the game can be fully set
// up and played, and this variable immediately set back to null so the future
// connections make new games.
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, 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 next up for us is HTTP. 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