一个例子看懂所有nodejs的官方网络demo

今天看群里有人用AI技术写了个五子棋,正好用的socket.io,本身我自己很久没看nodejs了,再加上Tcp/IP的知识一直很弱,我就去官网看了下net.socket

发现之前以为懂的一个官方例子今天再看又不懂了。所以我决定写下笔记,分析流程,别下次又搞半天。。。。。

代码是这样的,可以自行去官网的http模块去看:

const http = require('http'); const net = require('net'); const url = require('url');

// 创建一个 HTTP 代理服务器

const proxy = http.createServer((req, res) => {

res.writeHead(200, { 'Content-Type': 'text/plain' });

res.end('okay');

});

proxy.on('connect', (req, cltSocket, head) => {

// 连接到一个服务器

const srvUrl = url.parse(http://${req.url});

const srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {

cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +

'Proxy-agent: Node.js-Proxy\r\n' +

'\r\n');

srvSocket.write(head);

srvSocket.pipe(cltSocket);

cltSocket.pipe(srvSocket);

});

});

// 代理服务器正在运行

proxy.listen(1337, '127.0.0.1', () => {

// 发送一个请求到代理服务器

const options = {

port: 1337,

hostname: '127.0.0.1',

method: 'CONNECT',

path: 'www.google.com:80'

};

const req = http.request(options);

req.end();

req.on('connect', (res, socket, head) => {

console.log('已连接!');

// 通过代理服务器发送一个请求
socket.write('GET / HTTP/1.1\r\n' +
             'Host: www.google.com:80\r\n' +
             'Connection: close\r\n' +
             '\r\n');
socket.on('data', (chunk) => {
  console.log(chunk.toString());
});
socket.on('end', () => {
  proxy.close();
});

});

});

整个流程是这样的:
 1:先开启一个http服务器,用proxy引用这个资源

 2: proxy 监听了 connect 方法

 3:proxy 监听1337这个端口和回环主机地址,整个服务器正式生效。

 4: 服务器生效后,用http.ClientRequest 类去主动请求已经生成的服务器。使用http的connect方法,并监听connect事件。

 5: proxy监听了 connect方法。在成功接收到客户端这一次connect请求后,服务器又利用net.Socket 类,对谷歌发起一次TCP链接请求,

用srvSocket引用这个资源。

 6: 这里就开始了3次握手,谷歌服务器接受到SYN包后,发送给srvSocket一个ACK,srvSocket再发给谷歌一个ACK。这样TCP链接就构成了。

 7:构建完毕后,服务器用cltSocket 给 客户端发送链接成功的状态。自己看HTTP/1.1 200 Connection Established 。

同时谷歌的buffer数据就发送过来了。 注意看 srvSocket.pipe(cltSocket),这些数据全部又给了cltSocket,cltSocket自然就被写入了数据。

 8:客户端监听到connect,就开始在回调函数里打印了 "连接成功"。并且把刚才谷歌的返回的数据也打印了出来。这个过程是异步的,调用的是底层的socket

有数据在buffer里,就会被data事件监听到。

 9:同时给socket给cltSocket发送connectiond的close状态。 那接下来socket和cltSocket就要进行四次挥手了,同时cltSocket又把消息怼给了srvSocket,

srvSocket自然也就和谷歌进行四次挥手了。这里厉害就厉害在这个pipe函数!!!!把四次挥手的过程全部隐藏了!!!这里我们需要补充一些知识: connectiond的状态有两种,一种是keep-alive 另外一种就是close,keep-alive就是保持客户端与服务器的连接,close表示服务器给客户端发送信息之后就断开了..close对资源消耗占用的少一些.再完善一点,其实这和TCP三次握手有关,如果返回的是keep-alive表示之前的握手还可以用在接下来的请求当中去,如果是close的话当前请求完成后会进行四次握手关闭连接,在接下来的请求就要重新握手,这是HTTP/1.1相对1.0新增的一个部分,加快了网络传输

 10:最后大家都自动关闭了。然后  proxy.close(); 注意,就算你调用了 proxy.close();如果还存在没有处理完的connection,进程一样继续,只不过不会再接受新的链接而已。

---------------------------到此为止,可能看的懂我解释的朋友以为自己也懂了。。。。--------------------------------

下面再补充一些关于TCP的知识。。。。都是google搜出来的,凑合看吧:

  TCP 的 FIN 标记相当于“读到文件尾”这一信号。客户端在收到服务器端发来的 FIN 标记就知道数据已经发完,可以进一步处理 HTTP Response 报文。如果换客户端自己来计数也不是不可行,但要考虑到有一些 HTTP Response 报文可能缺少 Content-Length Header (该首部不是强制性要求的)。另外,HTTP 1.1 要求默认使用 Keep-Alive,以便复用 TCP 连接(即长连接,不设置 Connection Header 就会有此效果),服务器端不会主动关闭连接,此时要求客户端自己进行报文长度计数,是否关闭连接由客户端控制。如果通信过程出现异常,服务器端会返回 RST 标记,强制断开虚连接。解决 TIME_WAIT 的办法之一是修改内核参数将 2MSL 调小(影响整个系统,范围过大,不推荐)。


