传统的BIO模型下的,服务端代码:
/**
* 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;
//当线程不够处理到达的socket请求时,等待队列数量,超出数量拒绝
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();
}
}
}
}
客户端代码:
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();
}
}
}
上面就是一段很普通的BIO下的服务端和客户端的java代码实现。
一般来说,我们直观上会觉得一段连接的建立,是发生在服务端调用了accpet方法之后,此时客户端发来了连接请求,此时才被建立连接,那假如客户端的请求到来了,但服务器却还没来得及调用accpet,此时客户的socket请求被放在哪了呢?
首先开启tcp对9090端口号的监听:
启动服务端:
此时通过netstat查看,发现确实开启了9090的端口号:
通过jps查看server的进程号:
通过lsof查看进程的文件描述符详细情况:
发现开启了一个监听的文件描述符。
此时服务端的代码被System.in阻塞,因此没有到调用accpet方法的阶段,那这时候,客户端发来请求呢?
启动客户端:
此时去观察刚刚监听tcp9090端口号的shell的打印:
发现这里监听到了客户端11与服务端12建立三次握手的信息。
此时再通过netstat查看,发现也确实多了一条关于这个连接的socket描述:
只不过这个socket并未分配给任何的进程,此时只是存储于内核本身中,但确实说明了,此时双方都已经完成了握手,也在各自方进行了分配资源。
不仅如此,如果此时通过客户端进行发送信息给服务端,会在TCP的监听处发现监听信息:
并且此时,观察服务端这边,虽然连接没有分配给任何进程,但自身的接受队列中已经积压了客户端发送的内容:
接着,我们服务端代码继续执行,解除system.in 的阻塞,执行accept,此时在看netstat:
发现,这时候这个socket资源就已经分配给我们启动的服务端java进程了。
并且,服务端的java进程的文件描述符,也已经多了关于这个socket连接的文件描述符:
TCP是什么
通过上面的过程,我们做个小结:
TCP是基于三次连接的,完成后会各自分配资源。 这句话也得到了很好的印证。
socket是什么?
是一个四元组(ClientIP:ClientPort,ServerIP:ServerPort) ,由四个维度来唯一确定一个socket。
且socket的概念是内核级别的,而非进程,也就是说不是必须要java端调用一个accpet才行,而是内核去首先接手。
每一个socket最终可能会被分配到不同的进程中,然后以不同的文件描述符进行表示。
TCP通信模型图
TCP参数设置
BACK_LOG
服务端TCP设置参数:
private static final int BACK_LOG = 2;
上面我们可以看到,socket最初都是被内核先接纳的,如果没有分配给进程,都是存放在内核的,但也不能无休止的一直堆积下去,不然会撑爆内核了。
那内核可以存多少个未分配的socket呢?通过这个参数指定,如果是2的话,就是最多有3个socket可以被暂存,如果超过3个了呢?会被拒绝,换言之,TCP握手的时候不给予回应:
前三个成功建立连接,状态是ESTABLISHED,而第四个连接到来,是SYN_RECV,就是虽然请求来了,但是服务端没有给予回应。
SO_TIMEOUT
private static final int SO_TIMEOUT = 0;
服务端accept超时时间。
CLI_SEND_BUF
private static final int CLI_SEND_BUF = 20;
socket包发送的缓冲区大小
CLI_NO_DELAY
private static final boolean CLI_NO_DELAY = false;
是否尽可能进行延时发送优化,开启优化,发送的包大小将可能会超过缓冲区大小。
CLI_OOB
private static final boolean CLI_OOB = false;
是否积极的预先发送确认数据(少量1个字节)。
CLI_KEEPALIVE
private static final boolean CLI_KEEPALIVE = false;
在这里插入代码片
开启后,传输控制层会周期性双方互相发送一些心跳包,来保证确认存活。
三次握手的细节
可以发现每一次握手请求都带会有seq,win。
每一端自身都维护一个序列号seq 回复的请求的ack会基于对方发送过来的seq进行+1操作。
那win是什么?
窗口的概念,数据的传输最终都会作为一个数据包进行发送,而这个包多大呢?也就是一次发多少呢?
通过ifconfig可以查看网卡的MTU大小。
MTU 传输包总大小(包含请求头+数据)
MSS 传输包的数据大小
而在两端进行通信的时候,会带上自己的剩余缓存窗口大小,而发送方下次在发送数据包的时候,会根据上次收到的窗口大小来决定本次要发生多大的数据包合适,这样是为了在提高传输效率的同时避免拥塞。(拥塞:就是接收方因为可用接受空间不足,此时发送方需要阻塞等待接收方一点点接收完数据,而接收方满了的话,就需要剔除空间中的数据,剔除规则就是将后面接到数据进行丢弃,只收到了前面的数据,也就是会发生数据丢失。)