前端点滴(Node.js)(四)网络编程 —- 侧重(上)

释放双眼,带上耳机,听听看~!

Node 网络编程

前言

利用Node可以十分方便地搭建网络服务器,在WEB领域,大多数编程语言需要专门的web服务器作为容器,比如ASP,ASP.NET需要IIS作为服务器,PHP需要搭载在Apache或者Nignx环境等,JSP需要Tomcat服务器等。当对于Node而言,只需要几行代码就可以构建一个服务器,无需额外的容器。
Node提供了net、http、https、dgram这四个模块,分别用于处理TCP、HTTP、HTTPS、UDP,适用于服务器与客户端。

一、构建 TCP 服务器

1. 七层模型与TCP协议

TCP全名传输控制协议,在OSI模型中有以下七层,被称为七层网络协议。许多应用层议都是基于TCP构建,典型的有HTTP、SMTP、IMAP等协议。
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
TCP是面向连接的协议,其显著特征为3次握手后才形成会话。
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
注意:只有在会话形成之后,服务器端和客户端之间才能互相发送数据,在创建会话的过程中,服务器端和客户端分别提供一个套接字,这两个套接字共同形成了一个链接,服务器端与客户端则通过套接字实现两者之间连接的操作。
具体流程:请移步到
https://blog.csdn.net/Errrl/article/details/103662867

2. 创建TCP服务器

在基本了解TCP工作原理后,接下来就可以开始创建TCP服务器端来接受请求:

server.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1/* 引入net核心模块 */
2var net = require('net');
3/* 创建一个TCP服务器 */
4var server = net.createServer(function(socket){
5   socket.on('data',function(data){
6       socket.write('hello');
7   });
8   socket.on('end',function(){
9       socket.write('end');
10  });
11  socket.write('welcome to node tcp');
12});
13/* 监听端口号 */
14server.listen(8000,function(){
15  console.log('server is done');
16})
17
18

利用win10自带的telnet客户端对上述的见到服务器进行会话
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
通过net模块构造客户端进行会话,测试上述构建的TCP服务器:

client.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1var net = require('net');
2
3var client = net.connect({port:8000},function(){
4    console.log('client is connect');
5    client.write('world!\r\n');
6});
7
8client.on('data',function(data){
9    console.log(data.toString());
10    client.end();
11})
12
13client.on('end',function(){
14    console.log('client is disconnect');
15})
16
17

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)

3. TCP服务器事件

(1)服务器事件

对于通过net.createServer()创建的服务器而言,他是一个EventEmitter实例,他自定义事件有以下几种:

  • listening:在调用server.listen()绑定端口,简介写法为server.listen(port,listeningListener),通过listen()方法的的第二个参数传入。
  • connection:每个客户端套接字连接到服务端时触发,简介写法为通过net.createServer(),最后一个参数传入。
  • close:当服务器关闭时触发,在调用server.close()后,服务器将停止接受新的套接字连接,但保持当前的连接,对待所有连接都断开后会触发该事件。
  • error:当服务器发生异常时,将会触发该事件。比如监听一个使用中的端口,将会触发一个异常,如果不侦听error事件,服务器将会抛出异常。

(2)连接事件

服务器可以同时与多个客户端保持连接,对于每个连接而言是典型的可写可读Stream对象。Stream对象可以用于服务器与客户端之间的通信,既可以通过data事件从一端读取另一端发来的数据,也可以通过write()方法从一端向另一端发送数据,它具有如下自定义事件:

  • data:当一端调用write()发送数据时,另一端触发data事件,事件传递的数据即是write()发送的数据。
  • end:当连接中的任意一端发送FIN数据时,将会触发该事件。
  • connect:改时间用于客户端,当套接字与服务器端连接成功时被触发。
  • drain:当任意一端调用write()发送数据时,当前这端会触发该事件。
  • error:当发生异常时触发该事件。
  • close:当套接字完全关闭时触发事件。
  • timout:当一定时间后连续不在活跃时,该事件会被触发,通知用户当前该连接已经被闲置了。

二、构建 UDP服务器

1. 与 TCP 协议的区别

请移步到:
https://blog.csdn.net/Errrl/article/details/103662867

2. 创建 UDP 套接字

创建UDP套接字十分简单,UDP套接字一旦创建,既可以作为客户端发送数据,也可以作为服务器端接收数据,创建一个UDP套接字:


1
2
3
4
1var dgram = require('dgram');
2var socket = dgram.createSocket ('udp4')
3
4

