网络编程常见问题

InetSocketAddress

前段时间在使用 Netty 开发服务器时想在通信建立时加一个日志,用于打印连接过来的客户端信息(IP 和 Port),但是发现建立连接的过程很慢,于是有了下面的代码:

1
2
3
4
5
6
7
8
public void channelActive(ChannelHandlerContext ctx) {
InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
long startTime = new Date().getTime();
String hostName = socketAddress.getHostName();
String port = String.valueOf(socketAddress.getPort());
long endTime = new Date().getTime();
log.info("客户端通道已经建立完成, 客户端 IP: {},Port: {}, 用时 {} ms", hostName, port, endTime - startTime);
}

查看控制台发现获取信息竟然用了 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
2
3
4
// 获取远程 IP
String hostName = socketAddress.getAddress().getHostAddress();
// 获取远程 Port
String port = String.valueOf(socketAddress.getPort());

Netty 的 LengthFieldBasedFrameDecoder 使用

LengthFieldBasedFrameDecoder 是 netty 解决拆包粘包问题的一个重要的类,主要结构就是 header + body 结构。我们只需要传入正确的参数就可以发送和接收正确的数据。下面我们就具体了解一下这几个参数的意义。先来看一下LengthFieldBasedFrameDecoder主要的构造方法:

1
2
3
4
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip)

参数含义如下:

  • 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
2
3
4
5
6
7
8
.handler(new ChannelInitializer<SocketChannel>(){
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new NoThreadSecurityClientHandler()
);
}
});

如果只初始化一次业务 ChannelHandler ,然后加到多个 Channel 的 ChannelPipeline ,由于不同的 Channel 可能绑定不同的 NioEventLoop 线程,这样 ChannelHandler 就可能被多个 I/O 线程访问,存在并发访问风险了。

1
2
3
4
5
6
7
8
9
10
ChannelHandler clientHandler = new DemoHandler();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(clientHandler);
}
});

如果某个 ChannelHandler 需要全局共享,则通过 Sharable 注解就可以被添加到多个 ChannelPipeline 。

当 ChannelHandler 被添加到多个 ChannelPipeline ,就会面临多线程并发访问问题,需要 ChannelHandler 保证自身的线程安全,例如通过原子类、读写锁等方式对数据做并发保护。如果加锁,可能会阻塞 NioEventLoop 线程,所以 Sharable 注解的 ChannelHandler 要慎用。


网络编程常见问题
http://shijieq.github.io/2023/04/02/Java/网络编程常见问题/
Author
ShijieQ
Posted on
April 2, 2023
Licensed under