内容简介:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 socket(套接字),因此建立网络通信连接至少要一对端口号。Socket的英文原义是“孔”或“插座”。作为BSD UNIX的关于 Socket,可以总结以下几点:
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 socket(套接字),因此建立网络通信连接至少要一对端口号。 socket 本质是对 TCP/IP 协议栈的封装,它提供了一个针对 TCP 或者 UDP 编程的接口,并不是另一种协议 。通过 socket,你可以使用 TCP/IP 协议。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的 进程通信 机制,取后一种意思。通常也称作” 套接字 “,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的 主机 一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。—— 百度百科
关于 Socket,可以总结以下几点:
- 它可以实现底层通信,几乎所有的应用层都是通过 socket 进行通信的。
- 对 TCP/IP 协议进行封装,便于应用层协议调用,属于二者之间的中间抽象层。
- TCP/IP 协议族中,传输层存在两种通用协议: TCP、UDP,两种协议不同,因为不同参数的 socket 实现过程也不一样。
(图片来源 —— 初步研究node中的网络通信模块 )
Node.js 网络模块架构
在 Node.js 的模块里面,与网络相关的模块有: Net 、 DNS 、 HTTP 、 TLS/SSL 、 HTTPS 、 UDP/Datagram ,除此之外,还有 v8 底层相关的网络模块有 tcp_wrap.cc 、 udp_wrap.cc 、 pipe_wrap.cc 、 stream_wrap.cc 等等,在 JavaScript 层以及 C++ 层之间通过 process.binding 进行桥接相互通信。
(图片来源 —— Node.js之网络通讯模块浅析 )
Nagle 算法
Nagle 算法描述是当一个连接有未确认的数据,小片段应该保留。当足够的数据已被收件人确认,这些小片段将被分批成能够被传输的更大的片段。
在很多小的数据包传输的网络,理想的情况将小的包集合起来一起发送以减少拥堵。但有时等待时间比其他都重要,所以传送小包是非常重要的。
这对互动应用尤其重要,像 ssh 或者 X Window 系统。在这些应用中,体积小的消息应毫不延迟地输送,以给人实时反馈的感觉。针对这种需求场景,我们可以通过 setNoDelay(true) 方法,来关闭 Nagle 算法。
nc 命令
nc(netcat)可以用于涉及 TCP 或 UDP 的相关内容,比如通过它我们可以打开 TCP 连接,发送 UDP 数据包,监听任意的 TCP 和 UDP 端口,执行端口扫描和处理 IPv4 和 IPv6 等。
利用 nc 命令,我们可以方便地连接一个 UNIX 域套接字(socket)服务器,如:
$ nc -U /tmp/echo.sock # -U — Use UNIX domain socket
socket API 原本是为网络通讯设计的,但后来在 socket 的框架上发展出一种 IPC (Inter-Process Communication)机制,就是 UNIX Domain Socket 也称为本地域。虽然网络 socket 也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是 UNIX Domain Socket 用于 IPC 更有效率: 不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程 。这是因为,IPC 机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。UNIX Domain Socket 也提供面向流和面向数据包两种 API 接口,类似于 TCP 和 UDP,但是面向消息的 UNIX Domain Socket 也是可靠的,消息既不会丢失也不会顺序错乱。
在 Windows 上,本地域通过命名管道实现。路径必须是以 \\?\pipe\ 或 \\.\pipe\ 为入口。路径允许任何字符,但后面的字符可能会对管道名称进行一些处理,例如解析 .. 序列。尽管如此,管道空间是平面的。管道不会持续,当最后一次引用关闭时,管道就会被删除。
Node.js net
net 模块提供了创建基于流的 TCP 或 IPC 服务器 ( net.createServer() ) 和客户端 ( net.createConnection() ) 的异步网络 API。
net 基本使用
server.js
// 创建socket服务器
const net = require("net");
let clients = 0;
const server = net.createServer(client => {
clients++;
let clientId = clients;
console.log("Client connect:", clientId);
client.on("end", () => {
console.log("Client disconnected:", clientId);
});
client.write("Welcome client: " + clientId + " \r\n");
client.pipe(client); // 把客户端的数据返回给客户端
});
server.listen(8000, () => {
console.log("Server started on port 8000");
});
client.js
// 创建socket客户端
const net = require("net");
const client = net.connect(8000);
client.on("data", data => {
console.log(data.toString());
});
client.on("end", () => {
console.log("Client disconnected");
});
新开两个终端,按顺序执行 node server.js 和 node client.js 命令后,就可以在控制台看到输出的数据了。
接下来我们来分别分析一下 server.js 和 client.js 文件。
TCP Server
// 创建socket服务器
const net = require("net");
let clients = 0;
const server = net.createServer(client => {
// 参考net基本使用相关代码
});
server.listen(8000, () => {
console.log("Server started on port 8000");
});
以上代码通过调用 net.createServer() 方法来创建一个新的 TCP 服务器,该方法的实现如下:
function createServer(options, connectionListener) {
return new Server(options, connectionListener);
}
可以看到 net.createServer() 方法内部是通过调用 Server 的构造函数创建 TCP 服务器,Server 构造函数(代码片段)如下:
function Server(options, connectionListener) {
if (!(this instanceof Server)) // 确保以new形式调用构造函数
return new Server(options, connectionListener);
EventEmitter.call(this);
if (typeof options === 'function') {
connectionListener = options;
options = {};
this.on('connection', connectionListener);
} else if (options == null || typeof options === 'object') {
options = options || {};
if (typeof connectionListener === 'function') {
this.on('connection', connectionListener);
}
}
this._connections = 0;
this._handle = null;
}
util.inherits(Server, EventEmitter);
通过观察以上代码,我们发现 Server 类继承了 EventEmitter 类,Server 实例内部会监听 connection 事件,该事件触发后,会执行用户设置的 connectionListener 回调函数。那么何时会触发 connection 事件,通过源码我们发现在 onconnection 函数内部会触发 connection 事件,具体如下(代码片段):
function onconnection(err, clientHandle) {
var handle = this;
var self = handle.owner;
// 判断是否超过最大连接数
if (self.maxConnections && self._connections >= self.maxConnections) {
clientHandle.close();
return;
}
// util.inherits(Socket, stream.Duplex);
var socket = new Socket({
handle: clientHandle,
allowHalfOpen: self.allowHalfOpen,
pauseOnCreate: self.pauseOnConnect
});
socket.readable = socket.writable = true;
self._connections++;
self.emit('connection', socket);
}
在 onconnection 函数内部,我们通过调用 Socket 构造函数来创建 socket 对象,因为 Socket 类继承于 stream.Duplex 类,所以 socket 对象也是一个可读可写流,可以使用 stream.Duplex 中定义的方法。
那么接下来的问题就是何时调用 onconnection 函数,我们继续在源码中找答案。最终我们发现在 setupListenHandle 函数内部会通过执行 this._handle.onconnection = onconnection; 语句设置 onconnection 函数。
顾名思义,setupListenHandle 函数的作用是用于设置监听处理器,该函数对象会被绑定到 Server 原型对象的 _listen2 属性上:
Server.prototype._listen2 = setupListenHandle;
不知道小伙伴们,还记得以下这段代码:
server.listen(8000, () => {
console.log("Server started on port 8000");
});
以上代码我们通过 listen() 方法来设置 TCP 服务器的监听端口,这里的 listen() 方法与 _listen2() 方法是不是会有联系?嗯,没错,它们之间有紧密的联系,谁让它们长得像。
接下来我们先来看一下创建 TCP 服务器 listen() 方法的签名:
server.listen([port\][, host][, backlog][, callback])]
- 支持 port、host、backlog 和 callback 参数。
- 返回相应的 server 对象。
而创建 IPC 服务器 listen() 方法的签名为:
server.listen(path[, backlog][, callback])
- 支持 path(服务器需要监听的路径,详情可以查看 Identifying paths for IPC connections 。)backlog 和 callback 参数。
- 返回相应的 server 对象。
这里我们先来分析创建 TCP 服务器的情形:
Server.prototype.listen = function(...args) {
var normalized = normalizeArgs(args);
var options = normalized[0];
var cb = normalized[1];
var hasCallback = (cb !== null);
if (hasCallback) {
this.once('listening', cb);
}
options = options._handle || options.handle || options;
var backlog;
// options.port:8000
if (typeof options.port === 'number' || typeof options.port === 'string') {
// start TCP server listening on host:port
if (options.host) {
lookupAndListen(this, options.port | 0, options.host, backlog,
options.exclusive);
} else { // Undefined host, listens on unspecified address
// Default addressType 4 will be used to search for master server
listenInCluster(this, null, options.port | 0, 4,
backlog, undefined, options.exclusive);
}
return this;
}
};
而 listenInCluster 函数的内部实现如下(代码片段):
function listenInCluster(server, address, port, addressType,
backlog, fd, exclusive) {
// 如果exclusive是false(默认),则集群的所有进程将使用相同的底层句柄,允许共享连接处理任务。
// 如果exclusive是true,则句柄不会被共享,如果尝试端口共享将导致错误。
exclusive = !!exclusive;
// 引入cluster(集群)模块
// Node.js在单个线程中运行单个实例。 用户(开发者)为了使用现在的多核系统,有时候,
// 用户(开发者)会用一串Node.js进程去处理负载任务。
if (cluster === null) cluster = require('cluster');
// 当该进程是主进程时,返回true。
if (cluster.isMaster || exclusive) {
// Will create a new handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd);
return;
}
}
我们继续来看一下 _listen2() 方法(代码片段):
// Server.prototype._listen2 = setupListenHandle;
function setupListenHandle(address, port, addressType, backlog, fd) {
if (this._handle) {
debug('setupListenHandle: have a handle already');
} else {
debug('setupListenHandle: create a handle');
var rval = null;
if (rval === null)
rval = createServerHandle(address, port, addressType, fd);
this._handle = rval;
}
// 此处绑定onconnection函数
this._handle.onconnection = onconnection;
}
以上代码的核心在于 createServerHandle 函数,所以我们继续来分析一下该函数(代码片段):
function createServerHandle(address, port, addressType, fd) {
var handle;
var isTCP = false;
// server.listen(handle[, backlog][, callback])
// 启动一个服务器,监听已经绑定到端口、UNIX域套接字或Windows命名管道的给定句柄上的连接。
// 句柄对象可以是服务器、套接字(任何具有底层_handle成员的东西),也可以是含有fd成员的对象,
// 该成员是一个有效的文件描述符。
if (typeof fd === 'number' && fd >= 0) {
try {
handle = createHandle(fd, true);
} catch (e) {
// Not a fd we can listen on. This will trigger an error.
debug('listen invalid fd=%d:', fd, e.message);
return UV_EINVAL;
}
handle.open(fd);
handle.readable = true;
handle.writable = true;
} else if (port === -1 && addressType === -1) { // 处理UNIX domain socket或Windows pipe
handle = new Pipe(PipeConstants.SERVER);
} else {
handle = new TCP(TCPConstants.SERVER); // 创建TCP服务
isTCP = true;
}
return handle;
}
function createHandle(fd, is_server) {
// 基于文件描述符确认handle的类型,TTY(文本终端)
const type = TTYWrap.guessHandleType(fd);
if (type === 'PIPE') {
return new Pipe(
is_server ? PipeConstants.SERVER : PipeConstants.SOCKET
);
}
if (type === 'TCP') {
return new TCP(
is_server ? TCPConstants.SERVER : TCPConstants.SOCKET
);
}
}
需要注意的是 createHandle 函数中的 Pipe 和 TCP 类内部是由 C++ 实现:
const { TCP, constants: TCPConstants } = process.binding('tcp_wrap');
const { Pipe, constants: PipeConstants } = process.binding('pipe_wrap');
在 createServerHandle 函数内部,如果是创建 TCP 服务器,只需调用 new TCP(TCPConstants.SERVER) 即可。现在我们来简单总结一下示例中创建 TCP 服务器的过程:
- 调用
net.createServer()方法创建 server 对象,该对象创建完后,我们调用listen()方法执行监听操作。 - 在
listen()方法内,将解析相关参数,然后调用listenInCluster()方法。 - 由于当该进程是主进程,所以
listenInCluster()方法内会直接调用_listen2()方法。 - 因为
_listen2是指向setupListenHandle函数,所以最终调用的是setupListenHandle函数。该函数的主要作用是调用createServerHandle函数创建对应的 handle 对象(本示例为 TCP 对象),并为该对象设定onconnection处理器,然后在把返回的对象赋值给 server 对象的 _handle 属性。 - 最后当服务器接收到连接请求时,就会调用
onconnection处理器,随后创建 Socket 对象,并触发connection事件,然后就会执行我们设置的 connectionListener 监听函数。
UNIX Domain Socket
在预备知识章节,我们了解到 UNIX Domain Socket 用于 IPC (Inter-Process Communication)更有效率:
不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。
接下来我们就来介绍一下,如何创建简单的 UNIX 域套接字服务器。
在 createServerHandle 函数中,不知道小伙伴们有没有注意到 port === -1 && addressType === -1 这一行,竟然有 port 和 addressType(一般为 4 或 6 即表示 IPv4 或 IPv6)为 -1 的情况。其实这就是创建 UNIX domain socket 或 Windows pipe 服务器的场景。
最后我们来创建一个 UNIX 域套接字服务器(实现 echo 功能),具体的示例如下:
const net = require("net");
const server = net.createServer(c => {
c.on("end", () => {
console.log("client disconnected");
});
c.write("hello\r\n");
c.pipe(c);
});
server.on("error", err => {
throw err;
});
// server.listen(path[, backlog][, callback]) for IPC servers
server.listen("/tmp/echo.sock", () => {
console.log("server bound");
});
成功运行服务器后,我们就可以用前面介绍的 nc 命令来连接 UNIX 域套接字服务器:
$ nc -U /tmp/echo.sock
命令执行后,控制台首先会输出 hello ,当我们输入任何消息后,UNIX 域套接字服务器也会返回同样的消息:
➜ ~ nc -U /tmp/echo.sock hello semlinker semlinker i love node i love node
有兴趣的小伙伴可以亲自动手试一试,体验一下上面 echo 服务器。
总结
本文通过两个简单的示例,分别介绍了如何创建简单 TCP 和用于 IPC 的 UNIX Domain Socket 服务器,同时也介绍了 Socket、Nagle 算法、nc 命令等相关的知识。其实 Node.js 的 Net 模块还有挺多知识点的,比如核心的 Socket 类,这里就不做进一步介绍了。如果想更全面和深入了解 Net 模块的小伙伴,建议阅读相关的文章或源码。
以上所述就是小编给大家介绍的《深入学习 Node.js Net》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
TensorFlow实战
黄文坚、唐源 / 电子工业出版社 / 2017-2-1 / 79
Google近日发布了TensorFlow 1.0候选版,这个稳定版将是深度学习框架发展中的里程碑的一步。自TensorFlow于2015年底正式开源,距今已有一年多,这期间TensorFlow不断给人以惊喜,推出了分布式版本,服务框架TensorFlow Serving,可视化工具TensorFlow,上层封装TF.Learn,其他语言(Go、Java、Rust、Haskell)的绑定、Wind......一起来看看 《TensorFlow实战》 这本书的介绍吧!