先来看两个demo程序
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 IOServer {
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;
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;
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(9090),BACK_LOG);
serverSocket.setReceiveBufferSize(RECEIVE_BUFFER);
serverSocket.setReuseAddress(REUSE_ADDR);
serverSocket.setSoTimeout(SO_TIMEOUT);
}catch (Exception e){
}
System.out.println("server up 9090");
while (true){
try {
System.in.read();
Socket client = serverSocket.accept();
System.out.println("client port:"+client.getPort());
client.setKeepAlive(CLI_KEEPALIVE);
client.setOOBInline(CLI_OOB);
client.setReceiveBufferSize(CLI_REC_BUF);
client.setSendBufferSize(CLI_SEND_BUF);
client.setSoLinger(CLI_LINGER,CLI_LINGER_N);
client.setSoTimeout(CLI_TIMEOUT);
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 data "+ num+"val:"+new String(data,0,num));
}else if (num == 0){
System.out.println("read nothing");
continue;
}else{
System.out.println("read -1");
client.close();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}catch (Exception e){
}
}
}
}
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
public class IOClient {
public static void main(String[] args) {
try {
Socket client = new Socket("localhost", 909);
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 (Exception e){
}
}
}
先查看服务端开启后 netstat -antp 的变化. linux产生了一个 LISTEN 状态的 socket。由 进程号为 4143的java程序处理客户端的三次握手的请求,后续的业务逻辑会开辟新的线程来处理
使用 lsof -p pid 的方式查看文件描述符.看到server开启了一个 5的文件描述符监听
2. 启动客户端程序后,查看 服务端抓包情况。发现抓到了三次握手的请求
3. 查看服务端的 netstat -antp。 发现服务端建立了一个和客户端的socket 但是没有 PID来处理这个socket
4. 客户端发送数据后查看服务端
发现有数据在内核中产生积压
怎么理解TCP协议是面向连接的?
三次握手之后,双方就会开辟资源,可以为对方提供服务,就产生了连接。不需要应用程序,内核程序就已经完成了。
5. 服务器端开始接受客户端请求
再次查看 netstat,发现原来没人处理的socket已经被 5643 PID 的java进程处理了
查看 5643 PID 的文件描述符 ,发现多出来了一个 文件描述符为 6的读取 socket数据
socket = 客户端IP+客户端PORT+服务端IP+服务端PORT。
是一个内核级别的资源,就算应用程序不调用 accept 也就是生成一个文件描述符来消费socket。内核也会缓存socket的部分数据
只要是socket的四个维度有一个不一样,就可以开启一个socket
例如 客户端 A 的ip是 192.168.10.1 服务端B 的 ip 是 192.168.10.20, 服务端开启了一个tomcat 8080.然后还有一个nginx 80. 客户端A 同时访问tomcat 和nginx的时候 可以只开启 9999一个端口
但是为什么有的时候起服务器的时候经常会出现端口被占用的情况呢
因为在服务端开启程序监听请求的时候 创建的 socket 是 0.0.0.0:9090-> 0.0.0.0:* 这么一个socket,再次启动该服务端,还是这个socket,所以就冲突了。
client 和服务端三次握手之后,内核产生一个socket,并且开辟一个buffer空间。如果服务端程序accept了这个socket,就会产生一个 系统调用,生成一个文件描述符来对socket读写
back_log 是服务端可以缓存客户端的socket,再多余的客户端来就会被拒绝
当设置 BACK_LOG = 2 的时候,服务器没有接受socket,让内核自己缓存socket,发现最多可以建立 3个socket,再多的时候就会拒绝。
不延迟发送,如果数据量没有达到 buffersize 的时候是否先不发送。 默认是false, 表示延迟发送。开启延迟发送,客户端可以超过buffersize的数量还可以不发送数据。
Socket client = serverSocket.accept();
client.setKeepAlive(true);
TCP层面的KEEPALIVE 在建立连接之后,如果长时间不通信,如何知道对方还存活。 就是使用Keepalive。服务端会定时发送数据确认客户端还存活。如果超过多少次没有回就认为死亡,断开连接。
BIO->NIO->EPOLL
BIO 叫 Blocking IO ,阻塞IO,我们来看看为什么会阻塞
BIO服务端
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(9090);
System.out.println("server up 9090");
while (true) {
try {
Socket client = serverSocket.accept();
System.out.println("client port:" + client.getPort());
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 data " + num + "val:" + new String(data, 0, num));
} else if (num == 0) {
System.out.println("read nothing");
} else {
System.out.println("read -1");
client.close();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
}
}
}
BIO客户端
public static void main(String[] args) {
try {
Socket client = new Socket("192.168.0.94", 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();
out.write(bb);
}
}
}catch (Exception e){
}
}
通过命令 strace -ff -o out java BIOServer 追踪BIO服务端程序。 strace 命令是记录程序产生的系统调用。
看到java产生的系统调用如下, 创建一个socket 的文件描述符3,然后把3绑定到端口8090上,并开启listen监听状态。
在文件的最后看到打印阻塞住了 这个阻塞就对应着 Socket client = serverSocket.accept(); 这段代码
用 nc 连接 服务端
nc 192.168.0.94 9090
看到服务端的strace 打印。看到原来阻塞的accept后面产生了一个文件描述符5
clone 表示创建一个新的进程
clone 里面的flags 表示共享文件系统 CLONE_FS, CLONE_FILES 等等表示新创建出来的这个轻量级进程和父进程共享的资源。
然后在主线程的打印里面又看到重新阻塞的 accept 3 这个文件描述符,3文件描述符就是 serversocket监听的端口。
那么接下来看创建的子进程的 系统调用日志。子进程的日志就阻塞在等待5的输入
serverSocket的 accept 和 client的read 都会在系统调用产生阻塞,所以 BIO架构的服务端来了一个连接就需要创建一个线程来处理。如果客户端很多的话,假设10万个客户端连接,服务端就会产生10万个线程,线程间的切换也是一个重量级操作,也会浪费时间
BIO的问题是因为内核提供的 accept 和 receive 方法是阻塞的。其实内核也提供非阻塞的方法,我们来看看NIO的服务端如何实现
public static void main(String[] args) throws Exception{
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(9090));
server.configureBlocking(false);
List<SocketChannel> channelList = new ArrayList<>();
while (true){
Thread.sleep(1000);
SocketChannel client = server.accept();
if (client == null){
System.out.println("没有客户端连接.....");
}else{
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("client...port..."+port);
channelList.add(client);
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
for (SocketChannel socketChannel : channelList) {
int read = socketChannel.read(byteBuffer);
if (read > 0){
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println(socketChannel.socket().getPort()+":"+new String(bytes));
byteBuffer.clear();
}
}
}
}
需要注意的是NIO服务端的 server 和 client 都需要 configureBlocking(false)
同样用strace 追踪看日志
发现系统调用的accept已经,通过 NC连接发送请求也是正常的。 这个模型已经没有开辟新的线程了,一个线程就可以处理所有的客户端请求
**这个模型的一个弊端是随着客户端连接的增多,循环遍历socketClient的数量就会增多,而 socketChannel.read 虽然是非阻塞 但是还是系统调用!!!!! **
原有NIO模型, 有多少路IO就要调用多少次 read 方法,就算没有数据返回-1也是一次系统调用
多路复用器模型,调用 一次内核方法返回所有IO的 状态,然后再由用户自己去对有状态的IO进行读取,所以叫多路复用,一次复用了多次的调用
只要是用户自己读取,就是同步的模型
多路复用器并不是只能在非阻塞使用, jdk1.8的bio server accept底层就是poll
并不冲突,调用了多路复用器之后知道了客户端的fd,对于fd的读取是不是阻塞的才是关键.
没有最大fd限制,在java8 bio模型下的调用如下,会传入需要监听的文件描述符5,也就是serversocket的文件描述符,然后阻塞当解除阻塞的时候表示 5 有数据了,接下来的 accept就能马上读到数据. 本质 poll 和 select 没有太大区别
poll([{fd=5, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=5, revents=POLLIN}])
accept(5, {sa_family=AF_INET, sin_port=htons(53668), sin_addr=inet_addr("172.20.24.226")}, [16]) = 6
原本的NIO 模型 需要调用 n 次系统调用. 经过 多路复用器之后, 只需要经过一次询问状态,然后返回m个有数据的io ,在调用 m 次的 receive系统调用即可.
其实不论NIO还是select,poll都是需要遍历所有fd,只是一个是用户态内核态切换,一个是全部在内核态完成
epoll 通过在内核中开辟一个空间存储 fd,由红黑树组成. 当网卡中有事件产生的时候,遍历红黑树,找到对应的fd,把fd放入另一个链表中,当用户想获得fd的时候直接返回有链表即可.
epoll最核心的一点就是规避了对所有文件描述符的遍历过程,在产生中断的时候就已经把相应的fd放入链表中了
epoll 提供了3个 系统调用 epoll_create, epoll_ctl, epoll_wait
首先用户调用 epoll_create 在内核开辟一个红黑树空间,返回这个空间的文件描述符 fd6
当有一个新的链接进入的时候, 调用 epoll_ctl(fd6,ADD,fd7) 把fd7放入红黑树中. 最后用户想要获得数据的时候调用 epoll_wait 就把链表中的数据拿回来.
红黑树搬运到链表的操作由内核完成