出現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次數馬上爆了。
