当前位置: 首页 > 科技观察

深入理解ports的本质和Node.jsSocket的本质

时间:2023-03-17 10:09:48 科技观察

作为web工程师,我们每天都在和ports、socket打交道。许多人可能会使用它们,但当问及它们的本质时,答案可能是极少数。在本文中,让我们探索端口和套接字的本质。端口我们的网络是分层的,OSI分为7层,TCP/IP简化为5层或4层。网络层主要是IP协议,是路由器相关的协议。它的功能是将数据从一台主机传输到另一台主机。到达另一个主机后呢?每个主机都有很多进程,你怎么知道交给哪个进程呢?这就是传输层的TCP和UDP所做的。如何定位主机的进程?直接指定进程id可以吗?例如,x.x.x.x:进程ID。这种设计是可以的,但是进程id是动态的,不是固定的。可能下次重启某个服务进程时,进程id会变。所以我必须继续思考。加个中间层怎么样?难道不可以加一个中间层来解决计算机中的所有问题吗?数据不是直接给进程的,而是放在内存的某一段。这段内存称为一个端口,进程监听这个端口的数据。这样就不需要固定进程id,只需要将进程绑定到这个内存(端口)上,然后监听它的变化即可。这并不直接依赖于具体的实现,而是双方都依赖于抽象层的思想被称为IOC(inverseofcontrol控制反转)。为什么叫港口呢?因为在硬件中也存在端口的概念,如图:硬件的端口是设备与外界通信的入口,软件的端口也是同理定位,所以命名为端口被使用。这样我们就需要IP+端口+协议来定位一个进程在网络上的位置。这些是进程网络地址的三个元素。我们可以看到TCP、IP等协议是协同工作的,所以称为TCP/IP协议族。端口的本质是内存中的数据结构。我们可以监控它的变化,并在数据写入时接收消息。那么每个进程都指定端口就太麻烦了。如果协议可以统一,那一定是端口。这样只能访问protocol+ip,端口自动填满。所以有一个专门的组织来协调这些。这个组织叫做IANA(TheInternetAssignedNumbersAuthority),即互联网号码分配机构。因为网络不是中心化的,所以需要一个中介机构来协调各方。这个机构做的,包括域名、端口、协议等。端口是一个16位的二进制数,两个字节,所以范围是0到65535的整数,IANA把它们分成3段:0到1023是公认的端口,协议绑定固定的端口,比如如HTTP是80,HTTPS是443等。1024到49151是可注册的端口,我们绑定进程的时候从这里选择。49152到65535是动态分配端口,用于一些需要分配端口的进程,动态取自这里。通过固定协议的端口,我们只需要protocol+ip就可以在网络中定位一个进程。当然有时候还是需要指定协议+ip+端口。socket有了端口之后,我们就可以定位到网络中的进程,然后进行数据通信。但是不同协议的数据结构是不同的,也就是说需要不同的操作。直接操作网络传过来的数据比较复杂。这件事应该由操作系统来封装。因此,POSIX为套接字定义了标准的API,通过它我们可以方便地操作不同协议的数据。(关于POSIX可以看我的文章:Node.jsapi设计源码:POSIX)socketapi分为server和client两个方面:server:bind,listen,accept,read,write,closeclient端:connet,write,read,closePOSIX的思想是一切皆文件,所以网络通信的socketapi也设计成read和write的形式。服务端通过listen将进程绑定到端口,客户端连接到服务端的某个端口,通过网络向该端口传输数据,然后读写数据。各种语言都封装了socketapi,Node.js也不例外。Node.js中的SocketNode.js的文件读写是通过stream进行的,而POSIX将网络操作socket视为文件读写,所以Node.js的socket也是stream形式的API。服务器套接字API:constnet=require('net');constserver=net.Server((socket)=>{console.log('clientconnected');socket.on('data',(data)=>{console.log(data.toString('UTF-8'))})socket.on('end',()=>{console.log('clientdisconnected');});socket.write('你好\r\n');});server.on('error',(err)=>{throwerr;});server.listen(8124,()=>{console.log('serverbound');});可以看出是读写的形式,因为Node.js封装成流,所以监听数据事件。(关于stream可以看我的文章:彻底掌握Node.js的四大stream,解决burstbuffer的“背压”问题)Clientsocketapi:constnet=require('net');constsocket=net.Socket({host:'xxxx',port:8124},()=>{console.log('connectedtoserver!');client.write('world!\r\n');});socket.on('data',(data)=>{console.log(data.toString());client.end();});socket.on('end',()=>{console.log('disconnectedfromserver');});直接new的方式比较麻烦,所以Node.js进一步提供了工厂方法:newServer可以用net.createServernewSocket可以用net.createConnection来进一步简化这个。总结一下网络中两个进程通过ip+port进行通信,通过协议指定数据格式。端口是ioc的一个思想。不直接绑定进程id,而是往端口写数据,进程绑定到这个端口。端口号是一个16位的数字,表示范围是0到65535。IANA把它分为3类使用:0到1024是协议对应的端口,1024到49151是进程可以注册的端口,49152到65535是动态分配的使用端口。网络上的进程可以通过protocol+ip+port这三个元素来定位,具体协议的数据格式不同,所以POSIX规定了一系列的socketAPI,包括bind、read、write、closeonthe服务器端,在客户端读取和关闭。write、close等提供类似于文件读写的API。各种语言封装了这些操作系统的API,Node.js也是如此。Node.js使用流的形式读写文件,所以net.Socket和net.Server也是流的API。为了简化创建,还分别提供了工厂方法net.createConnect和net.createServer。希望这篇文章能帮助你理解端口的本质(内存中用来接收网络数据的数据结构),套接字的本质(POSIX定义的网络通信API),以及熟悉Node.js的netAPI。