3. 创建 UDP 服务器端

如果要想UDP套接字接受网路消息,只要调用dgram.bind(port,[address])进行绑定即可。

server.js


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1var dgram = require('dgram');
2var server = dgram.createSocket ('udp4');
3
4server.on('message',function(msg,rinfo){
5   console.log("server got:"+msg+"from"+rinfo.address+":"+rinfo.port);
6})
7
8server.on('listening',function(){
9   var address = server.address();
10  console.log("server listening"+address.address+":"+address.port);
11})
12
13server.bind(41234)
14
15

该套接字将接收所有网卡上41234端口信息上的消息。在绑定完成后,将会触发listening事件。

4. 创建 UDP 客户端

创建一个客户端与服务器端进行对话:

client.js


1
2
3
4
5
6
7
8
9
1var dgram = require('dgram');
2
3var message = new Buffer('hello node udp');
4var client = dgram .createSocket('udp4');
5client.send(message,0,message.length,41234,"localhost",function(err,bytes){
6   client.close();
7})
8
9

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
当套接字对象用在客户端时,可以调用send()方法发送消息到网络中。send()方法的参数如下:


1
2
3
1socket.send(buf,offset,length,port,address,[callback])
2
3
  • buf:buffer
  • offset:buffer偏移量
  • length:buffer的长度
  • port:目标端口
  • address:目标地址
  • callback:发送完成后的回调

5. UDP 套接字事件

UDP套接字相对于TCP套接字使用起来更加简单,它只是一个EventEmitter的实例,它具有如下自定义事件:

  • message:当UDP套接字侦听网卡端口后,接收到消息触发该事件,出发携带的数据为消息buffer对象和一个远程地址信息。
  • listening:当UDP套接字开始侦听时触发该事件。
  • close:调用close()方法时触发该事件,并不再触发message事件。如需再次触发message事件,重新绑定即可。
  • error:当异常发生时出发该事件,如果不侦听,异常将直接抛出,使进程退出。

三、构建 HTTP 服务器端、客户端

在Node中构建HTTP服务极其容易,Node官网上的经典例子就展示了如何用几行代码实现一个HTTP服务器:


1
2
3
4
5
6
7
8
9
1var http = require('http');
2/* 创建服务器 */
3http.createServer(function(req,res){
4   res.writeHead(200,{'content-type':'text/plain'});
5   res.end('hello world');
6}).listen(8000,'127.0.0.1');
7console.log('server run at http://127.0.0.1:8000/');
8
9

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)

1. HTTP

(1)HTTP 报文

在启动上述代码后,我们对经典的示例代码进行了一次报文的获取,这里使用的工具是curl,通过 -v选项,可以显示这次网络通讯所有的报文信息。
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
请求报文的四部分:

  • TCP三次握手。
  • 请求报文。(请求头、请求体)
  • 响应报文(包括响应头、响应体)。
  • 会话结束信息。

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
从上述可以看出HTTP的特点,它是基于请求响应式的,基于TCP协议。

2. http 模块、HTTP服务器端

Node的http模块包含对HTTP处理的封装,在Node中,HTTP服务继承自TCP服务器(net模块),它能够与多个客户端保持连接,由于采用事件驱动的形式,并不为每一个连接创建额外的线程或进程,并保持很低的内存占有率,所以能实现高校并发。

(1)HTTP 请求

请求头


