先从一段代码讲起,这是最经典的BIO服务器实现:
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created with IntelliJ IDEA.
* Description:
* User: CDz
* Create: 2020-08-16 15:08
**/
public class SocketBIO {
public static void main(String[] args) throws IOException {
byte[] buffer = new byte[1024];
ServerSocket serverSocket = new ServerSocket(9090);
System.out.println("创建server 9090");
while (true) {
Socket socket = serverSocket.accept();//堵塞
System.out.println("连接" + socket.getPort());
new Thread(() -> {
while (true) {
try {
socket.getInputStream().read(buffer);//堵塞
} catch (IOException e) {
e.printStackTrace();
}
String string = new String(buffer);
System.out.println(string);
}
}).start();
}
}
}
可以看到两点注释的地方:
serverSocket.accept();//堵塞
socket.getInputStream().read(buffer);//堵塞
先解释一下accept
操作是做什么的:
简单说就是连接,等待客户端进行连接,当有客户端进行连接的时候,就会往下执行,当没有客户端连接时,就会一直堵塞在这里。
那么我们为了其可以支撑多个客户端连接(如果只有一个连接该多好...也就没有了什么高并发、高可用,多线程balabala的)。
但是事实上,我们不可能让一个server只支持一个连接,一个连接进来之后,除非断开就再也不能有新的连接可以进入。
为了支持多个连接,于是发展出多线程处理——一个连接对应一个线程。
形象的图形是这样的:
如何证明?
我们使用strace
命令来追踪,看看最终的系统调用是什么。什么?不知道系统调用是什么意思?
什么是系统调用?
- 代码是如何执行的?
我们写的Java代码,最终会怎么样?会被编译成.class
文件。这个class文件被称为字节码。最终再JVM中被翻译然后通过CPU调用。
在计算机中程序的执行又分为两种,一种是系统调用,一种是程序自己的调用。
- 为什么分为这两种?
系统调用——kernel内核来掌管
- 为什么要内核来掌管?
因为实在有太多的设备需要进行处理,网卡、鼠标、显示器....等等,这些不可能让写程序的每每有调用的时候都自己去写硬件调用驱动。
于是kernel就是掌管这些硬件调用的。
真正疑问点(待解决):
我们写的程序是如何让kernel内核工作的?
CPU读到特定的指令直接切换?
还是程序可以直接调用kernel提供的API?
我们写的程序想要读取文件...一些的系统资源,那么就需要通过kernel来完成。
接下来我们使用strace
来追踪系统调用的过程。
将上述代码直接粘贴放入文件命名SocketBIO.java
然后在Linux系统下,进行javac SocketBIO.java
得到class文件。
命令:strace -ff -o out java SocketBIO
这个命令就是追踪执行过程中的系统调用,启动之后可以看到会有很多的out开头文件,这时找到文件最大的那个,进行查看。
vi out.16899
进入文件后,我们搜索9090(端口号)找到9090一行。
我们看到一个bind
这是映入眼帘的第一个系统调用,但是肯定没有那么简单,这里是直接bind绑定了,但是什么绑定的呢?看到传入的参数第一个5
,5是什么意思?kernel系统调用中所有的返回对象都是用数字来表示。
所以我们向上看,找到5是什么地方返回的。
在这里,建立socket的时候返回的5,所以是bind的一个socket。
接下来往下看:
梳理一下一共几个流程:
- 创建socket
- bind端口
- listen 1创建的socket
- poll等待(这里使用的poll的原因是因为我们使用Java8编译,编译器自动做了优化,在1.4的时候这里其实就是accept)
创建一个客户端:nc localhost 9090
建立连接之后,立马就出现了accept
函数调用。
往下看:
就此我们搞明白了BIO的这个过程是什么样子了,最关键点就是系统调用函数accept
是堵塞的。
那么这样肯定不行啊,我们同时也看到,每次创建一个新的thread都会clone,而clone是非常消耗资源的。并且线程也不是可以无限创建的,于是就有了新的模型NIO。
这也是时代发展的产物,开最开始的时候,并没有那么多人上网,连接肯定不算特别多,使用这个完全够用,只是发展的过程中不断的进行优化(其实Java的IO能力真正来源是Linux IO)——对应到我们程序架构也是一样,这么重要的Linux系统IO连接都是如此迭代过来的,更何况我们自己做的产品呢。
接下来就是优化的过程。