Node-网络编程

2019-11-25  本文已影响0人  Upcccz

构建TCP服务

TCP是面向连接的协议,其显著的特征是在传输之前需要3次握手形成会话,只有会话形成之后,服务端和客户端之间才能互相发送数据,在创建会话的过程中,服务端和客户端分别提供一个套接字,这两个套接字共同形成一个连接,服务端与客户端则通过套接字实现两者之间连接的操作。

创建TCP服务器端

var net = require('net');
var server = net.createServer(function (socket) {
  // 新的连接
  socket.on('data', function (data) {
    socket.write("你好");
  });

  socket.on('end', function () {
    console.log('连接断开');
  });
  socket.write("欢迎光临深入浅出Node.js");
});

server.listen(8124, function () {
  console.log('server bound');
});

然后可以通过net模块自行构造客户端进行会话,测试上面构建的TCP服务的代码。

// client.js
var net = require('net');
var client = net.connect({port: 8124}, function () { //'connect' listener
  console.log('client connected');
  client.write('world!\r\n');
});

client.on('data', function (data) {
  console.log(data.toString());
  client.end(); // 主动关闭连接
});

client.on('end', function () {
  console.log('client disconnected');
});
$ node client.js
client connected
欢迎光临深入浅出Node.js

你好
client disconnected

TCP服务的事件

服务器事件

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

连接事件

服务器可以同时与多个客户端保持连接,对于每个连接而言是典型的可写可读Stream对象,就是上面代码示例中的socket,用于服务端和客户端之间的通信。它的事件有:

{{% notice info %}}
值得注意的是,TCP针对网络中的小数据包有一定的优化策略:Nagle算法。TCP/IP协议中,无论发送多少数据,总要在数据前面加上协议头,同时,对方接到数据,也需要发送ACK表示确认,为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据(一个连接会设置MSS参数,因此,TCP/IP希望每次能够以MSS尺寸的数据块来发送数据),Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。




如果每次只发送1字节的数据,会在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据,这种情况转变成了4000%的消耗,对于轻负载的网络还是可以接受的,但是重负载的就受不了了。Nagle算法通常会在未确认数据发送的时候让发送器把数据送到缓存里,任何数据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。

{{% /notice %}}

在Node中,TCP默认开启Nagle算法,可调用socket.setNoDelay(true)去掉Nagle算法,使得write()可以立即发送数据到网络中。

另一个需要注意的是,尽管在网络的一段调用writre()会触发另一端得到data事件,但是并不意味着每次write()都会触发一次data事件,在关闭掉Nagle算法后,接受端可能会将接受到的多个小数据包合并,然后只触发一次data事件。

构建UDP服务

UDP又称用户数据包协议,与TCP一样同属于网络传输层,UDP和TCP最大的同是UDP不是面向连接的,在UDP中,一个套接字可以与多个UDP服务通信,它虽然提供面向事务的简单不可靠信息传输服务,在网络差的情况下存在丢到严重的情况,但是它无须连接、资源消耗低、处理快速且灵活,所以常常应用在那种偶尔丢一两个数据包也不会产生重大影响的场景,比如音频、视频等,DNS服务也是基于它实现的。

创建UDP套接字

UDP套接字一旦创建,既可以作为客户端发送数据,也可以作为服务端接受数据,

var dgram = require('dgram');
var socket = dgram.createSocket("udp4"); // 套接字

创建UDP服务端

var dgram = require('dgram');
var server = dgram.createSocket("udp4"); // 套接字

// 套接字事件
// 当UDP套接字侦听网卡端口后,接受到消息后触发,触发携带的数据为消息的Buffer对象和一个远程地址信息
server.on("message", function (msg, rinfo) {
  console.log("server got: " + msg + " from " + rinfo.address + ":" + rinfo.port);
});

// 当UDP套接字开始侦听时触发
server.on("listening", function () {
  var address = server.address();
  console.log("server listening " + address.address + ":" + address.port);
});

// 还有close事件 和error事件
// close:调用close()方式时触发该事件,并不再触发message事件,如需继续触发message 重新bind即可
// error:当异常发生时触发该事件,如果不侦听,异常将直接抛出,使进程退出