Server 关闭连接确实是历史原因:HTTP/0.9 协议中 response 是没有 header 的(RFC 1945 - Hypertext Transfer Protocol -- HTTP/1.0 参考 Simple-Response 的定义),所以 client 根本无从知道什么时候这个东西结束。这里的 FIN 就和读取文件时的 EOF 是一样的作用。想想 UNIX 甚至可以用 read(2) / write(2) 操作 socket fd 就明白这样的意义了。然后回到 RFC 2616:Connection: close 是一个 general-header( RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 )即:既可以作为 request header 也可以作为 response header。Connection: close 的作用在于"协商(signal)"。14.10 中:HTTP/1.1 defines the "close" connection option for the sender to   signal that the connection will be closed after completion of the   response.即双方都可以关闭。"HTTP: The Definitive Guide" 也是这样表述的。实际上,在 "HTTP: The Definitive Guide" (英文版)p85 中讨论了一模一样的关于 TIME_WAIT 的问题。基本结论是:这个 TIME_WAIT 在 benchmark 的情况下会影响同一台 client 可以发起的请求数量;在实际应用中几乎没有问题。Server 保持 TIME_WAIT 并不是保持 socket,而只是保持一个记录,表示“对应这两个 end-point 的 packet 应该丢弃”,仅此而已。话说这书里上下花了很大篇幅描述了一个相当复杂的关闭逻辑,为的是避免 RST 的出现,倒是有点意思。回头想想:如果是 server 主动关闭连接的情况,只要调用一次 close() 就可以释放连接,剩下的工作由内核 TCP 栈直接进行了处理,整个过程只有一次 syscall;如果是要求 client 关闭,则 server 在写完最后一个 response 之后需要把这个 socket 放入 readable 队列,调用 select / epoll 去等待事件;然后调用一次 read() 才能知道连接已经被关闭,这其中是两次 syscall,多一次用户态程序被激活执行,而且 socket 保持时间更长——到底哪样性能好?

这里再贴一下nodejs官网 对于net.socket end事件的描述

'end' 事件#

查看英文版 / 参与翻译

新增于: v0.1.90

Emitted when the other end of the socket sends a FIN packet, thus ending the readable side of the socket.

By default (allowHalfOpen is false) the socket will send a FIN packet back and destroy its file descriptor once it has written out its pending write queue. However, if allowHalfOpen is set to true, the socket will not automatically end() its writable side, allowing the user to write arbitrary amounts of data. The user must call end() explicitly to close the connection (i.e. sending a FIN packet back).

-----------------综上,下面给出我的理解,当然不一定对,我也不确定,但是我觉得能解释的通------------

客户端发送了connection:close的头,服务器接收到后,在数据全部发送完毕之后,返回给客户端一个FIN包,

客户端收到FIN包的话,就会调用end的监听函数,底层就会给服务器发送一个FIN包。服务器收到后,OK,大家都关闭了。。。。。

当然中间的proxy呢??其实管道pipe函数里就已经包括了end监听了。所以中间的过程也是一样关闭的。

----------------更新于2天后---------------------------------------------

这两天继续研究这个简单的demo,上面的demo其实有很多值得研究的。

我把上面的demo稍微改变一下,其中我不再使用pipe:

const http = require('http');

const net = require('net');

const url = require('url');

// 创建一个 HTTP 代理服务器

const proxy = http.createServer((req, res) => {

res.writeHead(201, { 'Content-Type': 'text/plain' });

res.end('okay');

});

proxy.on('connect', (req, cltSocket, head) => {

// 连接到一个服务器

const srvUrl = url.parse(http://${req.url});

const srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {

cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +

'Proxy-agent: Node.js-Proxy\r\n' +

'\r\n');

srvSocket.write('GET / HTTP/1.1\r\n' +

'Host: www.baidu.com:80\r\n' +

'Connection: close\r\n' +

'\r\n');

});

cltSocket.on("data",function(c){

console.log(c.toString())

})

cltSocket.on('end',function(){

console.log("C-->P:客户端发送来FIN")

})

srvSocket.on('data',function(c){

cltSocket.write(c)

//cltSocket.end();

});

srvSocket.on('end',function(){

console.log("S--->P:百度发送了FIN")

})

});

// 代理服务器正在运行

proxy.listen(1337, '127.0.0.1', () => {

// 发送一个请求到代理服务器

const options = {

port: 1337,

hostname: '127.0.0.1',

method: 'CONNECT',

path: 'www.baidu.com:80'

};

const req = http.request(options);

req.end();

req.on('connect', (res, socket, head) => {

// 通过代理服务器发送一个请求
socket.write("C-->P:与代理服务器建立HTTP连接后,客户端发送数据给代理服务器。");

//socket.end();
socket.on('data', (chunk) => {
  console.log("P-->C:",1);
});
socket.on('end', () => {

  console.log("P-->C:代理服务器发来FIN包")
  proxy.close();
});

});

});