出現Connection refused的問題原因一般有三種:
1. 服務器的端口沒有打開 這種直接就是一直會Connection refused,不會間歇出現,可以直接排除;
2. 服務器的防火牆沒有開白名單 很多跟外部對接的時候,是需要將公司出口ip加到對方防火牆白名單,這種也會直接Connection refused,不會間歇出現,可以直接排除;
3. 服務器上的backlog設置的太小,導致連接隊列滿了,服務器可能會報Connection refused,或者Connecttion reset by peer,這個看服務器上的連接隊列滿時的設置;
詳細的異常堆棧信息如下:
看報錯方法:
是個native方法,毫不意外。因為是跟第三方雲服務商對接,只能讓他們查服務器配置的backlog大小(最后通過將backlog從50調到了4096),這里回顧一下tcp三次握手的過程。
正常的發起請求的三次握手如下:
第一步:client 發送syn到server發起握手;
第二步: server收到syn后回復syn + ack 給client;
第三步:client收到syn + ack后,回復server一個ack表示收到server的syn + ack;
Tcp連接詳細狀態如下圖:
1. 服務端調用bind() & listen() 函數后,會監聽本地某個端口,例如8080;
2. 客戶端發SYN,服務端收到,連接狀態變為SYN_RCVD,將連接放到半連接隊列syns queue中,同時回復syn+ack給client;
3. 客戶端收到syn + ack,回復ack,客戶端連接狀態變為ESTABLISHED,服務器接收到客戶端的ack,先看accept queue是否已滿,如果沒有滿,將連接放到全連接隊列,如果滿了的話,有二種處理方式:
根據服務端tcp_abort_on_overflow的配置決定,如果配置為0,會丟棄客戶端的ack, 過段時間重發syn + ack,也就是三次握手的第二步(如果客戶端超時時間設置的太短,就容易引發Connection refused),如果配置為1,會直接返回RET,客戶端的表示就是Connection reset by peer。
其中半連接隊列的大小看: max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)
上面是我機器上半連接的配置,挺大的,26萬。
全連接隊列的大小: min(backlog, somaxconn), backlog是在socket創建的時候傳入的,somaxconn是一個os級別的系統參數,不同操作系統不一樣。
代碼涉及到Socket這一層的操作時,需要自己傳backlog的大小,否則默認值是50.
public ServerSocket(int port, int backlog) throws IOException { this(port, backlog, null); }
所有上面Connection Refused很容易因為backlog設置的太小而發生,例如,nginx的配置就有backlog, 默認是511,Tomcat 默認是100。
一般來說,如果是公司自己的服務器,可以通過TCP建連接的時候全連接隊列(accept隊列)滿了,通過一些命令可以查詢隊列情況:
netstat -s 命令
通過netstat -s | egrep "listen" 看隊列的溢出統計數據,多執行幾次,看全連接隊列overflow次數有沒有增長:
ss 命令
上面看Send-Q的值就是listen端口上全連接隊列的最大值,Recv-Q就是當前全連接隊列用了多少。
netstat跟ss命令一樣也能看到Send-Q、Recv-Q這些狀態信息,不過如果這個連接不是Listen狀態的話,Recv-Q就是指收到的數據還在緩存中,還沒被進程讀取,這個值就是還沒被進程讀取的 bytes;而 Send 則是發送隊列中沒有被遠程主機確認的 bytes 數。
因此如果出現間歇性Connection Refused,檢查是否有設置backlog, backlog設置的是否過小。
壓力測試實踐:
服務端代碼:
public class BaseSocketServer { private ServerSocket server; private Socket socket; private int port; private InputStream inputStream; private static final int MAX_BUFFER_SIZE = 1024; public int getPort() { return port; } public void setPort(int port) { this.port = port; } public BaseSocketServer(int port) { this.port = port; } public void runServerMulti() throws IOException { //故意設置backlog就只有10 this.server = new ServerSocket(this.port, 10); System.out.println("base socket server started."); ExecutorService executorService = Executors.newFixedThreadPool(10); while(true){ // the code will block here till the request come. this.socket = server.accept(); Runnable run = () ->{ InputStream inputStream = null; try { inputStream = this.socket.getInputStream(); byte[] readBytes = new byte[1024]; int msgLen; StringBuilder stringBuilder = new StringBuilder(); while ((msgLen = inputStream.read(readBytes)) != -1) { stringBuilder.append(new String(readBytes,0, msgLen,"UTF-8")); } System.out.println("get message from client: " + stringBuilder); }catch (Exception ex){ ex.printStackTrace(); }finally { try { Thread.sleep(10000); inputStream.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }; executorService.submit(run); } //server.close(); } public static void main(String[] args) { BaseSocketServer bs = new BaseSocketServer(9799); try { bs.runServerMulti(); }catch (IOException e) { e.printStackTrace(); } } }
backlog設置的只有10,這樣讓客戶端連接的時候可以快速隊列滿。
客戶端代碼如下:
public class BaseSocketClient { private String serverHost; private int serverPort; private Socket socket; private OutputStream outputStream; public BaseSocketClient(String host, int port) { this.serverHost = host; this.serverPort = port; } public void connetServer() throws IOException { this.socket = new Socket(this.serverHost, this.serverPort); this.outputStream = socket.getOutputStream(); // why the output stream? } public void sendSingle(String message) throws IOException { try { this.outputStream.write(message.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { System.out.println(e.getMessage()); } this.outputStream.close(); this.socket.close(); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(100); Runnable run = () ->{ BaseSocketClient bc = new BaseSocketClient("127.0.0.1",9799); try { bc.connetServer(); bc.sendSingle(String.format("%s, 你好, 我是吉米", Thread.currentThread().getName())); }catch (IOException e) { e.printStackTrace(); } }; for(int i = 0; i < 28; i++){ executorService.submit(run); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
然后ss 可以看到全隊列overflow次數馬上爆了。