可以把数据通道想象成一个管道,将数据从一端进入管道,数据到达另一端后被取出。

数据可以从任何一端进入管道,数据的流动是双向的。

收发操作之前,需要先建立管道。建立管道的关键在于管道两端的数据出入口,这些出入口被称为套接字。
需要先创建套接字,然后再将套接字连接起来形成管道。

  1. 服务器先创建套接字,等待客户端向该套接字连接管道
  2. 客户端也先创建一个套接字,然后该套接字延伸出管道,最后管道连接到服务器端的套接字上。
  3. 可以进行收发数据
  4. 断开连接,断开可以由客户端或服务器任一方发起,其中一方断开后,另一方也会随之断开。

上面四个操作,都是由操作系统协议栈来执行的,浏览器并不会自己去连接管道、放入数据这些操作,而是委托协议栈代劳。

四个阶段

套接字创建

image.png

  1. 首先要创建套接字,完成后,系统返回一个描述符,应用程序将收到的描述符放到内存中
  2. 该描述符可以用来识别不同的套接字

创建细节

image.png
套接字记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的操作。

  1. 应用程序调用socket申请创建套接字,协议栈根据应用程序的申请执行创建套接字的操作
    1. 首先协议栈分配一个用来存放一个套接字所需的内存空间
    2. 在这块内存空间中写入表示这一状态的控制信息
    3. 将这个套接字的描述符告知给应用程序
    4. 收到描述符后,应用程序在向协议栈进行收发数据委托时候就需要提供这个描述符。

连接阶段

委托协议栈把客户端创建的套接字与服务器那边的套接字连接起来。
调用connect组件来完成

需要使用三个参数

  1. 描述符,将刚才创建时候协议栈返回的那个描述符,connect将描述符告诉协议栈,协议栈根据这个描述符判断到底使用哪一个套接字去和服务端的套接字连接
    1. 描述符是用于和创建套接字的应用程序进行交互的,并不用来告诉网络的另一方,因此另一方并不知道这个描述符。
  2. 服务器IP,通过DNS查询到的ip地址放入
  3. 端口号,连接到的对象是一个具体的套接字,因此必须识别到具体的套接字才行
    1. 客户端在创建套接字时候,协议栈会为这个套接字随便分配一个端口号,接下来,当协议栈进行连接操作的时候,会将这个随便分配的端口号告诉服务器。
  • 如果说描述符是识别一台计算机内部套接字的机制,那么端口号就是用来让通信的另一方能够识别出套接字的机制。

描述符:应用程序用来识别套接字的机制 IP地址+端口号:客户端和服务器之间用来识别对方套接字的机制

连接细节

连接实际上就是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

  • 所谓控制信息,就是用来控制数据收发操作所需的一些信息,ip和端口号就是典型例子。

套接字刚创建完成的时候,里面没有任何数据,也不知道通信对象是谁。这个时候即使应用程序要求发送数据,协议栈也不知道发给谁。

连接的目的:

  1. 把服务器的IP和端口号告诉给客户机协议栈
  2. 将客户端向服务器报告必要的信息,例如客户端ip+端口号。
  3. 执行数据收发的时候,还需要一块用来临时存储要收发数据的内存空间,成为缓冲区,在连接阶段进行分配。

控制信息

控制信息分为两类:

  1. 客户端和服务器相互联络时候交换的控制信息。
    1. 不仅连接时候需要使用,数据收发、断开连接操作在内,都要使用。
    2. 在连接阶段,由于数据收发还没有开始,网络包中没有实际的数据,只用控制信息。
    3. 控制信息位于网络包的开头,也被称为头部
  2. 保存在套接字中,用于控制协议栈操作的信息。
    1. 应用程序传递来的信息以及从通信对象接收到的信息都会保存到这里,收发数据操作的执行状态也会保存在这里。协议栈根据这些信息来执行每一步操作。

连接的实际过程