1
2
3
4
5
6
7
1> GET / HTTP/1.1
2> Host: 127.0.0.1:8000
3> User-Agent: curl/7.55.1
4> Accept: */*
5>
6
7

请求头第一行GET / HTTP/1.1解析之后会分解成如下属性:

  • request.method:值是GET,一种请求方法,常用的请求方法还有:POST、DELETE、PUT、CONNECT等请求方法。
  • request.url:值为/,这就可以解释为什么在项目中查询request.url会返回/,原因就是取决于报文。
  • request.httpVersion:值为1.1,表示版本(规则)。

其余的包头就以简单、规律的key:value的格式,被解析后放置在request.headers属性上传递给业务逻辑以供调用。

(2)HTTP 响应

响应头


1
2
3
4
1< HTTP/1.1 200 OK
2< content-type: text/plain
3
4

在项目中经常要写入响应头,用于获取符合类型的数据。
除此之外,http模块会自动设置一些头信息:(用于处理缓存


1
2
3
4
5
6
1< Date: Fri, 07 Feb 2020 14:30:32 GMT
2< Connection: keep-alive            
3< Transfer-Encoding: chunked        
4<                                    
5
6

响应体
调用respone.write()或者调用respone.end()传入的内容称为响应体:


1
2
3
1hello world
2
3

调用respone.write()或者调用respone.end()的区别在于:
前者只发送,不结束响应,会造成客户端处于等待状态。
而后者就会先调用write()发送完后调用end()结束响应。

(3)HTTP 服务的事件

同TCP服务一样,HTTP服务器也抽象了一些事件,以供应用层使用,同样典型的是,服务器也是一个EventEmitter实例:

  • connection 事件:在开始HTTP请求和响应之前,客户端与服务器需要建立底层的TCP连接,这个连接可能因为开启了keep-alive的原因,可以在多次请求与响应之间使用;当这个连接建立时,服务器会触发一次connection事件。
  • request 事件:建立TCP连接后,HTTP模块底层将在数据流中抽出HTTP请求和HTTP响应,当请求数据发送到服务器端,在解析出HTTP请求头后,将会触发该事件;在res.end()后,TCP连接可能将用于下一次请求响应。
  • close 事件:与TCP服务器行为一致,调用server.close()方法停止接受新的连接,当已有的连接都断开时,触发该事件;可以给server.close()传递一个回调函数来快速注册该事件。
  • checkContinue 事件:某些客户端在发送较大的数据时,并不会之间将数据发送,而是先发送一个头部带有Expect:100-continue的请求到服务器,服务器将会触发checkContinue事件;如果没有为服务器监听这个事件,服务器将会自动响应客户端100 Continue的状态码,表示可以接受数据上传;如果不接受或者数据确实超出承载时响应客户端400 Bad Request拒绝客户端继续发送数据即可。需要注意的是:当该事件发生时不会触发request事件,两个事件互斥。当客户端收到100 Continue后重新发送请求时才会触发request事件。

与预检(Preflighted)的跨域请求类似

  • connect 事件:当客户端发起CONNECT请求时触发,二发起CONNECT请求通常在HTTP代理时出现;如果不监听该事件,发起该事件的连接就会中断。
  • upgrade 事件:当客户端要求升级连接的协议时,需要和服务器端协商,客户端会在请求头中带上Upgrade字段,服务器端会在接收到这样的请求时触发该事件。者在后面的WebSoket中会有详细的流程介绍。如果不监听该事件,发起该请求的连接就会中断。
  • clientError 事件:连接的客户端触发error事件时,这个错误会传递到服务器端,此时触发该事件。

扩展

keep-alive
在http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。

使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()的调用)

3. HTTP 客户端

http模块提供了一个底层的API:http.request(options,connect),用于构建HTTP客户端。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1var http = require('http');
2/* 请求报文 */
3var options = {
4   hostname:'127.0.0.1',
5   port:8000,
6   path:'/',
7   method:'GET'
8}
9/* 发送报文 */
10var req = http.request(options,function(res){
11  /* 获取状态码 */
12  console.log('status:'+res.statusCode);
13  /* 获取响应头 */
14  console.log('headers:'+res.headers);
15  res.setEncoding('utf8');
16  /* 获取响应体 */
17  res.on('data',function(datas){
18      console.log(datas);
19  })
20})
21/* 发送后断开连接,缓解服务器压力 */
22req.end();
23
24

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
修改:


1
2
3
4
1/* 获取响应头 */
2   console.log('headers:'+JSON.stringify(res.headers));
3
4

前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
其中options的参数配置:

  • host:服务器的域名或者ip地址,默认为localhost。
  • hostname:服务器名称。
  • port:端口号。默认80。
  • method:HTTP请求方法,默认GET。
  • path:请求路径,默认/
  • headers:请求头对象。
  • auth:Basic认证,这个值将会被计算成请求头中的Authorization部分。

报文体的内容由请求对象的write()和end()方式实现:通过write()方法向连接中写入数据,通过end()方法告知报文结束。他与前端中的Ajax调用非常相似,
Ajax的实质就是一个异步的网络HTTP请求。

(1)HTTP 响应

HTTP客户端的响应对象与服务器端类似,在客户端请求对象中,它的事件名叫做response。客户端请求后(也就是解析报文完成后)响应头就会触发response事件,同时传递一个响应对象以供客户端进行响应操作。对于上述代码而言,res就是response,datas就是响应对象。

(2)HTTP 代理

