网络IO-tcp是怎么建立通信的

学习马士兵课程-内存与IO,通过 netstatlsoftcpdump 观察tcp连接时内核在每一步都就行了什么相应的操作。

本文资料来自于马士兵MAC课程-内存与IO。

通过 netstat、lsof、tcpdump 观察 tcp 连接时内核在每一步都就行了什么相应的操作。

阅读本文,你可以知道在编写 java 代码时的每一步对应内核会产生哪些操作,其探究范围在客户端、服务端代码启动到客户端、服务端建立连接。

案例代码

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
* @author: 马士兵教育
* @create: 2020-05-17 05:34
* BIO 多线程的方式
*/
public class SocketIOPropertites {


//server socket listen property:
private static final int RECEIVE_BUFFER = 10;
private static final int SO_TIMEOUT = 0;
private static final boolean REUSE_ADDR = false;
private static final int BACK_LOG = 2;
//client socket listen property on server endpoint:
private static final boolean CLI_KEEPALIVE = false;
private static final boolean CLI_OOB = false;
private static final int CLI_REC_BUF = 20;
private static final boolean CLI_REUSE_ADDR = false;
private static final int CLI_SEND_BUF = 20;
private static final boolean CLI_LINGER = true;
private static final int CLI_LINGER_N = 0;
private static final int CLI_TIMEOUT = 0;
private static final boolean CLI_NO_DELAY = false;
/*

StandardSocketOptions.TCP_NODELAY
StandardSocketOptions.SO_KEEPALIVE
StandardSocketOptions.SO_LINGER
StandardSocketOptions.SO_RCVBUF
StandardSocketOptions.SO_SNDBUF
StandardSocketOptions.SO_REUSEADDR

*/


public static void main(String[] args) {

ServerSocket server = null;
try {
server = new ServerSocket();
server.bind(new InetSocketAddress(9090), BACK_LOG);
server.setReceiveBufferSize(RECEIVE_BUFFER);
server.setReuseAddress(REUSE_ADDR);
server.setSoTimeout(SO_TIMEOUT);

} catch (IOException e) {
e.printStackTrace();
}
System.out.println("server up use 9090!");
try {
while (true) {

System.in.read(); //分水岭:

Socket client = server.accept(); //阻塞的,没有 -1 一直卡着不动 accept(4,
System.out.println("client port: " + client.getPort());

client.setKeepAlive(CLI_KEEPALIVE);
client.setOOBInline(CLI_OOB);
client.setReceiveBufferSize(CLI_REC_BUF);
client.setReuseAddress(CLI_REUSE_ADDR);
client.setSendBufferSize(CLI_SEND_BUF);
client.setSoLinger(CLI_LINGER, CLI_LINGER_N);
client.setSoTimeout(CLI_TIMEOUT);
client.setTcpNoDelay(CLI_NO_DELAY);

//client.read //阻塞 没有 -1 0
new Thread(
() -> {
try {
InputStream in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
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 readed nothing!");
continue;
} else {
System.out.println("client readed -1...");
System.in.read();
client.close();
break;
}
}

} catch (IOException e) {
e.printStackTrace();
}

}
).start();

}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author: 马士兵教育
* @create: 2020-05-17 16:18
*/
public class SocketClient {

public static void main(String[] args) {

try {
Socket client = new Socket("192.168.150.11",9090);

client.setSendBufferSize(20);
client.setTcpNoDelay(true);
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();
}
}
}

在服务器端启动但客户端未连接时

  • 启动服务器端代码

    1
    2
    javac SocketIOPropertites.java
    java SocketIOPropertites

    此时客户端创建了 serverSocket 并绑定了端口9090,阻塞在 System.in.read() 处

  • netstat

    1
    2
    3
    4
    5
    6
    7
    -a 显示所有状态的socket  
    -n 不做名字解析,不加此参数,80端口会显示成http,127.0.0.1显示成localhost,uid为0显示成root等等
    -e 显示更多信息如用户,inode
    -p 显示pid和程序名字
    -t 显示tcp链接
    -u 显示udp链接
    -x 显示unix套接字

