前面我们已经讲过为什么read、write、recv、send 和 tcp 支持全双工,这其中涉及到发送缓冲区和接收缓冲区(点此查看)。我们创建的TCP套接字,实际上sockfd会指向一个操作系统给分配好的socket file control block(socket文件控制块),而这个socket文件控制块就是维护发送缓冲区和接收缓冲区的。
我们调用的所有的网络发送函数,write send sendto等实际就是将数据从应用层缓冲区拷贝到TCP协议层,也就是操作系统内部的发送缓冲区;而所有的网络接收函数,read recv recvfrom等实际上就是将数据从TCP协议层的接收缓冲区拷贝到用户层缓冲区。
而实际上双方主机的TCP协议层之间的数据发送完全是由TCP自主决定的,什么时候发?发多少?出错了怎么办?
这些全都是由TCP协议自主控制决定的,这是操作系统内部的事情,和我们用户层没有任何联系,这也就是为什么TCP叫做传输控制协议的原因,因为这个过程是由TCP自主控制的。
图中,每一行有4个字节,而前20个字节是固定长度的标准报头,解包步骤如下:
解析各字段含义:
TCP的报头是变长的,包括固定的20字节和变长的选项。其中,“数据偏移”也叫做“首部长度”,它占固定4位,作用是保存报头整体的长度,以便接收端能够正确解析报文中的字段。
值得注意的是,虽然首部长度占4个比特位,4个比特位能表示的范围0~15.但是它的单位并不是1字节,而是4字节,所以它实际能表示的范围就是[0,60]字节。 而我们看到,选项上面的报头是固定字节大小,也就是20字节,那么说明选项最大可以有40字节。但是我们今天不谈选项,因为固定首部20字节,所以选项字段最长40字节。当没有选项字段时,首部长度字段为5(20 = 5*4),即0101。
首先,在深入学习之前,我们应该掌握这两个问题:
协议报头如何与有效载荷有效分离?
提取报头:除了选项之外的报头叫做标准报头,一共 20 字节。 提取选项:根据 4 位首部长度获取报头的整体大小,减去 20 字节的标准报头(固定报头),得到选项。如果没有选项的话就能直接得到有效载荷。 提取有效载荷:有效载荷 = 报文-报头 (-选项) 这样子就 通过首部长度,我们就可以将TCP首部和有效载荷分离。
有效载荷如何向上交付? TCP是传输层的,上层是应用层。而应用层程序会绑定端口号,TCP首部中有16位目的端口号,根据端口号做到向上交付。
TCP保证数据安全传输的时候最基本的一个特点就是确认应答机制。
实际上为什么网络传输中会存在不可靠问题呢? 本质原因还是因为传输距离过长。 比如我在重庆给北京的网友发送消息,那数据包其实是要经过很多的路由器结点进行数据包转发,穿过很多的局域网,在局域网内部经过双绞线(以太网技术常用的物理介质)传输,还要经过运营商的基站,数据包在如此之长的传输距离中很有可能会丢失,数据里面的比特位翻转,又或是数据包中的字节乱序,又或是数据包重复发送给我的北京网友(发送方可能以为数据包丢失了)。
TCP应该如何解决网络传输中的不可靠问题呢? 就需要确认应答(ACK)机制 主机之间进行通信的时候,发完一条消息之后,不会马上接着发送下一条消息,而是等待对方主机传来的应答。确认对方主机收到了发送的消息之后,再发送数据。这样就能避免传输中不可靠的问题。 虽然可能在传输过程中面临着很多网络问题,但是只要我发送给对方的消息有回复应答,那么证明对方一定是收到了的。 但是其实可以发现,这样不就会发生无限套娃的情况吗,你发给我消息需要等待我的确认回应,我给你发消息需要等待你的确认回应,这样确认过去确认过来,无穷无尽。 所以可以知道,TCP没有绝对的可靠性,只有相对的!事实上,不只是TCP,所有的协议都没有绝对的可靠性! 所以TCP可靠性永远不谈最新的消息,只谈历史的消息,这样看的话就一定存在互相回应的消息。
首先根据前面的确认应答机制,我们知道是互相应答,那么我们可以设想:
这样子看来,可靠性是保证了,但是好像效率变低了。难道平时我们交流的时候,必须要回应一句“我收到了”,然后才说你想说的话吗?明显不是,我们可以将应答和想回复的消息一起发送回去。
这样显然效率高多了。但是上图中,好像一直都是一条一条消息发送的,难道我们谈话都是一句一句说的吗?
显然不是,实际情况是一次性发送多条消息,并且对方也给出多条应答 。但是这样又有一个问题了:我们咋知道哪条应答对应哪条消息呢?因为消息存在“后发先至”的情况,我们面临着一个顺序被打乱的情况。
针对这种情况,TCP的解决方法就是为每个消息与应答都编上独一无二的编号,这样就不会混乱了。
序列号与确认号
客户端在接收到响应之前,还是会把数据存在缓冲区里。 首先,我们客户端要发送的数据,已经存在TCP的发送缓冲区(内核里面的那个)中了,因为TCP是面向字节流的,这个缓冲区我们可以看作是char类型的大数组,那么每一个空间就是一个字节,并且还有对应的下标,那么也就是说,每一个字节天然就有自己的编号。我们拷贝在缓冲区里的数据是按顺序存储的。 我们只需要记住在缓冲区里的数据是按顺序存储的即可!!!
序列号:
例如下图,如果一个字段被赋予了序号1,并且他包含1000字节的数据,那么这个报文就代表了从序号1到1000的数据。随后的报文将继续这个序列。比如下一个报文段开始于序号1001,而他包含500的数据,那么就代表了1001到1500的数据。
我们可以简单理解为:数据中每个字节都有唯一的标识 --- 序列号。
确认号:
确认序号的定义是这样的:确认序号的值代表接收方收到了确认序号之前的所有报文,而且是连续的报文。比如确认序号的值为1001,那么代表接收方收到了1000号及之前所有序号的报文,发送端下次从1001序号开始发送报文即可。所以确认序号的值从发送方的角度来理解,可以理解为发送方下一次发送报文时,报文的序列号。
有同学可能会有这样的疑问,既然这样,我们为什么不设置一个32位序号就行了呀,没有必要设置两个序号。那这是为什么呢?
可以从两个场景来理解:
此外,我们深知在服务端,数据通常以批量形式连续发送。若采取逐个数据发送并等待服务器应答后再继续的方式,将会极大地降低效率。 然而,这里存在一个挑战:尽管这些数据原本是按照特定顺序发送的,但服务器接收时却可能并不遵循这一顺序,这便是数据乱序问题。对于UDP协议而言,这个问题是无解的;而TCP协议则凭借其报头中的32位序列号,不仅实现了应答确认,还确保了数据能够按照发送顺序准确到达。
总结一下,序列号作用:
确认序号的作用:
我们通过上面的知识知道,一条消息发送出去之后,他自己是不知道有没有发送成功的,需要等到收到对方发送过来的确认应答消息才能确认。
前面的确认应答机制是建立在每条数据都能成功发送的基础上的,实际情况中,不可能这么顺利。那么在有丢包可能的情况下该如何应对呢?就是依靠超时重传的机制(隔一段时间没有收到应答,则重新发送)。
丢包的情况有两种,第一是发的数据丢包了,第二是返回的ACK丢包了。不管是哪种情况,只要过了一段时间没有收到ACK,就会进行重发。
丢包的两种情况
数据丢包是一个概率事件,假设一条数据在传输的过程中丢包的概率是5%,传输成功的概率是95%,那么第一次传输丢包,第二次重传也丢包的概率是5%*5%=0.25%,第三次重传也丢包的概率可以忽略不计,在实际情况中,丢包的概率是一个非常小的数字,而上述假设的5%已经是一个很大的数字了,如果连续多次重传还是丢包的情况下,那么就要考虑是否是网线断了或是其他情况了。
我们知道发送的数据段是有可能没有收到ACK的,所以被发出的数据不应该立马被移除(计算机上的移除其实就是数据覆盖),应该先保存一段时间,如果发送的数据丢包了,则可以将保存的数据再重新发送,而像这样已经发送但没有收到ACK的数据,其实是存放在滑动窗口里面的,这个后面会讲,现在先提一下。
其实TCP有一个特殊的处理功能 --- 去重,TCP存在一个“接收缓冲区”的存储空间,接收端会将读到的数据放到对应的缓冲区,根据数据的序号,TCP就能识别是否有两条重复的数据,如果重复就把后面的这条数据给丢弃了。
其实这个时间应该是随着网络情况动态变化的,如果网络情况好,超时时间设定的非常长,这其实就会影响网络传输的效率,因为数据包发送的速度非常快,可能数据包来回一次共需要50ms,但你将超时时间设定为500ms,那中间的450ms的时间就会被平白无故浪费掉,如果网络情况特别差,超时时间设定的非常短,那更离谱了,数据包正在传输的过程当中就被判定为丢包了,这同样也会影响数据传输的效率。所以不同的网络环境具有不同的延迟特性。例如,局域网(LAN)的延迟通常很低且稳定,而广域网(WAN)或互联网则可能具有更高的延迟和更大的延迟变化(抖动)。 所以一个理想的情况就是,找出一个居中的时间,保证在绝大部分网络状态下,数据包能在这个时间内发送到对方手中,同时ACK报文也能发送回来。 Linux 中(Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍。 • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。 • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增。 TCP的重传机制并不是无限制的。如果数据包一直无法成功传输,重传次数会不断增加。为了避免因无限重传而导致的资源浪费和网络拥塞,TCP协议通常会对重传次数进行限制。当重传次数超过某个阈值时,TCP连接可能会被关闭,并报告一个错误。 此外,TCP的拥塞控制机制也会对重传过程产生影响。当网络出现拥塞时,TCP会降低发送速率,以减少数据包的丢失率。这可能会导致重传时间的增加和重传次数的减少。
标志位就在:
标志位字段共 6bit,有6个标志位,每个标志位占 1bit ,每个标志位只有 1 和 0 两种状态。
如下:
TCP标志位 | 中文意思 | 作用 |
---|---|---|
URG | 紧急标志 | 用于指示紧急数据 |
ACK | 确认标志 | 用于确认收到数据 |
PSH | 催促标志 | 用于立即传输数据 |
RST | 重连标志 | 用于重置连接 |
SYN | 同步标志 | 用于建立连接 |
FIN | 结束标志 | 用于关闭连接 |
ACK:表示本报文前面的确认字段是否有效
只有当ACK为1时,前面的确认字段才有效。
在TCP确认回复机制中,客户端和服务端任意一方发送数据后,另一方都需要给予应答以表明自己收到数据。在应答的报文中该标记位需要置1,同时应答的报文也可以携带数据。
TCP规定,建立连接后,ACK必须为1。
SYN:请求建立连接。携带SYN标识的称为同步报文段
作为服务端,如果我们想要知道客户端是想发消息还是想与我建立连接,就需要依靠这个标志位。
TCP是面向连接的协议,双方在通信之前需要建立连接。建立连接的过程是通过三次握手实现的。为什么说是三次握手呢?
TCP虽然能保证可靠性,但是TCP是允许建立连接失败的。
只有建立连接的前两次请求中 SYN 才为1。这两次的报文称为同步报文段。
FIN:表示断开连接
断开连接是一次四次挥手的过程:
RST:代表重新建立连接。
三次握手建立连接并不一定能成功建立连接,谁也无法保证过程中发生什么,同样四次挥手也一样。就算连接建立成功,也可能会因为某种特殊原因被断开。比如你在家正在峡谷激战呢,你爸突然把你们家网线拔了,这连接不就断开了嘛。
此时,你仍然一直点着你的技能。这时游戏服务器就会感到奇怪,你小子不是被断网了吗,为什么还在一直请求。
所以此时服务器就给客户端发送一个复位报文段,其报头中的 RST 标志位被设置,告诉客户端说,你别再给我发消息了,我们之间的连接早就断了,你再重新发起三次握手,重新和我建立连接吧。
所以复位标志位用于通信双方中,任何一方认为建立连接不一致时,认为连接异常的一方会发送复位报文段,告知对方我们需要重新建立连接。
下面来看几个例子:
客户端与服务器通过三次握手成功建立连接,但是正常通信时服务器端的操作系统资源满载,导致服务器无法对客户端做出应答,由于服务端建立连接后也要管理连接,操作系统描述管理这些连接数据结构,服务端OS为了解决资源满载的问题可能会释放掉建立的连接,服务端端必须重新发送RST标识为1的报文给对应客户端请求重新建立连接才可以进行通信。 就像下面这样子。
Client 在三次握手的时候,认为只要把三次握手中第三次报文发出,连接就建立好了。但是要是第三次握手失败的时候,就会是下面的情况:
PSH:PSH表示催促标记位,可以理解为PUSH
Client 在不断发消息,Server 在不断接收数据,同时给 Client 发送应答,应答报文中包含了16位窗口大小的字段。所谓的窗口大小字段其实就是在告诉 Client 自己接收缓冲区剩余空间的大小,以便于 Client 调整发送策略。
但是其实,当 Server 接收缓冲区快满了或者说已经满了的时候,此时 Client 会在发送的报文中设置 PSH 标记位来催促对方尽快处理缓冲区的数据。(让对方上层应用程序尽快把 Server 接收缓冲区的数据读取走,以便让 Client 能够继续向 Server 缓冲区写数据。)
这是一个相对的过程,就像跑步比赛,位于前面的人跑得慢了,而后面的人跑得快了,那么他们的距离就会缩短,直至反超。缓冲区也一样,当读出数据的数据慢于写入数据的速度时,在时间的积累下,缓冲区会被慢慢写满。或者换一种说法是 Server 应用程序还在处理上一条数据,导致没有时间调用read接口函数取走缓冲区的数据。
其实缓冲区有一个低水位与高水位标记,用于控制数据的读取。 (1)低水位:当缓冲区中的数据量低于低水位时,表示当前数据太少,OS可能不会立即催促上层应用读取数据。 (2)高水位:当缓冲区中的数据量达到或超过高水位时,表示缓冲区中的数据已经较多,OS会催促上层应用尽快读取数据,以防止缓冲区溢出和数据丢失。 当接收缓冲区中的数据量达到或超过高水位时,OS会通过某种机制催促上层应用尽快读取数据。这种机制可能涉及中断、轮询或其他方式,具体取决于操作系统的实现。
URG:表示紧急标志位。表示本报文中发送的数据是否包含紧急数据。
URG为1时表示有紧急数据,并且只有当URG为1时后续的16位紧急指针字段才有效。
当发送方想要对方尽快拿到一些数据时,就会设置这个标志位。URG标志位通常需要搭配紧急指针使用。紧急指针是一个正的偏移量,这个偏移量与TCP首部中的序号字段的值相加,可以表示紧急数据最后一个字节的序号。因此,紧急指针实际上是指向紧急数据最后一个字节的下一字节。
紧急指针字段如下:
网上看到一个说法:紧急数据只能有1字节。 这个说法是错误的。紧急数据显然并不是1字节的容量就能满足的。虽然紧急数据并不适用于大量的数据传输,但是实际的长度是由发送端和接收端之间的协商以及TCP协议的实现来决定的。 紧急指针的使用并不常见,它需要双方协商和支持。在实际应用中,紧急指针通常用于传递一些重要的控制信息或紧急指令,而不是用于大量的数据传输。例如,在交互式通信中,一端的应用进程可能在键入一个命令后立即希望收到对方的响应。在这种情况下,TCP就可以使用紧急指针来指示接收端尽快处理这些紧急数据。
这里引入一个名词:带外数据。
带外数据是一种特殊的数据类型,它通常与带内数据(即正常的数据流)分开处理。带外数据具有更高的优先级,应该尽快被接收和处理。在TCP中,带外数据通常通过紧急模式进行传输,但并非所有TCP实现都支持这一功能。
带外数据需要与紧急数据区分开来:
下面来看一下recv函数中的MSG_OOB标志位:
在recv函数中,MSG_OOB标志位用于接收带外数据。 当recv函数使用MSG_OOB标志位时,它会尝试从TCP连接中接收带外数据。带外数据不会进入接收缓冲区,而是直接交给上层进程处理。这使得带外数据能够绕过正常的数据流处理机制,以便尽快被接收端的应用程序所响应。
原文段在(点此查看) 。
事实上,我们说的TCP的三次握手建立的这个连接,其实是端到端的,也就是说客户端的应用层到服务端的应用层的连接。更详细说是客户端的进程与服务端的进程之间的连接。只要我客户端的应用层没和服务端的应用层连接上,我们就可以说这个TCP连接是失败的。
当上层(如应用层)调用connect
函数时,它实际上是请求传输层(TCP协议层)来建立TCP连接。这个连接是逻辑上的,他建立在两个端点的TCP协议栈之间。TCP协议层随后会进行三次握手过程,以在客户端和服务端之间建立连接。在这个过程中,SYN报文是由客户端的TCP协议层(传输层)发出的,用于发起连接请求。
需要注意的是,虽然我们说TCP连接是端到端的,但在实际的网络传输过程中,数据会经过多个网络设备和协议层的处理,如路由器、交换机、防火墙等。然而,这些处理对上层应用来说是透明的,它们只需要关注TCP连接是否成功建立,以及数据是否能够在应用层之间可靠地传输。
当一方和另一方的应用层断开连接时,传输层(TCP协议层)可能仍然保持某种状态,并继续发送SYN报头。
close
)来通知传输层。这个调用会触发传输层开始断开连接的过程。这些低层协议的状态和操作对上层应用来说是透明的,上层应用只需要关注应用层之间的连接和数据传输即可。
我们知道,TCP在【端对端】之间建立的信道,为上层【端】对应的进程提供服务,它由客户端与服务端的套接字(socket)以及它们之间交换的数据包组成。TCP连接的建立、维持和终止都需要遵循一定的协议和状态机制。 另外,现实模式下一台 Server 不可避免的需要与多台 Client 连接,所以 Server 需要对这些 来源不同的 Client 进行管理。 从数据结构的角度知道:TCP是位于传输层的协议,也就是说,TCP的各种逻辑由操作系统--特指Linux维护,那么这些数据就得按照操作系统的规则组织,即先描述再组织。 当连接建立成功时,内存中会创建对应的连接对象。而管理这些连接实际上就是对这些连接对象的增删查改等操作。 既然这些连接对象需要操作系统的维护,而维护是需要消耗成本的,因为每个连接都需要跟踪其状态、参数和传输的数据,而这些消耗主要是一些CPU与内存资源。这些消耗也是一些网络攻击的切入点。例如:SYN洪水攻击就是通过向服务器发送大量的半连接请求来耗尽其资源。 而TCP连接需要维护的那些状态信息和参数等,会被存储在一个称为传输控制块(TCB)的数据结构中。TCB是TCP连接的核心数据结构,它包含了连接的所有重要信息。每个TCP连接都有一个唯一的TCB与之对应。操作系统通常使用一张表(如TCB表)来存储所有的TCB。
最重要的原因就如刚才所说,两个具有代表性的协议:TCP 和 UDP 都是传输层的协议,而传输层由操作系统内核维护,那么协议的实现必须符合操作系统中的规则。 另外,在Linux中,传输控制块(Transmission Control Block,TCB)和线程控制块(Thread Control Block,TCB)或者进程控制块(Process Control Block,PCB)之间的关系是不同的,它们分别属于不同的层次(前者是传输层,后两者是内核),他们之间的关系是:
总结:
好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。
祝大家越来越好,不用关注我(疯狂暗示)