作者:Grey
準備兩個Linux例項(安裝好jdk1.8),我準備的兩個例項的ip地址分別為:
io1例項:192.168.205.138
io2例項:192.168.205.149
安裝必要工具:
yum install -y strace lsof pmap tcpdump
準備服務端程式碼
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO Socket Server
*/
public class SocketServerBIOTest {
private static final int PORT = 9090;
private static final int BACK_LOG = 2;
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket();
server.bind(new InetSocketAddress(PORT), BACK_LOG);
System.out.println("server started , port : " + PORT);
} catch (IOException e) {
e.printStackTrace();
}
try {
// 接受客戶端連線
while (true) {
// 先阻塞,這樣客戶端暫時無法連線進來
System.in.read();
// 這個方法也是阻塞的,如果沒有客戶端連線進來,會一直阻塞在這裡,除非設定了超時時間
Socket client = server.accept();
System.out.println("client " + client.getPort() + " connected!!!");
// 客戶端連線進來後,開闢一個新的執行緒去接收並處理
new Thread(() -> {
try {
InputStream inputStream = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
char[] data = new char[1024];
while (true) {
int num = reader.read(data);
if (num > 0) {
System.out.println("client read some data is :" + num + " val :" + new String(data, 0, num));
} else if (num == 0) {
System.out.println("client read nothing!");
continue;
} else {
System.out.println("client read -1...");
System.in.read();
client.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
死迴圈中,由於第一句:
System.in.read();
導致
Socket client = server.accept();
無法執行,即:服務端此時是無法接收客戶端的。
準備客戶端程式碼:
import java.io.*;
import java.net.Socket;
/**
* Socket Client
*/
public class SocketClientTest {
public static void main(String[] args) {
try {
Socket client = new Socket("192.168.205.138", 9090);
OutputStream out = client.getOutputStream();
InputStream in = System.in;
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
while (true) {
String line = reader.readLine();
if (line != null) {
byte[] bb = line.getBytes();
for (byte b : bb) {
out.write(b);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在例項io1中啟動服務端程式碼:
javac SocketServerBIOTest.java && java SocketServerBIOTest
在io1中開啟抓包工具:
tcpdump -nn -i ens33 port 9090
在io2中執行客戶端程式碼:
javac SocketClientTest.java && java SocketClientTest
由於我們在服務端加了一段:
System.in.read()
方法,導致服務端其實沒辦法執行accept()
但是在服務端檢視抓包資訊:
[root@io socket]# tcpdump -nn -i ens33 port 9090
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
15:19:56.021974 IP 192.168.205.149.56944 > 192.168.205.138.9090: Flags [S], seq 391962776, win 29200, options [mss 1460,sackOK,TS val 16515471 ecr 0,nop,wscale 7], length 0
15:19:56.022035 IP 192.168.205.138.9090 > 192.168.205.149.56944: Flags [S.], seq 2744580571, ack 391962777, win 28960, options [mss 1460,sackOK,TS val 16517545 ecr 16515471,nop,wscale 7], length 0
15:19:56.022349 IP 192.168.205.149.56944 > 192.168.205.138.9090: Flags [.], ack 1, win 229, options [nop,nop,TS val 16515472 ecr 16517545], length 0
核心已經為客戶端和服務端建立了連線並完成了三次握手,在服務端使用netstat檢視:
[root@io socket]# netstat -ntap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
...
tcp6 0 0 192.168.205.138:9090 192.168.205.149:56944 ESTABLISHED -
...
顯示已經建立了連線,只不過還沒有分配:PID/Program name。
在客戶端輸入一些資訊,
[root@io2 socket]# javac SocketClientTest.java && java SocketClientTest
asdfasdfasdfasf
在服務端再次執行netstat
[root@io socket]# netstat -ntap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
...
tcp6 15 0 192.168.205.138:9090 192.168.205.149:56944 ESTABLISHED -
...
也顯示出接收到了資料。
再服務端再次開啟抓包:
tcpdump -nn -i ens33 port 9090
再次在客戶端輸入一些資料:
[root@io2 socket]# javac SocketClientTest.java && java SocketClientTest
asdfasdfasdfasf
dfasdfasdfasdas
可以看到抓包資訊:
[root@io ~]# tcpdump -nn -i ens33 port 9090
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
15:26:48.632564 IP 192.168.205.149.56944 > 192.168.205.138.9090: Flags [P.], seq 391962792:391962793, ack 2744580572, win 229, options [nop,nop,TS val 16928082 ecr 16757410], length 1
15:26:48.632609 IP 192.168.205.138.9090 > 192.168.205.149.56944: Flags [.], ack 1, win 227, options [nop,nop,TS val 16930156 ecr 16928082], length 0
15:26:48.632791 IP 192.168.205.149.56944 > 192.168.205.138.9090: Flags [P.], seq 1:15, ack 1, win 229, options [nop,nop,TS val 16928082 ecr 16930156], length 14
15:26:48.632825 IP 192.168.205.138.9090 > 192.168.205.149.56944: Flags [.], ack 15, win 227, options [nop,nop,TS val 16930156 ecr 16928082], length 0
以上實驗主要說明了一個問題:
雖然在應用層面,服務端沒有呼叫accept() 去接收客戶端,但是,核心其實已經完成了客戶端和服務端的三次握手以及資料傳輸。
接下來,我們觸發服務端:
Socket client = server.accept();
這段邏輯
服務端可以顯示客戶端的資料
^C[root@io socket]# java SocketServerBIOTest
server started , port : 9090
client 56944 connected!!!
client read some data is :30 val :asdfasdfasdfasfdfasdfasdfasdas
服務端執行nestat -ntap,
[root@io ~]# netstat -ntap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
...
tcp6 0 0 192.168.205.138:9090 192.168.205.149:56944 ESTABLISHED 2266/java
...
可以看到已經分配了一個PID,通過:
lsof -p 2266
檢視這個java程式相關的檔案描述符
[root@io ~]# lsof -p 2266
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
...
java 2266 root 6u IPv6 26479 0t0 TCP 192.168.205.138:websm->192.168.205.149:56944 (ESTABLISHED)
...
6u就對應了服務端和客戶端連線的一個Socket。
在建立服務端的時候,我們指定了一個引數:backlog
// 指定了BACK_LOG = 2
server.bind(new InetSocketAddress(PORT), BACK_LOG);
backlog的解釋是
requested maximum length of the queue of incoming connections.
重新啟動我們的服務端
[root@io socket]# javac SocketServerBIOTest.java && java SocketServerBIOTest
server started , port : 9090
啟動三個客戶端連線這個服務端, 然後再服務端執行netstat -ntap
[root@io socket]# netstat -ntap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
...
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50532 ESTABLISHED -
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50536 ESTABLISHED -
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50534 ESTABLISHED -
...
可以看到,服務端建立了三個連線,但是,當我們再啟動一個客戶端連線進來的時候,新增的這個連線的狀態為:SYN_RECV
[root@io socket]# netstat -ntap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
....
tcp 0 0 192.168.205.138:9090 192.168.205.149:50538 SYN_RECV -
....
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50532 ESTABLISHED -
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50536 ESTABLISHED -
tcp6 0 0 192.168.205.138:9090 192.168.205.149:50534 ESTABLISHED -
SYN_RECV表示:
服務端收到報文後,向客戶端傳送確認的報文,服務端進入SYNC_RECV狀態,但是因為設定了backlog=2
超過了服務端設定的最大連線數,服務端就不再繼續向客戶端傳送報文。
在具體的程式設計中,服務端和客戶端都有很多配置的引數。
詳見:
關於MTU和MSS: MTU TCP-MSS詳解
原始碼:Github