本文分享以下内容
- Unsafe概况
- AbstractUnsafe 分析
- AbstractNioUnsafe分析
- NioByteUnsafe分析
- NioMessageUnsafe分析
Unsafe概况
unsafe是Channel的内部逻辑实现,实际的IO操作都是由unsafe完成的。
unsafe调用原生的NIO API处理IO操作,使用Pipeline传递触发的channel事件,使用EventLoop进行无锁化处理。
unsafe调用原生Nio执行bind(),register(),触发各种事件 ,更新网络标识位。
类继承图如下
NioByteUnsafe用于客户端,NioMessageUnsafe用于服务端
AbstractUnsafe 分析
举例一个典型方法register
@Override
public final void register(final ChannelPromise promise) {
if (eventLoop.inEventLoop()) {//根据 eventLoop 属性判断 是否 使用 线程池执行
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
closeForcibly();// 调用原生 NIO 关闭
closeFuture.setClosed();//设置关闭
promise.setFailure(t);// save Exception
}
}
}
private void register0(ChannelPromise promise) {
try {
if (!ensureOpen(promise)) {
return;
}
doRegister();//由子类实现
registered = true;
promise.setSuccess();
pipeline.fireChannelRegistered();//使用pipeline 传递 注册事件
if (isActive()) {
pipeline.fireChannelActive();
}
} catch (Throwable t) {
//...
}
}
}
处理逻辑: 先看是否在eventLoop的线程池中执行 ,执行完IO处理,pipeline触发Registered事件.
而 bind,disconnect,close,write 不需要线程控制和触发事件参与。
具体的IO 处理都由子类实现。
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
if (!ensureOpen(promise)) {
return;
}
boolean wasActive = isActive();
try {
doBind(localAddress);//调用原生NIO API 执行bind
} catch (Throwable t) {
promise.setFailure(t);
closeIfClosed();
return;
}
if (!wasActive && isActive()) {
invokeLater(new Runnable() { //提交任务执行bind()
@Override
public void run() {//
pipeline.fireChannelActive();
}
});
}
promise.setSuccess();
}
bind 实现比较简单 没有EventLoop介入。
doBind有2个实现
NioServerSocketChannel-doBind
protected void doBind(SocketAddress localAddress) throws Exception {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
NioSocketChannel-doBind
protected void doBind(SocketAddress localAddress) throws Exception {
javaChannel().socket().bind(localAddress);
}
NioSocketChannel是客户端,这个bind没有啥用。
protected void doDeregister() throws Exception {
//取消当前eventLoop中SelectionKey的所有注册的channel。
eventLoop().cancel(selectionKey());
}
public void beginRead() {
if (!isActive()) {
return;
}
try {
doBeginRead();
} catch (final Exception e) {
//...
}
}
doBeginRead由AbstractNioChannel实现 用于修改网络标识位,NioServerSocketChannel 修改成SelectionKey.OP_ACCEPT,NioSocketChannel 修改成SelectionKey.OP_READ
write和flush方法在写数据有详细分析。
小结:AbstractUnsafe 定义了原生NIO,EventLoop,Pipeline协作处理IO 的框架的多个方法,是Netty IO的操作的模板类。
AbstractNioUnsafe分析
public interface NioUnsafe extends Unsafe {
/** underlying :底层的,可理解为原生NIO的SelectableChannel
* Return underlying {@link SelectableChannel} 返回底层的SelectableChannel
*/
SelectableChannel ch();//获取原生channel
/**
* Finish connect 完成连接
*/
void finishConnect();
/**
* Read from underlying {@link SelectableChannel} //从原生NIO的SelectableChannel读取数据
*/
void read();
void forceFlush();//调用flush0
}
Unsafe 中没定义 read, NioUnsafe 定义了 read 不知道为啥这样搞
AbstractNioUnsafe
实现了 返回原生SelectableChannel的方法
实现了connect的框架
先分析一下 doConnect方法
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
if (localAddress != null) {
javaChannel().socket().bind(localAddress);
}
boolean success = false;
try {
boolean connected = javaChannel().connect(remoteAddress);
if (!connected) {
selectionKey().interestOps(SelectionKey.OP_CONNECT);//line A
}
success = true;
return connected;
} finally {
if (!success) {
doClose();
}
}
}
当SocketChannel设置非阻塞时 connect()不会等待服务端应答,会立即返回false, 当服务应答或连接被拒绝(服务端挂了,或网络不通)会触发 OP_CONNECT事件。netty 将SocketChannel默认设置成非阻塞。
OP_CONNECT事件触发执行finishConnect() ;
服务端接受连接后,客户端需要执行finishConnect()。
@Override
public void finishConnect() {
// Note this method is invoked by the event loop only if the connection attempt was
// neither cancelled nor timed out.
assert eventLoop().inEventLoop();
assert connectPromise != null;
try {
boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
// Use tryFailure() instead of setFailure() to avoid the race against cancel().
connectPromise.tryFailure(t);
closeIfClosed();
} finally {
// Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
// See https://github.com/netty/netty/issues/1770
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
}
}
处理逻辑:验证连接是否成功,如果失败 记录异常信息,尝试执行channel关闭。无论成功失败都执行取消定时任务,将connectPromise赋值为null。
connect是AbstractNioUnsafe中最重要的方法,用于客户端连接服务端。
@Override
public void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (!ensureOpen(promise)) {// 校验SocketChannel isOpen
return;
}
try {
if (connectPromise != null) {//校验 connection是否已经执行
throw new IllegalStateException("connection attempt already made");
}
boolean wasActive = isActive();//记录活动状态
if (doConnect(remoteAddress, localAddress)) {//NioSocketChannel实现,使用原生NIO连接服务端
fulfillConnectPromise(promise, wasActive);//填充执行结果
} else {//还未成功连接 // line 1
connectPromise = promise;
requestedRemoteAddress = remoteAddress;
// Schedule connect timeout.
int connectTimeoutMillis = config().getConnectTimeoutMillis();//默认值30000
if (connectTimeoutMillis > 0) {//添加一个延迟任务,connectTimeoutMillis 毫秒后只执行1次
connectTimeoutFuture = eventLoop().schedule(new Runnable() {{// line 2
@Override//这个任务处理连接超时,记录异常, 执行关闭 将channel从selector上卸载。
public void run() {
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
ConnectTimeoutException cause =
new ConnectTimeoutException("connection timed out: " + remoteAddress);
if (connectPromise != null && connectPromise.tryFailure(cause))
close(voidPromise()); //line 3
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
promise.addListener(new ChannelFutureListener() {// line 4
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {// line5
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
close(voidPromise());
}
}
});
}
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + remoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
promise.tryFailure(t);
closeIfClosed();
}
}
line 2 添加一个延迟任务处理连接超时,简称 "任务A" ,它用于处理连接超时,记录异常, 执行关闭 将channel从selector上卸载。
line 3 执行关闭 将channel从selector上卸载。
结合 doConnect() 的分析,通常会进入line1。如果超时时间大于0,添加任务A。注意当指定完doConnect() 后,当服务应答或连接被拒绝会触发事件调用finishConnect(),只要执行了finishConnect()后续都不会触发定时任务。推测当服务器长时间没有应答才有可能触发任务A。
line4 添加一个listener 执行connectPromise.tryFailure(cause) 后被触发。
line5 代码块内 取消定时任务,记录异常,执行关闭 将channel从selector上卸载。
future 类型是 DefaultChannelPromise future.isCancelled()==true 什么时候?
DefaultPromise-isCancelled0
private static boolean isCancelled0(Object result) {
return result instanceof CauseHolder && ((CauseHolder) result).cause instanceof CancellationException;
}
当result 保存的cause是CancellationException异常是 返回true
DefaultPromise
private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder(new CancellationException());
public boolean cancel(boolean mayInterruptIfRunning) {
Object result = this.result;
if (isDone0(result) || result == UNCANCELLABLE) {
return false;// 操作执行完成 (result==SUCCESS) 或 不能取消
}
synchronized (this) {
// Allow only once.
result = this.result;
if (isDone0(result) || result == UNCANCELLABLE) {
return false;再次检查
}
this.result = CANCELLATION_CAUSE_HOLDER;//设置 被取消
if (hasWaiters()) {
notifyAll();//解除阻塞
}
}
notifyListeners();//触发监听器
return true;
}
DefaultPromise 继承了Future,实现了其 cancel(boolean b)方法,也就是DefaultPromise和它的子类调用了cancel(boolean b)方法,future.isCancelled()==true 才能成立
综上所述:line2,line3 是连接超时处理, 而line 4添加监听器是为了用户主动执行 执行取消后 执行收尾工作。
填充连接用到的ChannelPromise
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
// trySuccess() will return false if a user cancelled the connection attempt.
//trySuccess() 将返回false 如果用户试图取消连接。
boolean promiseSet = promise.trySuccess();//给promise填充 执行成功
// Regardless if the connection attempt was cancelled, channelActive() event should be triggered,不管连接是否被取消,channelActive事件读应该被触发
// because what happened is what happened. 因为 已经发生的就是已经发生的
if (!wasActive && isActive()) {//成功的 connect ,触发 ChannelActive事件。
pipeline().fireChannelActive();
}
// If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {//用户取消连接,执行关闭SocketChannel处理。
close(voidPromise());
}
}
// 真正写数据的地方将缓存好的数据
@Override
protected void flush0() {
// Flush immediately only when there's no pending flush.
// If there's a pending flush operation, event loop will call forceFlush() later,
// and thus there's no need to call it now.
if (isFlushPending()) {
return;
}
super.flush0();
}
@Override
public void forceFlush() {
// directly call super.flush0() to force a flush now
super.flush0();
}
forceFlush 依赖 super.flush0(),在写事件触发时调用。
flush系列的详细分析请看 《写数据》篇
//检查是否是写事件触发的flush
private boolean isFlushPending() {
SelectionKey selectionKey = selectionKey();
return selectionKey.isValid() && (selectionKey.interestOps() & SelectionKey.OP_WRITE) != 0;
}
小结:AbstractNioUnsafe实现了 客户端连接系列方法,重写flush,如果是事件触发则不执行。
NioByteUnsafe分析
重要的方法 只有一个 read()
读数据分析。
@Override
public void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();//默认UnpooledByteBufAllocator
final int maxMessagesPerRead = config.getMaxMessagesPerRead();//默认 16
RecvByteBufAllocator.Handle allocHandle = this.allocHandle;
if (allocHandle == null) {
this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle();
}
if (!config.isAutoRead()) {
removeReadOp();
}
ByteBuf byteBuf = null;
int messages = 0;
boolean close = false;
try {
//allocHandle type is AdaptiveRecvByteBufAllocator$HandleImpl 用于容量预估
int byteBufCapacity = allocHandle.guess();容量预估
int totalReadAmount = 0;//统计读取的总字节数,用于容量预估的根据。
do {
byteBuf = allocator.ioBuffer(byteBufCapacity);//创建ByteBuf type is UnpooledUnsafeDirectByteBuf
int writable = byteBuf.writableBytes();
//会出现一次只能读取部分数据的情况么??问题A
int localReadAmount = doReadBytes(byteBuf);
if (localReadAmount <= 0) {//没读到数据或channel关闭
// not was read release the buffer
byteBuf.release();
close = localReadAmount < 0;//channel 关闭 localReadAmount ==-1
break;
}
pipeline.fireChannelRead(byteBuf);//触发channelRead
byteBuf = null;
if (totalReadAmount >= Integer.MAX_VALUE - localReadAmount) {
// Avoid overflow. 避免 totalReadAmount 数值越界
totalReadAmount = Integer.MAX_VALUE;
break;
}
totalReadAmount += localReadAmount;
if (localReadAmount < writable) {//读取的总数比buf可写数小,说明可读的数据已经读完,跳出循环即可。
// Read less than what the buffer can hold,
// which might mean we drained the recv buffer completely.
break;
}
} while (++ messages < maxMessagesPerRead);//循环超过maxMessagesPerRead次数停止。默认16
pipeline.fireChannelReadComplete();//触发 ChannelReadComplete事件
allocHandle.record(totalReadAmount);//调整容量。
if (close) {// 需要关闭 ,执行关闭流程
closeOnRead(pipeline);
close = false;
}
} catch (Throwable t) {//处理异常
handleReadException(pipeline, byteBuf, t, close);
}
}
}
问题A 需要分解为2个文问题
客户端发送过来的数据是否会只读到一部分?? 只要select() 一次获取到数据 就会全部读取。
问题A 会不会只读取了一部分 发送过来的数据??会的 ,因为 TCP 发送数据包 会有粘包的情况。
NioServerSocketChannel-doReadBytes
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
return byteBuf.writeBytes(javaChannel(), byteBuf.writableBytes());
}
从SocketChannel中读取数据到byteBuf中去
容量分配器AdaptiveRecvByteBufAllocator分析
这段代码获取了容量分配器
this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle();
跟踪代码
public RecvByteBufAllocator getRecvByteBufAllocator() {
return rcvBufAllocator;
}
DefaultChannelConfig
private volatile RecvByteBufAllocator rcvBufAllocator = DEFAULT_RCVBUF_ALLOCATOR;
private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = AdaptiveRecvByteBufAllocator.DEFAULT;
AdaptiveRecvByteBufAllocator
public static final AdaptiveRecvByteBufAllocator DEFAULT = new AdaptiveRecvByteBufAllocator();
private AdaptiveRecvByteBufAllocator() {
this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);//this(64,1024,65536)
}
static {
List<Integer> sizeTable = new ArrayList<Integer>();
for (int i = 16; i < 512; i += 16) {
sizeTable.add(i);
}
for (int i = 512; i > 0; i <<= 1) {
sizeTable.add(i);
}
SIZE_TABLE = new int[sizeTable.size()];
for (int i = 0; i < SIZE_TABLE.length; i ++) {
SIZE_TABLE[i] = sizeTable.get(i);
}
}
SIZE_TABLE数据:[16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320,336,352,368,384,400,416,432,448,464,480,496,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824]
SIZE_TABLE 是一个容量预估的数组,每个元素都是一个字节数, 512 之前 从16开始,每次递增16,512后每次 2倍。
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
//...
int minIndex = getSizeTableIndex(minimum);//二分查找获取minimum的位置
if (SIZE_TABLE[minIndex] < minimum) {//调整预估值,使预估值一定大于等于minimum
this.minIndex = minIndex + 1;
} else {
this.minIndex = minIndex;
}//minIndex 最后等于3
int maxIndex = getSizeTableIndex(maximum);
if (SIZE_TABLE[maxIndex] > maximum) {
this.maxIndex = maxIndex - 1;
} else {
this.maxIndex = maxIndex;//
}//minIndex 最后等于38
this.initial = initial;//1024
}
其内部类创建
public Handle newHandle() {
return new HandleImpl(minIndex, maxIndex, initial);// minIndex==3,maxIndex==38,initial==1024
}
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
index = getSizeTableIndex(initial);//二分查找获取initial的index
nextReceiveBufferSize = SIZE_TABLE[index];
}
上面的read方法 调用了 HandleImpl-guess 获取预估容量
public int guess() {
return nextReceiveBufferSize;
}
可知返回的是nextReceiveBufferSize 初始化HandleImpl,第一次获取 是1024
@Override
public void record(int actualReadBytes) {
//实际读取bytes数小于等于较小的容量数进缩容
// 注意 index 是 HandleImpl构造方法 中获取的, 是当前已经预估的容量所在的索引值
if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) {//INDEX_DECREMENT ==1 相当于同时减2
if (decreaseNow) {//是否立即缩容
index = Math.max(index - INDEX_DECREMENT, minIndex);//INDEX_INCREMENT==4
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
//实际读取bytes数大于等于当前的容量数进行扩容
} else if (actualReadBytes >= nextReceiveBufferSize) {
index = Math.min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
}
record方法 通过实际读取字节数与 预估的较小和较大容量对比,调整预估容量,实现了数组的容量调整
小结:AdaptiveRecvByteBufAllocator通过容量增长经验定义一个 容量预估的SIZE_TABLE ,然后通过实际读取字节数与 预估的较小和较大容量对比,调整预估容量,实现了 容量预估,和容量调整的功能。。
NioMessageUnsafe分析
服务端用到的Unsafe
private final List<Object> readBuf = new ArrayList<Object>();
NioMessageUnsafe中最重要的就是read方法了,这个方法用于初始化NioSocketChannel。
详情参照《NioSocketChanel初始化》篇
下面总结一下NioByteUnsafe和NioMessageUnsafe的区别
它们主要是read方法的区别,下面根据功能,使用频率,重要度区分。
NioByteUnsafe:read方法读取channel数据 常用 重要
NioMessageUnsafe:read方法初始化 NioSocketChannel 常用 重要
write方法也很重要,但不是NioByteUnsafe和NioMessageUnsafe的主要实现,详细见《写数据》篇。
总结:本文分析了Unsafe体系的各个类以及其重要的方法,AbstractUnsafe实现IO操作处理的框架,AbstractNioUnsafe实现了客户端连接系列方法强制刷新系列方法,NioByteUnsafe实现客户端读取数据的read方法,NioMessageUnsafe实现了 初始化 NioSocketChanne的read方法。