承接上文网络通信IO模型上
BIO的Java代码
服务端创建一个ServerSocket,绑定了端口号8090,目的是让客户端和服务端建立连接后进行通信,然后进入死循环,死循环里面会调用server.accept得到一个socket客户端,打印客户端的端口号,然后启动一个线程,为什么要启动一个线程?
因为accpet会阻塞,如果因为没有客户端建立连接,就没有返回值,会一直阻塞,只有客户端建立连接,才能从阻塞变成返回,然后再读取客户端发送过来的数据,读取输入流并打印用户发送的数据,读取的话,也有可能变成阻塞状态,如果客户端一直不发送数据过来,服务器端就会一直阻塞,所以要把客户端读取的过程放到另外一个线程里面去做。
通过strace命令追踪进程情况
strace -ff -o out java TestSocket
-
ff 代表追踪这个程序所有的线程
通过strace命令追踪java程序有多少个线程,每个线程对内核产生哪些系统调用,都会记录下来。
启动该程序,首先会打印
表示第一步创建一个ServerSocket,绑定了8090端口,然后进入阻塞状态,直到有客户端建立连接。
这时多了以out开头、数值结尾的8个文件,代表8个线程。
当有一个客户端连接的时候,会new一个新线程,就变成9个out文件了。
一个java程序刚启动的时候,它自身就是多线程的,有些线程负责gc回收,有些线程负责监控客户端建立的socket连接等。
上图中的主线程是8211,
查看out.8211就可以看到java程序在运行时和内核交互的整个过程。
每一行最前面代表系统调用的内核函数名称,接着是入参,最后是返回值。
先看这个文件的最后一行,accept阻塞在这里了,一直在等待客户端的连接。
首先系统调用socket方法得到一个文件描述符3即对应ServerSocket,将3绑定到8090端口上,绑定完之后,下面listen表示开始监听3,其实是在监听8090这个端口。
从阻塞到不阻塞的过程是怎样的?
启动TestSocket程序,进程编号是8211,主线程编号也是8211,监听端口是8090,目标地址可以是任何地址和任何端口。
用nc模拟一个客户端建立连接
nc localhost 8090
nc(net connection或net cat)就想象成一个网络通信的客户端,它能帮你完成tcp的三次握手。
TestSocket程序就会打印一个客户端连接进来了,
表示客户端申请了一个随机端口号连接到了服务端8090,
刚才accept 3 阻塞在这里了,一个客户端连接进来之后,就会立刻返回一个代表客户端连接的文件描述符5,并且绑定了客户端端口号,
现在多了一个连接状态,目标程序是本地服务(127.0.0.1)的8090端口,来自于本地的59033这个客户端程序,
59033正好对应nc进程。
TestSocket程序从开始只有一个文件描述符3,然后一个客户端建立了连接得到了文件描述符5,然后Java主线程启动一个子线程读取文件描述符5.
如何启动一个新的线程?
调用clone这个内核的系统调用创建了一个新的线程8281。
java的线程其实就是调用内核的系统调用clone,然后得到一个8281线程,这个线程同属于最开始的进程id 8211,
8211代表一个线程组。
此时又多了一个8281的线程文件,查看该文件
调用了内核的recv方法,文件描述符5是在主线程产生的一个客户端socket连接,用另外一个线程去读文件描述符5。
主线程继续accpet。
每多一个连接,就多了一个线程,如果有一万个连接,就会有一万个线程。
整体过程
先调用socket返回fd 3,将fd 3绑定8090端口,然后监听8090端口。
while死循环,先accept接收客户端的连接,如果没有连接就一直阻塞,如果有连接,则读取连接,如果客户端一直没有发送数据过来,就会一直阻塞,所以为了不影响主线程接收连接,再创建一个新的线程,用于读取每个客户端发送过来的数据。
这是最古老的网络IO模型BIO。
一个线程对应一个连接,优势是可以接受很多连接。
弊端:如果进程特别多或线程特别多的时候,就会造成内存的浪费,cpu调度也会消耗了更多cpu的时间片。
BIO中有2个方法是阻塞(BLOCKING)的,一个是主线程的accept,另一个是recv或read这2个方法。
这2个系统调用会阻塞,所以不能把都有可能阻塞的操作放到一个线程里 ,否则一个阻塞就会干预到另外一个阻塞,所以解决BIO模型的弊端 ,只需要非阻塞(NON BLOCKING)就可以了,非阻塞网络模型就是NIO了。