    通过 netstat 可以看出系统中多了一行监听9090端口的tcp连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    netstat -natp
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 7631/sshd
    tcp 0 0 192.168.203.133:22 192.168.203.1:3137 ESTABLISHED 7846/sshd: root@pts
    tcp 0 0 192.168.203.133:22 192.168.203.1:7070 ESTABLISHED 15711/sshd: root@pt
    tcp 0 36 192.168.203.133:22 192.168.203.1:6022 ESTABLISHED 7942/sshd: root@pts
    tcp6 0 0 :::22 :::* LISTEN 7631/sshd
    tcp6 0 0 :::9090 :::* LISTEN 15853/java
  • lsof

    通过 lsof 观察到运行java代码的进程出现了一个文件描述符处于Listen状态

    1
    2
    3
    4
    lsof -p 15853
    COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
    ...
    java 15853 root 6u IPv6 53184 0t0 TCP *:websm (LISTEN)

客户端请求连接但服务器端未accept

  • 启动客户端

    1
    2
    javac SocketClient.java
    java SocketClient
  • 解决异常java.net.NoRouteToHostException: 没有到主机的路由

    1
    2
    # 关闭防火墙
    systemctl stop firewalld.service
  • tcpdump

    1
    2
    3
    4
    5
    6
    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:00:49.829735 IP 192.168.203.132.37058 > 192.168.203.133.9090: Flags [S], seq 493363808, win 29200, options [mss 1460,sackOK,TS val 14267030 ecr 0,nop,wscale 7], length 0
    15:00:49.829764 IP 192.168.203.133.9090 > 192.168.203.132.37058: Flags [S.], seq 2693402151, ack 493363809, win 1152, options [mss 1460,sackOK,TS val 15102986 ecr 14267030,nop,wscale 0], length 0
    15:00:49.829979 IP 192.168.203.132.37058 > 192.168.203.133.9090: Flags [.], ack 1, win 229, options [nop,nop,TS val 14267031 ecr 15102986], length 0

    可以发现已经发生了三次握手

  • netstat

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    netstat -natp
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 7631/sshd
    tcp 0 0 192.168.203.133:22 192.168.203.1:3137 ESTABLISHED 7846/sshd: root@pts
    tcp 0 0 192.168.203.133:22 192.168.203.1:7070 ESTABLISHED 15711/sshd: root@pt
    tcp 0 36 192.168.203.133:22 192.168.203.1:6022 ESTABLISHED 7942/sshd: root@pts
    tcp6 0 0 :::22 :::* LISTEN 7631/sshd
    tcp6 1 0 :::9090 :::* LISTEN 16423/java
    tcp6 0 0 192.168.203.133:9090 192.168.203.132:37058 ESTABLISHED -

    在内核层面已经建立起了socket连接,并且当客户端发出信息后,服务器端是已经被接受了,即使这个连接没有分配给任何一个进程

当服务器端accept后

  • netstat

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    netstat -natp
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
    tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 7631/sshd
    tcp 0 0 192.168.203.133:22 192.168.203.1:3137 ESTABLISHED 7846/sshd: root@pts
    tcp 0 0 192.168.203.133:22 192.168.203.1:7070 ESTABLISHED 15711/sshd: root@pt
    tcp 0 36 192.168.203.133:22 192.168.203.1:6022 ESTABLISHED 7942/sshd: root@pts
    tcp6 0 0 :::22 :::* LISTEN 7631/sshd
    tcp6 0 0 :::9090 :::* LISTEN 16423/java
    tcp6 0 0 192.168.203.133:9090 192.168.203.132:37058 ESTABLISHED 16423/java

    该连接已经分配给了对应进程

  • lsof

    1
    2
    3
    4
    lsof -p 16423
    COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
    ...
    java 16423 root 7u IPv6 70977 0t0 TCP CentOS7.6:websm->192.168.203.132:37058 (ESTABLISHED)

    可以发现该进程已经被分配了文件描述符

总结

  • socket本质是一个四元组,通过 CIP_CPORT + SIP_SPORT 能确认唯一socket
  • new ServerSocket() 在内核层面的操作就是开启了一个监听 port 的 socket
  • 当两台服务器之间建立连接时并未将此 socket 分配给进程,即进程并未分配对应的文件描述符 fd,但服务器之间已经可以就行信息传递