前面我们简单介绍了网络之间是service和client是如何建立通信的,这次我们来看下两方是如何交换数据的。
使用的service和client代码和前一章一样。
启动service
首先我们先来看下基础的网络BIO模型是如何运作的,为了讲解清楚过程,我们还是以实际运行结果展示。
我们这次在service运行时,追踪一下service进程的系统调用情况:
我们通过命"strace -ff -o out java Service "令,将"java Service"命令的系统调用执行情况输出到out文件夹。
现在我们已经将Serice启动了,绑定的端口号是9092。
此时我们来观察下网络情况:
我们可以看到此时针对9092端口,已经有了一个线程在进行Listen,只是还没有客户端连接进来。
我们再来看下文件描述符的使用情况:lsof -p 1982
当前service端1982线程下分配了一个描述符17来进行端口9092的监听。
现在我们再来看下文件情况:
前面我们有讲,我们会将每个线程的系统调用追踪情况打印到以out开头的文件中,现在发现多出了很多out文件,这说明当我们在启动service之后,实际上是创建了很多的线程在一同执行。
下面我们进入主线程9184看下系统调用情况,通过vim查找日志,我们发现有一个write的系统调用:
通过这几个write我们便可以在控制台里面看到我们输出的那两行信息,同时线程通过System.in.read()调用系统read()方法阻塞在这边(read的FD就是0,标准输入),等待输入。
我们前面有查看进程1982有分配一个FD(17)用于监听9092端口,这是怎么实现的呢?我们继续查看系统调用:
通过这几行日志,我们便可以很清晰的看到,当我们代码中new ServerSocket(9092)之后,在OS层面,是帮我们创建了一个socket,分配了描述符17,同时将17与9092进行绑定,最后再进行监听。
启动client
再下面我们让service开始接收客户端的连接,并通过命令“nc 127.0.0.1 9092"与service建立一个连接,我们看下service端的情况:
然后我们来看下线程1982的文件描述:
我们可以看到,针对线程1982,OS已经分配了描述符FD(18),关联的端口号是9092,对应的client的IP是localhost,端口号是55342,状态是已经建立连接,可以传输数据。
那当我们启动nc建立连接之后,发生了什么事情呢?我们一起来看下系统调用的追踪情况:
我们可以看到在poll中遍历到 FD=17的描述符后,accept了 一个ip是127.0.0.1,端口号是55342的连接,同时分配了描述符FD(18),后面再是系统调用write(1),打印"service accept client request …",对应的servie代码如下:
其中关于poll相关的知识点,我们会在后续的NIO中进行讲解。
如此我们便了解了socket.accept()在OS层面执行了哪些步骤,同时也印证了我们上一章节见过的socket的四元组概念。
业务处理
我们再看一下上面贴的代码,当我们accept了一个client之后,创建了一个线程来读取数据。具体在OS层是怎么实现的呢?我们还是来追踪系统调用情况:
我们可以看到有触发一次clone的系统的调用,从当前主线程中clone出一个子线程,flags中规定了子线程持有父线程的FD,在同一个内存空间等信息,同时对clone出的子线程指定线程唯一编号2387。
当代码的new Thread().start()之后,子线程便开始运行了,现在我们来看下目录文件:
此时我们可以看到os已经创建好并运行了编号为2387的线程,strace追踪线程也已经将2387线程的系统调用情况记录进文件了。
我们来看下2387线程的系统调用做了啥,还是一样的,我们进入out.2387文件一探究竟:
一目了然,我们可以看到2387线程,阻塞在这边,一直在等待从FD(18)中读取数据。还记得FD(18)是什么吗?没错,就是前面介绍的service在accept到client之后分配的描述符FD(18)。
网络IO模型
我们现在来梳理一下Service的过程:
- 主线程启动,调用OS的Socket,创建出一个Socket,绑定9092端口,分配FD(17)进行监听
- 创建出socket之后,执行accept来接收客户端的连接
- 客户端接入之后,OS得到完整的四元组socket信息,分配FD(18)进行数据传输
- 主线程clone出一个子线程,不停的尝试从FD(18)中读取数据
PS:linux下的追踪命令:strace
Mac os下追踪命令:dtruss,但是显示的信息没有linux全。