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等协议。
TCP是面向连接的协议,其显著特征为3次握手后才形成会话。
注意:只有在会话形成之后,服务器端和客户端之间才能互相发送数据,在创建会话的过程中,服务器端和客户端分别提供一个套接字,这两个套接字共同形成了一个链接,服务器端与客户端则通过套接字实现两者之间连接的操作。
具体流程:请移步到
https://blog.csdn.net/Errrl/article/details/103662867
2. 创建TCP服务器
在基本了解TCP工作原理后,接下来就可以开始创建TCP服务器端来接受请求:
server.js
/* 引入net核心模块 */
var net = require('net');
/* 创建一个TCP服务器 */
var server = net.createServer(function(socket){
socket.on('data',function(data){
socket.write('hello');
});
socket.on('end',function(){
socket.write('end');
});
socket.write('welcome to node tcp');
});
/* 监听端口号 */
server.listen(8000,function(){
console.log('server is done');
})
利用win10自带的telnet客户端对上述的见到服务器进行会话
通过net模块构造客户端进行会话,测试上述构建的TCP服务器:
client.js
var net = require('net');
var client = net.connect({port:8000},function(){
console.log('client is connect');
client.write('world!\r\n');
});
client.on('data',function(data){
console.log(data.toString());
client.end();
})
client.on('end',function(){
console.log('client is disconnect');
})
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套接字:
var dgram = require('dgram');
var socket = dgram.createSocket ('udp4')
3. 创建 UDP 服务器端
如果要想UDP套接字接受网路消息,只要调用dgram.bind(port,[address])进行绑定即可。
server.js
var dgram = require('dgram');
var server = dgram.createSocket ('udp4');
server.on('message',function(msg,rinfo){
console.log("server got:"+msg+"from"+rinfo.address+":"+rinfo.port);
})
server.on('listening',function(){
var address = server.address();
console.log("server listening"+address.address+":"+address.port);
})
server.bind(41234)
该套接字将接收所有网卡上41234端口信息上的消息。在绑定完成后,将会触发listening事件。
4. 创建 UDP 客户端
创建一个客户端与服务器端进行对话:
client.js
var dgram = require('dgram');
var message = new Buffer('hello node udp');
var client = dgram .createSocket('udp4');
client.send(message,0,message.length,41234,"localhost",function(err,bytes){
client.close();
})
当套接字对象用在客户端时,可以调用send()方法发送消息到网络中。send()方法的参数如下:
socket.send(buf,offset,length,port,address,[callback])
- 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服务器:
var http = require('http');
/* 创建服务器 */
http.createServer(function(req,res){
res.writeHead(200,{'content-type':'text/plain'});
res.end('hello world');
}).listen(8000,'127.0.0.1');
console.log('server run at http://127.0.0.1:8000/');
1. HTTP
(1)HTTP 报文
在启动上述代码后,我们对经典的示例代码进行了一次报文的获取,这里使用的工具是curl,通过 -v选项,可以显示这次网络通讯所有的报文信息。
请求报文的四部分:
- TCP三次握手。
- 请求报文。(请求头、请求体)
- 响应报文(包括响应头、响应体)。
- 会话结束信息。
从上述可以看出HTTP的特点,它是基于请求响应式的,基于TCP协议。
2. http 模块、HTTP服务器端
Node的http模块包含对HTTP处理的封装,在Node中,HTTP服务继承自TCP服务器(net模块),它能够与多个客户端保持连接,由于采用事件驱动的形式,并不为每一个连接创建额外的线程或进程,并保持很低的内存占有率,所以能实现高校并发。
(1)HTTP 请求
请求头
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.55.1
> Accept: */*
>
请求头第一行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 响应
响应头
< HTTP/1.1 200 OK
< content-type: text/plain
在项目中经常要写入响应头,用于获取符合类型的数据。
除此之外,http模块会自动设置一些头信息:(用于处理缓存)
< Date: Fri, 07 Feb 2020 14:30:32 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
响应体
调用respone.write()或者调用respone.end()传入的内容称为响应体:
hello world
调用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客户端。
var http = require('http');
/* 请求报文 */
var options = {
hostname:'127.0.0.1',
port:8000,
path:'/',
method:'GET'
}
/* 发送报文 */
var req = http.request(options,function(res){
/* 获取状态码 */
console.log('status:'+res.statusCode);
/* 获取响应头 */
console.log('headers:'+res.headers);
res.setEncoding('utf8');
/* 获取响应体 */
res.on('data',function(datas){
console.log(datas);
})
})
/* 发送后断开连接,缓解服务器压力 */
req.end();
修改:
/* 获取响应头 */
console.log('headers:'+JSON.stringify(res.headers));
其中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个连接。实际上它就是一个连接池(循环代理)
那么如何进行代理,很简单:
重构options
/* 设置代理 */
var agent = new http.Agent({
maxSochets: 10,
keepAlive: true,
})
var options = {
hostname: '127.0.0.1',
port: 8000,
path: '/',
method: 'GET',
agent: agent
}
/* 发送报文 */
var req = http.request(options, function (res) {
/* 获取状态码 */
console.log('status:' + res.statusCode);
/* 获取响应头 */
console.log('headers:' + JSON.stringify(res.headers));
res.setEncoding('utf8');
/* 获取响应体 */
res.on('data', function (datas) {
console.log(datas);
})
})
/* 发送后断开连接,缓解服务器压力 */
req.end();
相关链接:
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)
<input id="content" type="text">
<button id="send">send</button>
以一个webSocket聊天室进行实例操作:
/* client.html */
var websocket = new WebSocket("ws://localhost:8000/");
websocket.onopen = function() {
console.log("webSocket open");
// 发送消息放在这里
document.getElementById("send").onclick = function() {
var txt = document.getElementById("content").value;
if (txt) {
/* 发送数据 */
websocket.send(txt);
}
}
}
websocket.onclose = function() {
console.log("websocket close");
}
/* 接收响应数据 */
websocket.onmessage = function(e) {
console.log(e.data);
var mes = JSON.parse(e.data);
showMessage(mes.data, mes.type);
}
/* server.js */
var ws = require("nodejs-websocket");
/* 端口号 */
const PORT = 8000;
// 每进来一个客户端就记录一下
var clientCount = 0;
var server = ws.createServer(function (conn) {
console.log("New connection")
clientCount++;
conn.nickname = 'user' + clientCount;
let mes = {};
mes.type = "enter";
mes.data = conn.nickname + ' comes in'
broadcast(JSON.stringify(mes));
/* 收到 text 文本触发 */
conn.on("text", function (str) {
console.log("Received " + str);
let mes = {};
mes.type = "message";
mes.data = conn.nickname + ' says: ' + str;
broadcast(JSON.stringify(mes));
})
/* 当任一侧关闭连接时发出 */
conn.on("close", function (code, reason) {
console.log("Connection closed");
let mes = {};
mes.type = "leave";
mes.data = conn.nickname + ' left'
broadcast(JSON.stringify(mes));
})
/* 发生错误时发出(例如尝试在仍然发送二进制数据的同时发送文本数据)。如果握手无效,也会发出响应。 */
conn.on("error", function (err) {
console.log("handle err");
console.log(err);
})
}).listen(PORT);//监听端口号
console.log("websocket server running on port: " + PORT);
/* 响应数据 */
function broadcast(str) {
server.connections.forEach(function (connection) {
connection.sendText(str);
})
}
上述代码中,浏览器与服务器端创建webSocket协议请求,onopen在请求完成后持续执行,通过事件绑定的方法绑定一个发送按钮发送数据,同时还可以通过onmessage()方法接收服务器端相应的数据的数据。这种行为与TCP客户端很相似,相较于HTTP,它能够双向通信。
并且相比于HTTP,webSocket更接近于传输层协议,它并没有在HTTP的基础上模拟服务器端的推送,而是在TCP上定义独立的协议,但是疑惑的是webSocket的握手部分由HTTP完成,这就是人们感觉webSocket是基于HTTP实现的原因。
webSocket协议主要分为两个部分:握手和数据传输。
2. webSocket 握手
客户端建立连接时,通过HTTP发起的请求报文:
上面的报文告知客户端正在更换协议(协议升级),更新应用层协议为webSocket协议,并在当前的套接字连接上应用新的协议。剩余的字段分别表示服务器端基于Sec-WebSocket-Key生成的字符串和选中的子协议。客户端将会校验Sec-WebSocket-Key的值,如果成功,将开始接下来的数据传输。
简而言之就是websocket复用了http的握手通道,客户端通过http请求与服务端进行协商,升级协议。协议升级完后校验Sec-WebSocket-Key的值,若成功后面的数据交换则遵照websocket协议,若否反之。
流程:
1、客户端申请协议升级
Request URL: ws://localhost:8888/
Request Method: GET
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: uR5YP/BMO6M24tAFcmHeXw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
-
Connection: Upgrade 表示要升级协议
-
Upgrade: websocket 表示升级到websocket协议
-
Sec-WebSocket-Version: 13 表示websocket的版本
-
Sec-WebSocket-Key 表示websocket的验证,防止恶意的连接,与服务端响应的Sec-WebSocket-Accept是配套。
2、服务端响应协议升级
Status Code: 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: eS92kXpBNI6fWsCkj6WxH6QeoHs=
Upgrade: websocket
- Status Code:101 表示状态码,协议切换。
- Sec-WebSocket-Accept 表示服务端响应的校验,与客户端的Sec-WebSocket-Key是配套的。
3、Sec-WebSocket-Accept是如何计算的
将 Sec-WebSocket-Key 的值与 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。
然后通过sha1计算,再转成base64。
const crypto = require('crypto');
function getSecWebSocketAccept(key) {
return crypto.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64');
}
console.log(getSecWebSocketAccept('uR5YP/BMO6M24tAFcmHeXw=='));
4、协议升级完后,后续的数据传输就需要按websocket协议来走。(了解即可)
websocket客户端与服务端通信的最小单位是 帧,由1个或多个帧组成完整的消息。
客户端:将消息切割成多个帧,发送给服务端。
服务端:接收到消息帧,将帧重新组装成完整的消息。
数据帧的格式
单位是1个比特位,FIN,PSV1,PSV2,PSV3 占1个比特位,opcode占4个比特位。
0 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
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------+-------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+
| Payload Data continued ... |
+---------------------------------------------------------------+
6、掩码的算法
Masking-key掩码键是由客户端生成的32位随机数,掩码操作不会影响数据载荷的长度。
function unmask(buffer, mask) {
const length = buffer.length;
for (var i = 0; i < length; i++) {
buffer[i] ^= mask[i & 3];
}
}
7、实现websocket的握手,数据传输
JavaScript:(up.js)
const crypto = require('crypto');
const net = require('net');
//计算websocket校验
function getSecWebSocketAccept(key) {
return crypto.createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
.digest('base64');
}
//掩码操作
function unmask(buffer, mask) {
const length = buffer.length;
for (var i = 0; i < length; i++) {
buffer[i] ^= mask[i & 3];
}
}
//创建一个tcp服务器
let server = net.createServer(function (socket) {
socket.once('data', function (data) {
data = data.toString();
//查看请求头中是否有升级websocket协议的头信息
if (data.match(/Upgrade: websocket/)) {
let rows = data.split('\r\n');
//去掉第一行的请求行
//去掉请求头的尾部两个空行
rows = rows.slice(1, -2);
let headers = {};
rows.forEach(function (value) {
let [k, v] = value.split(': ');
headers[k] = v;
});
//判断websocket的版本
if (headers['Sec-WebSocket-Version'] == 13) {
let secWebSocketKey = headers['Sec-WebSocket-Key'];
//计算websocket校验
let secWebSocketAccept = getSecWebSocketAccept(secWebSocketKey);
//服务端响应的内容
let res = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
`Sec-WebSocket-Accept: ${secWebSocketAccept}`,
'Connection: Upgrade',
'\r\n'
].join('\r\n');
//给客户端发送响应内容
socket.write(res);
//注意这里不要断开连接,继续监听'data'事件
socket.on('data', function (buffer) {
//注意buffer的最小单位是一个字节
//取第一个字节的第一位,判断是否是结束位
let fin = (buffer[0] & 0b10000000) === 0b10000000;
//取第一个字节的后四位,得到的一个是十进制数
let opcode = buffer[0] & 0b00001111;
//取第二个字节的第一位是否是1,判断是否掩码操作
let mask = buffer[1] & 0b100000000 === 0b100000000;
//载荷数据的长度
let payloadLength = buffer[1] & 0b01111111;
//掩码键,占4个字节
let maskingKey = buffer.slice(2, 6);
//载荷数据,就是客户端发送的实际数据
let payloadData = buffer.slice(6);
//对数据进行解码处理
unmask(payloadData, maskingKey);
//向客户端响应数据
let send = Buffer.alloc(2 + payloadData.length);
//0b10000000表示发送结束
send[0] = opcode | 0b10000000;
//载荷数据的长度
send[1] = payloadData.length;
payloadData.copy(send, 2);
socket.write(send);
});
}
}
});
socket.on('error', function (err) {
console.log(err);
});
socket.on('end', function () {
console.log('连接结束');
});
socket.on('close', function () {
console.log('连接关闭');
});
});
//监听8000端口
server.listen(8000);
html:(up.html)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
var ws = new WebSocket('ws://localhost:8888');
ws.onopen = function () {
console.log('连接成功');
ws.send('你好服务端');
};
ws.onmessage = function (ev) {
console.log('接收数据', ev.data);
};
ws.onclose = function () {
console.log('连接断开');
};
</script>
</body>
</html>
8、结束