在Java Socket中,當我們調用Socket的close方法時,默認的行爲是當底層網卡所有數據都發送完畢後,關閉連接
通過setSoLinger方法,我們可以修改close方法的行爲
1,setSoLinger(true, 0)
當網卡收到關閉連接請求後,無論數據是否發送完畢,立即發送RST包關閉連接
2,setSoLinger(true, delay_time)
當網卡收到關閉連接請求後,等待delay_time
如果在delay_time過程中數據發送完畢,正常四次揮手關閉連接
如果在delay_time過程中數據沒有發送完畢,發送RST包關閉連接
通過測試程序以及抓包文件詳細的觀察了一下這個方法的行爲
客戶端代碼如下:
package com.test.client;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Client {
private static int port = 9999;
private static String host = "192.168.52.131";
// private static String host = "127.0.0.1";
public static SimpleDateFormat sdf = new SimpleDateFormat(
"yy-MM-dd HH:mm:ss.SSS");
public static void main(String[] args) throws Exception {
Socket socket = new Socket();
// CASE 1 : default setting
socket.setSoLinger(false, 0);
// CASE 2 :
// socket.setSoLinger(true, 0);
// CASE 3 :
// socket.setSoLinger(true, 1);
SocketAddress address = new InetSocketAddress(
Client.host, Client.port);
socket.connect(address);
OutputStream output = socket.getOutputStream();
StringBuilder strB = new StringBuilder();
for(int i = 0 ; i < 10000000 ; i++ ){
strB.append("a");
}
byte[] request = strB.toString().getBytes("utf-8");
System.out.println("Client before write : " + sdf.format(new Date()));
output.write(request);
System.out.println("Client after write : " + sdf.format(new Date()));
socket.close();
System.out.println("Client after close : " + sdf.format(new Date()));
}
}
服務端代碼如下:
package com.test.server;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Server {
private static int queueSize = 10;
private static int port = 9999;
public static SimpleDateFormat sdf = new SimpleDateFormat(
"yy-MM-dd HH:mm:ss.SSS");
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.setReceiveBufferSize(128*1024);
serverSocket.bind(new InetSocketAddress(Server.port),
Server.queueSize);
Socket socket = null;
while(true){
socket = serverSocket.accept();
InputStream input = socket.getInputStream();
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = -1;
Thread.sleep(10*1000);
System.out.println("Server before read : " + sdf.format(new Date()));
while((length = input.read(buffer)) != -1){
output.write(buffer, 0, length);
}
System.out.println("Server after read : " + sdf.format(new Date()));
String req = new String(output.toByteArray(), "utf-8");
System.out.println(req.length());
socket.close();
}
}
}
CASE 1
客戶端運行結果:
Client before write : 16-09-01 16:49:45.396
Client after write : 16-09-01 16:49:55.307
Client after close : 16-09-01 16:49:55.307
服務端運行結果:
Server before read : 16-09-01 16:49:55.558
Server after read : 16-09-01 16:49:55.680
10000000
抓包結果如下:
雖然客戶端在16:49:55.307(客戶端運行結果)就已經關閉了連接,但是直到16:49:55.680107(抓包文件718行)客戶端才發出FIN包開始關閉連接
即底層會等待數據發送完畢才關閉連接
CASE 2
客戶端運行結果:
Client before write : 16-09-01 17:34:37.019
Client after write : 16-09-01 17:34:46.876
Client after close : 16-09-01 17:34:46.877
服務端運行結果:
Server before read : 16-09-01 17:34:47.107
Exception in thread "main" java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:196)
at java.net.SocketInputStream.read(SocketInputStream.java:122)
at java.net.SocketInputStream.read(SocketInputStream.java:108)
at com.test.server.Server.main(Server.java:34)
抓包結果如下:
雖然客戶端在17:34:46.877(客戶端運行結果)就已經關閉了連接,但是從抓包文件中可以看到,在17:34:47.238493(抓包文件906行),依然有數據從客戶端發往服務端
當然代碼中的關閉連接請求發送至內核、內核通過驅動控制網卡關閉連接,這一系列操作也需要不少時間,因此關閉連接請求有延遲也是正常的
可以看到服務端至接收了9442408字節連接就關閉了
當網卡接收到關閉連接請求後,即便數據並沒有完全發送完畢,網卡也會立即關閉連接
此時連接通過客戶端發送RST包的方式強行關閉連接,而不是通過TCP的四次揮手方式正常關閉
如果數據能夠發送完畢呢?修改一下客戶端代碼
for(int i = 0 ; i < 100 ; i++ ){
strB.append("a");
}
然後再次抓包觀察
可以看到這次所有數據都發送完成
但是客戶端依然會發送RST包關閉連接
關鍵問題是,數據能不能全部發送完,這並不是我們能控制的
CASE 3
客戶端運行結果:
Client before write : 16-09-01 18:37:30.523
Client after write : 16-09-01 18:37:40.419
Client after close : 16-09-01 18:37:40.424
服務端運行結果:
Server before read : 16-09-01 18:37:40.655
Server after read : 16-09-01 18:37:40.786
10000000
抓包結果如下:
從抓包結果可以看到這裏是正常關閉
即當網卡收到關閉連接請求時,等待一段時間,然後關閉連接
如果在等待的過程中,數據發送完畢,則通過四次揮手的方式正常關閉連接,否則和CASE 2一樣,通過發送RST包的方式強行關閉連接
有很多地方介紹可以通過setSoLinger方法避免主動關閉方處於time wait狀態,其實只有通過直接發送RST包關閉連接,而不是通過正常四次揮手的方式才能避免主動關閉方處於time wait狀態,但是從上面的CASE中可以看到,發送RST包的時候,數據有時是不完整的,所以不應該使用這種方式來避免主動關閉方處於time wait狀態