Node net模块与http模块一些研究

这周遇到一个有意思的需求,端上同学希望通过 socket 传送表单数据(包含文件内容)到 node 端,根据表单里的文件名、手机号等信息将文件数据保存下来。于是我这样写了一下--socket_server.js:

 1 const net = require(‘net‘);
 2 const fs = require(‘fs‘);
 3
 4 const server = net.createServer((c) => {
 5     let stream = fs.createWriteStream(‘test.txt‘);
 6     c.pipe(stream).on(‘finish‘, () => {
 7         console.log(‘Done‘);
 8     });
 9     c.on(‘error‘, (err) => {
10         console.log(err);
11     });
12 }).listen(‘4000‘, ‘127.0.0.1‘);

当后端同学发送数据过来后,我保存在 test.txt 里的数据是:

POST / HTTP/1.1
Host: 127.0.0.1:4000
Connection: keep-alive
Content-Length: 513
Accept: */*
Origin: http://localhost:63342
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytjiObRhDyrWvl3QP
Referer: http://localhost:63342/phone-upload/testSocket/index.html?_ijt=f8r6n5990ic71peiekdapbs02r
Accept-Encoding: gzip, deflate, br
Accept-Language: en,zh-CN;q=0.8,zh;q=0.6,ja;q=0.4,zh-TW;q=0.2

------WebKitFormBoundarytjiObRhDyrWvl3QP
Content-Disposition: form-data; name="phone"

11111111111
------WebKitFormBoundarytjiObRhDyrWvl3QP
Content-Disposition: form-data; name="file"; filename="index.js"
Content-Type: text/javascript

var koa = require(‘koa‘);
var app = koa();
var statistics = require(‘../中间件/statistics.js‘);

app.use(statistics({
    whiteList: [‘‘, ‘cq‘]
}));

app.use(function *(){
  this.body = ‘Hello World‘;
});

app.listen(3000);
------WebKitFormBoundarytjiObRhDyrWvl3QP--

也就是说,我需要在 node 端做解析的工作(实际上就是 http 模块做的事),如果一直发送的是 txt 文件还好说,我可以根据 boundary 和换行解析文本数据,但如果发送的文件内容是 zip 之类的二进制数据,那么我该如何解析?于是,我打算自己好好研究一下这个问题,但也不能一直麻烦端上同学发文件让我调试,于是我不假思索的写出了如下代码--index.html:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6     <script src=‘http://libs.baidu.com/jquery/2.1.1/jquery.min.js‘></script>
 7 </head>
 8 <body>
 9     <input type="file" id="file" multiple/>
10     <input type="button" onclick="PostData()" value="提交">
11     <script>
12         function PostData() {
13             var form = $(this);
14
15             var files = document.querySelector(‘#file‘).files;
16             var form_data = new FormData();
17             form_data.append(‘phone‘, `111111111111`);
18             form_data.append(‘file‘, files[i]);
19             $.ajax({
20                 type: ‘POST‘,
21                 url: ‘http://127.0.0.1:4000‘,
22                 data: form_data,
23                 mimeType: "multipart/form-data",
24                 contentType: false,
25                 cache: false,
26                 processData: false
27             }).success(function () {
28                 //成功提交
29                 console.log(‘success‘);
30             }).fail(function (jqXHR, textStatus, errorThrown) {
31                 //错误信息
32                 console.log(‘err‘);
33             });
34         }
35     </script>
36 </body>
37 </html>

当我在网页端选定文件,点击提交后,一件有趣的事情发生了:网页端的 AJAX 请求一直在 pending,后端也一直没打出 ‘Done‘ 的 log,当我刷新页面后,后端才显示 ‘Done‘ 并获取到文件内容。我抱着疑问又写了一份 socket 客户端--socket_client.js:

1 const client = net.createConnection(‘4000‘, ‘127.0.0.1‘, () => {
2     let stream = fs.createReadStream(‘test2.txt‘);
3     stream.pipe(client).on(‘finish‘, () => {
4         console.log(‘Done‘);
5     });
6     stream.on(‘error‘, (err) => {
7         console.log(err);
8     });
9 });

这次发现 socket 客户端和服务端表现正常,都及时打出了 ‘Done‘ 的日志,那么问题一定就出在 http 和 tcp 的差异上了。为了验证自己的想法,我又写了一份 http 服务端--http_server.js:

 1 const http = require("http");
 2 const fs = require("fs");
 3
 4 const server = http.createServer((req, res) => {
 5     let stream = fs.createWriteStream(‘test.txt‘);
 6     req.pipe(stream).on(‘finish‘, () => {
 7         console.log(‘Done‘);
 8         res.writeHead(200, { ‘Content-Type‘: ‘text/plain‘ });
 9         res.end(‘Done‘);
10     });
11 });
12
13 server.listen(4000);

再次通过网页端上传文件,网页这边 AJAX 立即返回,没有出现 pending 现象,当然去掉第 8、9 行能复现 pending。后端这边也立即打出 ‘Done‘。

于是带着种种疑问参考了源码

 1 //_http_server.js
 2 function Server(requestListener) {
 3   if (!(this instanceof Server)) return new Server(requestListener);
 4   net.Server.call(this, { allowHalfOpen: true });
 5
 6   if (requestListener) {
 7     this.on(‘request‘, requestListener);
 8   }
 9
10   // Similar option to this. Too lazy to write my own docs.
11   // http://www.squid-cache.org/Doc/config/half_closed_clients/
12   // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
13   this.httpAllowHalfOpen = false;
14
15   this.on(‘connection‘, connectionListener);
16
17   this.timeout = 2 * 60 * 1000;
18   this.keepAliveTimeout = 5000;
19   this._pendingResponseData = 0;
20   this.maxHeadersCount = null;
21 }

上一部分是 http 模块 createServer 函数的代码,发现实际上就是调用 net.Server,并监听 ‘request‘ 事件运行 requestListener (对应 http_server.js 就是5-10行)。当有 socket 连接过来的时候会触发 ‘connection‘ 事件:

 1 //_http_server.js
 2 function connectionListener(socket) {
 3   //...
 4   var parser = parsers.alloc();
 5   parser.reinitialize(HTTPParser.REQUEST);
 6   parser.socket = socket;
 7   socket.parser = parser;
 8   parser.incoming = null;
 9
10   //...
11   state.onData = socketOnData.bind(undefined, this, socket, parser, state);
12   //...
13 }
14
15 function socketOnData(server, socket, parser, state, d) {
16   assert(!socket._paused);
17   debug(‘SERVER socketOnData %d‘, d.length);
18
19   var ret = parser.execute(d);
20   onParserExecuteCommon(server, socket, parser, state, ret, d);
21 }

通过 HTTP parser 来解析 TCP 传输过来的数据,而 HTTP parser 来自:

 1 //_http_common.js
 2 //...
 3 const HTTPParser = binding.HTTPParser;
 4 //...
 5 var parsers = new FreeList(‘parsers‘, 1000, function() {
 6   var parser = new HTTPParser(HTTPParser.REQUEST);
 7
 8   parser._headers = [];
 9   parser._url = ‘‘;
10   parser._consumed = false;
11
12   parser.socket = null;
13   parser.incoming = null;
14   parser.outgoing = null;
15
16   // Only called in the slow case where slow means
17   // that the request headers were either fragmented
18   // across multiple TCP packets or too large to be
19   // processed in a single run. This method is also
20   // called to process trailing HTTP headers.
21   parser[kOnHeaders] = parserOnHeaders;
22   parser[kOnHeadersComplete] = parserOnHeadersComplete;
23   parser[kOnBody] = parserOnBody;
24   parser[kOnMessageComplete] = parserOnMessageComplete;
25   parser[kOnExecute] = null;
26
27   return parser;
28 });
29
30 //_http_server.js
31 function connectionListener(socket) {
32   //...
33   parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
34   //...
35 }
36
37 function parserOnIncoming(server, socket, state, req, keepAlive) {
38   //...
39   server.emit(‘request‘, req, res);
40   //...
41 }

从上述代码可以看到 parser 解析得到请求头、请求体,触发 ‘request‘ 事件,但由于 HTTPParser 是内置的用 C 实现的模块(还有个用 JS 实现的 HTTPParser),具体如何解析以及事件触发还没去细细了解,但总体流程大概清晰了起来。实际上 http 模块本质上就是在 net 模块的基础上添加了 HTTPParser 等功能,

在这里还有一点值得注意,http 模块创建 server 的时候设置 allowHalfOpen 为 true,默认为 false

官网上的解释是:“If allowHalfOpen is set to true, when the other end of the socket sends a FIN packet, the server will only send a FIN packet back when socket.end() is explicitly called, until then the connection is half-closed (non-readable but still writable).”

结合 ‘end’ 事件的解释:“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).”。

大概意思是,当客户端和服务端建立了 socket 连接后,net.Socket 对象是 duplex stream,能读能写。当客户端调用 socket.end 后,触发 end 事件, 并发送 FIN 包给服务端,表示自己不再写数据了,当服务端 allowHalfOpen 设置为 false 时,一旦服务端将所有数据发送完,也会回发 FIN 包给客户端并释放文件描述符(在 linux 上,一切都是文件,socket 实际上也是文件资源)。当服务端 allowHalfOpen 设置为 true 时,只有显式的调用 socket.end 才会关闭连接,此时服务端仍能写数据给客户端。测试如下:

socket_server.js:

 1 const net = require(‘net‘);
 2 const fs = require(‘fs‘);
 3
 4 const server = net.createServer({allowHalfOpen:false}, listener => {
 5     console.log(‘connected‘);
 6     listener.on(‘data‘, (data) => {
 7         console.log(data.toString());
 8         listener.write(‘one‘);
 9     });
10     listener.on(‘end‘, () => {
11         console.log(‘RECV FIN‘);
12         listener.write(‘two‘);
13     });
14 }).listen(‘4000‘, ‘127.0.0.1‘);

socket_client.js:

 1 const net = require(‘net‘);
 2 const client = net.createConnection({ port: 4000 }, () => {
 3     console.log(‘connected to server!‘);
 4     client.write(‘hello‘);
 5 });
 6 client.on(‘data‘, (data) => {
 7     console.log(data.toString());
 8     client.end();
 9     console.log(‘SEND FIN‘);
10 });
11 client.on(‘end‘, () => {
12     console.log(‘RECV FIN‘);
13 });
14 client.on(‘close‘, () => {
15     console.log(‘client closed‘);
16 });

运行服务端,再运行客户端后会报错:Error: This socket has been ended by the other party。当客户端调用 socket.end 后,连接就会中断并释放,所以服务端再写数据就会出错。将 allowHalfOpen 设置为 true 后,客户端再发送 FIN 后,仍能接收服务端的数据。但注意此时客户端不会关闭,直到服务端显示的调用 socket.end 后,客户端才会关闭。

这个现象是不是很像最初遇到的网页端 pending 现象?实际上我猜想原因就在于此,具体原因也没有去深究了。

				
时间: 2024-10-09 22:50:33

Node net模块与http模块一些研究的相关文章

node之子线程child_process模块

node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,用于新建子进程,子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果.由此来实现对多核CPU的利用,且可以实现简单又使实用的非阻塞操作. 1. exec() exec()方法用于执行bash命令,它的参数是一个命令字符串. const exec = requi

Node.js权威指南 (4) - 模块与npm包管理工具

4.1 核心模块与文件模块 / 574.2 从模块外部访问模块内的成员 / 58 4.2.1 使用exports对象 / 58 4.2.2 将模块定义为类 / 58 4.2.3 为模块类定义类变量或类函数 / 614.3 组织与管理模块 / 61 4.3.1 从node_modules目录中加载模块 / 61 4.3.2 使用目录来管理模块 / 62 4.3.3 从全局目录中加载模块 / 624.4 模块对象的属性 / 634.5 包与npm包管理工具 / 65 4.5.1 Node.js中的包

node.js(七) 子进程 child_process模块

众所周知node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,通过多进程来实现对多核CPU的利用. child_process模块提供了四个创建子进程的函数,分别是spawn,exec,execFile和fork. 1.spawn函数的简单用法 spawn函数用给定的命令发布一个子进程,只能运行指定的程序,参数需要在列表中给出.如下示例: var child_process

NODE.JS API —— Modules(模块)

// 说明 Node API 版本为 v0.10.31.    中文参考:http://nodeapi.ucdok.com/#/api/,http://blog.sina.com.cn/oleoneoy 本段为博主注解. 目录 ● 模块    ○ Cycles    ○ Core Modules    ○ File Modules    ○ Loading from node_modules Folders    ○ Folders as Modules    ○ Caching ■ Modul

高性能Web服务器Nginx的配置与部署研究(13)应用模块之Memcached模块+Proxy_Cache双层缓存模式

通过<高性能Web服务器Nginx的配置与部署研究——(11)应用模块之Memcached模块的两大应用场景>一文,我们知道Nginx从Memcached读取数据的方式,如果命中,那么效率是相当高的.那么: 1. 如果不命中呢? 我们可以到相应的数据服务器上读取数据,然后将它缓存到Nginx服务器上,然后再将该数据返回给客户端.这样,对于该资源,只有穿透 Memcached的第一次请求是需要到数据服务器读取的,之后在缓存过期时间之内的所有请求,都是读取Nginx本地的.不过Nginx的 pro

Node.js笔记(0001)---connect模块

首先来看这一部分代码 1 /** 2 * Created by bsn on 14-7-1. 3 */ 4 var connect = require('connect'); 5 6 var app = connect(); 7 function hello(req, res, next) { 8 console.log(req.url); 9 res.end('hello bsn'); 10 next(); 11 } 12 13 function helloAgain(req, res) {

Node中的包和模块

一.模块和包 概念:模块(Module)和包(Package)是Node.js最重要的支柱.开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分.分装.然后组合起来.模块正式为了实现这种方式而诞生,在浏览器JavaScript中,脚本模块的拆分和组合通常使用HTML的script标签来实现,Node.js提供了require函数来调用其他模块,而且模块都是基于文件,机制非常简单,模块和包的区别是透明的,因此经常不作区分. 模块 1.什么是模块 模块和文件是一一对应的.一个Node

node.js第二天之模块

一.模块的定义 1.在Node.js中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,这时的我们可以将应用程序划分为各个不同的部分. 2.狭义的说,每一个JavaScript文件都是一个模块:而多个JavaScript文件之间可以相互require,他们共同实现了一个功能,他们整体对外,又称为一个广义上的模块. 3.Node.js中,一个JavaScript文件中定义的变量.函数,都只在这个文件内部有效.当需要从此JS文件外部引用这些变量.函数时,必须使用exports对象进行暴露

Node.js 实现第一个应用以及HTTP模块和URL模块应用

/* 实现一个应用,同时还实现了整个 HTTP 服务器. * */ //1.引入http模块 var http=require('http'); //2.用http模块创建服务 /* req获取url信息 (request) res 浏览器返回响应信息 (response) * */ http.createServer(function(req,res){ // 发送 HTTP 头部 // HTTP 状态值: 200 : OK //设置 HTTP 头部,状态码是 200,文件类型是 html,字

node.js创建并引用模块

app.js var express = require('express'); var app = express(); var con = require('./content'); con.hello(); app.listen(3000); 模块content.js exports.hello=function(){ console.log("hello world"); } 扩展性更好点的,把模块做成对象 模块content.js var content={}; conten