netty源码调试过程中遇到的小问题总结
问题背景
在一次调试netty源码的过程,惊讶的发现writeAndFlush方法没有将消息从客户端发送至服务端,起初以为是调试代码有bug, 后来发现加上\n 或者字节数超过10个字节服务端就会收到数据,而且只是第一个数据包是这种情况。想到netty可能会有缓存,数据并没有及时发送,于是顺着这条线进行下面的摸索。
调试代码展示
服务端代码
ServerTest
import com.dzl.im.broker.EventLoopGroupFactory;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ServerTest {
private static final Logger LOGGER = LoggerFactory.getLogger(ServerTest.class);
private static final EventLoopGroup bossGroup = EventLoopGroupFactory.getEvtLoopGroup(1);
private static final EventLoopGroup workerGroup = EventLoopGroupFactory.getSingletonEvtLoopGroup();
private static final int port = 8080;
public static void main(String[] args) {
ServerBootstrap boot = new ServerBootstrap();
boot.group(bossGroup, workerGroup);
boot.channel(NioServerSocketChannel.class);
boot.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ByteBuf delemiter= Unpooled.buffer();
delemiter.writeBytes("&".getBytes());
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, delemiter));
pipeline.addLast(new SimpleServerHandler());
}
});
try {
boot.bind(port).sync();
LOGGER.info("netty server started on port:{}", port);
} catch (Exception e) {
LOGGER.warn("server start error:", e);
}
}
}
SimpleServerHandler
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.Charset;
public class SimpleServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取客户端通道的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof ByteBuf) {
System.out.println(((ByteBuf) msg).toString(Charset.defaultCharset()));
}
//返回给客户端的数据,告诉我已经读到你的数据了
String result = "1111&";
ByteBuf buf = Unpooled.buffer();
buf.writeBytes(result.getBytes());
ctx.channel().writeAndFlush(buf);
System.out.println("==========");
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.err.println("channelActive");
}
}
客户端代码
ClientTest
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.io.UnsupportedEncodingException;
public class ClientTest {
private static final NioEventLoopGroup WORK_GROUP = new NioEventLoopGroup(2);
public static void main(String[] args) throws InterruptedException, UnsupportedEncodingException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(WORK_GROUP).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ByteBuf delemiter= Unpooled.buffer();
delemiter.writeBytes("&".getBytes());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(
Integer.MAX_VALUE, delemiter));
ch.pipeline().addLast(new SimpleClientHandler());
}
});
NioSocketChannel socketChannel = (NioSocketChannel) bootstrap.connect("127.0.0.1", 8080).sync().channel();
ByteBuf buf = Unpooled.copiedBuffer("hello&", CharsetUtil.UTF_8);
socketChannel.writeAndFlush(buf);
}
SimpleClientHandler
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.Charset;
public class SimpleClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
String value = ((ByteBuf) msg).toString(Charset.defaultCharset());
System.out.println("服务器端返回的数据:" + value);
}
//把客户端的通道关闭
ctx.channel().close();
}
}
判断是否是服务端问题
是否是因为netty读取数据策略导致。
我们知道netty采用的主从reactor多线程模型。架构如下图:
因为我只是调试源码,所以服务端主线程和工作线程都只设置1个。通过阅读源码和上面架构图不难发现,netty实现reactor模式的核心类是NioEventLoop,这个类主要是一直循环做三件事件:轮询IO事件,处理IO事件,处理非IO事件。代码如下:
所以我们进入processSelectedKeys这个方法,这个方法是真正处理事件的,点进去:
我们发现这个方法有两个分支,第一种是优化过的,第二种是普通的。Netty会尝试获取权限去操作原生Selector,如果可以,selectedKeys不为null,都是走的优化过的处理方式。我们进入processSelectedKeysOptimized方法。
在这里打个断点待会调试一下看是否是服务端问题,下面分析一下为什么在这里断点,我们知道ServerBootstrap 的bind端口会把NioServerSocketChannel的实例注册到 bossGroup 中 EventLoop 中的 Selector 上,当客户端连接时候会触发我们上面的流程,最终触发到读事件,我们追踪到NioServerSocketChannel的doReadMessages, 它会调用accept方法,产生一个socketChannel用于处理这个客户端后续的请求。
紧接着执行NioServerSocketChannel的pipeline的所有入站handler的channelRead方法,其中包括ServerBootstrapAcceptor,它会把accept生成的socketChannel注册到workEventGoop中的一个EventLoop的selector上用来处理后续的IO事件。(SocketChannel这个是Java原生的channel非nio的,所以会进行一层包装,变成NioSocketChannel)
这个时候我们调试的客户端只要链接上服务端,并且服务端执行了上述accept和注册事件,那么客户端和服务端数据通道就建立。一旦客户端发送了数据,刚才断点的地方selectKeys就不会为空。但是其实断点发现并没有数据到达,压根都没有读数据,所以暂时可以排除服务端问题。
判断是否是客户端问题
是否是因为netty本身写缓存导致。
继续分析源码,点击writeAndFlush方法一直往里,直到AbstractChannelHandlerContext类的invokeWriteAndFlush方法。
这里面会涉及两个方法,一个write一个flush,点进去会发现,调用了出站handler的write()方法和flush()方法,我们知道出站handler的处理顺序是尾到头,所以把目光投向了HeadContext这个handler。它是最终负责将数据写入缓存并刷出数据至socket缓冲区。下面来具体分析一下HeadContext这个类的write以及flush方法。先看一下write,点进去发现最终会调用AbstractUnsafe这个类,然后把数据写入ChannelOutboundBuffer这个缓存类里面,所以说write方法并没有真正的将数据写入socket缓冲区,还得分析flush方法。
继续分析flush方法,最终也是调用AbstractUnsafe这个类的flush方法,它会先将ChannelOutboundBuffer缓存里的数据标记为可刷新,然后调用flush0这个方法,继续点进去发现会调用doWrite()方法,找到AbstractNioByteChannel实现,然后继续往里面点,链路比较深,不拿出来一一说明,最终定位到SocketDispatcher的write0方法,这是个本地方法,用来往socket缓冲区写数据,在这里打个断点。
通过调试发现数据的确是通过这个方法发送到了socket缓冲区,那为什么服务端会没有收到数据并打印出来呢,难道还是服务端哪个环节出了问题。于是我通过抓包工具验证一下这个猜想,看看数据包到底有没有从客户端发送至服务端。发现除了三个握手包以外并没有其他数据包发送,为了对比我又抓了一下数据末尾加上\n的数据包,如下
发现的确是握手完立马发送了一个数据包,由此可以断定,不是服务端的问题。还是客户端没有发生相应的数据包,但是通过断点代码发现数据的确是写入了socket缓冲区,由此把目光投向另外一个地方操作系统的socket缓冲区。
操作系统socket缓冲区相关知识。
一开始我是在windows本地环境调试发现这个问题的。后来将服务部署到linux环境,发现数据没有加\n换行符,也能发送到服务端,由此可以知道不同的操作系统对于socket缓冲区发送策略是不同。从网上搜寻知识也是说数据发送到socket缓冲区,并不是立马刷新出去,至于什么时候会发数据,发多少数据,全听操作系统安排,由此其实问题原因已经清晰了,程序发送第一个数据包至windows的socket缓冲区,发送权交给系统,但是系统并没有立马发送,所以服务端没有收到相应的数据包。如果想要查看socket缓冲区,可以在linux环境下执行 netstat -nt 命令观察缓冲区数据,windows命令没有找到。由于时间问题并没有继续深究下去。
收发数据:
总结
对于操作系统socket缓冲区发送策略并没有继续深入了解,这块以后有时间还是需要看一下的。了解底层知识对于撸代码和问题定位排查都是有帮助的。