这周遇到一个有意思的需求,端上同学希望通过 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. docker中制作自己的JDK+tomcat镜像

    方式一 首先,准备好想要的jdk和tomcat,另外,我们需要创建一个Dockerfile文件.下面展示一个Dockerfile文件的完整内容: FROM ubuntu:14.10 MAINTAINE ...

  2. python全栈开发从入门到放弃之网络基础

    一.操作系统基础 操作系统:(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才 ...

  3. directorjs和requirejs和artTemplate模板引擒建立的SPA框架

    分为4块:A : index.html壳子.    加载B  init-config.js,   加载D  header.html模板B : init-config.js 个人信息+路由配置+权限+渲 ...

  4. Android 属性自定义及使用获取浅析

    一.概述 相信你已经知道,Android 可使用 XML 标签语言进行界面的定义.每个标签中有一个一个的属性,这些属性有相应的属性值.例如: <cn.neillee.composedmenu.R ...

  5. iPhone获取手机里面所有的APP(私有库)+ 通过包名打开应用

    1.获取到手机里面所有的APP包名 - (void)touss { Class lsawsc = objc_getClass("LSApplicationWorkspace"); ...

  6. Google ProtocolBuffer

    https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html 1. Protocol Buffers 简介 Protocol Buff ...

  7. 单元测试JUnit 4

    介绍   JUnit 4.x 是利用了 Java 5 的特性(Annotation)的优势,使得测试比起 3.x 版本更加的方便简单,JUnit 4.x 不是旧版本的简单升级,它是一个全新的框架,整个 ...

  8. vue项目配置less预编译语言

    当所有东西都 准备好之后 : 第一步: 安装less依赖,npm install less less-loader --save 第二步:找到webpack配置文件webpack.base.conf. ...

  9. jQuery动画二级下拉菜单

    在线演示 本地下载

  10. cisco笔记

    交换机 show cdp neighbors 显示邻居信息 路由 show ip interface brief 显示接口ip