connect(描述符,ip地址+端口号),会将信息传递给协议栈的TCP模块;然后tcp与该ip地址对应的对象交换控制信息,步骤:

  1. 客户端先创建一个包含表示开始数据收发操作的控制信息的头部
    1. 重点关注发送方和接收方的端口号
  2. 客户端的套接字可以准确找到了服务器的套接字
  3. 将头部中的控制为的SYN设置1
  4. 头部创建好以后,tcp模块会将信息传递给ip模块并委托它进行发送
  5. 服务器收到后ip模块将其传递给tcp模块,服务器的tcp模块根据头部中记录的端口号找到相应的套接字。
  6. 找到相应的套接字,套接字中写入相应的信息,状态变成正在连接
  7. 完成后,tcp模块会返回响应,需要在tcp的头部中设置发送方和接收方的端口号以及SYN,此外ACK还要设置为1,
  8. 网络包到达客户端,通过tcp头部的信息确认连接服务器的操作是否成功。
    1. 如果SYN=1表示连接成功,套接字中写入服务器ip地址,端口号等,该状态为连接完毕
    2. 到这里,客户端的操作已经完成
  9. 客户将ACK=1返回给服务器,告诉服务器刚才的响应已经收到,服务器收到这个包时候,连接操作才完成。

通信阶段

  1. 应用程序需要在内存中准备好要发送的数据,根据用户输入的网址生成的http请求消息就是我们要发送的数据
  2. 当调用write的时候,需要指定描述符和发送的数据,协议栈将其发送给服务器
    1. 因为描述符指定了套接字,套接字保存了服务器的相关信息,所以只需要制定描述符就可以找到服务器进行通信
  3. 服务器进行接受,解析数据并执行响应的操作,向客户端返回响应
  4. 消息返回后,需要进行接收。接收消息是通过socket库的read程序组件委托协议栈来完成的。
    1. 调用read时候需要指定存放用于接收到的响应消息的内存地址,称为接收缓冲区。
    2. 服务器返回响应消息时候,read就会负责将接受到的响应消息存放到接收缓冲区中。
    3. 因为接收缓冲区位于应用程序的内部的内存空间,相当于已经转交给了应用程序。

数据收发流程

应用程序调用write将要发送的数据交给协议栈开始的,协议栈收到数据后执行发送操作

  • 协议栈不关心应用程序传来的数据是什么,write只是指定了发送数据的长度,在协议栈看来,要发送的数据就是一系列二进制字节序列
  • 协议栈并不是收到数据马上发送,而是将数据存放到内部发送缓冲区中,并等待应用程序的下一段数据。在积累到一定量时再发送出去,临界值可以如下判断
    • 每个网络包可以容纳的数据长度,协议栈根据一个叫做MTU(最大传输单元)的参数进行判断,以太网一般是1500字节。MTU是包含头部的总长度,需要减去头部长度,得到MSS(最大分段大小),应用程序数据长度超过或者接近MSS时在发送出去,就可以避免发送大量小包的问题。
    • 时间。如果应用程序发送数据频率不高,如果每次都等到MSS,可能会造成等待时间太长,所以超过一定时间以后,就把网络包发送出去。
    • 应用程序可以指定立即发送

拆分大数据

比如提交表单数据,长度就可能超过一个网络包所能容纳的数据量,比如博客。
这时候,发送缓冲区的数据就会超过MSS的长度,发送缓冲区中的数据会以MSS长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。

使用ACK确认网络包收到

TCP需要确认对方成功收到了网络包,以及当对方没收到时候进行重发。

  1. TCP在拆分数据时候,会先计算好每一块数据相当于从头开始的第几个字节,在头部中写入这个字节数, 就是“序号”字段。
  2. 收到之后返回ACK的值,如果ACK=1001,那么代表1000及之前的数据都已经收到了
  3. 注意序号不是从1开始的,序号最开始是一个随机数,所以需要在开始收发数据之前将初始值告知给对象。
  4. 客户端先计算出一个序号,然后将序号和数据发送给服务器,服务器收到之后会计算ack号返回给客户端;相反,服务器也会计算一个序号,然后将数据和序号一起发送给客户端,客户端收到之后计算ACK号并返回给服务器。即:客户端和服务器双方都需要各自计算序号,因此双方需要在连接过程中互相告知自己的序号初始值。

