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

 const net = require('net');
const fs = require('fs'); const server = net.createServer((c) => {
let stream = fs.createWriteStream('test.txt');
c.pipe(stream).on('finish', () => {
console.log('Done');
});
c.on('error', (err) => {
console.log(err);
});
}).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:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src='http://libs.baidu.com/jquery/2.1.1/jquery.min.js'></script>
</head>
<body>
<input type="file" id="file" multiple/>
<input type="button" onclick="PostData()" value="提交">
<script>
function PostData() {
var form = $(this); var files = document.querySelector('#file').files;
var form_data = new FormData();
form_data.append('phone', `111111111111`);
form_data.append('file', files[0]);
$.ajax({
type: 'POST',
url: 'http://127.0.0.1:4000',
data: form_data,
mimeType: "multipart/form-data",
contentType: false,
cache: false,
processData: false
}).success(function () {
//成功提交
console.log('success');
}).fail(function (jqXHR, textStatus, errorThrown) {
//错误信息
console.log('err');
});
}
</script>
</body>
</html>

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

 const client = net.createConnection('4000', '127.0.0.1', () => {
let stream = fs.createReadStream('test2.txt');
stream.pipe(client).on('finish', () => {
console.log('Done');
});
stream.on('error', (err) => {
console.log(err);
});
});

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

 const http = require("http");
const fs = require("fs"); const server = http.createServer((req, res) => {
let stream = fs.createWriteStream('test.txt');
req.pipe(stream).on('finish', () => {
console.log('Done');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Done');
});
}); server.listen(4000);

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

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

 //_http_server.js
function Server(requestListener) {
if (!(this instanceof Server)) return new Server(requestListener);
net.Server.call(this, { allowHalfOpen: true }); if (requestListener) {
this.on('request', requestListener);
} // Similar option to this. Too lazy to write my own docs.
// http://www.squid-cache.org/Doc/config/half_closed_clients/
// http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false; this.on('connection', connectionListener); this.timeout = 2 * 60 * 1000;
this.keepAliveTimeout = 5000;
this._pendingResponseData = 0;
this.maxHeadersCount = null;
}

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

 //_http_server.js
function connectionListener(socket) {
//...
var parser = parsers.alloc();
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null; //...
state.onData = socketOnData.bind(undefined, this, socket, parser, state);
//...
} function socketOnData(server, socket, parser, state, d) {
assert(!socket._paused);
debug('SERVER socketOnData %d', d.length); var ret = parser.execute(d);
onParserExecuteCommon(server, socket, parser, state, ret, d);
}

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

 //_http_common.js
//...
const HTTPParser = binding.HTTPParser;
//...
var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser(HTTPParser.REQUEST); parser._headers = [];
parser._url = '';
parser._consumed = false; parser.socket = null;
parser.incoming = null;
parser.outgoing = null; // Only called in the slow case where slow means
// that the request headers were either fragmented
// across multiple TCP packets or too large to be
// processed in a single run. This method is also
// called to process trailing HTTP headers.
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
parser[kOnMessageComplete] = parserOnMessageComplete;
parser[kOnExecute] = null; return parser;
}); //_http_server.js
function connectionListener(socket) {
//...
parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
//...
} function parserOnIncoming(server, socket, state, req, keepAlive) {
//...
server.emit('request', req, res);
//...
}

从上述代码可以看到 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:

 const net = require('net');
const fs = require('fs'); const server = net.createServer({allowHalfOpen:false}, listener => {
console.log('connected');
listener.on('data', (data) => {
console.log(data.toString());
listener.write('one');
});
listener.on('end', () => {
console.log('RECV FIN');
listener.write('two');
});
}).listen('4000', '127.0.0.1');

socket_client.js:

 const net = require('net');
