网络编程常见问题
InetSocketAddress
前段时间在使用 Netty 开发服务器时想在通信建立时加一个日志,用于打印连接过来的客户端信息(IP 和 Port),但是发现建立连接的过程很慢,于是有了下面的代码:
1 |
|
查看控制台发现获取信息竟然用了 7000+ ms。
2023-04-02 19:52:44.300 INFO 96712 --- [ntLoopGroup-3-1] o.l.xxx.netty.handler.ServerMsgHandler : 客户端通道已经建立完成, 客户端 IP: xxx.xxx.xxx.xxx,Port: xxxxx, 用时 7518 ms
去查了一下才了解到 InetSocketAddress 的 getHostName() 方法进行一个反向主机名查找,这个方法调用的性能取决于 JVM 和目标主机的域名服务器之间的网络性能,也就是说这个方法引发了一个系统调用执行反向查找。如果仅仅是想获取客户端的 IP 可以通过以下方法获取:
1 |
|
Netty 的 LengthFieldBasedFrameDecoder 使用
LengthFieldBasedFrameDecoder 是 netty 解决拆包粘包问题的一个重要的类,主要结构就是 header + body 结构。我们只需要传入正确的参数就可以发送和接收正确的数据。下面我们就具体了解一下这几个参数的意义。先来看一下LengthFieldBasedFrameDecoder主要的构造方法:
1 |
|
参数含义如下:
- maxFrameLength:最大帧长度。也就是可以接收的数据的最大长度。如果超过,此次数据会被丢弃;
- lengthFieldOffset:长度域偏移。就是说数据开始的几个字节可能不是表示数据长度,需要后移几个字节才是长度域;
- lengthFieldLength:长度域字节数。用几个字节来表示数据长度;
- lengthAdjustment:数据长度修正。因为长度域指定的长度可以是 header +
body 的整个长度,也可以只是 body 的长度。如果表示 header + body
的整个长度,那么我们需要修正数据长度;
- 默认值为 0
- initialBytesToStrip:跳过的字节数。如果你需要接收 header + body
的所有数据,此值就是 0 ,如果你只想接收 body 数据,那么需要跳过 header
所占用的字节数。
- 可以通过这个字段来只传递数据段
Netty 中 ChannelHandler 并发安全分析
如果 ChannelHandler 是非共享的,则它就是线程安全的,原因:当链路完成初始化会创建 ChannelPipeline ,每个 channel 对应一个 ChannelPipeline 实例,业务的 ChannelHandler 会被实例化并加入 ChannelPipeline 中执行,由于某个 Channel 只能被特定的 NioEventLoop 线程执行,因此 ChannelHandler 不会被并发调用,不用考虑线程安全问题。
1 |
|
如果只初始化一次业务 ChannelHandler ,然后加到多个 Channel 的 ChannelPipeline ,由于不同的 Channel 可能绑定不同的 NioEventLoop 线程,这样 ChannelHandler 就可能被多个 I/O 线程访问,存在并发访问风险了。
1 |
|
如果某个 ChannelHandler 需要全局共享,则通过 Sharable 注解就可以被添加到多个 ChannelPipeline 。
当 ChannelHandler 被添加到多个 ChannelPipeline ,就会面临多线程并发访问问题,需要 ChannelHandler 保证自身的线程安全,例如通过原子类、读写锁等方式对数据做并发保护。如果加锁,可能会阻塞 NioEventLoop 线程,所以 Sharable 注解的 ChannelHandler 要慎用。