内容简介:P本文使用jdk1.8.0_45spring boot 2.1.4.RELEASE
P本文使用jdk1.8.0_45
spring boot 2.1.4.RELEASE
涉及源码都放在
https://github.com/sabersword/Nio
前因
这周遇到一个连接断开的问题,便沿着这条线学习了一下Java NIO,顺便验证一下Tomcat作为spring boot默认的web容器,是怎样管理空闲连接的。
Java NIO(new IO/non-blockingIO)不同于BIO,BIO是堵塞型的,并且每一条学习路线的IO章节都会从BIO说起,因此大家非常熟悉。而NIO涉及 Linux 底层的select,poll,epoll等,要求对Linux的网络编程有扎实功底,反正我是没有搞清楚,在此推荐一篇通俗易懂的入门文章:
https://www.jianshu.com/p/ef418ccf2f7d
此处先引用文章的结论:
-
对于socket的文件描述符才有所谓BIO和NIO。
-
多线程+BIO模式会带来大量的资源浪费,而NIO+IO多路复用可以解决这个问题。
-
在Linux下,基于epoll的IO多路复用是解决这个问题的最佳方案;epoll相比select和poll有很大的性能优势和功能优势,适合实现高性能网络服务。
底层的技术先交给大神们解决,我们着重从 Java 上层应用的角度了解一下。
从 JDK 1.5 起使用 epoll 代替了传统的 select/poll ,极大提升了 NIO 的通信性能,因此下文提到 Java NIO 都是使用 epoll 的。
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 ,注释如下:
可以简单理解成空闲超时时间,超时后 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 秒)的时间。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。