本文使用jdk1.8.0_45和spring boot 2.1.4.RELEASEjava
涉及源码都放在https://github.com/sabersword/Niogit
这周遇到一个链接断开的问题,便沿着这条线学习了一下Java NIO,顺便验证一下Tomcat做为spring boot默认的web容器,是怎样管理空闲链接的。github
Java NIO(new IO/non-blocking IO)不一样于BIO,BIO是堵塞型的,而且每一条学习路线的IO章节都会从BIO提及,所以你们很是熟悉。而NIO涉及Linux底层的select,poll,epoll等,要求对Linux的网络编程有扎实功底,反正我是没有搞清楚,在此推荐一篇通俗易懂的入门文章聊聊BIO,NIO和AIOweb
此处先引用文章的结论:spring
底层的技术先交给大神们解决,咱们着重从Java上层应用的角度了解一下。编程
从JDK 1.5起使用epoll代替了传统的select/poll,极大提高了NIO的通讯性能,所以下文提到Java NIO都是使用epoll的。tomcat
Java NIO涉及到的三大核心部分Channel、Buffer、Selector,它们都十分复杂,单单其中一部分都能写成一篇文章,就不班门弄斧了。此处贴上一个本身学习NIO时设计的样例,功能是服务器发布服务,客户端连上服务器,客户端向服务器发送若干次请求,达到若干次答复后,服务器率先断开链接,随后客户端也断开链接。服务器
NIO服务器核心代码网络
public void handleRead(SelectionKey key) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
try {
long bytesRead = sc.read(buf);
StringBuffer sb = new StringBuffer();
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
sb.append((char) buf.get());
}
buf.clear();
bytesRead = sc.read(buf);
}
LOGGER.info("收到客户端的消息:{}", sb.toString());
writeResponse(sc, sb.toString());
if (sb.toString().contains("3")) {
sc.close();
}
} catch (IOException e) {
key.cancel();
e.printStackTrace();
LOGGER.info("疑似一个客户端断开链接");
try {
sc.close();
} catch (IOException e1) {
LOGGER.info("SocketChannel 关闭异常");
}
}
}
复制代码
NIO客户端核心代码多线程
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isConnectable()) {
while (!socketChannel.finishConnect()) ;
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
LOGGER.info("与服务器链接成功,使用本地端口{}", socketChannel.socket().getLocalPort());
}
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead;
try {
bytesRead = sc.read(buf);
} catch (IOException e) {
e.printStackTrace();
LOGGER.info("远程服务器断开了与本机的链接,本机也进行断开");
sc.close();
continue;
}
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
TimeUnit.SECONDS.sleep(2);
String info = "I'm " + i++ + "-th information from client";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
sc.write(buffer);
}
}
iter.remove();
}
复制代码
服务器日志
客户端日志
从这个样例能够看到,客户端和服务器都能根据自身的策略,与对端断开链接,本例中是服务器首先断开链接,根据TCP协议,必然有一个时刻服务器处于FIN_WAIT_2状态,而客户端处于CLOSE_WAIT状态
咱们经过netstat命令找出这个状态,果不其然。
可是JDK提供的NIO接口仍是很复杂很难写的,要用好它就必须借助于Netty、Mina等第三方库的封装,这部分就先不写了。
接下来考虑另一个问题,在大并发的场景下,成千上万的客户端涌入与服务器链接,链接成功后不发送请求,浪费了服务器宝贵的资源,这时服务器该如何应对?
答案固然是设计合适的链接池来管理这些宝贵的资源,为此咱们选用Tomcat做为学习对象,了解一下它是如何管理空闲链接的。
Tomcat的Connector组件用于管理链接,Tomcat8默认使用Http11NioProtocol,它有一个属性ConnectionTimeout,注释以下:
/* * When Tomcat expects data from the client, this is the time Tomcat will * wait for that data to arrive before closing the connection. */
复制代码
能够简单理解成空闲超时时间,超时后Tomcat会主动关闭该链接来回收资源。 咱们将它修改成10秒,获得以下配置类,并将该spring boot应用打包成tomcat-server.jar
@Component
public class MyEmbeddedServletContainerFactory extends TomcatServletWebServerFactory {
public WebServer getWebServer(ServletContextInitializer... initializers) {
// 设置端口
this.setPort(8080);
return super.getWebServer(initializers);
}
protected void customizeConnector(Connector connector) {
super.customizeConnector(connector);
Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
// 设置最大链接数
protocol.setMaxConnections(2000);
// 设置最大线程数
protocol.setMaxThreads(2000);
// 设置链接空闲超时
protocol.setConnectionTimeout(10 * 1000);
}
}
复制代码
咱们将上文的NIO客户端略微修改一下造成TomcatClient,功能就是连上服务器后什么都不作。
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isConnectable()) {
while (!socketChannel.finishConnect()) ;
socketChannel.configureBlocking(false);
socketChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
LOGGER.info("与远程服务器链接成功,使用本地端口{}", socketChannel.socket().getLocalPort());
}
if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long readCount;
readCount = sc.read(buf);
while (readCount > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
System.out.println();
buf.clear();
readCount = sc.read(buf);
}
// 远程服务器断开链接后会不停触发OP_READ,并收到-1表明End-Of-Stream
if (readCount == -1) {
LOGGER.info("远程服务器断开了与本机的链接,本机也进行断开");
sc.close();
}
}
iter.remove();
}
复制代码
分别运行服务器和客户端,能够看到客户端打印以下日志
30:27连上服务器,不进行任何请求,通过10秒后到30:37被服务器断开了链接。
此时netstat会发现还有一个TIME_WAIT的链接
根据TCP协议主动断开方必须等待2MSL才能关闭链接,Linux默认的2MSL=60秒(顺带说一句网上不少资料说CentOS的/proc/sys/net/ipv4/tcp_fin_timeout能修改2MSL的时间,实际并无效果,这个参数应该是被写进内核,必须从新编译内核才能修改2MSL)。持续观察netstat发现31:36的时候TIME_WAIT链接还在,到了31:38链接消失了,能够认为是31:37关闭链接,对比上文30:37恰好通过了2MSL(默认60秒)的时间。