Socket的连接过程、TCP的一些参数
前置知识
用到的命令
netstat -natp
查看网络连接和占用的端口
tcpdump -nn -i eth0 port 9090
开监听抓取数据包
lsof -p <进程号>
查看某个进程已经打开的文件状态
Socket
服务端代码
package com.bjmashibing.system.io;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;public class SocketIOPropertites {//server socket listen property: 这些配置不是JVM层级的,是关联到内核的TCP协议栈的一些选项参数。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_NODELAYStandardSocketOptions.SO_KEEPALIVEStandardSocketOptions.SO_LINGERStandardSocketOptions.SO_RCVBUFStandardSocketOptions.SO_SNDBUFStandardSocketOptions.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 0new 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();}}}
}
客户端代码
package com.bjmashibing.system.io;import java.io.*;
import java.net.Socket;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); // 如果数据量比较小,会不会积攒起来再发,默认是trueclient.setOOBInLine(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();}}
}
下面详细跟踪建立连接的过程
启动服务端
开启服务端后,出现了一个对于 9090 的 listen 状态。
TCP 三次握手是走 listen 的,建立连接之后,后面走文件描述符,那就是另外一个环节了,我们后面再讲。
使用jps
得到服务端的进程id号:7932
使用lsof -p 7932
查看7932端口的文件描述符的分配情况。
启动客户端
客户端启动,进入代码的阻塞等待用户输入逻辑
在服务端抓到了三次握手的包
在服务端看到建立了连接,虽然连接还未被使用。
在客户端进行用户输入之后(服务端也有的阻塞的逻辑,需要回车才能接收client的数据)
继续查看服务端抓包监听
查看服务端的连接状态:双方开辟了资源。即便你程序不要我,我也在内核里有资源用来接收或者等待一类的。
服务端输入回车之后
接受到了客户端发过来的数据
刚才的socket连接已经被分配给7932了
lsof 得到了新的文件描述符 6
总结一下
TCP:面向连接的,可靠的传输协议
Socket:是一个四元组。ip:port ip:port
四元组的任何一个元的不同,都可以区分不同的连接。
面试题 1:服务端80端口接收客户端连接之后,是否需要为客户端的连接分配一个随机端口号?
答:不需要。
面试题 2:现在,有一个客户端,有一个服务端,
客户端的ip地址是AIP,程序使用端口号CPORT想要建立连接。
服务端的IP地址是XIP,端口号是XPORT。
现在假设某一个客户端A开了很多连接占满了自己的65535个端口号,那客户端A是否还能与另一个服务端建立建立连接?
答:可以,因为只要能保证四元组唯一即可
注:一台服务器是可以与超过65535个客户端保持长连接的,调优到超过百万连接都没问题,只要四元组唯一就可以了。客户端来了之后,服务端是不需要单独给它开辟一个端口号的。
下面这个图可以说明,无论再多的连接,服务端始终是使用的同一个<ip:端口>
那么,我们常见的报错“端口号被占用”是什么原因?
我们常见的报错“端口号被占用”实际上是在启动SocketSocket
的时候,而不是Socket
,两者不是一个概念。如果两个服务使用了相同的端口号,这时如果来了一个数据包,内核无法区分是哪一个服务在LISTEN,不知道要发给哪一个服务了,如下图例子
每一个独立的进程只要维护它自己的文件描述符唯一即可。
keepalive
三个不同层级的 keepalive
- TCP协议中规定,如果双方建立的连接(虚无的,并不是物理的连接),如果双方很久都不说话,你能确定对方还活着吗?不能,因为可能突然断电。所以规定了这么一种机制,哪怕是周期性的消耗一些网络资源,也要及时把无效的连接踢掉,节省内存。
- HTTP级别
- 负载均衡keepalived
网络IO的变化 演进模型(BIO)
一句话概括BIO?
BIO就是,客户端来一个连接,抛出一个线程,来一个连接,抛出一个线程…
几个维度
同步、异步、阻塞、非阻塞
用到的命令:
strace -ff -o out /usr/java TestSocket
用来追踪Java程序和内核进行了哪些交互(进行了哪些系统调用)
详细追踪 BIO 的连接过程
在服务端用jps
找到进程的id号是8384
在服务端使用tail监控out.8384
文件的输出(8384是main线程的输出,其他的out可能是一些垃圾回收线程等其他线程的输出)
(这里注意一下一共有8个线程,待会儿建立连接之后再看)
可以看到JVM用到了内核系统调用的accept
,main线程正在阻塞
在一个客户端上建立一个连接
在服务端我们看到,刚才阻塞 accept(3,
的位置继续执行。34178
是客户端连接进来的随机端口号,192.1618.150.12
是来自于客户端的ip地址
clone是linux的一个系统调用。Java当中的一个线程,就是操作系统的一个子线程。下图我们看到,(客户端连接进来之后),服务端调用clone
函数,开启了一个线程号为8447
的新线程。flags
里面记录的是子线程共享的文件系统、打开的文件等父线程的系统资源。
下面又开始阻塞的accept
查看用strace
输出的out文件,也可以证明8447
这个新线程的存在。
在服务端可以看到,多了一个文件描述符5,表示的是从node01(服务端机器名称)
到node02(客户端机器名称)
的已连通的状态(socket四元组)
服务端 8447.out 正在recv
阻塞接收
想学好Linux,去学习文档中这些man帮助手册,有时候比网络上的博客文章更准确(也可以 man man 查看帮助文档本身的帮助文档)
使用man 2 socket
,你会发现所谓socket系统调用,其实就是调用了一个有返回值(文件描述符)的函数(用于LISTEN)
稍稍总结一下
BIO 模型的整个连接过程
无论哪种IO模型,application想要和外界通信,都要进行上面所展示的一系列的(3步)系统调用,都是不可缺少的。
之后服务端进入阻塞状态accept(3,
等待客户端的连接。此次阻塞被成功地连接之后,又进入一的新的阻塞,等待新的客户端连接。
一旦连接成功之后,会为这个连接抛出去一个新的线程,新的线程中又进入一个阻塞状态recv(5,
等待接收消息。