如服务器端的实现一般http模块提供的客户端请求对象也是基于TCP层实现的,在keep-alive机制下,一个底层会话连接可以多次用于请求。为了重复使用TCP连接,http模块包含一个默认的客户端代理对象http.globalAgent。它对每一个服务端(host+port)创建连接进行了管理,默认情况下,通过客户端请求对象对同一服务器端发起的HTTP请求最多可以创建5个连接。实际上它就是一个连接池(循环代理)
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
那么如何进行代理,很简单:
重构options


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1/* 设置代理 */
2var agent = new http.Agent({
3    maxSochets: 10,
4    keepAlive: true,
5})
6var options = {
7    hostname: '127.0.0.1',
8    port: 8000,
9    path: '/',
10    method: 'GET',
11    agent: agent
12}
13/* 发送报文 */
14var req = http.request(options, function (res) {
15    /* 获取状态码 */
16    console.log('status:' + res.statusCode);
17    /* 获取响应头 */
18    console.log('headers:' + JSON.stringify(res.headers));
19    res.setEncoding('utf8');
20    /* 获取响应体 */
21    res.on('data', function (datas) {
22        console.log(datas);
23    })
24})
25
26/* 发送后断开连接,缓解服务器压力 */
27req.end();
28
29

相关链接:
http://nodejs.cn/api/http.html\#http_class_http_agent

(3)HTTP 客户端事件

  • response 事件:与服务器端的request事件对应的客户端在请求发送后得到服务器的响应时会触发该事件。
  • socket 事件:在底层连接池中建立的连接分配给当前请求对象时,触发该事件。
  • connect 事件:当客户端向服务器端发送CONNECT请求时,如果服务器响应了200状态码,客户端将会触发该事件。
  • upgrade 事件:客户端向服务器端发起Upgrade请求时,如果服务器端响应了101 Switching Protocols状态,客户端将会触发该事件。
  • continue 事件:客户端向服务器端发起Expect:100-continue头信息,以试图发送叫大数据量,如果服务器响应了100 Continue状态,客户端将触发该事件。

四、构建 webSocket 服务端

1. 客户端下的 webSocket

HTML:(client.html)


1
2
3
4
1<input id="content" type="text">
2<button id="send">send</button>
3
4

以一个webSocket聊天室进行实例操作:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1/* client.html */
2var websocket = new WebSocket("ws://localhost:8000/");
3        websocket.onopen = function() {
4            console.log("webSocket open");
5            // 发送消息放在这里
6            document.getElementById("send").onclick = function() {
7                var txt = document.getElementById("content").value;
8                if (txt) {
9                   /* 发送数据 */
10                    websocket.send(txt);
11                }
12            }
13        }
14        websocket.onclose = function() {
15            console.log("websocket close");
16        }
17        /* 接收响应数据 */
18        websocket.onmessage = function(e) {
19            console.log(e.data);
20            var mes = JSON.parse(e.data);
21            showMessage(mes.data, mes.type);
22        }
23
24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
1/* server.js */
2var ws = require("nodejs-websocket");
3/* 端口号 */
4const PORT = 8000;
5// 每进来一个客户端就记录一下
6var clientCount = 0;
7
8var server = ws.createServer(function (conn) {
9    console.log("New connection")
10    clientCount++;
11    conn.nickname = 'user' + clientCount;
12    let mes = {};
13    mes.type = "enter";
14    mes.data = conn.nickname + ' comes in'
15    broadcast(JSON.stringify(mes));
16    /* 收到 text 文本触发 */
17    conn.on("text", function (str) {
18        console.log("Received " + str);
19        let mes = {};
20        mes.type = "message";
21        mes.data = conn.nickname + ' says: ' + str;
22        broadcast(JSON.stringify(mes));
23    })
24    /* 当任一侧关闭连接时发出 */
25    conn.on("close", function (code, reason) {
26        console.log("Connection closed");
27        let mes = {};
28        mes.type = "leave";
29        mes.data = conn.nickname + ' left'
30        broadcast(JSON.stringify(mes));
31    })
32    /* 发生错误时发出(例如尝试在仍然发送二进制数据的同时发送文本数据)。如果握手无效,也会发出响应。 */
33    conn.on("error", function (err) {
34        console.log("handle err");
35        console.log(err);
36    })
37}).listen(PORT);//监听端口号
38
39console.log("websocket server running on port: " + PORT);
40/* 响应数据 */
41function broadcast(str) {
42    server.connections.forEach(function (connection) {
43        connection.sendText(str);
44    })
45}
46
47

