Implementation of websocket protocol based on node

  • 2021-02-17 06:16:10
  • OfStack

1. The agreement
WebSocket is a full-duplex protocol for client-server communication based on TCP. It is defined in HTML5 and is part of the foundation specification of the new webapp.

It breaks through the earlier AJAX restrictions, the key is real-time, the server can actively push content to the client! Possible applications include: multiplayer online games, instant messaging, real-time monitoring, remote desktops, news servers, and more.

For myself, what I want to try at the moment is what canvas+websocket can do in combination.

2. Implement
Since the handshake process is a standard HTTP request, there are two options for websocket implementation: 1) TCP implementation; 2) Implementation on existing HTTP software. The latter has the advantage of being able to share existing HTTP server ports without having to re-implement authentication and parsing of HTTP requests.

The HTTP module for node used in this example. (See the attachment for TCP version and all documents)

1. node server-side code:


var http = require('http');
var url = require('url');
// var mime = require('mime');
var crypto = require('crypto');

var port = 4400;
var server = http.createServer();
  server.listen(port,function() {
    console.log('server is running on localhost:',port);
    server
    .on('connection',function(s) {
      console.log('on connection ',s);
    })
    .on('request',onrequest)
    .on('upgrade',onupgrade);
  });

var onrequest = function(req,res) {
  console.log( Object.keys(req) ,req.url,req['upgrade']);
  if( !req.upgrade ){
    //  non upgrade Request choice: interrupt or provide a normal web page 
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.write( 'WebSocket server works!' );
    
  }
  res.end();
  return;
};

var onupgrade = function (req,sock,head) {
  // console.log(' methods :',Object.keys(sock));
  if(req.headers.upgrade !== 'WebSocket'){
    console.warn(' Illegal connection ');
    sock.end();
    return;
  }
  
  bind_sock_event(sock);

  try{
    handshake(req,sock,head);
  }catch(e){
    console.error(e);
    sock.end();
  }
};

//  Wrap the frame to be sent 
var wrap = function(data) {
  var fa = 0x00, fe = 0xff, data = data.toString()
    len = 2+Buffer.byteLength(data),
    buff = new Buffer(len);

  buff[0] = fa;
  buff.write(data,1);
  buff[len-1] = fe;
  return buff;
}
//  Untie the received frame 
var unwrap = function(data) {
  return data.slice(1,data.length-1);
}

var bind_sock_event = function(sock) {
  sock
  .on('data',function(buffer) {
    var data = unwrap(buffer);
    console.log('socket receive data : ',buffer,data,'\n>>> '+data);
    // send('hello html5,'+Date.now())
    sock.emit('send',data);
  })
  .on('close',function() {
    console.log('socket close');
  })
  .on('end',function() {
    console.log('socket end');
  })
  .on('send',function(data) { // Custom event 
    sock.write(wrap(data),'binary');
  })
};

var get_part = function(key) {
  var empty  = '',
    spaces = key.replace(/\S/g,empty).length,
    part  = key.replace(/\D/g,empty);
  if(!spaces) throw {message:'Wrong key: '+key,name:'HandshakeError'}
  return get_big_endian(part / spaces);
}

var get_big_endian = function(n) { 
  return String.fromCharCode.apply(null,[3,2,1,0].map(function(i) { return n >> 8*i & 0xff }))
}

var challenge = function(key1,key2,head) {
  var sum = get_part(key1) + get_part(key2) + head.toString('binary');
  return crypto.createHash('md5').update(sum).digest('binary');
}

var handshake = function(req,sock,head) {
  var output = [],h = req.headers, br = '\r\n';

  // header
  output.push(
    'HTTP/1.1 101 WebSocket Protocol Handshake','Upgrade: WebSocket','Connection: Upgrade',
    'Sec-WebSocket-Origin: ' + h.origin,
    'Sec-WebSocket-Location: ws://' + h.host + req.url,
    'Sec-WebSocket-Protocol: my-custom-chat-protocol'+br
  );
  // body
  var c = challenge(h['sec-websocket-key1'],h['sec-websocket-key2'],head);
  output.push(c);

  sock.write(output.join(br),'binary');
}

2. Browser client code:


<html>
<head>
  <title>WebSocket Demo</title>
</head>
<style type="text/css">
 textarea{width:400px;height:150px;display:block;overflow-y:scroll;}
 #output{width:600px;height:400px;background:whiteSmoke;padding:1em .5em;color:#000;border:none;}
 button{padding:.2em 1em;}
