上一篇文章讲了协议栈在网络请求中的作用,本篇将继续探讨协议栈的功能,并且会深入协议栈内部,看看协议栈是如何工作的。
【网络知识入门,探索一次网页请求的旅程(一)】 https://blog.csdn.net/ck784101777/article/details/103741398
【网络知识入门,探讨DNS服务器在网页请求中的作用(二)】 https://blog.csdn.net/ck784101777/article/details/103741398
【网络知识入门,何为协议栈(三)】https://blog.csdn.net/ck784101777/article/details/103746921
目录
一、回顾前三篇文章
二、协议栈内部结构
三、深入了解协议栈、套接字
1.window查看当前套接字
2.Socket库调用socket时的操作
一、回顾前三篇文章
(1)创建套接字
从应用程序收到委托后
,
协议栈通过
TCP
协议收发数据的操作可 以分为 4
个阶段
。
首先是创建套接字
,
在这个阶段
,我们需要了解
协议栈的内部结构、
套接字的实体
,
以及创建套接字的操作过程
。
到这里,
大家应该可以对套接字到底是什么样的一个东西有一个比较具体
的理解,或者说我们至少能够知道套接字用来做什么
。
(2)连接服务器
接下来是客户端套接字向服务器套接字进行连接的阶段
。
我们将介绍 “连接
”
具体是进行怎样的操作
,
在这个过程中协议栈到底是如何工作的
, 以及客户端和服务器是如何进行交互的。
(3)收发数据
两端的套接字完成连接之后
,
就进入收发消息的阶段了
。
在这个阶段
, 协议栈会将从应用程序收到的数据切成小块并发送给服务器,
考虑到通信 过程中可能会出错导致网络包丢失,
协议栈还需要确认切分出的每个包是 否已经送达服务器,
对于没有送达的包要重新发送一次
。
这里我们将对收发数据的情形加以说明。
(4)从服务器断开连接并删除套接字
收发消息的操作全部结束之后
,
接下来要断开服务器的连接并删除套 接字。
断开操作的本质是当消息收发完成后客户端和服务器相互进行确认 的过程,
但这个过程并不只是相互确认并删除套接字那么简单
,
其中有些 地方是很有意思的。
(5)IP 与以太网的包收发操作
在介绍
TCP
协议收发消息的操作之后
,
我们再来看看实际的网络包是如何进行收发的。
协议栈会与网卡进行配合
,
将数据切分成小块并封装成网络 包,
再将网络包转换成电信号或者光信号发送出去
。
介绍完这个过程之后
, 大家应该就可以对计算机网络功能有一个完整的概念了。
(6)用 UDP 协议收发数据的操作
TCP
协议有很多方便的功能
,
比如网络包出错丢失时可以重发
,
因此 很多应用程序都是使用 TCP
协议来收发数据的
,
但这些方便的功能也有帮倒忙的时候,
在这种情况下我们还有另外一种叫
UDP
的协议
。
这里我们将介绍 UDP
的必要性以及它与
TCP
的差异
。
二、协议栈内部结构
本文我们将探索操作系统中的网络控制软件(协议栈)和网络硬件(网 卡)是如何将浏览器的消息发送给服务器的。和浏览器不同的是,协议栈的工作我们从表面上是看不见的,可能比较难以想象。因此,在实际探索之前,我们先来对协议栈做个解剖,看看里面到底有些什么。 协议栈的内部如下图所示,分为几个部分,分别承担不同的功能。 这张图中的上下关系是有一定规则的,上面的部分会向下面的部分委派工作,下面的部分接受委派的工作并实际执行,这一点大家在看图时可以参考一下。当然,这一上下关系只是一个总体的规则,其中也有一部分上下关系不明确,或者上下关系相反的情况,所以也不必过于纠结。此外,对于图中的每个部分以及它们的工作方式,本章将按顺序进行介绍,因此对于里面的细节现在看不明白也没关系,只要大体上看出有哪些组成要素就可以了。
下面我们从上到下来看一遍 。 图中最上面的部分是网络应用程序 , 也就是浏览器、 电子邮件客户端 、 Web 服务器 、 电子邮件服务器等程序 , 它们会将收发数据等工作委派给下层的部分来完成。 当然 , 除了浏览器之外 , 其他应用程序在网络上收发数据的操作也都是类似上面这样的, 也就是说 , 尽管不同的应用程序收发的数据内容不同, 但收发数据的操作是共通的。
因此
,
下面介绍的内容不仅适用于浏览器
,
也适用于各种应用程序
。 应用程序的下面是 Socket
库
,
其中包括解析器
,
解析器用来向
DNS 服务器发出查询(第二篇文章有介绍)
。 再下面就是操作系统内部了,
其中包括协议栈
。
协议栈的上半部分有两块,
分别是负责用
TCP
协议收发数据的部分和负责用
UDP
协议收发数 据的部分,
它们会接受应用程序的委托执行收发数据的操作
。
关于
TCP
和 UDP 我们将在后面讲解
,
现在大家只要先记住下面这句话就可以了
:
像浏 览器、
邮件等一般的应用程序都是使用
TCP
收发数据的
,
而像
DNS
查询 等收发较短的控制数据的时候则使用 UDP
。 浏览器、邮件等一般应用程序收发数据时用 TCP; DNS 查询等收发较短的控制数据时用 UDP。 下面一半是用 IP
协议控制网络包收发操作的部分
。
在互联网上传送数
据时
,
数据会被切分成一个一个的网络包
,而将网络包发送给通信对象的操作就是由 IP
来负责的
。
此外
,
IP
中还包括
ICMP
协议和
ARP
协议
。 ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息
,
ARP
用 于根据 IP
地址查询相应的以太网
MAC
地址
。 IP 下面的网卡驱动程序负责控制网卡硬件
,
而最下面的网卡则负责完成实际的收发操作,
也就是对网线中的信号执行发送和接收的操作
。
我们已经了解了协议栈的内部结构 , 而对于在数据收发中扮演关键角色的套接字, 让我们来看一看它具体是个怎样的东西 。
在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的 IP 地址、端口号、通信操作的 进行状态等 。
对套接字的解释:
本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体 。
协议栈在执行操作时需要参阅这些控制信息 。 例如 , 在发送数据时 , 需要看一看套接字中的通信对象 IP 地址和端口号 , 以便向指定的 IP 地址 和端口发送数据。 在发送数据之后 , 协议栈需要等待对方返回收到数据的 响应信息, 但数据也可能在中途丢失 , 永远也等不到对方的响应 。 在这样 的情况下, 我们不能一直等下去 , 需要在等待一定时间之后重新发送丢失的数据, 这就需要协议栈能够知道执行发送数据操作后过了多长时间 。 为 此 , 套接字中必须要记录是否已经收到响应 ,以及发送数据后经过了多长时间, 才能根据这些信息按照需要执行重发操作 。 上面说的只是其中一个例子。 套接字中记录了用于控制通信操作的 种控制信息,协议栈则需要根据这些信息判断下一步的行动,这就是套接字的作用。 协议栈是根据套接字中记录的控制信息来工作的 。
1.window查看当前套接字
讲了这么多抽象的概念,
可能大家还不太容易理解
,
所以下面来看看真正的套接字。
在
Windows
中可以用
netstat
命令显示套接字内容,如图所示
。 图中每一行相当于一个套接字,
当创建套接字时
,
就会在这里增加一行新的控制信息,
赋予
“
即将开始通信
”
的状态
,
并进行通信的准备工作
,
如 分配用于临时存放收发数据的缓冲区空间。既然有图,
我们就来讲讲图上这些到底都是什么意思
。
比如第
8
行
, 它表示 PID
为
4
的程序正在使用
IP
地址为
10.10.1.16
的网卡与
IP
地址为 10.10.1.80 的对象进行通信,看到State状态栏为ESTABLISHED(已连接)
。
此外我们还可以看出
,
本机使用
1031
端口
, 对方使用 139
端口
,
而
139
端口是
Windows
文件服务器使用的端口
,
因此我们就能够看出这个套接字是连接到一台文件服务器的。
们再来看第
1 行,
这一行表示
PID
为
984
的程序正在
135
端口等待另一方的连接
,
其中 本地 IP
地址和远程
IP
地址都是
0.0.0.0
,
这表示通信还没开始
,
IP
地址还不确定,所以State状态栏为LISTENING。
2.Socket库调用socket时的操作
看过套接字的具体样子之后
,
我们的探索之旅将继续前进
,
看一看当浏览器调用 socket
、
connect
等
Socket
库中的程序组件时
,
协议栈内部是如何工作的。
本节一共分为5个小段,分别介绍
1)创建套接字阶段
2)连接阶段
3)连接阶段发生的事
4)收发数据阶段
5)补充:何为控制位
6)断开阶段
1)创建套接字阶段
首先,
我们再来看一下浏览器通过
Socket
库向协议栈发出委托的一系列操作,如图所示
。
浏览器委托协议栈使用
TCP
协 议来收发数据
,
因此下面的讲解都是关于
TCP
的
。 首先是创建套接字的阶段
。
如图
①所示
,
应用程序调用
socket
申请创建套接字,
协议栈根据应用程序的申请执行创建套接字的操作
。 在这个过程中,
协议栈首先会分配用于存放一个套接字所需的内存空间。用于记录套接字控制信息的内存空间并不是一开始就存在的,因此我们先要开辟出这样一块空间来
,
这相当于为控制信息准备一个容器
。
但光一个容器并没有什么用,
还需要往里面存入控制信息
。
套接字刚刚创建时
,数据收发操作还没有开始,
因此需要在套接字的内存空间中写入表示这一
初始状态的控制信息
。
到这里
,
创建套接字的操作就完成了。
接下来,需要将表示这个套接字的描述符告知应用程序。描述符相当于用来区分协议栈中的多个套接字的号码牌
。收到描述符之后,
应用程序在向协议栈进行收发数据委托时就需要提供这个描述符。
由于套接字中记录了通信双方的信息以及通信处于怎样的状态,
所以只要通过描述符确定了相应的套接字
,
协议栈就能够获取所有的相关信息,
这样一来
,
应用程序就不需要每次都告诉协议栈应该和谁进行通信了。
2)连接阶段
创建套接字之后
,
应用程序
(
浏览器
)
就会调用
connect
,
随后协议栈会将本地的套接字与服务器的套接字进行连接。
话说
,
以太网的网线都是 一直连接的状态,
我们并不需要来回插拔网线
,
那么这里的
“
连接
”
到底 是什么意思呢?
连接实际上是通信双方交换控制信息,在套接字中记录这 些必要信息并准备数据收发的一连串操作。
在讲解具体的过程之前
,
我们先来说一说“
连接
”
到底代表什么意思
。 网线是一直连接着的,
随时都有信号从中流过
,
如果通信过程只是将数据转换为电信号,
那么这一操作随时都可以进行
。
不过
,
在这个时间点
,也就是套接字刚刚创建完成时,
当应用程序委托发送数据的时候
,
协议栈会如何操作呢? 套接字刚刚创建完成的时候,
里面并没有存放任何数据
,
也不知道通信的对象是谁。
在这个状态下
,
即便应用程序要求发送数据
,
协议栈也不知道数据应该发送给谁。
浏览器可以根据网址来查询服务器的
IP
地址
,
而且根据规则也知道应该使用 80
号端口
,
但只有浏览器知道这些必要的信息是不够的,
因为在调用
socket
创建套接字时
,
这些信息并没有传递给协议
栈
。
因此,我们需要把服务器的 IP 地址和端口号等信息告知协议栈,这是连接操作的目的之一。
那么
,
服务器这边又是怎样的情况呢
?
服务器上也会创建套接字
,
但
服务器上的协议栈和客户端一样
,
只创建套接字是不知道应该和谁进行通
信的
。
而且
,
和客户端不同的是
,
在服务器上
,
连应用程序也不知道通信
对象是谁
,
这样下去永远也没法开始通信
。
于是
,
我们需要让客户端向服
务器告知必要的信息
,
比如
“
我想和你开始通信
,
我的
IP
地址是
xxx.xxx.
xxx.xxx
,
端口号是
yyyy
。”
可见
,
客户端向服务器传达开始通信的请求
,
也是连接操作的目的之一
。
之前我们讲过
,
连接实际上是通信双方交换控制信息
,
在套接字中记录这些必要信息并准备数据收发的一连串操作,
像上面提到的客户端将
IP地址和端口号告知服务器这样的过程就属于交换控制信息的一个具体的例子。
所谓控制信息
,
就是用来控制数据收发操作所需的一些信息
,
IP
地址和端口号就是典型的例子。
除此之外还有其他一些控制信息
,
我们后面会逐一进行介绍。
连接操作中所交换的控制信息是根据通信规则来确定的
,只要根据规则执行连接操作,
双方就可以得到必要的信息从而完成数据收发的准备。
此外
,
当执行数据收发操作时
,
我们还需要一块用来临时存放要收发的数据的内存空间,
这块内存空间称为缓冲区
,
它也是在连接操作的过程中分配的。
上面这些就是
“
连接
”
这个词代表的具体含义
。
3)连接操作中发生的事
我们已经了解了连接操作的含义
,
下面来看一下具体的操作过程
。
这个过程是从应用程序调用 Socket
库的
connect
开始的
(上
图
②
)。
看到connect这一行:
connect(<
描述符
>, <
服务器
IP
地址和端口号
>,
…)
上面的调用提供了服务器的 IP
地址和端口号
,
这些信息会传递给协议 栈中的 TCP
模块
。
然后
,
TCP
模块会与该
IP
地址对应的对象
,
也就是与 服务器的 TCP
模块交换控制信息
,
这一交互过程包括下面几个步骤
。
①
首先,
客户端先创建一个包含表示开始数据收发操作的控制信息的头部
。
头部包含很多字段
,
这里要关注的重点是发送方和接收方的端口号。
到这里
,
客户端
(
发送方
)
的套接字就准确找到了服务器
(
接收方)
的套接字
,
也就是搞清楚了我应该连接哪个套接字
。
②
然后
,
我们将头部中的控制位的 SYN 比特设置为 1(此时仅在客户端发生该操作)
,
大家可以认为它表示连接
。
此外还需
要设置适当的序号和窗口大小
,
这一点我们会稍后详细讲解。连接操作的第一步是在 TCP 模块处创建表示连接控制信息的头部。 通过 TCP 头部中的发送方和接收方端口号可以找到要连接的套接字。
③ 当
TCP
头部创建好之后
,
接下来
TCP
模块会将信息传递给
IP
模块并委托它进行发送
。
IP
模块执行网络包发送操作后
,
网络包就会通过网络到达服务器,
然后服务器上的
IP
模块会将接收到的数据传递给
TCP
模块
,服务器的 TCP
模块根据
TCP
头部中的信息找到端口号对应的套接字
,
也就是说,
从处于等待连接状态的套接字中找到与 TCP 头部中记录的端口号相同的套接字就可以了。
当找到对应的套接字之后
,
套接字中会写入相应的信息,
并将状态改为正在连接
。
④ 上述操作完成后
,
服务器的
TCP
模块会返回响应,
这个过程和客户端一样
,
需要在
TCP
头部中设置发送方和接收方端口号以及 SYN
比特
。
此外,在返回响应时还需要将 ACK 控制位设为 1,这表示已经接收到相应的网络包
。
网络中经常会发生错误
,
网络包也会 发生丢失,
因此双方在通信时必须相互确认网络包是否已经送达
,
而设置 ACK 比特就是用来进行这一确认的
。
接下来
,
服务器
TCP
模块会将
TCP 头部传递给 IP
模块
,
并委托
IP
模块向客户端返回响应
。然后,
网络包就会返回到客户端
,
通过
IP
模块到达
TCP
模块
,
并通
过
TCP
头部的信息确认连接服务器的操作是否成功
。
如果 SYN 为 1 则表示连接成功,这时会向套接字中写入服务器的 IP 地址、端口号等信息,同时还会将状态改为连接完毕。到这里,客户端的操作就已经完成,但其实还剩下最后一个步骤。刚才服务器返回响应时将 ACK 比特设置为 1,相应地,客户端也需要将 ACK 比特设置为 1 并发回服务器,
告诉服务器刚才的响应包已经收到。
当这个服务器收到这个返回包之后
,
连接操作才算全部完成
。现在,
套接字就已经进入随时可以收发数据的状态了
,
大家可以认为这时有一根管子把两个套接字连接了起来。
当然
,
实际上并不存在这么一根管子,
不过这样想比较容易理解
,
网络业界也习惯这样来描述
。
这根管子,
我们称之为连接
。
只要数据传输过程在持续,也就是在调用 close 断开之前,连接是一直存在的。
建立连接之后,
协议栈的连接操作就结束了
,
也就是说
connect
已经执行完毕,
控制流程被交回到应用程序
。
4)补充:何为控制信息,控制信息存在于何处
关于控制信息
,
这里再补充一些
。
之前我们说的控制信息其实可以大体上分为两类。 第一类是客户端和服务器相互联络时交换的控制信息。
这些信息不仅连接时需要,
包括数据收发和断开连接操作在内
,
整个通信过程中都需要
, 这些内容在 TCP
协议的规格中进行了定义
。如下表所示:
字段名称
长度(比特)
含 义
发送方端口号
16
发送网络包的程序的端口号
接收方端口号
16
网络包的接收方程序的端口号
序号 (发送数据的顺序编号)
32
发送方告知接收方该网络包发送的数据相当于所 有发送数据的第几个字节
ACK号 (接收数据的顺序编号)
32
接收方告知发送方接收方已经收到了所有数据的 第几个字节。其中,ACK 是 acknowledge 的缩写
数据偏移量
4
表示数据部分的起始位置,也可以认为表示头部
的长度
保留
6
该字段为保留,现在未使用
控制位
6
该字段中的每个比特分别表示以下通信控制含义。
URG:表示紧急指针字段有效
ACK:表示接收数据序号字段有效,一般表示数
据已被接收方收到
PSH:表示通过 flush 操作发送的数据
RST:强制断开连接,用于异常中断的情况
SYN:发送方和接收方相互确认序号,表示连接
操作
FIN:表示断开连接
窗口
16
接收方告知发送方窗口大小(即无需等待确认可
一起发送的数据量)
校验和
16
用来检查是否出现错误
紧急指针
16
表示应紧急处理的数据位置
可选字段
可变字段
除了上面的固定头部字段之外,还可以添加可选
字段,但除了连接操作之外,很少使用可选字段
具体来说
,在上表
中的这些字段就是 TCP
规格中定义的控制信息
。
这些字段是固定的,在连接、收发、 断开等各个阶段中,每次客户端和服务器之间进行通信时,都需要提供这些控制信息
。
具体来说
,
如下图
所示
,
这些信息会被添加在客户端与服务器之间传递的网络包的开头。
在连接阶段
,
由于数据收发还没有开始
, 所以下图
所示
,
网络包中没有实际的数据
,
只有控制信息
。
这些控制信息位于网络包的开头,
因此被称为头部
。
此外
,
以太网和
IP
协议也有 自己的控制信息,
这些信息也叫头部
,
为了避免各种不同的头部发生混淆
,我们一般会记作 TCP
头部
、
以太网头部
、
IP
头部
。 客户端和服务器在通信中会将必要的信息记录在头部并相互确认。
例如下面这样。
发送方:“
开始数据发送
。”
接收方
:“
请继续
。”
发送方
:“
现在发送的是
××
号数据
。”
接收方
:“××
号数据已收到
。”
……(
以下省略
)
正是有了这样的交互过程
,
双方才能够进行通信
。
头部的信息非常重要,
理解了头部各字段的含义
,
就等于理解了整个通信的过程
。
在后面介
绍协议栈的工作过程时
,
我们将根据需要讲解头部各字段的含义
,现在大家只要先记住头部是用来记录和交换控制信息的就可以了。
控制信息还有另外一类,那就是保存在套接字中,用来控制协议栈操作的信息。
应用程序传递来的信息以及从通信对象接收到的信息都会保存在这里,
还有收发数据操作的执行状态等信息也会保存在这里
,
协议栈会 根据这些信息来执行每一步的操作。
我们可以说
,
套接字的控制信息和协 议栈的程序本身其实是一体的,
因此
,“
协议栈具体需要哪些信息
”
会根据协议栈本身的实现方式不同而不同,但这并没有什么问题
。
因为协议栈中的控制信息通信对方是看不见的,
只要在通信时按照规则将必要的信息写入头部,
客户端和服务器之间的通信就能够得以成立
。
例如
,
Windows
和
Linux
操作系统的内部结构不同
,
协议栈的实现方式不同
,必要的控制信息也就不同。
但即便如此
,
两种系统之间依然能够互相通信
,
同样地
,
计算机和手机之间也能够互相通信。
正如前面所说
,
协议栈的实现不同
,
因此我们无法具体说明协议栈里到底保存了哪些控制信息,
但可以用命令来显示一些重要的套接字控制信息
,
这些信息无论何种操作系统的协议栈都是共通的,
通过理解这些重要信息
,
就能够理解协议栈的工作方式了。
通信操作中使用的控制信息分为两类:
- 头部中记录的信息
- 套接字(协议栈中的内存空间)中记录的信息
5)数据收发阶段
当控制流程从
connect
回到应用程序之后
,
接下来就进入数据收发阶段了。
数据收发操作是从应用程序调用
write
将要发送的数据交给协议栈开始的(例1
图
③
),
协议栈收到数据后执行发送操作
,
这一操作包含如下要点。
首先
,
协议栈并不关心应用程序传来的数据是什么内容
。
应用程序在调用 write
时会指定发送数据的长度
,
在协议栈看来
,
要发送的数据就是一定长度的二进制字节序列而已。其次,
协议栈并不是一收到数据就马上发送出去
,
而是会将数据存放
在内部的发送缓冲区中
,
并等待应用程序的下一段数据
。这样做是有道理的。
应用程序交给协议栈发送的数据长度是由应用程序本身来决定的,不同的应用程序在实现上有所不同,有些程序会一次性传递所有的数据,有些程序则会逐字节或者逐行传递数据。总之,一次将多少数据交给协议栈是由应用程序自行决定的,协议栈并不能控制这一行为。在这样的情况下,如果一收到数据就马上发送出去,就可能会发送大量的小包,导致网络效率下降,因此需要在数据积累到一定量时再发送出去
。
至于要积累多少数 据才能发送,
不同种类和版本的操作系统会有所不同
,
不能一概而论
,
但都是根据下面几个要素来判断的。 第一个判断要素是每个网络包能容纳的数据长度,
协议栈会根据一个叫作 MTU
的参数来进行判断
。
MTU
表示一个网络包的最大长度
,
在以太 网中一般是 1500
字节
(下
图
)
。
MTU
是包含头部的总长度
,
因此需要从MTU 减去头部的长度
,
然后得到的长度就是一个网络包中所能容纳的最大数据长度,
这一长度叫作
MSS
。
当从应用程序收到的数据长度超过或者接
近
MSS
时再发送出去
,
就可以避免发送大量小包的问题了。
另一个判断要素是时间
。
当应用程序发送数据的频率不高的时候
,
如
果每次都等到长度接近
MSS
时再发送
,可能会因为等待时间太长而造成发送延迟,
这种情况下
,
即便缓冲区中的数据长度没有达到
MSS
,
也应该果断发送出去。
为此,协议栈的内部有一个计时器,当经过一定时间之后, 就会把网络包发送出去。
判断要素就是这两个,
但它们其实是互相矛盾的
。
如果长度优先
,
那么网络的效率会提高,
但可能会因为等待填满缓冲区而产生延迟
;
相反地
,如果时间优先,
那么延迟时间会变少
,
但又会降低网络的效率
。
因此
,
在进行发送操作时需要综合考虑这两个要素以达到平衡。
不过,TCP 协议规格中并没有告诉我们怎样才能平衡,因此实际如何判断是由协议栈的开发者来决定的
,
也正是由于这个原因
,
不同种类和版本的操作系统在相关操作上也就存在差异。 正如前面所说,
如果仅靠协议栈来判断发送的时机可能会带来一些问 题,
因此协议栈也给应用程序保留了控制发送时机的余地
。
应用程序在发送数据时可以指定一些选项,
比如如果指定
“
不等待填满缓冲区直接发送”,
则协议栈就会按照要求直接发送数据
。
像浏览器这种会话型的应用程序在向服务器发送数据时,
等待填满缓冲区导致延迟会产生很大影响
,
因此一般会使用直接发送的选项。
单次数据包过大怎么解决
HTTP
请求消息一般不会很长
,
一个网络包就能装得下
,
但如果其中要提交表单数据,
长度就可能超过一个网络包所能容纳的数据量
,
比如在博客或者论坛上发表一篇长文就属于这种情况。这种情况下,
发送缓冲区中的数据就会超过
MSS
的长度
,
这时我们当然不需要继续等待后面的数据了。
发送缓冲区中的数据会被以
MSS
长度为 单位进行拆分,
拆分出来的每块数据会被放进单独的网络包中
。 根据发送缓冲区中的数据拆分的情况,
当判断需要发送这些数据时
,
就在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给 IP 模块来执行发送数据的操作
(下
图
)
。
6)数据断开阶段
毫无疑问,收发数据结束的时间点应该是应用程序判断所有数据都已经发送完毕的时候。 这时 , 数据发送完毕的一方会发起断开过程 , 但不同的应用程序会选择不同的断开时机。 以 Web 为例 , 浏览器向 Web 服务器发送请求消息, Web 服务器再返回响应消息 , 这时收发数据的过程就全部结束了, 服务器一方会发起断开过程 。 当然 , 可能也有一些程序是客户端发送完数据就结束了, 不用等服务器响应 , 这时客户端会先发起断开过程 。 这一判断是应用程序作出的,协议栈在设计上允许任何一方先发起断开过程 。无论哪种情况, 完成数据发送的一方会发起断开过程 , 这里我们以服务器一方发起断开过程为例来进行讲解。
首先 , 服务器一方的应用程序会调用 Socket 库的 close 程序 。 然后 , 服务器的协议栈会生成包含断开信息的 TCP 头部 , 具体来说就是将控制位中的 FIN 比特设为 1 。 接下来 , 协议栈会委托 IP 模块向客户端发送数据 (下图 )。 同时 , 服务器的套接字 中也会记录下断开操作的相关信息。接下来轮到客户端了。 当收到服务器发来的 FIN 为 1 的 TCP 头部时 , 客户端的协议栈会将自己的套接字标记为进入断开操作状态。 然后 , 为了告知服务器已收到 FIN 为 1 的包 , 客户端会向服务器返回一个 ACK (图 ② )。 这些操作完成后 , 协议栈就可以等待应用程序来取数据了 。 过了一会儿 , 应用程序就会调用 read 来读取数据 。 这时 ,协议栈不会向应用程序传递数据 , 而是会告知应用程序 ( 浏览器 ) 来自服务器的数据已经全部收到了。 根据规则 , 服务器返回请求之后 , Web 通信操作就全部结束了, 因此只要收到服务器返回的所有数据 , 客户端的操作也就随之结束了。 因此 , 客户端应用程序会调用 close 来结束数据收发操作 , 这时客 户端的协议栈也会和服务器一样, 生成一个 FIN 比特为 1 的 TCP 包 , 然后委托 IP 模块发送给服务器 ( 图 ③ )。 一段时间之后 , 服务器就会返回ACK 号 ( 图 ④ )。 到这里 , 客户端和服务器的通信就全部结束了 。
7)最后一步:删除套接字
和服务器的通信结束之后 , 用来通信的套接字也就不会再使用了 , 这时我们就可以删除这个套接字了。 不过 , 套接字并不会立即被删除 , 而是会等待一段时间之后再被删除。 等待这段时间是为了防止误操作, 引发误操作的原因有很多 , 这里无法全部列举, 下面来举一个最容易理解的例子 。 假设 客户端先发起断开 , 则断开的操作顺序如下 。
( 1 ) 客户端发送 FIN
( 2 ) 服务器返回 ACK 号
( 3 ) 服务器发送 FIN
( 4 ) 客户端返回 ACK 号
如果最后客户端返回的 ACK 号丢失了 , 结果会如何呢 ?
这时 , 服务器没有接收到 ACK 号 , 可能会重发一次 FIN 。 如果这时客户端的套接字已经删除了, 会发生什么事呢 ? 套接字被删除 , 那么套接字中保存的控制信息也就跟着消失了, 套接字对应的端口号就会被释放出来 。 这时 , 如果别的应用程序要创建套接字, 新套接字碰巧又被分配了同一个端口号,而服务器重发的 FIN 正好到达,会怎么样呢 ? 本来这个 FIN 是要发给刚刚删除 的那个套接字的 , 但新套接字具有相同的端口号 , 于是这个 FIN 就会错误地跑到新套接字里面, 新套接字就开始执行断开操作了 。 之所以不马上删除套接字, 就是为了防止这样的误操作 。 至于具体等待多长时间, 这和包重传的操作方式有关 。 网络包丢失之后会进行重传, 这个操作通常要持续几分钟 。 如果重传了几分钟之后依然无效, 则停止重传 。 在这段时间内 , 网络中可能存在重传的包 , 也就有可能发生前面讲到的这种误操作, 因此需要等待到重传完全结束 。 协议中对于这个等待时间没有明确的规定, 一般来说会等待几分钟之后再删除套接字 。