网络相关

2020-05-12  本文已影响0人  万福来

网络相关

Linux内核将所有的外部设备都看作一个文件来操作,对一个文件的读写操作都会调用内核提供的一个系统命令,返回一个file descriptor (fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socket描述符。

UNIX提供了5种I/O模型

I/O多路复用技术原理

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
目前支持I/O多路复用的系统调用有select、pselect、poll、epoll;

select/poll 与 epoll 对比

epoll内部实现大概如下:

基于BIO实现的伪异步I/O模型

采用单线程循环处理accept事件,当有客户端接入时,将客户端的socket封装成一个实现Runnable的Task放到后端的线程池中进行,线程池中维护一个消息队列N个活跃线程对队列中的任务进行异步处理。队列大小和线程数量可以固定大小,资源占用也就可控的,不用为每个客户端创建一个线程。

基于I/O多路复用实现NIO

NIO 是利用了单线程轮询事件的机制,通过高效地遍历就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。

  1. 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色;
  2. 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求;
  3. 为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常;
  4. Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒;
  5. 然后根据selectionKey与客户端建立链接,链接成功后,获取ServerSocketChannel通道,
    并将该通道向Selector注册,开始关注通道可读状态;
  6. 当有客户端写入数据,通道状态为可读状态时,selector线程会通知开始处理读取客户端数据;
  7. 主要通过缓冲区ByteBuffer来读取数据,读是非阻塞的。
  8. 读完客户端数据,开始写处理,写也是非阻塞的。

缓冲区 Buffer

在NIO类库中,所有数据都是通过缓冲区处理的,在读取数据时,直接读到缓冲区中,写入数据时,写到缓冲区中。任何时候访问NIO中的数据都是通过缓冲区进行操作。
缓冲区本质是数组,常用的实现类有


image.png

通道 Channel

Channel 与InputStream/OutputStream区别
Channel 是一个通道,可以用于读、写或者同时读写,是全双工的,可以双向流通,不阻塞;
Channel 主要有两类,分别用于网络读写的和文件读写操作;
InputSteam 是输入流,只能用于读数据,而且没有数据时会一直阻塞;
OutputStream 是输出流,只能用于写入输出数据;

多路复用器 Selector

Selector可以通过一个线程轮询注册在其上的Channel,如果某个channel上有了新的TCP连接接入、读和写事件,这个channel就处于就绪状态,会被selector轮询出来,然后通过selectionKey可以获取就绪channel的集合,进行后续的I/O操作。
一个Selector可以同时轮询多个Channel,在jdk5update10版本中进行了优化,使用epoll代替了原来的select/poll实现,没有了最大连接接句柄的限制。

NIO2.0 又叫AIO

NIO2.0引入了新的异步通道的概念,提供了异步文件通道和异步套接字通道的实现,异步通道提供两种方式获取操作结果。

Reactor单线程模型

由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会被阻塞,理论上一个线程可以独立处理所有的IO操作。这时Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分发请求到处理链中。


image.png

对于一些小容量应用场景,可以使用到单线程模型。但对于高负载,大并发的应用却不合适,主要原因如下:
当一个NIO线程同时处理成百上千的链路,性能上无法支撑,即使NIO线程的CPU负荷达到100%,也无法完全处理消息
当NIO线程负载过重后,处理速度会变慢,会导致大量客户端连接超时,超时之后往往会重发,更加重了NIO线程的负载。
可靠性低,一个线程意外死循环,会导致整个通信系统不可用
为了解决这些问题,出现了Reactor多线程模型。

Reactor多线程模型

image.png

相比上一种模式,该模型在处理链部分采用了多线程(线程池)。
在绝大多数场景下,该模型都能满足性能需求。但是,在一些特殊的应用场景下,如服务器会对客户端的握手消息进行安全认证。这类场景下,单独的一个Acceptor线程可能会存在性能不足的问题。为了解决这些问题,产生了第三种Reactor线程模型

Reactor主从模型

image.png

该模型相比第二种模型,是将Reactor分成两部分,mainReactor负责监听server socket,accept新连接;并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,读写网络数据,对业务处理功能,其扔给worker线程池完成。通常,subReactor个数上可与CPU个数等同。

Netty其实本质上就是Reactor模式的实现,Selector作为多路复用器,EventLoop作为转发器,Pipeline作为事件处理器。但是和一般的Reactor不同的是,Netty使用串行化实现,并在Pipeline中使用了责任链模式。Netty可以同时支持Reactor单线程模型、多线程模型和主从模型。默认使用主从模型的变种,
将Reactor分成两部分,mainReactor负责监听server socket,accept新连接;并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,主要负责读写网络数据。
当系统在运行过程中,如果频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。
为了解决上述问题,Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。这也解释了为什么Netty线程模型去掉了Reactor主从模型中线程池。
Netty提供的经过扩展的Buffer相对NIO中的有个许多优势,作为数据存取非常重要的一块,我们来看看Netty中的Buffer有什么特点。

1.ByteBuf读写指针

在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。

2.零拷贝

Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

3.引用计数与池化技术

在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源,因此为了获得对于内存的申请与回收更多的控制权,Netty自己根据引用计数法去实现了内存的管理。Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点,即对于DirectBuffer的申请相比HeapBuffer效率更低,因此Netty结合引用计数实现了PolledBuffer,即池化的用法,当引用计数等于0的时候,Netty将Buffer回收致池中,在下一次申请Buffer的没某个时刻会被复用。

Netty中的关键概念

EventLoopGroup:一个 Netty 程序启动时, 至少要指定一个 EventLoopGroup对象,EventLoopGroup内有很多个EventLoop,负责处理事件;
EventLoop: 负责处理各种事件,相当于NIO的一个selector;
Channel:即通讯的通道,发送接受数据
ChannelPipeline:Channel的数据管道
ChannelHandler:可看作是处理ChannelPipeline中传输的数据的工具
ChannelHandlerContext:使得ChannelHandler可以和ChannelPipeline或其他handler进行交互等。
netty默认采用的是主从Reactor模型,一个是bossGroup,一个是workerGroup;
每个分组都可以分别指定里边的线程数量或者是EventLoop数量。
boosGroup主要负责处理连接事件,连接成功后,会将该连接重新注册到workerGroup,该分组不涉及具体业务逻辑,一般设置一个线程即可,而workerGroup主要用来处理连接成功后的数据读写事件,每个线程都会负责该连接下的所有读写事件,不会进行线程切换,所以该分组线程池设置多些。在Netty中,当有连接请求时,会从EventLoopGroup中拿到一个EventLoop,EventLoop会绑定一个Channel,随后做出具体的处理。每一个请求都有一个EventLoop去处理。EventLoop在这里扮演的角色就相当于一个线程用来监听selector事件,而EventLoopGroup就相当于线程池,负责管理调度EventLoop。

Netty的数据读写流程

那么在完成绑定Channel的操作后,具体怎么处理数据呢?Netty中,将Channel的数据管道抽象为ChannelPipeline,消息数据会在ChannelPipeline中流动和传递。ChannelPipeline是ChannelHandler的容器,持有I/O事件拦截器ChannelHandler的链表,负责对ChannelHandler的管理和调度。ChannelHandler可以看成处理ChannelPipeline中数据的工具,可以处理I/O事件或者拦截I/O操作, 并转发给它所在ChannelPipeline中的下一个handler。我们可以方便地新增和删除ChannelHandler来实现不同业务逻辑的处理。但是ChannelPipeline不是直接管理ChannelHandler的,而是通过ChannelHandlerContext来间接管理。Channel处理框图如下:


image.png

Netty中根据事件源头的不同将handler分为两种:InBound和OutBound。InBound事件通常由I/O线程触发,例如TCP连接建立和关闭、读事件等等,分别会触发相应的handler的方法。而OutBound事件则一般由用户主动发起的网络I/O操作,例如用户发起的连接操作,绑定操作和消息发送操作等,也会分别触发相应的方法。
可以从上图看到,Handler由headHandler到tailHandler组成了一条双向链表,Handler链使用了责任链的设计模式(类似在web开发中的filter和拦截器),当事件传入ChannelPipeline中后,会经过一个个handler的处理。当触发ChannelRead事件的时候,消息将从headHandler至tailHandler依次处理;当调用ChannelHandlerContext的write方法发送消息进行通讯时,消息先从tailHandler开始,经过一系列handler处理后传递至headHandler,最终被添加到消息发送缓冲区后刷新输出。

网络体系结构分层

image.png

TCP/IP基础

TCP/IP 的具体含义

从字面意义上讲,有人可能会认为 TCP/IP 是指 TCP 和 IP 两种协议。实际生活当中有时也确实就是指这两种协议。然而在很多情况下,它只是利用 IP 进行通信时所必须用到的协议群的统称。具体来说,IP 或 ICMP、TCP 或 UDP、TELNET 或 FTP、以及 HTTP 等都属于 TCP/IP 协议。他们与 TCP 或 IP 的关系紧密,是互联网必不可少的组成部分。TCP/IP 一词泛指这些协议,因此,有时也称 TCP/IP 为网际协议群。
互联网进行通信时,需要相应的网络协议,TCP/IP 原本就是为使用互联网而开发制定的协议族。因此,互联网的协议就是 TCP/IP,TCP/IP 就是互联网的协议。

数据包

包、帧、数据包、段、消息
以上五个术语都用来表述数据的单位,大致区分如下:
包可以说是全能性术语;
帧用于表示数据链路层中包的单位;
数据包是 IP 和 UDP 等网络层以上的分层中包的单位;
段则表示 TCP 数据流中的信息;
消息是指应用协议中数据的单位。
每个分层中,都会对所发送的数据附加一个首部,在这个首部中包含了该层必要的信息,如发送的目标地址以及协议相关信息。通常,为协议提供的信息为包首部,所要发送的内容为数据。在下一层的角度看,从上一层收到的包全部都被认为是本层的数据。

传输层中的 TCP 和 UDP

TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP。

TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提高网络利用率等众多功能。
UDP 是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在 UDP 的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。
TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况;而在一方面,UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。

端口号

数据链路和 IP 中的地址,分别指的是 MAC 地址和 IP 地址。前者用来识别同一链路中不同的计算机,后者用来识别 TCP/IP 网络中互连的主机和路由器。在传输层也有这种类似于地址的概念,那就是端口号。端口号用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。

三次握手建立连接

  1. 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认。
  2. 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
  3. 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。

四次握手断开连接

中断连接端可以是客户端,也可以是服务器端。

  1. 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
  2. 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
  3. 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
  4. 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。

通过序列号与确认应答提高可靠性

超时重发机制(TCP重传机制)

以段为单位发送数据

利用窗口控制提高速度

上一篇下一篇

猜你喜欢

热点阅读