UNIX网络编程IO模型分类
- 阻塞IO
- 非阻塞IO
- IO复用(select/poll/epoll)
- 信号驱动
- 异步IO
poll相比select优势
- FD数目不受限制
- IO效率不随着FD数目增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
- API更为简单
套接字read接口阻塞场景
- 有数据可读
- 可用数据已经读取完毕
- 发送空指针或者IO异常
伪异步IO(线程池方案)的问题
- 服务端处理缓慢,返回应答耗费60s,平时只需要10ms
- 线程读取故障节点由于读取输入流堵塞,因而会被阻塞60s
- 所有可用线程都被阻塞,后续消息只能在队列中排队等待
- 线程池通常采用阻塞队列实现,队列满之后,后续入队的操作将被阻塞
- 前端只有一个Acceptor接收客户端请求,新的客户端请求将会被拒绝,产生连接超时
- 几乎所有连接超时,调用方则认为系统已经崩溃
为何不使用原生NIO编程
- API复杂,需要熟练掌握Selector,ServerSocketChannel,ByteBuffer等
- 需要具备其他技能铺垫,例如多线程编程,网络编程
- 可靠性低,工作量与难度都很大
- JDK NIO本身也可能有bug
Why Netty
- API简单
- 功能强大,预置多种安编码解码,支持多种协议
- 定制能力强,可通过ChannelHandler对框架灵活扩展
- 性能高
- 成熟稳定,社区活跃,经历大规模商业应用考验
TCP粘包和拆包
- 服务端一次性收到两个数据包,D1和D2粘合在一起,成为粘包
- 服务端分两次读取两个数据包,第一次读取了完整的D1和D2部分内容,第二次读取到了D2剩余内容,成为拆包
- 如果TCP滑动窗口笔记小,数据包又比较大,那么容易发生多次拆包
产生的原因:
- 应用程序写入的字节大小大于套接字发送缓冲区大小
- 进行MSS大小的TCP分段
- 以太网帧的payload大于MTU进行IP分片
如何处理:
- 消息定长,不够的话使用空格填充
- 包尾使用会车换行分割
- 消息头+消息体
- 更复杂协议
通常Netty使用LineBasedFrameDecoder和StringDecoder来解决粘包问题
Netty高性能
- 异步非阻塞IO类库,基于Reactor模式实现
- TCP接收和发送缓冲区使用直接内存代替堆内存,避免内存复制
- 支持通过内存池的方式循环利用ByteBuf,避免频繁创建和销毁ByteBuf带来额外开销
- 可配置的IO线程数,TCP参数等,满足不同应用场景需要
- 采用环形数组缓冲区实现无锁化并发编程
- 合理使用线程安全容器,原子类
- 关键资源处理使用线程串行方式,避免并发访问带来的锁竞争
- 通过引用计数器及时申请不再被引用的对象,细粒度的内存管理降低了GC频率
Netty可靠性
- 链路有效性检测
- 内存保护机制
- 优雅停机