在Netty中,每种Channel都有对应的配置,用ChannelConfig
来表示,ChannelConfig是一个接口,每个特定的Channel实现类都有自己对应的ChannelConfig实现类,如:
1、NioSocketChannel的对应的配置类为NioSocketChannelConfig
2、NioServerSocketChannel的对应的配置类为NioServerSocketChannelConfig
ChannelConfig的部分类图继承关系如下:
在Channel接口中定义了一个方法
config()
,用于获取特定通道实现的配置,子类需要实现这个接口。
public interface Channel extends AttributeMap, Comparable<Channel> {
...
ChannelConfig config();
...
}
通常Channel实例,在创建的时候,就会创建其对应的ChannelConfig实例。例如NioServerSocketChannel和NioSocketChannel都是在构造方法中创建了其对应的ChannelConfig实现。
NioServerSocketChannel
public class NioServerSocketChannel extends AbstractNioMessageChannel
implements io.netty.channel.socket.ServerSocketChannel {
...
private final ServerSocketChannelConfig config;
...
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());//构造方法中创建NioServerSocketChannelConfig实例
}
...
@Override
public ServerSocketChannelConfig config() {//覆写config方法,返回ServerSocketChannelConfig实例
return config;
}
...
}
NioSocketChannel
public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {
...
private final SocketChannelConfig config;
...
public NioSocketChannel(Channel parent, SocketChannel socket) {//构造方法中创建SocketChannelConfig实例
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
...
@Override
public SocketChannelConfig config() {//覆写config方法,返回SocketChannelConfig实例
return config;
}
...
}
Netty提供了一个ChannelOption
类,其定义了ChannelConfig支持的所有参数类型,可以认为ChannelConfig中用了一个Map来保存参数,Map的key是ChannelOption,ChannelConfig 定义了相关方法来获取和修改Map中的值。
public interface ChannelConfig {
...
Map<ChannelOption<?>, Object> getOptions();//获取所有参数
boolean setOptions(Map<ChannelOption<?>, ?> options);//替换所有参数
<T> T getOption(ChannelOption<T> option);//获取以某个ChannelOption为key的参数值
<T> boolean setOption(ChannelOption<T> option, T value);//替换某个ChannelOption为key的参数值
....
}
当我们想修改一个Map中的参数时,例如我们希望NioSocketChannel在工作过程中,使用PooledByteBufAllocator来分配内存,则可以使用类似以下方式来设置
Channel ch = ...;
SocketChannelConfig cfg = (SocketChannelConfig) ch.getConfig();
cfg.setOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
特别的,针对每个ChannelOption选项,还提供了一个便捷的方法来设置参数,例如下面的代码与上述代码是等价的
Channel ch = ...;
SocketChannelConfig cfg = (SocketChannelConfig) ch.getConfig();
cfg.setAllocator(PooledByteBufAllocator.DEFAULT);
因为在ChannelConfig的默认实现DefaultChannelConfig
中,setOption
方法内部还是通过判断ChanneOption方法来选择对应的方法设置的。
io.netty.channel.DefaultChannelConfig#setOption
public class DefaultChannelConfig implements ChannelConfig {
....
public <T> boolean setOption(ChannelOption<T> option, T value) {
validate(option, value);
if (option == CONNECT_TIMEOUT_MILLIS) {//判断ChannelOption类型,再调用对应的方法
setConnectTimeoutMillis((Integer) value);
} else if (option == MAX_MESSAGES_PER_READ) {
setMaxMessagesPerRead((Integer) value);
} else if (option == WRITE_SPIN_COUNT) {
setWriteSpinCount((Integer) value);
} else if (option == ALLOCATOR) {
setAllocator((ByteBufAllocator) value);
} else if (option == RCVBUF_ALLOCATOR) {
setRecvByteBufAllocator((RecvByteBufAllocator) value);
} else if (option == AUTO_READ) {
setAutoRead((Boolean) value);
} else if (option == AUTO_CLOSE) {
setAutoClose((Boolean) value);
} else if (option == WRITE_BUFFER_HIGH_WATER_MARK) {
setWriteBufferHighWaterMark((Integer) value);
} else if (option == WRITE_BUFFER_LOW_WATER_MARK) {
setWriteBufferLowWaterMark((Integer) value);
} else if (option == MESSAGE_SIZE_ESTIMATOR) {
setMessageSizeEstimator((MessageSizeEstimator) value);
} else {//如果不是上述类型,返回false,表示不支持此ChannelOpiton
return false;
}
return true;
}
....
}
从上述代码中,我们还可以注意到另外一点,ChannelConfig接口的默认实现DefaultChannelConfig只支持了部分ChannelOption类型,这是所有的Channel都支持的ChannelOpiton。
ChannelConfig的特定子类在DefaultChannelConfig的基础上,支持更多的ChannelOption 。
ChannelConfig支持的通用ChannelOption
- ChannelOption.CONNECT_TIMEOUT_MILLIS
- ChannelOption.WRITE_SPIN_COUNT
- ChannelOption.ALLOCATOR
- ChannelOption.AUTO_READ
- ChannelOption.MAX_MESSAGES_PER_READ
- ChannelOption.RCVBUF_ALLOCATOR
- ChannelOption.ALLOCATOR
- ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK
- ChannelOption.WRITE_BUFFER_LOW_WATER_MARK
- ChannelOption.MESSAGE_SIZE_ESTIMATOR
- ChannelOption.AUTO_CLOSE
事实上这里的每一种ChannelOption,除了可以使用setOption方法来进行设置,在ChannelConfig接口中都为其设置了对应的快捷set/get方法,不再赘述。
SocketChannelConfig在ChannelConfig基础上额外支持的ChannelOption
- ChannelOption.SO_KEEPALIVE
- ChannelOption.SO_REUSEADDR
- ChannelOption.SO_LINGER
- ChannelOption.TCP_NODELAY
- ChannelOption.SO_RCVBUF
- ChannelOption.SO_SNDBUF
- ChannelOption.IP_TOS
- ChannelOption.ALLOW_HALF_CLOSURE
ServerSocketChannelConfig在ChannelConfig基础上额外支持的ChannelOption
- ChannelOption.SO_REUSEADDR
- ChannelOption.SO_RCVBUF
- ChannelOption.SO_BACKLOG
细心的读者会发现,除了ChannelConfig接口中定义的公共ChannelOption,SocketChannelConfig 和ServerSocketChannelConfig 支持的ChannelOption基本上都是TCP连接参数相关的
,基本上每一个都对应java.net.StandardSocketOptions
定义的一个标准TCP参数,下面逐一进行讲解。
ChannelOption讲解
ChannelOption.SO_KEEPALIVE-->StandardSocketOptions.SO_KEEPALIVE
是否启用心跳机制,默认false。
套接字本身是有一套心跳保活机制的,不过默认的设置并不像我们一厢情愿的那样有效。在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。
很多人认为两个小时的时间设置得很不合理。为什么不设置成为10分钟,或者更短的时间?(可以通过SO_KEEPALIVE选项设置。)但是这样做其实并不被推荐。实际上这套机制只是操作系统底层使用的一个被动机制,原理上不应该被上层应用层使用。当系统关闭一个由KEEPALIVE机制检查出来的死连接时,是不会主动通知上层应用的,只有在调用相应的IO操作在返回值中检查出来。
在《UNIX网络编程第1卷》中也有详细的阐述:
SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方 发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个探测分节。对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接口本身则被关闭。对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟 15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为 EHOSTUNREACH。
因此,忘记SO_KEEPALIVE,在应用层自己写一套保活机制比较靠谱。
ChannelOption.SO_REUSEADDR-->StandardSocketOptions.SO_REUSEADDR
是否重用处于TIME_WAIT状态的地址。默认为false。
ChannelOption.SO_LINGER-->StandardSocketOptions.SO_LINGER
优雅地关闭套接字,或者立刻关闭。当调用closesocket关闭套接字时,SO_LINGER将决定系统如何处理残存在套接字发送队列中的数据。处理方式无非两种:丢弃或者将数据继续发送至对端,优雅关闭连接。事实上,SO_LINGER并不被推荐使用,大多数情况下我们推荐使用默认的关闭方式。SO_LINGER 以秒为单位,最大取值为65535,也就是说,指定时间内残余数据尚未发送完成,那么也立即关闭。
ChannelOption.SO_SNDBUF-->StandardSocketOptions.SO_SNDBUF
发送缓冲区的大小设置,默认为8K。
ChannelOption.SO_RCVBUF-->StandardSocketOptions.SO_RCVBUF
接收缓冲区大小设置,默认为8K。该属性既可以在ServerSocket实例中设置,也可以在Socket实例中设置。
ChannelOption.TCP_NODELAY-->StandardSocketOptions.TCP_NODELAY
是否一有数据就马上发送。在TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。这里就涉及到一个名为Nagle的算法,该算法的目的就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
TCP_NODELAY选项,就是用于启用或关于Nagle算法。如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false。
ChannelOption.IP_TOS-->StandardSocketOptions.IP_TOS
ChannelOption#SO_BACKLOG
BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。在Netty中,这个值默认是去读取文件/proc/sys/net/core/somaxconn的值,如果没有读到,默认取值为3072(参见:NetUtil.SOMAXCONN
)。
理解了这些参数之后,还有一点需要注意的是,由于Netty只是对java网络编程API的一层封装,因此表面上NioSocketChannelConfig、NioServerSocketChannelConfig 包含的参数分别设置给了NioSocketChannel,NioServerSocketChannel。但事实上这些参数必须要设置给java原生的Socket对象才能生效。
以NioSocketChannelConfig为例,NioSocketChannel创建的时候,就会创建其对应的NioSocketChannelConfig实例。
public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {
...
private final SocketChannelConfig config;
...
public NioSocketChannel(Channel parent, SocketChannel socket) {//构造方法中传入了java原生socket对象参数
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
...
}
可以看到 在创建NioSocketChannelConfig实例的时候,传递了2个参数,一个是this,即当前NioSocketChannel,另外一个是通过SocketChannel的socket方法获得的原生socket。
当NioSocketChannelConfig相关设置ChannelOption的方法被调用时,则会直接将参数设置给对应的socket。
public class DefaultServerSocketChannelConfig extends DefaultChannelConfig
implements ServerSocketChannelConfig {
....
protected final Socket javaSocket;
.....
@Override
public SocketChannelConfig setTcpNoDelay(boolean tcpNoDelay) {
try {
javaSocket.setTcpNoDelay(tcpNoDelay);//直接将参数设置给了对应的java Socket
} catch (SocketException e) {
throw new ChannelException(e);
}
return this;
}
.....
}
网友评论