經過前面的學習,我們已經學會了Netty的使用。本章節開始我們要進行一些細節方面的學習,使其能更好的運用在我們以後的工作當中。
一、什麼是option?
前面學習了Netty的服務端,和客戶端,知道了創建服務要分別使用ServerBootStrap和BootStrap,不知道有沒有關注到其中有一個方法叫做Option的?
我們分別看下ServerBootStrap和BootStrap的option:
1)ServerBootStrap
如上圖所示,有兩種option,一個是自己的option(ServerSocketChannel),一個childOption(ScoketChannel的option)。
2)BootStrap
只有一個option方法。
無論是上述哪兩種,參數都是ChannelOption<T>,而這個ChannelOption Netty已經幫我們準備好了,可以直接使用。
下面我們會針對幾種重要的配置講解一下。
二、常用參數
2.1 CONNECT_TIMEOUT_MILLIS
ScoketChannel的參數。
用在客戶端建立連接時,如果超過指定的時間仍未連接,則拋出timeout異常。
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,500);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8080);
//阻塞等待連接
channelFuture.sync();
//阻塞等待釋放連接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 釋放EventLoopGroup
worker.shutdownGracefully();
}
}
只啓動客戶端,拋出如下異常:
Exception in thread "main" io.netty.channel.ConnectTimeoutException: connection timed out: /127.0.0.1:8080
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe$1.run(AbstractNioChannel.java:261)
at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:98)
at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:170)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
若果該參數設置過長,且服務端確實沒啓動,則會拋出java層面的異常,拒絕連接:
Exception in thread "main" io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: /127.0.0.1:8080
Caused by: java.net.ConnectException: Connection refused: no further information
at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)
at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:330)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:334)
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:702)
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
2.2 SO_TIMEOUT
這個參數適用於阻塞IO,比如阻塞IO當中的read,accept等方法,修飾阻塞的,如果不想一直阻塞,可以通過改參數設置超時時間。
不要與CONNECT_TIMEOUT_MILLIS弄混了。
2.3 SO_BACKLOG
ServerSocketChannal 參數。
在瞭解這個參數之前,要先了解下TCP的三次握手,sync_queue(半連接隊列)和accept_queue(全連接隊列)。
其中半連接隊列是在首次握手時,將請求放入半連接隊列,當三次握手全部成功後,將請求從半連接隊列放入全連接隊列。
下圖展示netty和三次握手的關係:
- 第一次握手,client 發送 SYN 到 server,狀態修改爲 SYN_SEND,server 收到,狀態改變爲 SYN_REVD,並將該請求放入 sync queue 隊列
- 第二次握手,server 回覆 SYN + ACK 給 client,client 收到,狀態改變爲 ESTABLISHED,併發送 ACK 給 server
- 第三次握手,server 收到 ACK,狀態改變爲 ESTABLISHED,將該請求從 sync queue 放入 accept queue。
在上面的過程中,提到的sync_queue和accept_queue是我們本篇文章需要關注的重點。
在linux2.2之前,backlog包括了兩個隊列的大小。在之後的版本當中,由如下兩個參數來控制:
-
sync queue - 半連接隊列
- 大小通過 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在
syncookies
啓用的情況下,邏輯上沒有最大值限制,這個設置便被忽略
- 大小通過 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在
-
accept queue - 全連接隊列
- 其大小通過 /proc/sys/net/core/somaxconn 指定,在使用 listen 函數時,內核會根據傳入的 backlog 參數與系統參數比較,取二者的較小值。
- 如果 accpet queue 隊列滿了,server 將發送一個拒絕連接的錯誤信息到 client。
下面迴歸正題,在netty當中,通過ChannelOption.SO_BACKLOG設置大小,如下所示:
public class Server {
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
//此處爲了模擬,設置爲2
serverBootstrap.option(ChannelOption.SO_BACKLOG,2);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待連接
channelFuture.sync();
//阻塞等待釋放連接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 釋放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
如上代碼所示,設置了一個backlog爲2的值。然後我們需要啓動至少三個客戶端看結果。
通過前面的三次握手的圖,可以知道,只有當服務端處理不過來時,纔會使用全連接隊列,並將其佔滿,否則會直接走accept()方法,導致我們看不到測試結果。
所以我們這裏不做測試了。
我們看下這個backlog的默認值在nio當中是多少:
在NIO當中backlog在ServerSocketChannel當中的bind方法被調用,所以我們從這裏跟蹤進去找到bind方法:
public final ServerSocketChannel bind(SocketAddress local)
throws IOException
{
return bind(local, 0);
}
查看bind被哪些地方調用,NioServerSocketChannel:
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
@Override
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
跟蹤config.getBacklog():
private final ServerSocketChannelConfig config;
這個config是接口,直接看它的實現DefaultServerSocketChannelConfig:
private volatile int backlog = NetUtil.SOMAXCONN;
找SOMAXCONN:
public static final int SOMAXCONN;
找到SOMAXCONN賦值的位置,默認是windows200,Linux或mac默認128,如果有前面我們提到的文件/proc/sys/net/core/somaxconn,則走此配置中的文件:
SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction<Integer>() {
@Override
public Integer run() {
// Determine the default somaxconn (server socket backlog) value of the platform.
// The known defaults:
// - Windows NT Server 4.0+: 200
// - Linux and Mac OS X: 128
int somaxconn = PlatformDependent.isWindows() ? 200 : 128;
File file = new File("/proc/sys/net/core/somaxconn");
BufferedReader in = null;
try {
// file.exists() may throw a SecurityException if a SecurityManager is used, so execute it in the
// try / catch block.
// See https://github.com/netty/netty/issues/4936
if (file.exists()) {
in = new BufferedReader(new FileReader(file));
somaxconn = Integer.parseInt(in.readLine());
if (logger.isDebugEnabled()) {
logger.debug("{}: {}", file, somaxconn);
}
} else {
// Try to get from sysctl
Integer tmp = null;
if (SystemPropertyUtil.getBoolean("io.netty.net.somaxconn.trySysctl", false)) {
tmp = sysctlGetInt("kern.ipc.somaxconn");
if (tmp == null) {
tmp = sysctlGetInt("kern.ipc.soacceptqueue");
if (tmp != null) {
somaxconn = tmp;
}
} else {
somaxconn = tmp;
}
}
if (tmp == null) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}", file,
somaxconn);
}
}
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to get SOMAXCONN from sysctl and file {}. Default: {}",
file, somaxconn, e);
}
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
// Ignored.
}
}
}
return somaxconn;
}
});
2.4 ulimit
屬於操作系統參數。
使用ulimit -n 可以查看當前的最大打開文件數。使用ulimit -a 可以查看當前系統的所有限制值。
linux默認1024,當服務器負載較大時,會發生too many open files的錯誤,所以我們爲了提供併發量,需要手動將其調整。
使用如下命令可以將其調整,但是是臨時性的,可以考慮將其放在啓動腳本當中:
ulimit -n 4096
2.5 TCP_NODELAY
屬於 SocketChannal 參數。
在前面的文章當中,我們提到過TCP的nagle算法,我們使用netty時,它的默認開始的。
nagle算法會使我們某些較小的數據包造成延遲,因爲爲了提升效率,nagle會等到收集到一定數據後進行發送,這樣可能造成我們消息的延遲。
可以通過如下方式設置,開啓無延遲的配置:
serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
2.6 SO_SNDBUF & SO_RCVBUF
SO_SNDBUF 屬於 SocketChannal 參數
SO_RCVBUF 既可用於 SocketChannal 參數,也可以用於 ServerSocketChannal 參數
這兩個參數不建議我們手動進行設置,因爲操作系統會根據當前佔用,進行自動的調整。
2.7 ALLOCATOR
屬於 SocketChannal 參數。
ByteBuf的分配器。
serverBootstrap.childOption(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT);
這個參數只有一個DEFAULT可以使用。
這個參數與ch.alloc().buffer()命令有關,關係着我們分配的buf是池化還是非池化,是直接內存還是堆內存。
我們從上面的Default跟蹤進去:
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
繼續跟蹤DEFAULT_ALLOCATOR:
static final ByteBufAllocator DEFAULT_ALLOCATOR;
找到對其賦值的位置,發現瞭如下的靜態代碼塊,此處就是設置buf是pooled還是unpooled,通過環境變量:"io.netty.allocator.type" 指定,我們可以在啓動項目時指定-Dio.netty.allocator.type=unpooled設置成非池化。從源碼可以看到,安卓是unpooled,其他事pooled。
static {
MAX_BYTES_PER_CHAR_UTF8 = (int)CharsetUtil.encoder(CharsetUtil.UTF_8).maxBytesPerChar();
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
Object alloc;
if ("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if ("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = (ByteBufAllocator)alloc;
THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16384);
logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
FIND_NON_ASCII = new ByteProcessor() {
public boolean process(byte value) {
return value >= 0;
}
};
}
如上代碼中的UnpooledByteBufAllocator.DEFAULT和PooledByteBufAllocator.DEFAULT就指定了我們使用的直接內存還是堆內存,跟蹤其中的UnpooledByteBufAllocator.DEFAULT看一下:
public static final UnpooledByteBufAllocator DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
跟蹤PlatformDependent.directBufferPreferred():
private static final boolean DIRECT_BUFFER_PREFERRED;
找DIRECT_BUFFER_PREFERRED賦值的位置:
DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
重點關注上述代碼後半段!SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);,我們可用通過-Dio.netty.noPreferDirect=true環境變量指定我們使用堆內存。
2.8 RCVBUF_ALLOCATOR
屬於 SocketChannal 參數。
控制 netty 接收緩衝區大小。
這個RCVBUF_ALLOCATOR不要與前面的ALLOCATOR弄混。
負責入站數據的分配,決定入站緩衝區的大小(並可動態調整),統一採用 direct 直接內存,具體池化還是非池化由 allocator 決定。
通俗的講在handler內部分配的byteBuf可以是直接內存,也可以是堆內存,但是經過網絡io的內存,netty會強制爲直接內存。
我們啓動一個客戶端和服務端去驗證一下上述的描述:
服務端:
public static void main(String[] args) {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg);
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待連接
channelFuture.sync();
//阻塞等待釋放連接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 釋放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
客戶端:
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf byteBuf = ctx.alloc().buffer();
byteBuf.writeBytes("hello world!".getBytes());
System.out.println(byteBuf);
ctx.writeAndFlush(byteBuf);
super.channelActive(ctx);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080);
//阻塞等待連接
channelFuture.sync();
//阻塞等待釋放連接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 釋放EventLoopGroup
worker.shutdownGracefully();
}
}
我們重點關注服務端接收到的Object msg,它應該是一個Bytebuf對象,那麼他是什麼類型的?如何生成的?
我們通過打斷點的方式,找到線程調用的堆棧,發現第一個channel的位置:
點擊進去發現如下代碼,並且就是我們得到的ByteBuf:
重點關注這部分有註釋代碼:
ChannelPipeline pipeline = AbstractNioByteChannel.this.pipeline();
// 上一小節提到的allocator,負責ByteBuf是池化還是非池化
ByteBufAllocator allocator = config.getAllocator();
//此處Handle是RecvByteBufAllocator內部類
Handle allocHandle = this.recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null;
boolean close = false;
try {
do {
//allocate方法創建byteBuf
byteBuf = allocHandle.allocate(allocator);
下面重點關注這個allocate方法,這裏會分配一個ioBuffer,即直接內存buffer:
public ByteBuf allocate(ByteBufAllocator alloc) {
return alloc.ioBuffer(this.guess());
}
如上的guess()會根據數據量大小,動態分配buffer的大小,範圍如下,自適應的AdaptiveRecvByteBufAllocator,最小不會小於64,最大不會超過65536:
public AdaptiveRecvByteBufAllocator() {
this(64, 1024, 65536);
}
關於主要參數就介紹這麼多,有用的話幫忙點個贊吧~~