image.png

在得到对方确认之前,发送过的包都会保存在发送缓冲区中,如果对方没有返回某些包的确认ACK号,那么就重新发送这些包。
如果TCP尝试几次都重传无效,那么就强制停止通信,并向应用程序报错。

窗口

滑动窗口就是在发送一个包以后,不等待ACK号返回,而是直接发送后续一系列包。
image.png
问题:如果不等返回ACK号就连续发包,就会有可能出现发包的频率超过接收方处理能力的情况。
接收方的TCP收到包后,会将数据存放到接收缓冲区,然后接收方计算ACK号,将数据块组装起来还原成原本的数据并返回给应用程序。如果数据达到的速率高于接收方处理的速率,一段时间后会溢出
如何避免?
接收方告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制
image.png

何时进行更新窗口大小?
当接受方将数据传送给应用程序,导致接收缓冲区容量变化的时候,就需要告知发送方更改窗口大小。

将ACK与窗口更新的发送合并起来,把这两个通知放到一个包里面,减少包的数量。
如果需要发送连续ACK的时候,只要发送最后一个ACK号就可以了,中间的可以全部省略(累计确认)。

接收消息

  1. 协议栈检查收到的数据块和TCP头部,判断是否内容丢失,没有则返回ACK号
  2. 协议栈将数据块暂存到接收缓冲区中,按数据块的顺序连接起来还原出原始的数据,最后将数据交给应用程序。
    1. 协议栈会将接收到的数据赋值给应用程序指定的内存地址,谭厚将控制流程交给应用程序。
  3. 将数据交给应用程序以后,协议栈在合适时机向发送方发送窗口更新。

    断开阶段

    当服务器发送完响应消息之后,应当主动断开。
    因此服务器会首先调用close来断开连接
    断开操作传达到客户端之后,客户端套接字也会执行断开操作,
    接下来浏览器调用read时候,read会告知浏览器收发数据已经结束,连接断开,浏览器也会调用close进入断开阶段。
  • 协议栈在设计上允许任何一方先发起断开进程。
  • 完成数据发送的一方会先发起断开进程。
  • image.png
  1. 服务器先调用Socket的close程序
  2. 服务器的协议栈生成包含断开信息的TCP头部,具体来说就是将控制位的FIN比特设置1
  3. 协议栈委托IP模块向客户发送数据,同时,服务器的套接字中也会记录下断开操作的相关信息。
  4. 客户端收到FIN=1的TCP头部,客户端的协议栈会将自己的套接字标记为进入断开操作状态
  5. 告知服务器已收到FIN=1的包,客户端发送一个ACK给服务器
  6. 客户端应用程序调用read读取数据,协议栈不会向应用程序传递数据,而是告知应用程序来自服务器的数据已经全部收到。
  7. 只要收到服务器返回的所有数据,客户端的操作也就结束了。
  8. 客户端应用程序调用close来结束数据收发操作。
  9. 客户端的协议栈生成一个FIN=1的TCP包,然后委托IP模块发送给服务器。
  10. 一段时间后,服务器返回ACK号,结束通信。

删除套接字

套接字不会立即删除,而是会等待一段时间再进行删除。

  1. 客户端发送FIN
  2. 服务器回复ACK
  3. 服务器发送FIN
  4. 客户端返回ACK

如果4中客户端发送的ACK丢失了,那么服务器会重新发FIN,如果客户端在发送完4以后立即删除了套接字,那么套接字中的控制信息就丢失了,套接字端口被释放出来,这时候如果刚好有一个新的应用程序使用这个端口创建了新的套接字,新套接字碰巧又分配了同一个端口号,这个时候服务器重发的FIN到达,那么新套接字被错误的断开。

image.png

这个就是http的工作流程:
http将html或者图片等文件作为单独的对象来处理,每获取一次数据,就执行一次连接、发送请求消息、接收响应消息、断开的过程。
因此如果一个页面包含很多图片,就有必须重复进行多次连接,收发,断开操作。
因此在http1.1中,一次连接中可以收发多个请求和相应,当所有数据都请求完成后,浏览器主动触发断开连接操作。