内容简介:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
XML Hacks
Michael Fitzgerald / O'Reilly Media, Inc. / 2004-07-27 / USD 24.95
Developers and system administrators alike are uncovering the true power of XML, the Extensible Markup Language that enables data to be sent over the Internet from one computer platform to another or ......一起来看看 《XML Hacks》 这本书的介绍吧!