netty源码调试过程中遇到的小问题总结

本文通过调试 Netty 源码,探讨了服务端和客户端数据传输中遇到的问题。作者发现数据未被立即发送至服务端,原因是操作系统对 Socket 缓冲区的管理策略。在 Windows 和 Linux 环境下,数据发送行为存在差异,表明操作系统对何时发送数据有不同的策略。通过抓包工具验证,确认数据包并未发送,从而排除了 Netty 缓存策略导致的问题。最后,文章强调了了解操作系统底层知识对于问题排查的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题背景

在一次调试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缓冲区发送策略并没有继续深入了解,这块以后有时间还是需要看一下的。了解底层知识对于撸代码和问题定位排查都是有帮助的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值