上述代码中,浏览器与服务器端创建webSocket协议请求,onopen在请求完成后持续执行,通过事件绑定的方法绑定一个发送按钮发送数据,同时还可以通过onmessage()方法接收服务器端相应的数据的数据。这种行为与TCP客户端很相似,相较于HTTP,它能够双向通信。
并且相比于HTTP,webSocket更接近于传输层协议,它并没有在HTTP的基础上模拟服务器端的推送,而是在TCP上定义独立的协议,但是疑惑的是webSocket的握手部分由HTTP完成,这就是人们感觉webSocket是基于HTTP实现的原因。

webSocket协议主要分为两个部分:握手和数据传输。

2. webSocket 握手

客户端建立连接时,通过HTTP发起的请求报文:
前端点滴(Node.js)(四)网络编程 ---- 侧重(上)
上面的报文告知客户端正在更换协议(协议升级),更新应用层协议为webSocket协议,并在当前的套接字连接上应用新的协议。剩余的字段分别表示服务器端基于Sec-WebSocket-Key生成的字符串和选中的子协议。客户端将会校验Sec-WebSocket-Key的值,如果成功,将开始接下来的数据传输。
简而言之就是websocket复用了http的握手通道,客户端通过http请求与服务端进行协商,升级协议。协议升级完后校验Sec-WebSocket-Key的值,若成功后面的数据交换则遵照websocket协议,若否反之。

流程:
1、客户端申请协议升级


1
2
3
4
5
6
7
8
9
1Request URL: ws://localhost:8888/
2Request Method: GET
3Connection: Upgrade
4Upgrade: websocket
5Sec-WebSocket-Version: 13
6Sec-WebSocket-Key: uR5YP/BMO6M24tAFcmHeXw==
7Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
8
9
  • Connection: Upgrade 表示要升级协议

  • Upgrade: websocket 表示升级到websocket协议

  • Sec-WebSocket-Version: 13 表示websocket的版本

  • Sec-WebSocket-Key 表示websocket的验证,防止恶意的连接,与服务端响应的Sec-WebSocket-Accept是配套。

2、服务端响应协议升级


1
2
3
4
5
6
1Status Code: 101 Switching Protocols
2Connection: Upgrade
3Sec-WebSocket-Accept: eS92kXpBNI6fWsCkj6WxH6QeoHs=
4Upgrade: websocket
5
6
  • Status Code:101 表示状态码,协议切换。
  • Sec-WebSocket-Accept 表示服务端响应的校验,与客户端的Sec-WebSocket-Key是配套的。

3、Sec-WebSocket-Accept是如何计算的

将 Sec-WebSocket-Key 的值与 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

然后通过sha1计算,再转成base64。


1
2
3
4
5
6
7
8
9
10
11
1const crypto = require('crypto');
2
3function getSecWebSocketAccept(key) {
4    return crypto.createHash('sha1')
5        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
6        .digest('base64');
7}
8
9console.log(getSecWebSocketAccept('uR5YP/BMO6M24tAFcmHeXw=='));
10
11

4、协议升级完后,后续的数据传输就需要按websocket协议来走。(了解即可)

websocket客户端与服务端通信的最小单位是 帧,由1个或多个帧组成完整的消息。

客户端:将消息切割成多个帧,发送给服务端。

服务端:接收到消息帧,将帧重新组装成完整的消息。

数据帧的格式

单位是1个比特位,FIN,PSV1,PSV2,PSV3 占1个比特位,opcode占4个比特位。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
10 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
2+-+-+-+-+-------+-+-------------+-------------------------------+
3|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
4|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
5|N|V|V|V|       |S|             |   (if payload len==126/127)   |
6| |1|2|3|       |K|             |                               |
7+-+-+-+-+-------+-+-------------+-------------------------------+
8|     Extended payload length continued, if payload len == 127  |
9+-------------------------------+-------------------------------+
10|                               |Masking-key, if MASK set to 1  |
11+-------------------------------+-------------------------------+
12| Masking-key (continued)       |          Payload Data         |
13+-------------------------------+-------------------------------+
14|                     Payload Data continued ...                |
15+---------------------------------------------------------------+
16|                     Payload Data continued ...                |
17+---------------------------------------------------------------+
18
19

6、掩码的算法

Masking-key掩码键是由客户端生成的32位随机数,掩码操作不会影响数据载荷的长度。