server.bind(41234); // 接受网卡上所有41234端口上的消息,绑定完成后,触发listening事件

创建UDP客户端

var dgram = require('dgram');
var message = new Buffer("深入浅出Node.js");
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])

分别为要发送的Buffer、Buffer的偏移、Buffer的长度、目标端口、目标地址、发送完成后的回调。虽然参数列表相对复杂,但是它更灵活的地方在于可以随意发送数据到网络中的服务器端,而TCP如果要发送数据给另一个服务器端,则需要重新通过套接字构造新的连接。

构建HTTP服务

HTTP是应用层协议,Node提供了http和https模块用于HTTP和HTTPS的封装。

HTTP协议构建在请求和响应的概念上,对应在Node.js中就是由http.ServerRequest和http.ServerResponse这两个构造器构造出来的对象。

当用户浏览一个网站时,用户代理(浏览器)会创建一个请求,该请求通过TCP发送给Web服务器,随后服务器会给出响应。

在构建TCP服务器时,createServer()中接受的回调的参数是一个连接对象(connection)对象,而在HTTP服务器中则是请求和响应对象。

尽管我们可以通过req.connection获取TCP连接对象,但大多数情况下你还是与请求和响应的抽象打交道,默认情况下,Node会告诉浏览器始终保持连接(请求头:connection: keep-alive),通过它发送更多的请求,这是为了提高性能,因为不想浪费时间去重新建立和关闭TCP连接,当然我们可以使用writeHead()传递一个不同的值,如Close,将连接关闭。

HTTP

HTTP全称是超文本传输协议(HyperText Transfer Protocol),在其两端是服务器和浏览器,即B/S模式,Web即是HTTP应用。

HTTP是基于请求响应式的,以一问一答的方式实现服务,虽然基于TCP会话,但是本身并无会话的特点,从协议的的角度来说,浏览器其实是一个HTTP的代理,用户的行为会通过它转化为HTTP请求报文发送给服务端,服务器端在处理请求后,发送响应报文给代理,代理在解析报文后,将用户需要的内容呈现在界面上。简而言之。HTTP服务只做两件事,处理HTTP请求发送和发送HTTP响应。

无论是请求报文还是响应报文,报文内容都包含两个部分报文头和报文体,但是GET的请求报文中没有包含报文体,传递的消息包含在报文头中。

http模块

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

HTTP服务于TCP服务模型有区别的地方在于,在开启keepalive后,一个TCP会话可以用于多次请求和响应,TCP服务以connection为单位进行服务,HTTP服务以request进行服务。

http模块将连接所用套接字的读写抽象为ServerRequest和ServerResponse对象,它们分别对应请求和响应操作(http服务回调中常用的req, res),在请求产生的过程中,http模块拿到连接中传来的数据,调用二进制模块http_parser进行解析,在解析完请求报文的报头后,触发request事件,调用用户的业务逻辑。

http请求

对于TCP连接的读操作,http模块将其封装为ServerRequest对象,报文头通过http_parser进行解析。

报文头解析为如下属性。

报文体部分则抽象为一个只读流对象,如果业务逻辑需要读取报文体中的数据,则需要在数据流结束后才能进行操作,即req对象上的end事件触发后。

http响应

http模块将其封装为ServerResponse对象,可以将其看成一个可写的流对象,它影响响应报文头部信息的API为res.setHeader()和res.writeHead()

我们可以调用setHeader进行多次设置,但只有调用writeHead后,报头才会写入到连接中,除此之外,http模块会自动帮你设置一些头信息。

报文体部分则是调用res.write()和res.end()方法实现,res.end()会调用write()发送数据,然后发送信号告知服务器这次响应结束。

值得注意的是,报头是在报文体发送前发送的,一旦开始了数据的发送,writeHead()和setHeader()将不再生效。

{{% notice tip %}}
无论服务端在处理业务逻辑时是否发生异常,务必在结束时调用res.end()结束请求,否则客户端将一直处于等待的状态。
{{% /notice %}}

http服务的事件

HTTP客户端

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

var options = { 
  hostname: '127.0.0.1', // 服务器名称
  port: 1334, // 服务器端口
  path: '/', // 具体请求的路由
  method: 'GET' // 请求的方法
};

