Socket

本文最后更新于:星期一, 九月 12日 2022, 1:25 凌晨

OSI七层协议和TCP/IP四层协议里都没有这个socket,它是从哪里出来的?

概念

Socket介绍

Socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。个人认为在OSI七层协议中,它应该属于会话层,正好在传输层之上。上图是按照TCP/IP四层协议来画的,可能不清楚

工作原理

Socket工作原理

服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

图中read/write操作,并不是直接从物理设备把数据读取到内存中,也不是直接把数据写入到物理设备。无论是read,还是write,都会涉及到缓冲区。具体来说,read是把数据从内核缓冲区复制到进程缓冲区,而write是把数据从进程缓冲区复制到内核缓冲区。

在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程),有自己独立的缓冲区,叫作进程缓冲区。所以,用户程序的IO读写(不仅仅是指socket读写),在大多数情况下,并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间直接进行数据的交换

Socket的读写调用流程

读写流程

  • 客户端请求: Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区
  • 获取请求数据: Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入Java进程缓冲区
  • 服务器端业务处理: Java服务器在自己的用户空间中处理客户端的请求。
  • 服务器端返回数据: Java服务器完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到的是write系统调用
  • 发送给客户端: Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端

5种IO模型

所有以下IO过程,都分为两个阶段: 数据的准备和数据的复制

阻塞IO模型(BIO,Blocking IO)

又名同步阻塞IO,最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。

阻塞IO模型

  1. Java启动IO读的read系统调用开始,用户线程就进入阻塞状态
  2. 当系统内核收到read系统调用,就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这个时候内核就要等待
  3. 内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)
  4. 直到内核返回后,用户线程才会解除阻塞的状态,重新运行起来

特点

在内核进行IO执行的两个阶段(数据准备和数据复制),用户线程都被阻塞了

优点

应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU资源

缺点

一般情况下,会为每个连接配备一个独立的线程;反过来说,就是一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,在高并发应用场景下是不可用的

非阻塞IO模型(Non-blocking IO)

又名同步非阻塞IO,应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,就把数据拷贝到用户缓冲区中。

非阻塞IO模型

  1. 在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户线程需要不断地发起IO系统调用
  2. 内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到的用户缓冲区的字节数)
  3. 用户线程读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户进程需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行

特点

应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止

优点

每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

缺点

不断地轮询内核,这将占用大量的CPU时间,效率低下。总体来说,在高并发应用场景下,同步非阻塞IO也是不可用的。一般Web服务器不使用这种IO模型。这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。在实际的Java开发中,也不会涉及这种IO模型

非阻塞IO,可简称为NIO,但它不是Java中的NIO,虽然它们的英文缩写一样,希望大家不要混淆。Java的NIO(New IO),对应的不是非阻塞IO模型,而是IO多路复用模型(IO multiplexing)

IO复用模型(IO multiplexing)

经典的Reactor反应器设计模式,有时也称为异步阻塞IO, Java中的Selector选择器和Linux中的epoll都是这种模型

IO复用模型

  1. 选择器注册。在这种模式中,首先,将需要read操作的目标socket网络连接,提前注册到select/epoll选择器中,Java中对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程
  2. 就绪状态的轮询。通过选择器的查询方法,查询注册过的所有socket连接的就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中。当用户进程调用了select查询方法,那么整个线程会被阻塞掉
  3. 用户线程获得了就绪状态的列表后,根据其中的socket连接,发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区
  4. 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行

特点

IO多路复用模型的IO涉及两种系统调用(System Call),另一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的

优点

与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用

缺点

本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的

信号驱动IO模型(signal driven IO)

应用进程在读取文件时通知内核,如果某个socket的某个事件发生了,请向应用进程发一个信号。收到信号后,信号对应的处理函数会进行后续处理

应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中

异步IO模型(AIO,asynchronous IO)

异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。有点类似Java中的回调模式,用户空间的线程向内核空间注册了各种IO事件的回调函数,由内核去主动调用。

异步IO模型

  1. 当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞
  2. 内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)
  3. 内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了
  4. 用户线程读取用户缓冲区的数据,完成后续的业务操作

特点

在数据准备和数据复制两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数

缺点

应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显的优势。大多数的高并发服务器端的程序,一般都是基于Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型

Netty框架,使用的就是IO多路复用模型,而不是异步IO模型

5种IO模型深入理解

上面那节说的公式化和概念化,可能并不是很好理解,使用参考资料中提供的链接,打个比方进一步说明

钓鱼时,刚开始鱼是在鱼塘里面的,钓鱼动作的最终结束标志是鱼从鱼塘中被人钓上来,放入鱼篓中。

这里鱼塘就可以映射成磁盘,中间过渡的鱼钩可以映射成内核缓冲区,最终放鱼的鱼篓可以映射成用户缓冲区。一次完整的钓鱼(IO)操作,是鱼(文件)从鱼塘(磁盘)中转移(复制)到鱼篓(用户缓冲区)的过程

阻塞IO模型

我们钓鱼的时候,有一种方式比较惬意,比较轻松,那就是我们坐在鱼竿面前,这个过程中我们什么也不做,双手一直把着鱼竿,就静静的等着鱼儿咬钩。一旦手上感受到鱼的力道,就把鱼钓起来放入鱼篓中。然后再钓下一条鱼

这种钓鱼方式相对来说比较简单,对于钓鱼的人来说,不需要什么特制的鱼竿,拿一根够长的木棍就可以悠闲的开始钓鱼了(实现简单)。缺点就是比较耗费时间,比较适合那种对鱼的需求量小的情况(并发低,时效性要求低)

非阻塞IO模型

我们钓鱼的时候,在等待鱼儿咬钩的过程中,我们可以做点别的事情。但是,我们要时不时的去看一下鱼竿,一旦发现有鱼儿上钩了,就把鱼钓上来

这种方式钓鱼,和阻塞IO比,所使用的工具没有什么变化,但是钓鱼的时候可以做些其他事情,增加时间的利用率

信号驱动IO模型

我们钓鱼时,为了避免自己一遍一遍的去查看鱼竿,可以给鱼竿安装一个报警器。当有鱼儿咬钩的时候立刻报警。然后我们再收到报警后,去把鱼钓起来

这种方式钓鱼,和前几种相比,所使用的工具有了一些变化,需要有一些定制(实现复杂)。但是钓鱼的人就可以在鱼儿咬钩之前彻底做别的事儿去了。等着报警器响就行了

IO复用模型

我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来

这种方式的钓鱼,通过增加鱼竿的方式,可以有效的提升效率

以上4种都是同步模型,因为钓鱼过程,可以拆分为两个步骤:1、鱼咬钩(数据准备)。2、把鱼钓起来放进鱼篓里(数据复制)。无论以上提到的哪种钓鱼方式,在第二步,都是需要人主动去做的,并不是鱼竿自己完成的。所以,这个钓鱼过程其实还是同步进行的

异步IO模型

我们钓鱼时,采用一种高科技钓鱼竿,即全自动钓鱼竿。可以自动感应鱼上钩,自动收竿,更厉害的可以自动把鱼放进鱼篓里。然后,通知我们鱼已经钓到了,他就继续去钓下一条鱼去了

这种方式的钓鱼,无疑是最省事儿的。啥都不需要管,只需要交给鱼竿就可以了,但是目前linux实现并不完善

5种模型对比图如下
5种模型对比

参考资料

1.漫话:如何给女朋友解释什么是Linux的五种IO模型?

推荐书单


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!