1
2
3
4
5
6
7
8
1function unmask(buffer, mask) {
2    const length = buffer.length;
3    for (var i = 0; i < length; i++) {
4        buffer[i] ^= mask[i & 3];
5    }
6}
7
8

7、实现websocket的握手,数据传输
JavaScript:(up.js)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
1const crypto = require('crypto');
2const net = require('net');
3
4//计算websocket校验
5function getSecWebSocketAccept(key) {
6    return crypto.createHash('sha1')
7        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
8        .digest('base64');
9}
10
11//掩码操作
12function unmask(buffer, mask) {
13    const length = buffer.length;
14    for (var i = 0; i < length; i++) {
15        buffer[i] ^= mask[i & 3];
16    }
17}
18
19//创建一个tcp服务器
20let server = net.createServer(function (socket) {
21
22    socket.once('data', function (data) {
23        data = data.toString();
24
25        //查看请求头中是否有升级websocket协议的头信息
26        if (data.match(/Upgrade: websocket/)) {
27            let rows = data.split('\r\n');
28            //去掉第一行的请求行
29            //去掉请求头的尾部两个空行
30            rows = rows.slice(1, -2);
31            let headers = {};
32            rows.forEach(function (value) {
33                let [k, v] = value.split(': ');
34                headers[k] = v;
35            });
36            //判断websocket的版本
37            if (headers['Sec-WebSocket-Version'] == 13) {
38                let secWebSocketKey = headers['Sec-WebSocket-Key'];
39                //计算websocket校验
40                let secWebSocketAccept = getSecWebSocketAccept(secWebSocketKey);
41                //服务端响应的内容
42                let res = [
43                    'HTTP/1.1 101 Switching Protocols',
44                    'Upgrade: websocket',
45                    `Sec-WebSocket-Accept: ${secWebSocketAccept}`,
46                    'Connection: Upgrade',
47                    '\r\n'
48                ].join('\r\n');
49                //给客户端发送响应内容
50                socket.write(res);
51
52                //注意这里不要断开连接,继续监听'data'事件
53                socket.on('data', function (buffer) {
54                    //注意buffer的最小单位是一个字节
55                    //取第一个字节的第一位,判断是否是结束位
56                    let fin = (buffer[0] & 0b10000000) === 0b10000000;
57                    //取第一个字节的后四位,得到的一个是十进制数
58                    let opcode = buffer[0] & 0b00001111;
59                    //取第二个字节的第一位是否是1,判断是否掩码操作
60                    let mask = buffer[1] & 0b100000000 === 0b100000000;
61                    //载荷数据的长度
62                    let payloadLength = buffer[1] & 0b01111111;
63                    //掩码键,占4个字节
64                    let maskingKey = buffer.slice(2, 6);
65                    //载荷数据,就是客户端发送的实际数据
66                    let payloadData = buffer.slice(6);
67
68                    //对数据进行解码处理
69                    unmask(payloadData, maskingKey);
70
71                    //向客户端响应数据
72                    let send = Buffer.alloc(2 + payloadData.length);
73                    //0b10000000表示发送结束
74                    send[0] = opcode | 0b10000000;
75                    //载荷数据的长度
76                    send[1] = payloadData.length;
77                    payloadData.copy(send, 2);
78                    socket.write(send);
79                });
80            }
81        }
82    });
83
84    socket.on('error', function (err) {
85        console.log(err);
86    });
87
88    socket.on('end', function () {
89        console.log('连接结束');
90    });
91
92    socket.on('close', function () {
93        console.log('连接关闭');
94    });
95});
96
97//监听8000端口
98server.listen(8000);
99
100

html:(up.html)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1<!doctype html>
2<html lang="zh-CN">
3<head>
4    <meta charset="UTF-8">
5    <title>Document</title>
6</head>
7<body>
8<script>
9    var ws = new WebSocket('ws://localhost:8888');
10    ws.onopen = function () {
11        console.log('连接成功');
12        ws.send('你好服务端');
13    };
14    ws.onmessage = function (ev) {
15        console.log('接收数据', ev.data);
16    };
17    ws.onclose = function () {
18        console.log('连接断开');
19    };
20</script>
21</body>
22</html>
23
24

8、结束

给TA打赏
共{{data.count}}人
人已打赏
安全技术

c# 实现MD5,SHA1,SHA256,SHA512等常用加密算法

2021-8-18 16:36:11

安全技术

C++ 高性能服务器网络框架设计细节

2022-1-11 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索