// 其他选项
// host: 服务器的域名或IP地址,默认为localhost
// localAddress: 建立网络连接的本地网卡
// socketPath: Domain套接字路径
// headers: 请求头对象
// auth: Basic认证,这个值计算成请求头中Authorization部分

var req = http.request(options, function(res) { // response listener
  console.log('STATUS: ' + res.statusCode); 
  console.log('HEADERS: ' + JSON.stringify(res.headers)); 
  res.setEncoding('utf8');
  res.on('data', function (chunk) { 
    console.log(chunk);
  }); 
});

req.end();

HTTP代理

为了重用TCP连接,http模块包含一个默认的客户端代理对象http.globalAgent,它对每个服务器端(host + port)创建的连接进行管理,默认情况下,通过ClientRequest对象对同一个服务器端发起的HTTP请求最多创建5个连接,如果调用HTTP客户端同时对一个服务器发送10次HTTP请求时,其实质只有5个请求处于并发状态,后续的请求需要等待某个请求完成服务后才真正发出。

可以通过在options中传递agent选项来改变这个限制。

var agent = new http.Agent({
  maxSockets: 10
});

var options = {
  hostname: '127.0.0.1', 
  port: 1334,
  path: '/',
  method: 'GET',
  agent: agent  // 直接设为false,可以使请求不受并发的控制
};

// Agent对象的sockets和requests属性分别表示当前连接池中使用中的连接数和处于等待状态的请求数
// 在业务中监视这两个值有助于发现业务状态的繁忙程度

http客户端事件

构建WebSocket服务

{{% notice tip %}}
WebSocekt前端使用讲解

{{% /notice %}}

WebSocket与Node之间的配合堪称完美,其理由有两条:

相比于HTTP,WebSocket有如下优势:

{{% notice tip %}}
相比于HTTP,WebSocket更接近于传输层协议,它并没有在HTTP的基础上模拟服务器端的推送,而是在TCP上定义独立的协议,让人迷惑的部分在于WebSocket的握手部分是由HTTP完成的,使人觉得它可能是基于HTTP实现的
{{% /notice %}}

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

WebSocket握手

客户端建立连接时,通过HTTP发起请求报文:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 
Sec-WebSocket-Protocol: chat, superchat 
Sec-WebSocket-Version: 13

其中UpgradeConnection字段代表请求服务器升级协议为WebSocket,其中Sec-WebSocket-ProtocolSec-WebSocket-Version指定子协议和版本,

Sec-WebSocket-Key字段用于安全校验,它的值是客户端随机生成的Base64编码的字符串。服务端接收之后,将其与字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11(固定的)相拼接,然后通过sha1安全散列算法计算出结果后,再进行Base64编码,最后当做响应头Sec-WebSocket-Accept的值返回给客户端。

// 服务端的对Sec-WebSocket-Key的处理
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

var key = req.headers['sec-websocket-key'];
key = crypto.createHash('sha1').update(key + WS).digest('base64');
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade  
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

服务器应答之后,Client 拿到Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。

WebSocket数据传输

在握手顺利完成后,当前连接不再进行HTTP的交互,而是开始WebSocket的数据帧协议,实现客户端和服务器端的数据交换。

当我们调用send()发送一条数据时,协议可能将这个数据封装为一帧或多帧数据,然后逐帧发送。

为了安全考虑,客户端需要对发送的数据进行掩码处理,服务器一旦收到无掩码帧,连接将关闭。

服务器发送到客户端的数据则需要做无掩码处理,客户端如果收到带掩码的数据帧,连接将关闭。

ws.png

如果客户端发送hello world!到服务端,12个字符,则长度为12 * 8 = 96位,转换为二进制位1100000,则报文应当如下:

fin(1) + res(000) + opcode(0001) + masked(1) + payload length(1100000) + masking key(32位) + payload data(hello world!加密后的二进制)

服务器回复yakexi,报文则如下,无需掩码。

fin(1) + res(000) + opcode(0001) + masked(0) + payload length(0110000) +  + payload data(yakexi加密后的二进制)
上一篇 下一篇

猜你喜欢

热点阅读