本文使用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秒)的時間。