动机
目前用户写入的数据暂存在输出队列 (OutputBuffer) 中, writeable 事件时, nio线程遍历这个队列, 将数据“一个接着一个” 写入 socket.
存在的问题是, 这些数据很可能“小块的”, 以 Linux 的默认 recive buffer size 8KB 为例, 写入总大小 8KB 数据, 如果分为小块, 每次 1KB 则需要 8 次 write
系统调用 , 而如果借助 Scatter/gather I/O 采用合并写, 则只需一次系统调用, 写入 8KB.
the default size of the receive buffer for a TCP
socket. This value overwrites the initial default
buffer size from the generic global
net.core.rmem_default defined for all protocols.
The default value is 87380 bytes. (On Linux 2.4,
this will be lowered to 43689 in low-memory
systems.
参考:
关于 receive buffer 默认值 tcp_rmem
关于 Scatter/gather I/O
方案
**: 使输出缓冲队列( OutputBuffer ) 具有合并 buffer 的能力
如何判断是否应当合并? 这需要知道底层 socket 的 send buffer size, 每当用户通过 write 投入数据时, 检查队列中的“上一个数据” 是否足够大(相对于 send buffer size) , 不够大则合并.
怎么合并?
用户写入的数据可以是 byte[]
、ByteBuffer
、 RingBuffer
、FileChannel
, 其中前 3 个都是可以使用统一的 ByteBuffer 来表示的, 合并后使用 ByteBuffer[] 表示.
这里需要特别注意的一点是 RingBuffer, 其可读出数据可能分布为两段:
writePosition readPosition
│ │
┌───────▼──────────────▼──────────┐
│ unread│ │ unread │
└───────┴──────────────┴──────────┘
可使用如下方法生成两个分片
//class ByteBuffer
public abstract ByteBuffer slice(int index, int length)
在得到 ByteBuffer[] 后, 调用 java 中的 Scatter/gather I/O 接口, 写入数据到 socket
//GatheringByteChannel
public long write(ByteBuffer[] srcs, int offset, int length)
还有一点需要注意, 由于 Lighty 中, 写入是异步的, 用户每次写入都会返回一个 ListenableFutureTask
, 在数据实际被写入 socekt 后, 需要由这个 future 回调用户的 listener.
未合并前, 每次写入都有一个唯一的 future, 而合并后则对应多个. 合并过程中, 必须要维护 buffer 与 future 的映射关系, 可能一对一, 可能多对一(RingBuffer
的情况), 每次合并写后都需要根据实际写入的字节数来判定 ByteBuffer[]
中哪个写完了, 哪个没写完, 写完的就调用对应的 future.
最后还有一点, 按照 Lighty 的隐式约定, conetxt.write()
写入的如果是 RecycleBuffer
类型, 则会在实际写入 socket 后被自动回收, 这个事可以通过 future.addListener 来做.