Socket通信编程浅谈及Netty框架的优势点总结 - Go语言中文社区

Socket通信编程浅谈及Netty框架的优势点总结


Socket之于操作系统/进程

Socket通信在操作系统层面主要体现在I/O多路复用上,即每个进程通过一定的逻辑去检测具体哪个文件描述符(fd)发生了I/O事件。这个逻辑主要有select、poll、epoll/kqueue这几种。

select的缺点在于两次拷贝耗时轮询所有fd耗时,支持的文件描述符受限且太小,其优点在于跨平台支持。

poll的优点在于通过链表存储使得连接数(也就是文件描述符)没有限制,但仍然存在大量拷贝,且是水平触发,会出现当报告了fd没有被处理,会重复报告,很耗性能。

epoll是Linux操作系统中对poll进行改进,epoll分为ET与LT模式,具体如下:

LT延迟处理,当检测到描述符事件通知应用程序,应用程序不立即处理该事件。那么下次会再次通知应用程序此事件。

ET立即处理,当检测到描述符事件通知应用程序,应用程序会立即处理。

ET模式减少了epoll被重复触发的次数,效率比LT高。我们在使用ET的时候,必须采用非阻塞套接口,避免某文件句柄在阻塞读或阻塞写的时候将其他文件描述符的任务饿死。epoll的优点在于没有最大并发连接的限制,只有活跃可用的fd才会调用callback函数,内存拷贝是利用mmap()文件映射内存的方式加速与内核空间的消息传递,减少复制开销。(内核与用户空间共享一块内存)。

kqueueUnix操作系统对poll进行的一些优化后的模式,kqueue与epoll非常相似,最初是2000年Jonathan Lemon在FreeBSD系统上开发的一个高性能的事件通知接口。

下面是上述函数的调用过程:

  • select函数的调用过程
  1. 将fd_set从内核空间拷贝到用户空间
  2. 如果遍历完所有的fd都没有返回一个可读写的mask掩码,就会让select的进程进入休眠模式,直到发现可读写的资源后,重新唤醒等待队列上休眠的进程。如果在规定时间内都没有唤醒休眠进程,那么进程会被唤醒重新获得CPU,再去遍历一次fd。
  3. poll方法会返回一个描述读写是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  4. 调用其对应的poll方法
  5. 注册回调函数
  6. 从用户空间将fd_set拷贝到内核空间
  • poll函数的调用过程(与select完全一致),主要是通过链式结构去掉了文件描述符个数的限制
  • epoll的函数调用流程

  1. 当调用epoll_wait函数的时候,系统会创建一个epoll对象,每个对象有一个evenpoll类型的结构体与之对应

  2. 文件的fd状态发生改变,就会触发fd上的回调函数

  3. 回调函数将相应的fd加入到rdlist,导致rdlist不空,进程被唤醒,epoll_wait继续执行。

  4. 有一个事件转移函数——ep_events_transfer,它会将rdlist的数据拷贝到txlist上,并将rdlist的数据清空。

  5. ep_send_events函数,它扫描txlist的每个数据,调用关联fd对应的poll方法去取fd中较新的事件,将取得的事件和对应的fd发送到用户空间。如果fd是LT模式的话,会被txlist的该数据重新放回rdlist,等待下一次继续触发调用。

  • kqueue的函数调用过程
  1. 注册一批socket描述符到 kqueue
  2. 当其中的描述符状态发生变化时,kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。

Socket之于JDK

JDK在1.4版本增加了NIO,1.7版本增加了NIO2.0,IO是面向流的处理,NIO是面向块(缓冲区)的处理。

BIO是JDK最原始的IO类库,其中的主要类有Socket、ServerSocket、各种Stream和各种Reader等,其实现都是阻塞的,且是直接对byte数组进行读写操作。

NIO主要有三个核心部分组成:Buffer缓冲区、Channel管道和Selector选择器

Buffer缓冲区本质上是可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。相对于直接对数组的操作,Buffer API更容易操作和管理。

Buffer三个重要属性capacity容量、position位置、limit限制。capacity容量是作为一个内存块,Buffer具有一定的固定大小。position位置在写入模式代表写数据的位置,在读取模式时代表读取数据的位置。limit限制在写入模式时等于Buffer的容量,在读取模式时等于写入的数据量。

ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。堆外内存会比堆内存少一次拷贝,堆外内存在GC范围之外,降低GC压力,而且内部有个Cleaner对象(PhantomReference)实现了自动管理,Cleaner对象被GC前会执行clean方法,从而清理了堆外内存。