</style>
<link href="layout.css" rel="stylesheet" type="text/css" /> 
<body>

<textarea id="output" readonly="readonly"></textarea>
<br>
<textarea id="input"></textarea>
<button id="send">send</button>

<script type="text/javascript">
// localhost
var socket = new WebSocket('ws://192.168.144.131:4400/')
socket.onopen = function(e) {
  log(e.type);
  socket.send('hello node');
}
socket.onclose = function(e) {
  log(e.type);
}
socket.onmessage = function(e) {
  log('receive @ '+ new Date().toLocaleTimeString() +'\n'+e.data);
 output.scrollTop = output.scrollHeight
}
socket.onclose = function(e) {
  log(e.type);
}
socket.addEventListener('close',function() {
  log('a another close event handler..');
},false);

// dom
var id = function(id) {
  return document.getElementById(id);
}
var output = id('output'), input = id('input'), send = id('send');
var log = function(msg) {
  output.textContent += '> '+msg + '\n'
}
send.addEventListener('click',function() {
  socket.send(input.value);
},false);

</script>
</body>
</html>

3. The details
The implementation of the websocket protocol on top of the http protocol has only two steps: shaking hands and sending data.

1, shake hands
The process of shaking hands is called challenge-response. Firstly, the client initiates an HTTP GET request named Upgrade. The server validates this request and gives a 101 response to indicate acceptance of this protocol upgrade. The handshake is completed.

chrome inspector Beautified handshake information:

Request URL:ws://192.168.144.131:4400/pub/chat?q=me
Request Method:GET
Status Code:101 WebSocket Protocol Handshake

Request Headers
Connection:Upgrade
Host:192.168.144.131:4400
Origin:http://localhost:800
Sec-WebSocket-Key1:p2 G 947T 80 661 jAf2
Sec-WebSocket-Key2:z Z Q ^326 5 9= 7s1 1 7H4
Sec-WebSocket-Protocol::my-custom-chat-protocol
Upgrade:WebSocket
(Key3):7C:44:56:CA:1F:19:D2:0A

Response Headers
Connection:Upgrade
Sec-WebSocket-Location:ws://192.168.144.131:4400/pub/chat?q=me
Sec-WebSocket-Origin:http://localhost:800
Sec-WebSocket-Protocol:my-custom-chat-protocol
Upgrade:WebSocket
(Challenge Response):52:DF:2C:F4:50:C2:8E:98:14:B7:7D:09:CF:C8:33:40

Request header section

Host: websocket server host
Connection: Connection type
ES106en: Protocol upgrade type
Origin: Access the source
Sec-WebSocket-Protocol: Optional, subprotocol name defined by the application, multiple protocols separated by Spaces. (* The only other option left is cookie)
Sec-WebSocket-Key1: Security authentication key, xhr requests cannot be forged with a request header beginning with 'sec-'.
Sec WebSocket - Key2: same as above
Key3: Response body content,8 bytes random.
Response header section

Sec-WebSocket-Protocol: Must contain the requested sub-protocol name
Sec-WebSocket-Origin: Must be equal to the source of the request
Sec-WebSocket-Location: Must equal the requested address
Challenge: The body of the response, calculated from 3 key in the request, 16 bytes.
Pseudocode for the reply string calculation procedure:


part_1 = key1 All the numbers in the  / key1 Number of Medium Spaces 
part_2  Same as above 
sum = big_endian(part_1)+big_endian(part_2)+key3
challenge_response = md5_digest(sum);

big_endian calculation strategy for 32-bit integers:


#  Very similar to rgba Color calculation , The calculation can be seen in the following function 
var big_endian = function(n) {
  return [3,2,1,0].map(function(i) { return n >> 8*i & 0xff });
}
big_endian(0xcc77aaff);
// -> [204, 119, 170, 255]

2. Send data
WebSocket is designed to handle data with events, so clients can retrieve the complete data as soon as they are notified of the event, without having to handle the buffer manually.

In this case, each stroke of data is called a frame. In the specification definition, its header must start at 0x00, and its tail attribute must end at 0xff, so that every time the data is sent, there are at least two bytes.

Server implementation, when receiving data to cut off the tail; And send the data is to wrap the head and tail. The format is as follows:

The original binary representation of # 'hello', the request header and here are utf8 encoding
< Buffer e4 bd a0 e5 a5 bd >
# Packed binary representation.
< Buffer 00 e4 bd a0 e5 a5 bd ff >

The above is all the content of this article, I hope to help you learn.


Related articles: