最近在学nio,遇到了多路复用这个概念,然后我又想了想自己大学学的《计算机网络》,那个时候学网络总是感觉是一堆空洞洞的理论,因为我学到的和实践没有联系在一起。学习NIO的过程中,觉得对计算机网络又有新的认识,于是打算整理一下对网络的认知。注意本篇文章只是概论,并不会讨论具体的协议,只是搭建一个理解网络的简单模型。

程序和协议的桥梁

如今的互联网已经很普及了,每天的地铁上都会有许多人在用手机做很多事情,比如浏览新闻、玩王者荣耀、刷抖音,这些都是在使用网络应用程序。有趣的是,所有的网络应用都是基于基本相同的编程模型,有着相似的整体逻辑结构,并且依赖相同的编程接口。
网络是很复杂的系统,在这里我能说介绍的是一点点皮毛,我的目标是从程序员的角度建立一个容易理解的网络通信模型。

当我们说起网络通信时,事实上说的是位于两台计算机上的进程之间通过互联网交换信息,如果说我们将网络当做一个黑盒子的话。每个网络应用都是基于客户端-服务器模型的,服务器管控着某些资源,并且通过操纵这些资源向客户端提供某种服务。

粗略的说,进程就是正在运行中的程序,那我们现在要使我们的应用程序获得互联网支持,或者说就是要进行网络通信,但是我们又不能直接使用已经标准化了的互联网的应用协议,那我们应当怎么做呢? 要回答这个问题,实际上就要了解下面介绍的Socket,事实上更完备的回答应该由《网络编程》给出。

Socket

通常各个高级编程语言都有对应的接口或者类库,供开发者调用。 但是事实上程序还是需要借助操作系统才能获得互联网的支持。

对计算机而言,网络又是一种I/O设备,是数据源和接收方。网卡提供了到网络的物理接口(什么你不知道什么叫网卡?,等着我)。从网络上接收的数据从网卡经过I/O和内存总线复制到内存,通常 通过DMA传送。相似地,数据也能从内存复制到网络。
看过我的《操作系统与通用计算机组成原理简论》这篇文章的应该知道程序是无法直接接触硬件的,程序只能通过操作系统提供的服务,来进行I/O操作。同样的从网络上获取信息,发送信息,也是需要调用操作系统提供的接口的。

现在TCP/IP协议软件已经驻留在操作系统中。由于TCP/IP协议族被设计成能运行在多种操作系统的环境中,因此TCP/IP协议标准没有规定应用程序与TCP/IP协议软件如何接口(调用)的细节,而是允许系统设计者能够选择有关API的具体实现细节。

目前来说只有几种可供应用程序使用的TCP/IP的应用程序接口,最著名的就是美国加利福利亚大学伯克利分校为Berkeley UNIX操作系统定义的API,被称为套接字接口(socket interface)。微软在其操作系统中采用了套接字 API,但是有一点不同,我们称之为Windows Socket。AT&T的Unix System V版本定义的接口,简写为TLI(Transport Layer port)

我们可以认为套接字作为进程和运输层协议之间的接口,像下面这样。

请注意: 在套接字以上的进程是受应用程序控制的,而在套接字以下则属于操作系统的控制。因此,只要程序要使用TCP/IP协议进行通信,它就必须调用操作系统提供的网络通信接口。这里的套接字是一个相对来说有些抽象的概念,那么该怎么理解这里的套接字呢? 为什么说这里的套接字? 因为在其他语境下套接字拥有其他语义,但是也叫套接字。我认为这里的套接字的就是一个规则、协约、机制 : 当程序需要使用网络通信时,必须首先调用操作系统提供的Socket接口,也可以称之为发出Socket系统调用,请求操作系统创建一个"套接字"。这个调用的实际效果就是请求操作系统把网络通信所需要的的一些资源(CPU时间,网络带宽、存储器空间等)分配给该应用进程。操作系统为这些资源的总和创建一个套接字描述符的号码(小的整数)来表示,然后将这个套接字描述符返回给应用进程。此后,应用进程所进行的网络操作(建立连接、收发数据、调整网络通信参数等)都必须使用这个套接字描述符。

所以几乎所有的网络调用都把这个套接字描述符作为作为第一个参数,在调用操作系统提供的网络通信接口时,通过套接字描述符,就可以识别应该使用哪些资源来完成应用进程所请求的服务。

通信完毕后,应用程序通过一个关闭套接字的close调用通知操作系统回收与该套接字描述符相关的所有资源。

这里可能有些抽象,通俗的说,我们可以将Socket理解为一份合约,这份合约由操作系统提供,合约上规定了网络通信的一些事宜,应用进程遵循此合约,即可享受到操作系统提供的网络通信服务。

一个操作系统同时存在多个网络应用程序是十分自然的,因此需要有一个存放套接字描述符的表,而每一个套接字描述符都有一个指针指向存放套接字的地址。

Socket的不同语义

Socket在不同的语境,有不同的语义。

  • 在TCP连接下的语义

我们知道TCP将连接作为最基本的抽象,每一套TCP连接有两个端点,TCP连接的端点即为套接字(Socket)。根据RFC 793的定义: 端口号拼接到IP地址即构成了套接字。因此套接字的表示方法是在点分十进制的ip地址后面写上端口号,中间用冒号或逗号隔开。总之我们有

socket = (ip地址:端口号)
每一条TCP连接唯一地被通信两端的套接字所确定。
  • 高级语言访问互联网的接口,即运输层和应用层的一个接口,也可以称之为socket.
java 中就有一个叫Socket的类。
  • 操作系统内核中与互联网通信的加利福利亚大学伯克利分校实现,称之为socket实现。

网络通信的过程

我们以操作系统提供的TCP协议服务来介绍,网络通信的过程。

  • 套接字 等待连接所做的准备工作

首先进程调用操作系统提供的服务创建套接字,操作系统将对应的资源分配给对应申请的进程。在套接字创建后它的端口号和IP地址都是空的。因此应用进程需要绑定ip地址和端口号来指明套接字的本地地址,这里的绑定也是操作系统提供的服务,调用绑定服务事实上就是将ip地址和端口号填写调已创建的套接字中。

我们知道TCP是面向连接的,那么此时仅仅创建套接字是不够的,应当调用操作系统提供的监听服务(我们常常称为listen),将套接字设置为被动方式,以便随时接受客户的服务请求。UDP服务由于只提供无连接服务,不使用listen服务。

再接着应用程序就调用接受服务,这个接受我们常常称之为accept服务,以便把连接请求提取出来。

调用accept服务之后要完成的动作比较多,因为服务器必须同时处理多个连接。这块在java的方案是多线程来处理,一个线程处理一个连接,当连接过多的时候,我们就需要换别的思路了,这也就是下文提到的多路复用。

  • 数据传送阶段
这个其实就是连接建立之后,客户端和服务端互相发送数据的过程。
  • 连接释放阶段
我们上文讲到创建套接字是要向操作系统申请资源的,我们也知道TCP的连接时间不可能无限制的一直连接下去,当连接关闭,就需要释放向操作系统申请的资源。

I/O 多路复用

我们知道在客户端比较少的时候,采用多线程去处理客户端的连接是没有什么问题的,这种模型一般被称为多线程并发模型,
线程在某种意义上可以称之为轻量级的进程,所以这种模型也被称为多进程并发模型。

现在常常也是多核CPU,能充分的利用多核。但是这个模型也有不合理之处,我们需要为每一个客户端创建一个线程。不管客户端是否有发送数据。客户端一旦变多,这种开销就变得难以承受起来。

我们来分析一下这个模型不合理在哪里? 服务端一直在随时监听客户端的连接,当连接建立完成之后,服务端不管客户端有没有数据发过来就随即启动一个线程处理这个客户端的通信。很多线程在数据还未到来就处于空闲状态,相当的浪费。改进的方案就是抽出一个线程来记录这些客户端的状态。这个线程就是管理者,连接建立好了,数据还没来,先不启动线程,有数据来了再处理,这样的好处就是减少了线程开销,不必是每一个客户端都一个线程,建立连接,客户端的数据总会有先后顺序,谁先到来我就处理谁。
再重复一遍,连接就绪了服务端进程暂时不处理,哪个客户端的数据到来就处理谁。这就是多路复用,英文名是 I/O multiplexing。

Linux下的select,poll,epoll都是I/O多路复用的具体实现,为什么会有三个呢? 因为这三个的出现是有先后顺序的。
widnows下多路复用的具体实现为 select function (winsock2.h) - Win32 apps | Microsoft Docs

具体的可以参看《IO 多路复用是什么意思?》

参考资料:


北冥有只鱼
147 声望35 粉丝