针对计算机网络的分层,我们看下各层是由谁实现的
上图比较清晰的给出了答案,应用层由用户进程实现(比如tomgcat、netty等服务器组件),传输层和网络层由内核实现,链路层由网卡驱动实现,物理层就是网卡以及网线等物理材料。
数据收发准备工作
服务器接收客户端的请求之前,要做一系列的准备工作。
服务器需要同时和多个客户端通信, 但一个程序来处理多个客户端的请求是很难的, 因为服务器必须把握每一个客户端的操作状态。 每有一个客户端连接进来, 就启动一个新的服务器程序。首先, 我们将程序分成两个模块, 即等待连接模块和负责与客户端通信的模块。当服务器程序启动并读取配置文件完成初始化操作后, 就会运行等待连接模块。接下来, 当客户端连发起连接时, 这个模块会恢复运行并接受连接,然后启动客户端通信模块, 并移交完成连接的套接字。
在数据收发层面, 客户端和服务端是能够以左右对称的方式自由发送数据,也就是说TCP及以下层级在发送数据这一方面对于客户端和服务端并无二样。但是有一个层面是无法左右对称的,那就是连接,只有一个端在等待接收连接,另一个端才可以发起连接。我们称等待连接的一方为服务端,发起请求的一端是客户端。
服务端主要的步骤有:
1、创建套接字
2、调用 bind 将端口号写入套接字中
3、协议栈会调用 listen 向套接字写入等待连接状态这一控制信息
4、协议栈会调用 accept 来接受连接
5、包达到后,复制一个副本(即创建一个新的套接字)与客户端通信
服务器的接收操作
服务器接收到电信号并还原成数字信息是在网卡上完成的, 还原后的数字信息被保存在网卡内部的缓冲区中。在这个过程中, 服务器的 CPU 并不是一直在监控网络包的到达, 而是
在执行其他的任务, 因此 CPU 并不知道此时网络包已经到达了。接下来,网卡会通过硬中断将网络包到达的事件通知给CPU,然后,CPU会切换到网卡任务,网卡驱动会将网络包从缓冲区读出来,根据头部的以太类型判断协议的种类将包转交给网路层的某个协议(比如链路层判断出来包的类型是ip协议,就会调ip模块,将包转交给它)。
当网络包转交到协议栈时, IP 模块会首先开始工作, 检查 IP 头部。 IP模块首先会检查 IP 头部的格式是否符合规范, 然后检查接收方 IP 地址,看包是不是发给自己的。 确认包是发给自己的之后, 接下来需要检查包有没有被分片。检查 IP头部的内容就可以知道是否分片 , 如果是分片的包, 则将包暂时存放在内存中, 等所有分片全部到达之后将分片组装起来还原成原始包; 如果没有
分片, 则直接保留接收时的样子, 不需要进行重组。然后ip协议会检查 IP 头部的协议号字段, 并将包转交给相应的模块。 例如, 如果协议号为 06( 十六进制), 则将包转交给 TCP 模块; 如果是 11( 十六进制), 则转交给 UDP 模块。
传输层以TCP为例,TCP模块会检查包的头部中控制位 SYN 字段,为1时,表示这是一个发起连接的包,这时TCP模块会确认该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字。如果没有,则会向客户端返回一个表示接收方端口不存在等待连接的套接字的 ICMP 消息。如果存在等待连接的套接字, 则为这个套接字复制一个新的副本, 并将发送方 IP 地址、 端口号、 序号初始值、 窗口大小等必要的参数写入这个套接字中, 同时分配用于发送缓冲区和接收缓冲区的内存空间。然后生成代表接收确认的 ACK 号, 用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小, 并用这些信息生成 TCP 头部, 委托IP 模块发送给客户端。这个包到达客户端之后, 客户端会返回表示接收确认的 ACK 号, 当这个 ACK 号返回服务器后, 连接操作就完成了。这时, 服务器端的程序应该进入调用 accept 的暂停状态, 当将新套接字的描述符转交给服务器程序之后, 服务器程序就会恢复运行。
如果是数据收发,TCP 模块会检查收到的包对应哪一个套接字。 在服务器端, 可能有多个已连接的套接字对应同一个端口号, 因此仅根据接收方端口号无法找到特定的套接字。 这时我们需要根据 IP 头部中的发送方 IP 地址和接收方 IP 地址, 以及 TCP 头部中的接收方端口号和发送方端口号共 4 种信息, 找到上述 4 种信息全部匹配的套接字 。找到 4 种信息全部匹配的套接字之后,TCP 模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配, 以确定数据收发操作是否正常。 具体来说, 就是根据套接字中保存的上一个序号和数据长度计算下一个序号, 并检查与收到的包的 TCP 头部中的序号是否一致 。 如果两者一致, 就说明包正常到达了服务器, 没有丢失。 这时, TCP模块会从包中提出数据, 并存放到接收缓冲区中, 与上次收到的数据块连接起来。 这样一来, 数据就被还原成分包之前的状态了。
收到的数据块进入接收缓冲区, 意味着数据包接收的操作告一段落了。接下来, 应用程序会调用 Socket 库的 read 来获取收到的数据,这时数据会被转交给应用程序。