本章重点
客户端和服务端的区别以及客户端响应的连接过程。
客户端和服务端的区别
服务器的分类和功能种类有很多,但是网络相关的部分, 如网卡、协议栈、Socket 库等功能和客户端却并无二致。
另外我们可以回顾第一章笔记中介绍了关于互联网的历史部分,网络自诞生开始就是为了军事通信,意味着最好是在数据收发层面不需要区分客户端和服务器,而是能够以左右对称的方式自由发送数据。
所以我们常说的客户端和服务端仅仅是从发送者和接受者的角度来区分,如果服务器发送请求到客户端,也可以认为服务器本身是“客户端”。
关于服务端和客户端我们从Socket库调用上查看两者差别:
客户端的数据收发需要 经过下面 4 个阶段。
(1)创建套接字(创建套接字阶段)
(2)用管道连接服务器端的套接字(连接阶段)
(3)收发数据(收发阶段)
(4)断开管道并删除套接字(断开阶段)
服务器是将阶段(2)改成了等待连接
(1)创建套接字(创建套接字阶段)
(2-1)将套接字设置为等待连接状态(等待连接阶段)
(2-2)接受连接(接受连接阶段)
(3)收发数据(收发阶段)
(4)断开管道并删除套接字(断开阶段)
连接过程
下面和第二章介绍客户端连接类似,介绍服务端连接的步骤。
首先调用 bind 将端口号写入套接字中,并且要设置端口,之后协议栈会调用accept连接,注意这时候包可能是没有到来的,如果包没有到来服务端会阻塞等待客户端的请求,一旦接收到连接就会开始响应并且进行连接操作。
接下来协议栈会给等待连接的套接字复制一个副本, 然后将连接对象等控制信息写入新的套接字中,为什么这里要创建副本简单解释一下,因为如果直接使用原有的套接字连接,那么当新的客户端请求过来,就必须要再次创建新的套接字然后再次进行连接。使用复制套接字的方式,原有的套接字依然可以完成等待连接的工作,和新建的套接字副本是没有关联的。
创建套接字除了复制套接字这个特点外,还有一个是端口号的使用,因为一个套接字需要对应一个端口号,但是需要注意新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号,原因是防止类似客户端本来想要连接 80 端口上的套接字, 结果从另一个端口号返回了包这样的情况。
针对这个问题,服务端的套接字除了确定端口之外,还需要带上IP信息和客户端的端口号信息,最终依靠下面四个变量来确定和哪一个套接字交互。
- 客户端 IP 地址
- 客户端端口号
- 服务器 IP 地址
- 服务器端口号
从上面这幅图可以看到,服务端可能会在一个端口上创建副本绑定很多个套接字,但是客户端的端口是完全不同并且随机的,同时IP地址也不一样,所以可以确定套接字之间是不会存在冲突的。
套接字准备完成之后,接着是对于网络包进行FCS 的校验,当 FCS 一致确认数据没有错误时,接下来需要检查 MAC 头部中 的接收方 MAC 地址,看看这个包是不是发给自己的,之后网卡的 MAC 模块将网络包从信号还原为数字信息, 校验 FCS并存入缓冲区。
网卡收到消息之后,接着是执行中断处理机制告知CPU开始进行网卡的数据处理,关于中断处理的内容可以通过的另一本书《Linux是怎么样工作的》了解CPU的中断处理机制了解整个执行过程,之后网卡驱动会根据 MAC 头部判断协议类型,并将包交给相应的协议栈。
为什么还要使用描述符呢?
这里回顾一下描述符的内容,描述符指的是在创建套接字之后,服务端需要返回给客户端一条标识信息,目的是告知客户端自己是谁,协议栈也需要返回描述符用于标识是哪一个套接字在进行传数据。
这里可以简单理解为我们在网络聊天的时候虽然知道对方是谁和自己聊天,但是如果对方没有“开摄像头”告诉你我是本人,很有可能是别人伪装你认识的人在和你聊天。而我们知道对方是本人在和我们聊天也是因为对方的一些“性格”所以了解。
当网络包转交到协议栈时,IP 模块会首先开始工作检查 IP 头部。IP 头部主要是检查规范,检查双方的IP地址,确认包是不是发给自己的,确认包是发给自己的之后,接下来需要检查包有没有被分片,然后检查 IP 头部的协议号字段,并将包转交给相应的模块。
IP模块接收操作小结
协议栈的 IP 模块会检查 IP 头部:
(1)判断是不是发给自己的;
(2)判断网络包是否经过分片;
(3)将包转交给 TCP 模块或 UDP模块。
根据IP头部的协议好找到06发现是TCP协议判断之后的内容是TCP模块的包,此时检查控制位 SYN 是否为 1,这也表示这是一个发起连接的包。
TCP 模块会执行接受连接的操作,此时需要同时检查端口是否存在对应的套接字连接,如果没有则会向客户端返回错误通知的包,如果存在则复制套接字的副本,并且双方需要互相交换信息存储在套接字的缓冲区,这时候服务器端的程序应该进入调用 accept 的暂停状态,当新套接字的描述符转交给服务器程序之后,服务器程序就会恢复运行。
接下来是TCP模块处理数据部分,首先是检查收到的包对应哪一个套接字,这里对应之前说的四种信息判断唯一套接字,因为服务端的一个端口可能绑定非常多的客户端端口。
对上套接字之后,TCP 模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配,比如检查收到的包序号是否匹配等,如果数据确认无误,将会生成对应的应答头部并且计算ACK号码,然后自己再生成一个序号返回给客户端。
收到的数据块进入接收缓冲区,意味着数据包接收的操作告一段落了,之后传递数据会通过read等待然后直接交给应用程序处理了,最后应用程序根据请求的内容向浏览器返回相应的数据。
TCP 模块操作小结
(1)根据收到的包的发送方 IP 地址、发送方端口号、接收方 IP 地址、接收方端口号找到相对应的套接字;
(2)将数据块拼合起来并保存在接收缓冲区中;
(3)向客户端返回 ACK。
最后是断开操作,断开操作的主要区别在HTTP协议上,HTTP1.0需要服务器发起,而HTTP1.1当中断开由客户端开始。
为什么HTTP1.0和HTTP1.1在断开的时候有如此差别,可以看下面的补充内容,这部分内容来自网络:
http1.0
- 如果在HTTP请求中携带content-length,此时请求body长度可知,客户端在接收body时就可以依据这个长度来接受数据。接受完毕后,就表示这个请求完毕了。客户端主动调用close进入四次挥手。
- 反之,如果不带content-length ,则body长度不可知,客户端一直接受数据,直到服务端主动断开。
http1.1
- 如果HTTP请求中携带content-length,此时body长度可知,则由客户端主动断开。
- 如果发现HTTP中带Transfer-encoding:chunked body会被分成多个块,每块的开始会标识出当前块的长度,body就不需要通过content-length来指定了,但依然可以知道body的长度,此时客户端主动断开。
- 如果请求不带不带Transfer-encoding:chunked且不带content-length,客户端接收数据,直到服务端主动断开连接。
也就是说如果能够有办法知道服务器传来的长度,都是客户端首先断开。如果不知道就一直接收数据直到服务端断开。
总结
这一章节的内容更像是对于前面几章内容的查漏补缺,以及对于之前内容做了一整体的简单复习,在后半部分介绍了关于应用程序返回数据的介绍,这部分比较偏向WEB 所以就没有收录到笔记当中了。
整体看下来这本书需要重点学习的是前面的三章内容,后面两章内容更像是理论知识的补充以及对于前面内容的补充。
对于最后一章服务端响应数据的细节建议和客户端结合学习,效果会事半功倍。