系列文章目录
学懂IO必备的操作系统知识(一)
学懂IO必备的操作系统知识(二)
学懂IO必备的TCP、socket知识(三)
文章目录
前言
io分为磁盘io和网络io,前两篇文章,唠叨了一些操作系统磁盘io的知识,接下来继续唠叨网络io的知识。要想扒光网络io的衣服,看到真容,那就必须找的祖辈了。什么io模型的升级呀,BIO、NIO、AIO,socket、tcp协议等等,你要是连她这些七大姑八大姨都不认识,你好意思好了解io吗?
一、socket是什么?
1.1 socket定义
socket就是一个四元组(保证唯一),c_ip:c_port + s_ip:s_port
特点:
- 服务器可以分配的端口个数最大为65525(两个字节存储端口,最大2的16次大小)
- 服务端在与客户端建立了连接后,服务端不需要再为连接新分配一个端口号,一直是固定的。
- 只要服务器的资源足够可以建立成百万级的连接
1.2 socket参数
//server socket 监听参数:
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 监听参数:
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_TIMEOUT = 0;
private static final boolean CLI_NO_DELAY = false;
server socket参数:
- RECEIVE_BUFFER : receive_Q 接收缓存字节大小,TCP发送缓存区和接收缓存区,默认是87380个字节,
- SO_TIMEOUT: 等待客户连接的超时时间,单位毫秒,当服务器等待的时间超过了超时时间,就会抛出SocketTimeOutException,它是InterruptedException的子类;如果把serverSocket.setSoTimeOut(6000)去掉,那么服务器端会阻塞,直到接收到了客户的连接,才会从accept()方法返回
- REUSE_ADDR: 是否允许重用服务器所绑定的地址,有些操作系统是不允许重用端口的。两个serversocket在监听相同端口前,设置true,等一个serversocket 进程关闭后,另个serversocket能立刻重用相同接口,类似于容错。
- BACK_LOG: 备胎的连接个数,如果是2,则最多有3个可以建立全连接,连接状态为完成状态ESTABLISHED,新的连接服务端会是SYN_RECV状态。见图:
client socket参数
-
ReceiveBufferSize: receive_Q 大小,TC接收缓存区,默认是87380个字节,
-
SendBufferSize: send_Q 大小, TCP发送缓存区,默认是16384个字节,16K
-
tcpNoDelay: 用来控制是否开启Nagle算法,是为了提高较慢的广域网传输效率,减小小分组的报文个数,该算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到来之前,不能发送其他小分组。这个跟报文中的MSS有关。开启了Nagle算法与没有开启的一个效果,如图:
-
OOBInline: 表示是否支持发送一个字节的TCP 紧急数据。默认 false,当接收方收到紧急数据时不作任何处理,直接丢弃
-
KeepAlive: tcp如果双方建立了连接,2H没有通信,会发送心跳检测,如果没有响应,会持续11分钟,如果还是没有响应会断开。默认false
-
SoLinger: 用来控制Socket关闭时的行为。默认情况下,当执行Socket的close()方法,该方法会立即返回,但底层的Socket实际上并不立即关闭,它会延迟一段时间知道发送完所有剩余的数据,才会真正关闭Socket,才断开连接。socket.setSoLinger(true,0):方法立即返回, 会导致socket底层立即关闭,未发送和未接收的数据会丢弃。socket.setSoLinger(true,1000); /该方法不会立即返回,而是进入阻塞状态。 只有当底层的Soket发送完所有的剩余数据或阻塞时间已经超过了1000秒,再返回,但是剩余未发送的数据被丢弃。
-
SoTimeout: 设置socket调用InputStream读数据的超时时间,以毫秒为单位,如果超过这个时候,会抛出java.net.SocketTimeoutException。
示例 服务端代码:
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;
/**
* 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);//获取连接超时时间
System.out.println("server up use 9090!");
while (true) {
try {
System.in.read(); //分水岭:
Socket client = server.accept();
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);//read()读取数据超时时间
client.setTcpNoDelay(CLI_NO_DELAY);
new Thread(
() -> {
while (true) {
try {
InputStream in = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
char[] data = new char[1024];
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...");
client.close();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
).start();
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客服端代码:
import java.io.*;
import java.net.Socket;
/**
* @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); //自己修改IP地址
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();
}
}
}
监听 tcp 通信 ,
shell> tcpdump -nn -i oth0 port 9090
//通过新的客户端nc发送连接 或 直接从socketclient 发送连接,可以在tcpdump 的窗口看到三次握手的信息,及后续发送的报文
shell> nc 192.168.149.11:8080
三次握手+报文信息:
报文相关
-
窗口大小:缓存区多大,mtu(ifconfig 可看)整个数据包大小,报文中的mss为数据内容大小,双方协商一个窗口大小win(窗口剩余代表着可发送数据包的大小),每次发送不一样。
-
拥塞: 表示对方窗口没空间接受数据了,需要阻塞一下,有空间了后,会发送一个信息。
clientsocket 、serversocket 对应关系图:
服务端不需要再为client 的连接分配一个随机端口号,而是会分配一个fd来进行通信。
二、TCP三次握手、四次分手
2.1 TCP定义
TCP 是 TCP/IP 协议栈中的传输层协议,它通过序列确认以及包重发机制,提供可靠的数据流发送和到应用程序的虚拟连接服务。
特点
Tcp是面向连接的,可靠的传输协议(为什么说可靠呢,每次发送都有应答),通过三次握手创建通信链路后,内核级会分配资源及双方会存储四元组信息(连接信息),即使服务端没有对应处理进程。
2.2 三次握手
有个哥们写的非常详细,我就不赘述了,老话说的好,要站在巨人的肩膀上:TCP三次握手详解-深入浅出(有图实例演示)
2.3 四次分手
简化版流程
client 发送了断了连接请求,server端还未发起,状态如下:
三、io 模型
这部分,劳烦老铁们,查看:深层次详解同步IO、异步IO、阻塞IO、非阻塞IO
四、BIO
提前在这里讲下bio,以后就重点讲nio了。
BIO 多线程的机制
服务端通过创建一个serverSocket得到一个fd,再通过fd bind绑定到监听的端口上,再对外开启监听listen(例如:通过netstate -antp命令得到下面的9090 监听 ,0.0.0.0:9090 0.0.0.0:* );客服端创建clientSocket的一个fd,再通过三次握手完成连接,当完成全连接后放到一个全连接的列表中,此时通过netstate -antp命令,会发现多了一条记录 ,192.168.150.11:9080 192.168.150.12:47513 ESTABLISHED - (图1,建立了连接,内核给双方分配了资源,但是没有分配对应的进程来处理),服务端进行socket的accept之后,会从全连接列表中把全连接取出去,并对每个连接分配对应的进程及新的fd处理该连接的操作,图2。
图1
图2
strace -ff -o out: 跟踪内核线程,信息输出到out文件,每个线程对应一个out前缀的文件,out.pid。在执行class文件时使用。
例子:strace -ff -o -out java TestSocket 。out.pid 主进程文件,可以看到主进程在内核执行的操作,socket(…)返回一个fd,bind(fd,…) fd绑定到端口,listen(fd,backlog) 开始进行fd 的监听。里面再开始阻塞到accept(fd ,直到客户端连接进入,会对该连接新建一个fd,并通过clone(…)开启一个新的线程。 主线程是个死循环,一直在accept(…),获取连接,开启新的线程。(对照上面的代码例子)
netstat -antp 看到网络状态
nc 192.168.150.11 9090进行连接
lsof -p pid
man 2 socket :通过man 可查看帮助手册,分8类。
五、总结
所有的io模型都是会有以下几步
socket=fd 3>>bind(fd,port)>>listen(fd 3)>>accept(fd)> fd 5 ,会发生阻塞blocking >>recv(fd 5) blocking 会发生阻塞
socket建立连接的架构图: