Deep Into NodeJs

Node for Networking

Posted by 許敲敲 on May 4, 2021

Node for Networking

refer from jscomplete.com, author Samer Buna

TCP Networking with the Net Module

Just build a demo for network example by using net’s module.Usethe net’s module, create server method. We then register a connection handler that fires every time a client connects to this server. When that happens, let’s console. log the client is connected. The handler also gives us access to a connected socket itself. This socket object implements a duplex stream interface, which means that we can read and write to it. So let’s write “Welcome New Client. “ And to run the server, we need to listen to a port, and the callback here is just to confirm it. Let’s test. Testing this is simple, we can use either telnet or netcat. For example, nc localhost 8000, we get the client is connected message in the server console, and the welcome message is sent to the client, and then node keeps running, because we did not terminate that connection. Now the client can write to that socket, but we have not registered a handler to read from the socket. The socket being a duplex stream means that it’s also an EventEmitter. So we can listen to data event on the socket. The handler for this event gives us access to a buffer object. Let’s console log it. Now when the client types something, we get it as a buffer. This is great, because Node does not assume anything about encoding. The user can be typing here in any language. Let’s now echo this data object back to the user using socket. write. When we do so, you’ll notice that the data we are writing back is actually a string. This is because the write method on the socket assumes a utf8 encoding here. This second argument can be used to control the encoding and the default is utf8. We can also globally set the encoding if we need to, so the global encoding is now utf8 and the data argument here becomes a string instead of a buffer, because we now told Node to assume a utf8 encoding for anything received from this socket. Both the console. log line and the echoed data are assumed to be utf8. I’m gonna keep an example without global encoding, just in case we need access to the buffer object. What happens when the client disconnects? The socket emitter has an on ‘end’ event that gets triggered when the client disconnects. At that point, we can’t write to the socket any more. Let’s console log that client is disconnected. Let’s test that now. Connect, and we can disconnect a netcat session with Ctrl+D. The server logs the line.

const server = require('net').createServer();

server.on('connection', socket => {
  console.log('Client connected');
  socket.write('Welcome new client!\n');

  socket.on('data', data => {
    console.log('data is:', data);
    socket.write('data is: ');
    socket.write(data);
  });

  socket.on('end', () => {
    console.log('Client disconnected');
  });
});

server.listen(8000, () => console.log('Server bound'));

Working with Multiple Sockets

Let’s review what we did in the previous clip real quick. We created a network server using the net’s module createServer method. This server object is an EventEmitter, and it emits a connection event every time a client connects to it. So we register the handler on this connection event. This handler gives us access to the connected socket. The connected socket is also an EventEmitter, and it emits a dataEvent whenever the user types into that connection, and it also emits an end event if the user disconnects from that server. So now we have a server, we can connect to this server, type to it, and many users can connect to that server and also type to it. No problem there. They can both write to their sockets we can read from both sockets. These two connected clients get two different sockets. In fact, let’s give every socket a unique id. We can simply use a counter. I’m going to do counter, start from 0, every time a client connects, we’ll define an id property for that socket, and assign it a counter++ the counter for the next socket. And now on data, when we receive the data from the user, let’s identify the socket, and let me remove this console. log line here, we don’t need it. And in here, instead of writing “data is, “ I am going to write ${socket. id} to identify it, just like that. So let’s test one client, two clients, and now when this client types, it says 0, and when this client types it says one. So this makes the log clear. We have two different clients connecting here, and we can write to their sockets. To make these two clients chat with each other, all we need to do is, when we receive data from one of them, we write it to all the connected sockets. So, we need to keep a record of all connected sockets and loop over them in the data event handler. We can use a simple object to keep a record of all connected sockets. Let’s do that. Let’s define a sockets variable and start it as an empty object. Every time a socket connects, let’s store it in this sockets object. Let’s index it with the id we just defined, just like that. Then on a data event, we can loop over this object. We can use object. entries, which is actually a new method in JavaScript, just like keys, but gives us both the key and the value. So, entries for sockets. forEach, which gives us a handler. Inside the each handler argument we can destructure both the key and the socket object, which I’m going to alias as client socket here, so that it doesn’t conflict with the global socket that we’re working with. And now inside the forEach, we can just use our previous code to write to the socket. But instead of socket here, it’s going be cs, the socket that we have in the loop, and actually key is not used, so we can just remove it. We only care about the connected socket. And I do want to keep this as socket. id because that’s the socket that is actually sending the data, so it’s good to identify who is writing in that case. So let’s test all that. Connect multiple clients, and try typing in all of them. Hello, so you’ll see how zero said hello zero said hello in both of them. And then hi, you’ll see how one said hi and one said hi in both of them. So let’s review. On data from any socket, we loop over this sockets object, and we write the received data to every socket in there. This is simply act like a chat server, because every connected client is receiving all the messages from every other connected client. However, if one client disconnect, our chat application will simply crash, because now we’re trying to write to a socket that was closed. So, to fix that, in the end event here, when the socket emits an end event, we should delete it from the sockets object. So sockets for the socket. id. This would delete the disconnecting socket from the sockets object. And this way, if one client disconnects, the other client can still type and the server will not crash. So now we can say that we have a bare bone chat server.

