Netty线程模型

本文介绍Netty的线程模型以及服务端、客户端启动、客户端接入等流程。

Netty Reactor

![](Netty Reactor工作架构图.png)

ServerBootstrap启动流程

Client接入流程

Bootstrap启动流程

TCP粘包/拆包问题

TCP粘包/拆包的基础知识

TCP是一个”流”协议,在业务上认为,一个完整的包可能会被拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包/拆包问题。

由于底层的TCP协议无法理解上层的业务数据,所以在底层不能保证数据包不被拆分和重组,这个问题只能通过上层的应用协议栈设计来解决:

  • 消息定长,不够补空格
  • 在包尾增加回车换行符进行分割
  • 将消息分为消息头和消息体,消息头中包含消息总长度或消息体长度的字段
  • 更复杂的应用层协议

没考虑TCP粘包/拆包的问题案例

TCP粘包导致的读半包问题
查看example模块中的\ TimeServerTcpStickyException 和 TimeClientTcpStickyException:

服务端只收到2条消息,说明客户端发送的消息发生了TCP粘包:

服务端只收到2条消息,因此只发送2条应答,但实际上客户端值收到一条包含2个”BAD ORDER”的消息,说明服务端返回的应答消息也发生了TCP粘包:

使用Netty解决读半包问题

为了解决TCP粘包/拆包导致的问题,Netty默认提供了多种编解码器用于处理半包。

查看example模块中的 TimeServerFixTcpStickyException 和 TimeClientFixTcpStickyException:

分别在服务端和客户端添加 LineBasedFrameDecoder 和 StringDecoder 解决问题。

服务端正常收到客户端的100次请求:

客户端正常收到服务端的100次应答消息:
avatar

问题

Netty的消息可靠性机制

  • 网络通信类故障

    1. 客户端指定连接超时时间
    2. TCP心跳机制
    3. 故障定制:客户端的断连重连机制,消息的缓存重发,接口日志中详细记录故障细节,运维相关功能,例如告警、触发邮件/短信等

select、poll 与 epoll 的区别

IO多路复用:I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。
  IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。

select机制

基本原理:
  客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。一个连接对应一个fd。
优点:
  几乎在所有的平台上支持,跨平台支持性好
缺点:
  由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
  每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
  默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。

select的调用过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

poll机制

  基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制(使用链表的方式存储fd),使用 pollfd 结构而不是 select 的 fd_set 结构。

epoll机制

基本原理:
  没有fd个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。
epoll之所以高性能是得益于它的三个函数
  1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd
  2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
  3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点:
  没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
  效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
  内核和用户空间mmap同一块内存实现(mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)

例子:100万个连接,里面有1万个连接是活跃,我们可以对比 select、poll、epoll 的性能表现
  select: 不修改宏定义默认是1024,l则需要100w/1024=977个进程才可以支持 100万连接,会使得CPU性能特别的差。
  poll: 没有最大文件描述符限制,100万个链接则需要100w个fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。
  epoll: 请求进来时就创建fd并绑定一个callback,主需要遍历1w个活跃连接的callback即可,即高效又不用内存拷贝。