const client = net.createConnection({ port: 4000 }, () => {
console.log('connected to server!');
client.write('hello');
});
client.on('data', (data) => {
console.log(data.toString());
client.end();
console.log('SEND FIN');
});
client.on('end', () => {
console.log('RECV FIN');
});
client.on('close', () => {
console.log('client closed');
});

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

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

												

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

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

    4.1 核心模块与文件模块 / 574.2 从模块外部访问模块内的成员 / 58 4.2.1 使用exports对象 / 58 4.2.2 将模块定义为类 / 58 4.2.3 为模块类定义类变量或类 ...

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

    众所周知node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,通过多进程来实现 ...

  3. node.js第二天之模块

    一.模块的定义 1.在Node.js中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,这时的我们可以将应用程序划分为各个不同的部分. 2.狭义的说,每一个JavaScript文件都是一个 ...

  4. node之子线程child_process模块

    node.js是基于单线程模型架构,这样的设计可以带来高效的CPU利用率,但是无法却利用多个核心的CPU,为了解决这个问题,node.js提供了child_process模块,用于新建子进程,子进程的 ...

  5. node.js中使用http模块创建服务器和客户端

    node.js中的 http 模块提供了创建服务器和客户端的方法,http 全称是超文本传输协议,基于 tcp 之上,属于应用层协议. 一.创建http服务器 const http = require ...

  6. node学习笔记6——自定义模块

    自定义模块三大关键词: require——引入模块: exports——单个输出: module——批量输出. 从例子下手: 1.创建module.js: exports.a=22; exports. ...

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

    通过<高性能Web服务器Nginx的配置与部署研究——(11)应用模块之Memcached模块的两大应用场景>一文,我们知道Nginx从Memcached读取数据的方式,如果命中,那么效率 ...

  8. Node.js学习笔记(一) --- HTTP 模块、URL 模块、supervisor 工具

    一.Node.js创建第一个应用 如果我们使用 PHP 来编写后端的代码时,需要 Apache 或者 Nginx 的 HTTP 服务器, 来处理客户端的请求相应.不过对 Node.js 来说,概念完全 ...

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

    /* 实现一个应用,同时还实现了整个 HTTP 服务器. * */ //1.引入http模块 var http=require('http'); //2.用http模块创建服务 /* req获取url ...

  10. node.js创建并引用模块

    app.js var express = require('express'); var app = express(); var con = require('./content'); con.he ...

随机推荐

  1. ie6不能播放视频问题

    前几天做项目时碰到一个非常棘手的问题.在我自己本机的ie8上能正常播放视频的程序(ie6也能够),放用户的电脑上就是不能正常播放(可能是用户的机子系统太老或是别的什么原因.详细的我也不太清楚).没办法 ...

  2. mysql聚合函数操作

    1.mysql对中文进行排序 注:是用convert函数用gb2312编码转换 SELECT * FROM 表名 ORDER BY CONVERT(字段名 USING gb2312 ) ASC;

  3. 信息安全意识教育日历——By 安全牛

    安全牛:企业即使投入再好的信息安全技术和产品,也难以解决内部威胁以及社会工程等攻击手段,无法做到全面有效地保护企业信息资产.而通过开展员工的信息安全意识培训教育工作,不仅能降低企业风险.满足合规要求, ...

  4. 系列文章(三):WAPI为无线局域网WLAN安全而生——By Me

    导读:无线局域网(又称为WLAN,Wireless Local Area Network),其应用领域不断拓展,无线接入所具有的前所未有的连接性和自动化能够为人们带来巨大的便利和商机.与此同时,在信息 ...

  5. PAT 1081 Rational Sum[分子求和][比较]

    1081 Rational Sum (20 分) Given N rational numbers in the form numerator/denominator, you are suppose ...

  6. django-admin自定义登录

    这个效果,单位代码是User model 的一个外键Company 通过修改form,然后在前端显示 修改form class AuthenticationForm(forms.Form): &quo ...

  7. jQuery文档节点处理,克隆,each循环,动画效果,插件

    文档节点处理 //创建一个标签对象 $("<p>") //内部插入 $("").append(content|fn) ----->$(&quo ...

  8. SQL联接 外联接 内联接 完全联接 交叉联接

    联接分为: 内联接                        [inner join] 外联接        (左外联接,右外联接)        [left join/left outer jo ...

  9. 官方微信接口(全接口) - 微信摇一摇接口/微信多客服接口/微信支付接口/微信红包接口/微信卡券接口/微信小店接口/JSAPI

    微信入口绑定,微信事件处理,微信API全部操作包含在这些文件中.微信支付.微信红包.微信卡券.微信小店. 微信开发探讨群 330393916 <?php /**  * Description o ...

  10. OpenGL核心技术之Gamma校正

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你/2.2次幂.Gamma校正后的暗红色就会成为(0.5,0.0 ...