Channel通道的API涵盖了UDP/TCP网络和文件IO,如FileChannel、DatagramChannel、SocketChannel、ServerScoektChannel。和标准IO Stream操作的区别在于:在一个通道内进行读取和写入,而stream通常是单向的,要么input要目output,通道可以非阻塞读取和写入,通道始终读取或写入缓冲区。

SocketChannel用于建立TCP网络连接,类似java.net.socket。有两种创建socketChannel形式:

  1. 客户端主动发起和服务器的连接。
  2. 服务器获取的新连接。

ServerSocketChannel可以监听新建的TCP连接通道,类似ServerSocket。serverSocketChannel.accept()是非阻塞的,默认没有连接需要处理时直接返回null。

Selector选择器是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已准备好进行读取或写入。实现单个线程可以管理多个通道,从而管理多个网络连接。一个线程使用Selector监听多个channel的不同事件:四个事件分别对应SelectionKey四个常量:Connect连接(OP_CONNECT)、Accept准备就绪(OP_ACCEPT)、Read读取(OP_READ)、Write写入(OP_WRITE)。

Selector实现一个线程处理多个通道的核心在于事件驱动机制,在非阻塞的网络通道下,通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行,其底层是上面提及的IO多路复用。

Socket之于Netty框架

Netty是一个高性能、高可拓展性的异步事件驱动的网络应用程序框架,极大地简化了TCP和UDP客户端和服务器端开发等网络编程,它的四个重要内容:

Reactor线程模型:一种高性能的多线程程序设计思路

重新定义的Channel概念:增强版的通道概念

ChannelPipeline责任链设计模式:事件处理机制

内存管理:增强的ByteBuf缓冲区

下图为Netty官网的结构图,可以看出主要包含三大块:支持Socket等多种传输方式,提供多种协议的编解码实现,核心设计包含事件处理模型、API的使用、ByteBuffer的增强。

Netty实现了Reactor线程模型,Reactor模型有四个核心概念:Resources资源(请求/任务)、Synchronous Event Demultiplexer同步事件复用器、Dispatcher分配器、Request Handler请求处理器。主要是通过2个EventLoopGroup(线程组,底层是JDK的线程池)来分别处理连接和数据读取,从而提高线程的利用率。

Netty中的Channel是一个抽象的概念,可以理解为对JDK NIO Channel的增强和拓展。增加了很多属性和方法。

责任链模式为请求创建了一个处理对象的链,将发起请求和具体处理请求的过程进行解耦,责任链上的处理者负责处理请求,客户只需要将请求发送到责任链上,无须关心请求的处理细节和请求的传递。

ChannelPipeline责任链保存了通道所有处理器信息。创建新channel时自动创建一个专有的pipeline,并且在对应入站事件(通常指I/O线程生成了入站数据,详见ChannelInboundHandler)和出站事件(经常是指I/O线程执行实际的输出操作,详见ChannelOutboundHandler)时调用pipeline上的处理器。当入站事件时,执行顺序是pipeline的first执行到last。当出站事件时,执行顺序是pipeline的last执行到first。处理器在pipeline中的顺序由添加的时候决定。

Netty自己的ByteBuf是为解决JDK的ByteBuffer的问题,如无法动态扩容、API使用复杂。

ByteBuf实现了四个方面的增强:API操作便捷,动态扩容,多种ByteBuf实现,高效的零拷贝机制

ByteBuf的三个重要属性:capacity容量、readerIndex读取位置、writerIndex写入位置。

ByteBuf的capacity默认256字节,最大值Integer.MAX_VALUE即2GB,write*方法被调用时,会先检测是否可写入,若不可写入,则进行扩容,新的capacity的计算方式有2种:在没超过4MB时,从64字节开始,每次增加一倍,直到满足容量需求;超过4MB时,新容量=新容量最小要求/4MB*4MB+4MB。

ByteBuf在堆内堆外存储、是否池化(是否复用ByteBuf对象、内存复用)、访问方式是否safe三种维度进行划分,排列组合后有8种实现类。Netty默认使用的是PooledUnsafeDirectByteBuf

Netty的零拷贝机制,是一种应用层的实现。CompositeByteBuf类将多个ByteBuf合并成一个逻辑上的ByteBuf,避免多个ByteBuf之间的拷贝。wrapedBuffer()方法,将byte[]数组包装成ByteBuf对象。slice()方法将一个ByteBuf对象切分成多个ByteBuf对象。

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/a7151593/article/details/102885109
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 16:04:32
  • 阅读 ( 858 )
  • 分类:Go Web框架

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