const server = require('net').createServer();
let counter = 0;
let sockets = {};

server.on('connection', socket => {
  socket.id = counter++;
  sockets[socket.id] = socket;

  console.log('Client connected');
  socket.write('Welcome new client!\n');

  socket.on('data', data => {
    Object.entries(sockets).forEach(([, cs]) => {
      cs.write(`${socket.id}: `);
      cs.write(data);
    });
  });

  socket.on('end', () => {
    delete sockets[socket.id];
    console.log('Client disconnected');
  });
});

server.listen(8000, () => console.log('Server bound'));

Improving the Chat Server

We created a very basic chat server, so how about we improve it a little bit? Right now, we can run the server. Clients can connect, we can identify them with their IDs, and they all receive all the messages. So usually, when you chat, you don’t really get echoed back your own messages. So let’s have a condition here inside the for each to implement that. Basically, if the currently connected socket, which we can identify with socket. ID, if that equals to the current socket, which is identified by the key back to key here, if that condition is true, then the current socket in the loop is the same socket that is sending the data, so we should probably just return here and not write back to the user. So let’s test that. So now when the first client types, client 1 and 2 both get the hello message, but client 0 does not get the hello message. Let’s try hi in here. Client 0 and client 2 both get the message, but client 1 does not. Perfect. So how about we identify our chatters with names instead of numbers? Maybe we can ask them about their name when they first connect. So instead of this welcome new client message, I’m going to type “please type your name. “ Just like that. And this means that the first data event I’ll receive is going to be the name of the client. So I need a condition to identify if a client is just giving me their name for the first message or is this a data event that’s coming from a client who already gave me their name and they’re just chatting now? So how about instead of adding every socket to the officially connected socket, we make that a condition. We only add the socket when we receive the name of the user. So in here, we’ll do a condition. We’ll say if the socket is not part of the sockets connected, so if not sockets for the socket. ID, then this means that we have not registered this socket yet, and this current data message is the name of the user. So we’ll go ahead and register this socket now, and we’ll store the name of the user, which is basically the data object. And remember the data object is a buffer, because we don’t have the global encoding, so we’ll do a toString on it to convert it into UTF8, and also we trim it to remove the new line. And in this case, we want to return. We don’t want to echo this name back to all connected user, because this is just a socket that’s just connecting and giving me their name. How about we give the user a welcome message here, so socket. write, and say something like “welcome socket. name, “ just like that. So now that we have a name for every socket, we can actually use the name here instead of the ID to identify the socket. Socket. name in here. And I think we can test. Connect three clients, and you’ll notice how the server is asking for the name. Let’s name the first chatter Alice, Bob for the second, Charlie for the third, and you see how the server welcomed the names, so now when Alice types something, Bob and Charlie get the message Alice said “hello. “ Let’s try it here. So Bob is saying “Hi. “ Charlie is saying “Hey. “ So that’s a good improvement. How about we also show the timestamp on the message? Because without timestamps, it’s not really a chat server. So we can simply display the timestamp here right after the name, so let’s make it into a variable, and how about we create a function for the timestamp, just like that, and let’s go define this function. So function timestamp is a very simple function that will basically format the current date, so let’s do now = newDate. This is to just give me the current date, and let’s return a string that has the hours, which is now. getHours, and the minutes, which is now. getMinutes. Very simple. So this would be like hhmm format. This is very simple. If you really want to do an actual time format, you have to handle multiple cases, and you have to be aware of time zones and all that. I would recommend a library called moment if you want to actually work with timestamps, but this is just an example to improve our simple chat server. So let’s test that. Alice, Bob and Charlie, and now we see that Alice said “Hello” at 10:47.

const server = require('net').createServer();
let counter = 0;
let sockets = {};

function timestamp() {
  const now = new Date();
  return `${now.getHours()}:${now.getMinutes()}`;
}

server.on('connection', socket => {
  socket.id = counter++;

  console.log('Client connected');
  socket.write('Please type your name: ');

  socket.on('data', data => {
    if (!sockets[socket.id]) {
      socket.name = data.toString().trim();
      socket.write(`Welcome ${socket.name}!\n`);
      sockets[socket.id] = socket;
      return;
    }
    Object.entries(sockets).forEach(([key, cs]) => {
      if (socket.id == key) return;
      cs.write(`${socket.name} ${timestamp()}: `);
      cs.write(data);
    });
  });

  socket.on('end', () => {
    delete sockets[socket.id];
    console.log('Client disconnected');
  });
});

server.listen(8000, () => console.log('Server bound'));

The DNS Module

