前言


  以前对IO、NIO还算了解,也写过Netty的项目。但是对底层的数据传递不是很了解,一直存有这方面的疑惑。但是由于有其他事情就被打断了。前阵子因为想要了解volatile关键字的原理,学习了下JMM(Java内存模型),了解到对象数据是如何存储的。后来又想知道Tomcat是如何传递Http报文的,源码翻着翻着就到了Socket,想来Socket还有些东西没学清楚,就干脆乘着兴致查阅了不少资料。

这里就以数据读写位置为中心,整理分享一下相关内容吧。

整体视图



从“互联网” 到“本机网卡”

网卡会判断网络数据报是否是给本机的,如果是则接收,否则丢弃。它是如何判断的?数据报中有目的地址,如果为本机IP地址,则接收下来。

网卡的存储空间

网卡是有存储空间的,不过很小,只有几KB。它只能作为临时缓冲用的,一般需要存入内存。

从“本机网卡”到“内核空间”

网卡会使用DMA把数据报写入到内核空间中,这个过程不需要CPU干预。

内核空间与用户空间

内存分为两大块,用户空间和内核空间。内核空间是归属于操作系统使用的,为了安全,用户空间中的程序只能访问分配给它的地址空间,一般不能访问内核空间。


地址空间:也就是操作系统分配给进程的内存空间,它只能访问自己的内存空间,不能干预其他进程。即指针只能在一定范围内活动。地址空间是可以扩容的,这是后话了。

Socket的读写队列


每个Socket都在内核空间中都有与之相关联的读写队列(存储空间),一个读队列,一个写队列。且读队列的大小一般要大于写队列。Socket要读数据就从对应的读队列中读,写数据就写到相应的写队列。

数据报如何正确地写入到相关的Socket队列中?


换句话说,如何知道数据报是归属于哪个socket。首先IP地址肯定有了,其次TCP/UDP数据报中就有"目的端口"的字段,这自然就能映射到相关的Socket了,因为本机中的socket就是用占用的端口来彼此区分的。

Linux如何查看读写队列大小

相关信息在这两个配置文件中,内容依次是最小,默认,最大
/proc/sys/net/ipv4/tcp_rmem (读队列大小配置) /proc/sys/net/ipv4/tcp_wmem (写队列大小配置)
从“内核空间”到“用户空间”

socket对象调用read方法,就是从内核空间中读取数据到用户空间。

系统调用


前面说了,用户空间的程序一般是不能访问内核空间的。但是程序要运行,有时候不得不访问磁盘和网络数据。于是乎,操作系统就提供一些库函数,用户程序可以调用这些库函数来间接使用操作系统的功能。

注:这里与socket相关的操作都是系统调用

如果读队列没有数据可读会怎样?


这取决于socket的mode,默认是阻塞的。也就是说,如果读队列中没有数据可读,那么当前执行这个read函数的线程将被挂起,然后等到内核空间来数据的时候再唤醒这个线程开始读数据,这就是同步阻塞。当然也有非阻塞式的,就是说,如果没有数据可读,执行线程不会被挂起,而是完成read函数,返回一个"-1"的错误码。同步非阻塞,说的就是,反复调用read函数直到成功。

待解决:内核空间如何唤醒这个线程,用的是什么机制。

读出来的数据放在哪里?


一般,我们会分配一个空间来存储,也就是创建一个byte数组来缓存读取进来的数据。为什么说是缓存?因为我们使用socket肯定不是简单的把数据读出来,肯定还要进行下一步的处理,byte数组只是用来暂时存储数据的。

IO复用的思想


前面说的,不管是同步阻塞,还是同步非阻塞。根本上都是说,线程要等到可以读写的时候,才开始读写操作。这样看来,这段等待的时间就算是浪费了。(不管你等待的方式是挂起,还是轮询),IO复用的思想就是认为,这段等待的时间可以利用起来,去执行其他socket的IO操作(当然是满足读写状态的socket)。或者说,就是只有你满足读写条件后,你准备好后,我(也就是线程)才来处理你的读写操作,而不是我来了,还要等你梳妆打扮半小时才能出发。

select、poll、epoll等函数的使用


IO复用中,一个线程同时负责多个socket连接的读写。select、poll、epoll函数简单地说,就是把满足读写状态的socket挑选出来。不同的是,它们挑选的方式不同而已。这里由于博主涉猎不深,也就不展开介绍了。

FAQ 常见问题

说是常见问题,其实只是我个人想到的,看客可能会存在的疑惑。

1.Java的socket API与window或linux底层的socket API是什么关系?


Java的socket是上层封装的API,它使得不管什么平台,都能使用同一套API。它的底层实现还是c语言的库函数。到底用哪个看运行环境,如果是window,那底层用的就是windows的socket
api,否则就是linux的socket api。其实你装JDK的时候就已经确定了,因为下jdk的时候就已经选择了windows/linux。

2.如果读队列已满,发送方继续发送的数据会丢失吗?

 这就涉及到TCP的拥塞控制了,当队列已满的时候,新来的数据不会被确认。没有确认收到的数据,它是会重新发的。读者可以往拥塞控制(congestion
control)方向去看。

参考资料

1.How TCP Socket works <https://eklitzke.org/how-tcp-sockets-work>

2.Network Interface Controller
<https://en.wikipedia.org/wiki/Network_interface_controller>

3.Network Socket <https://en.wikipedia.org/wiki/Network_socket>

4.How to find the socket buffer size of Linux?
<https://en.wikipedia.org/wiki/Network_socket>

5.system call about socket <https://linux.die.net/man/2/socketcall>

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信