陈同学
微服务
Accelerator
About
# 数据从网卡到应用的过程 最近看的《网络是怎样连接的》非常有趣,真的是 **"计算机网络概论" 图解趣味版**。 本文写写数据从网卡到应用的过程,内容与图片很多整理自《网络是怎样连接的》、《Tomcat内核设计与剖析》,有的图片因清晰度不够我进行了重绘。 ## 总览 本文围绕这张图从下至上展开。假设一个HTTP请求的数据到达网卡,那数据是如何被层层处理并到达应用呢? <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/11.png" width="70%"/> ## 网卡 网卡(Network Adapter),也称网络适配器,是一个 **硬件设备**,有全球唯一的 MAC(Media Access Control)地址,MAC地址在网卡生产时就被烧制在ROM中,网卡初始化时恢复到计算机中。 网卡收到的数据是 **光信号或电信号**,然后将其还原成 **数字信息(1和0组成)**。 <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/1.png" width="60%"/> 下图是还原的数字信息结构。 <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/2.png" width="60%"/> 根据 FCS(帧校验序列,Frame Check Sequence) **校验数据**,判断数据在传输过程是否因噪音等影响导致信号失真,从而导致数据错误,需要**丢弃这种无效的数据包**。 <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/4.png" width="40%"/> 然后 **检查** 数据包中MAC头部中的 **接收方的MAC地址**,若不是发给自己,则丢弃数据包;若数据包是发给自己,则将数字信息保存到网卡内部缓冲区。 以上过程网卡自行搞定,不需要CPU参与,CPU也不知道数据包的到达。 ## 网卡驱动 硬件需要驱动程序来控制,就像电脑需要操作系统一样,而网卡驱动就是CPU控制和使用网卡的程序。 网卡处理完数字信号后,接下来的数据接收需要CPU参与,此时**网卡通过中断将数据包达到的事件通知给CPU**。接着,CPU暂停手头工作,开始用网卡驱动来干活。 * 从网卡缓冲区读取接收到的数据 * 根据MAC头部的以太类型字段判断协议种类并调用处理该协议的软件(即协议栈) 通常我们接触的以太类型是 **IP协议**,因此会调用TCP/IP协议栈来处理。 ## 协议栈 因各层协议看上去像堆叠状态,也就取名"协议栈"。 像TCP、UDP、IP等协议都是规范,而**协议栈则是实现各类协议的网络控制软件**。例如:Windows、Linux各自对协议进行了实现,因此不同系统之间能够通讯,与JVM跨平台原理一致。 ### IP模块 当MAC头部以太类型为 IP 协议时,网卡驱动数据包交给TCP/IP协议栈来处理。 * IP模块会检查IP头部以判断数据是不是发给自己 * 判断数据包是否分片,如果分片则缓存起来等待分片全部到达再还原成数据包 * 根据IP头部的协议号字段,将包转给TCP模块或UDP模块处理 下面是IP头部的部分字段。 <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/5.png" width="40%"/> ### TCP模块 下面是TCP报文首部结构。 <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/6.png" width="80%"/> TCP模块会根据 **标志位** 来进行不同处理,假设服务端收到该报文,会进行如下处理: * 如果 SYN=1,表示这是请求连接的包。 * 首先检查接收方端口号,然后检查有没有与该端口号相同且**处于等待连接状态的套接字**。 * 如果没有,则返回错误通知的包; * 如果有,则**为这个套接字复制一个新副本**,将发送方IP、端口等必要信息写入套接字,同时分配用于发送缓冲区和接收缓冲区的内存空间。 * 最后返回给数据客户端,客户端会再次确认,这属于TCP连接三次握手的一部分。 * 如果是正常数据包,**TCP模块需要检查该包对应的套接字**。然后提取出数据,存放到缓冲区。此时,如果应用程序调用socket的read(),数据就可以转交给应用程序了。如果应用程序不来获取数据,数据则一直保存在缓冲区中。 在上述处理过程中,不同协议层会逐层处理,剥洋葱一样剔除协议头部,将数据转交到上层。而发送数据时,TCP、IP等也会一层层的为数据包加上头部。 最后,数据就到应用层,应用层通过socket来操作数据,下面说说socket。 ## Socket ### socket 是什么 socket 译为套接字,由加州大学伯克利分校的研究人员在20世纪80年代早期提出,因此也叫伯克利套接字。其研究人员使得套接字接口适用于任何底层协议,首个实现的协议就是针对TCP/IP。 从不同角度来看,其含义不同: * 对应用层来说,可通过 socket 与内核中的网络协议栈通信。应用不能直接使用协议栈,更不能控制网卡驱动。所以 socket 提供了网络编程的系统调用接口。 * 从Linux文件系统来说,socket 是一个打开的文件。下面命令找到的就是一个套接字类型的文件。 ```bash $ find / -type s /run/docker.sock ``` 在普通 Java 应用中的文件描述符文件夹中也可以看到,下图都是一些指向socket的软链接。 ```bash $ cd /proc/25693/fd $ ls -l | grep socket ``` <img src="https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/7.png" width="80%"/> * 从Linux内核来说,socket 是一个通信的端点(Endpoint)。 常说 "连接",通过C/S两端的socket真的是建立了一个物理通道吗? 连接实际上指的是通信双方的信息,套接字中记录了这些必要信息。下面是两个连接信息,包含了通信双方的IP、端口信息。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/8.png) 因此,套接字也可以认为是一个概念,它包含了通信双方的信息。 ### 文件描述符 文件描述符(File Description)简称fd,是一个正整数,起到一个文件索引的作用,保存了一个指向文件的指针。 每创建或打开文件都会返回一个fd(一个数字),其实主流操作系统将TCP/UDP连接也当做fd管理,每新建一个连接就会返回一个fd。 ### 创建 socket 应用程序申请创建套接字,具体实现由协议栈来搞定。协议栈首先会分配用于存放一个套接字所需要的内存空间,然后往其中写入控制信息(双方IP、port等信息)。套接字刚创建时,数据收发还没有开始,因此需要写入初始状态的控制信息。 接下来,需要将这个套接字的fd告诉应用程序。收到fd后,应用程序再向协议栈委托收发数据时,就要提供fd。 另,服务端在接收数据时,每来一个新连接,都会拷贝当前处于等待连接状态的socket,然后写入控制信息,而原先的socket则继续等待新的连接。 ### socket 编程 demo 一个最简单的网络编程例子。 服务端: ```java // 创建套接字并绑定到8080端口 ServerSocket serverSocket = new ServerSocket(8080); while (true) { // 将套接字设置为等待连接状态,并阻塞线程 Socket socket = serverSocket.accept(); // 读数据时,其实要向协议栈提供套接字fd, 读取数据 System.out.println(IOUtils.toString(socket.getInputStream(), "utf8")); } ``` 客户端 ```java // 创建客户端套接字(会有fd, 完成TCP握手) Socket socket = new Socket("127.0.0.1", 8080); // 使用套接字收发数据 socket.getOutputStream().write("Hello World".getBytes()); ``` ### Socket 基于内核的回调机制 应用通过socket通信,socket保存了通信双方信息,相当于一个连接信息。当并发量大时,比如 [C10K](http://www.kegel.com/c10k.html) 即单机1W并发连接,1W连接也对应着1W个fd。那如何判断哪些连接有新数据呢? * 传统IO模型**一个连接一个线程处理**,然后遍历连接,CPU光遍历连接就已经满负荷。耗费大量线程,频繁切换线程环境,只适合低并发应用。 * 进一步,可以一个线程管理多个socket(也就是多个fd),这也是NIO中的select机制。虽然降低了线程数,提高了并发能力,但遍历的瓶颈一直都在。 * 问题最终由异步机制搞定。linux内核2.6版本提出了新的多路复用机制 epoll,套接字提供了回调函数,内核从网卡读取数据后就会回调该函数。 下面是《Tomcat内核设计与剖析》的一张图。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2019/04/6/10.png) * 首先,应用告诉内核对每个套接字感兴趣的事件,例如:socket1的读事件。 * 当客户端发送数据过来时,内核从网卡读取数据,然后调用socket1回调函数,将socket1作为可读事件加入事件列表 * 应用层获取读写事件列表时,拿到的就是实际可读写的事件,没有无效数据。 这种回调机制的最终目的就是避免无效工作,提高处理效率。 ## 小结 应用层开发人员,比如Java工程师,更多的会好奇数据如何从Tomcat到Servlet,这个过程这些应用层框架是如何处理数据的。 信息技术迅猛发展的这些年,虽然应用层技术更新很快,但这些底层设施一直非常经久耐用,非常经典。所以多学习些Linux基础、计算机网络等基础知识,也大有裨益。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
数据从网卡到应用的过程
文章链接:
https://chenyongjun.vip/articles/108
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力