We can use it to translate network names to addresses and vice versa. For example, we can use the lookup method to lookup a host, say Pluralsight. com. This gives us a callback, error first callback, and it gives us the address for that host. So we can log the address here. And running this will give us the IP address for Pluralsight. com. The lookup method on the DNS module is actually a very special one, because it does not necessarily perform any network communication, and instead uses the underlying operating system facilities to perform the resolution. This means that it will be using libuv threads. All the other methods on the DNS module uses the network directly and does not use libuv threads. For example, the equivalent network method for lookup is resolve4, 4 is the family of IP address, so we’re interested in IPv4 addresses. And this will give us an array of addresses in case the domain has multiple A records, which seems to be the case for Pluralsight. com. If we just use resolve, instead of resolve4, the default second argument here is A, which is an A record so this is what just happened, but we can also resolve other types, like for example, if we’re interested in MX record we just do that, and here are all the MX records for Pluralsight. com. You can tell they’re using Google apps. And all the available types also have equivalent method names, so MX here is the same as resolving with the MX argument. Another interesting method to know about is the reverse method. The reverse method takes in an IP, so let’s actually pass one of the IPs for Pluralsight, and it gives us a callback error first and hostnames, so it translates an IP back to its host names. Let’s test that, and you’ll notice that it translates this IP back to an Amazon AWS hostname. If we actually tried to open this in a browser it will probably go to Pluralsight. com. There you go.

const dns = require('dns'); // name -- addresses

// dns.lookup('pluralsight.com', (err, address) => {
//   console.log(address);
// });

// dns.resolve4('pluralsight.com', (err, address) => {
//   console.log(address);
// });

// dns.resolveMx('pluralsight.com', (err, address) => {
//   console.log(address);
// });

// dns.reverse('35.161.75.227', (err, hostnames) => {
//   console.log(hostnames);
// });

UDP Datagram Sockets

Let’s see an example for working with UDP sockets in Node. To do so, we need to require the dgram module, which provides an implementation for the UDP datagram sockets. We then can use the dgram createSocket method to create a new socket. The argument for the createSocket could be udp4 or udp6, depending on the type of socket that you want to use. So we’re using udp4 in here. And to listen on this socket, we use server. bind and give it port and host. I’m putting port and host here in constants, because I’m going to reuse them for communication. So the server object here is an event emitter and it has a few events that we can register handlers for. The first event is the listening event, which happens when the UDP server starts listening. And the other event that we want to register a handle for is the message event. This event happens every time the socket receives a message. It’s callback exposes the message itself and the remote information, so let’s console log the remote information address and port and the message so that we know where the message is coming from. And that’s it for the UDP server. We can actually run this code and the server will start listening for connections. So this part is the server, and I’m going to create a client in the same file here for simplicity. So since we do have this dgram required already and we can actually share these three variables, so all we need to do is create a client socket, which we can use the same call for. Let’s call this client. And to send a UDP packet, we can use client. send. This method takes the first argument, can be a string, so let’s send “Pluralsight rocks. “ And we need to tell the UDP socket which port and host to send this message to, so we can use port and host in here. And this exposes an optional callback in case we want to handle the error in here. So, if there’s an error, we should throw it, and in here we can console. log something like udp message send, and potentially after we send this message, we can close the client, in case this is the only udp packet that we want to send. So we can test that. You’ll see that server is listening, message is sent, and you’ll see this code is getting fired with the right address port in there. Every time you create a new socket, it will use a different port. For example, let’s put all this code in an interval function, and every second do all of this sending. And let’s see what happens now. You’ll see that every second we’re getting a different port for the remote information. Let’s undo this. So back to sending a message. This first argument here is currently a string, but it can also be a buffer, so let’s create a new buffer. I’m going to call it message and it will be buffer from this message, and I want to send this message, which is a buffer in this case. And when we have a buffer, we can specify where to start from this buffer, and where to end from this buffer. So 0 and msg. length will be equivalent to sending a string, so you’ll see I got the same thing there. So we’re sending the buffer from 0 to msg. length, and I can actually send the packet in multiple steps. So, for example, let’s send the first 11 bytes, which is Pluralsight. And after we do that, let’s send from 11, 6 characters, which is going to give me the rocks message, so let’s take a look at that. Now you’ll see that the packet was sent on two steps, so we have Pluralsight in the first step and then space rocks in the second step. Let me undo that so we don’t need it. So specifying an offset and the length is only needed if you use a buffer. The first argument can actually be an array of messages if we need to send multiple things.

const dgram = require('dgram');
const PORT = 3333;
const HOST = '127.0.0.1';

// Server
const server = dgram.createSocket('udp4');

server.on('listening', () => console.log('UDP Server listening'));

server.on('message', (msg, rinfo) => {
  console.log(`${rinfo.address}:${rinfo.port} - ${msg}`);
});

server.bind(PORT, HOST);

// Client

const client = dgram.createSocket('udp4');
const msg = Buffer.from('Pluralsight rocks');

client.send(msg, 0, msg.length, PORT, HOST, (err) => {
  if (err) throw err;

  console.log('UDP message sent');
  client.close();
});

Summary

Enjoy the demo!