如今,除了实时流媒体或少数专业应用程序外,人们用来构建互联网应用程序的首选传输层协议是TCP。
问题是,对于我们正在使用的东西来说,TCP并不是真的那么好。
除了阐明我的观点所必需的之外,我不会详细说明它是如何工作的,但是让我们来看看应用程序是如何使用TCP的。
在某个地方,程序希望通过TCP提供服务,因此它告诉网络堆栈它希望侦听选定的TCP端口(例如,Web服务器的端口80或443)。
一旦发生这种情况,某处的客户端就可以通过指定运行服务器的计算机的地址和端口号来尝试连接到它。当三次握手发生时,会有一个短暂的延迟-客户端向服务器发送请求(称为SYN),服务器以提供响应(称为SYN/ACK),客户端回复接受该提供(称为ACK)。客户端使用连接所用的时间是SYN到达服务器和SYN/ACK返回所用的时间,称为连接的往返时间。
在此之后,服务器上的网络堆栈通知服务器程序有一个新连接,而客户端上的网络堆栈通知客户端程序连接已就绪-如果一切正常。
但是,如果连接已建立,则两端都可以向对方发送字节流。TCP采用底层网络-它是基于数据包的,这意味着小块数据以消息的形式发送-并创造了类似串行电缆或进程之间的UNIX管道的错觉:仅仅是字节流。在幕后,它处理检测丢失的数据包并重新传输它们,确保无序到达的数据包以正确的顺序传递,而不是随机重新排列字节流的部分,以及流量控制:检测正在发送的流量何时超出了客户端和服务器之间的网络中的某个瓶颈,并降低其发送的速率,以避免浪费资源。
它通过检测拥塞来执行流量控制,这是在Internet上真正可能的唯一方法:查看它发送的数据包实际到达接收方的速率。有些数据包由于通信链路上的设备故障或噪声而丢失,这是传输中数据包的一种背景致死率,而不管发送了多少数据。但是,当网络中某处的路由器接收数据包的速度超过其沿链路发送数据包的速度时,它们会排队,如果队列太满,它会开始丢弃数据包,而不是在内存中保存太长时间。因此,当发送数据的速率上升时,链路上的丢包率将保持在后台,直到我们达到系统中最弱链路的容量,此时丢包率将上升-在短暂的延迟之后,队列填满,直到它们开始丢弃东西。
TCP使用一个相对简单的技巧:当它发送的数据包丢失时(它知道这一点,因为另一端没有确认接收到它),它会稍微降低发送速率。但是,当流量畅通无阻地通过时,发送速率会略有提高。这意味着,只要后台丢包的基本速率不是太高,它就会慢慢增加发送速率,直到它压倒最慢的链路,然后在注意到它之后(队列需要一段时间才会超载,会丢弃一些东西,丢失会被接收方注意到,这一事实会传回发送方),它就会降低发送速率-使得发送速率将徘徊在最大值附近,稍微推动它,直到它得到一些分组丢失,然后再次移动回来。(#**$$}{##**$$}。(还有一种称为随机早期检测(RED)的技术,用于在链路过载变得严重之前尝试用信号通知链路过载并改善反应时间,还有一种称为显式拥塞通知(ECN)的机制也有帮助,但在撰写本文时ECN尚未得到广泛应用)。
如果您在进行大型文件传输时查看带宽消耗图表,则有时会在发生此情况时看到图表中的小波动。
首先,当新的传输开始时,TCP堆栈不知道有多少带宽可用;它必须猜测,然后快速向上或向下调整到实际带宽。在实践中,它开始较低,然后迅速增加,这一过程称为缓慢启动。然而,因为它正在快速提高速率以找到丢失的起始点,任何背景丢失都会使它感到困惑,并导致连接开始变慢,然后只会慢慢提高速度;对于短暂的连接(或流量较短的连接),它可能永远无法在结束之前找到最高速度。
其次,此过程针对每个TCP连接独立进行。如果您有两个从一台计算机到另一台计算机的连接,则这两个连接都将独立地增加和降低其发送速率-这两个连接中的任何一个都会使瓶颈链路饱和,但这两个连接都可能会感觉到由此导致的数据包丢失。这意味着,如果一个连接发送得太快,则可能是另一个连接因为丢失了数据包而受到限制。当进出不同计算机的多个连接碰巧聚集在网络中的单个瓶颈处时,也会发生这种情况;在通常情况下,一条链路过载,通过它的所有连接都会丢失一些数据包,并且它们都会同时缩减规模,它们会集体反应过度,因为它们无法相互协调--导致链路未得到充分利用。
TCP通过标记每个充满字节的数据包及其在流中的位置来确保它按顺序传送它发送的字节。如果接收方接收到字节0-100,然后是字节201-300,然后是字节101-200,它将直接将字节0-100传递给应用程序;位于字节201-300上,因为它们不是下一个字节;一旦字节101-200到达,就传递字节101-200,然后是保存的字节201-300。因此,应用程序按顺序接收每个字节。
将东西下载到文件中。如果执行下载的应用程序刚被告知此处的字节201-300&34;,它可以将它们保存在文件中的位置201,然后一旦字节101-200出现,就将它们保存在文件中的位置101。将它们缓冲到TCP堆栈中并不能真正帮助任何人,也许只是稍微减轻了从磁盘I/O调度器中避免磁盘寻道的负担……。
发送离散的请求/响应/消息。通常,应用程序需要发送一条消息。由于TCP只传输字节流,因此应用程序需要标记每条消息的开始和结束。TCP将负责将消息拆分成小到足以装入数据包的块,然后发送它们,然后将它们按顺序放回原处,这在消息中是很棒的;但是,如果应用程序发送了两条消息,并且第一条消息的一部分被延迟(或者在网络中采用了很长的路由,或者它没有到达,必须重新发送),那么整个第二条消息就会位于TCP堆栈中,而不是提供给应用程序。(=。
有时应用程序会对此感到高兴--如果这很重要,那么消息是按顺序处理的,当然,它需要等待。但这通常并不重要,事实上,如果消息以任何顺序到达就能得到处理,那么它实际上会更有用。因为TCP不知道应用程序是如何将TCP字节流分解成消息的,所以它不知道是否有完整的消息可以传递-它只知道字节。
正如前面提到的,大多数TCP应用程序在TCP之上引入了它们自己的消息结构,因为原始字节流对于大多数应用程序的通信需求来说太低了(几乎没有TCP连接只不过是原始字节流;telnet几乎就是这样,但是嵌入了基于消息的控制信号-也许是FTP数据流?)。问题是,这给应用程序带来了一大堆复杂性,每次这一领域的标准化都很差,必须重新发明。
很多TCP应用程序都采用简单的请求/响应模型。假设建立连接的一端需要来自等待连接的一端的一些服务,因此一旦建立连接,服务提供商就会坐在那里等待请求。客户端将以某种形式发送请求,并以某种方式标记请求的结尾(或者先发送长度,以便另一端知道要读取的字节数,或者为结尾设置特殊的标记-如果出于某种原因需要将该标记作为请求的一部分发送,则具有随之而来的复杂性)。然后,它将等待读回数据,因为返回的数据是对最后发送的请求的响应(同样,还有一些关于如何知道响应何时实际结束的详细信息)。
这很容易实现-客户端中的代码类似于";发送请求;读取响应;处理响应;而服务器中的代码类似于";While(连接未关闭):读取请求;处理请求;发送响应";。
但是,当您想要提升应用程序的性能时,这就成了一个问题。如果您的应用程序启动了一个长时间运行的请求,则其他请求不能同时发生-如果您愿意,您可以发送它们,但即使服务器在处理请求时正在监视其他请求到达(这在服务器实现时会增加复杂性),它也只能按发送的顺序发送回响应。因此,一些协议向请求添加请求ID,响应可以以任何顺序到达,因为它们包含它们正在响应的请求的ID。现在,客户端和服务器应用程序可以随意发送请求,并在准备好后立即发回响应,代价是需要实现自己的多线程路由逻辑,并锁定对TCP连接的访问,以确保部分请求或响应不会因为尝试同时发送两个请求或响应而相互插入。但是,它仍然不完美-如果客户端请求一个大文件,大小为几GB,并且该文件是作为单个响应发送的(毕竟,它是对单个请求的响应,客户端可能事先不知道该文件有多大),那么从服务器到客户端的TCP连接将在一段时间内用于发送该大响应,而其他快速响应在该响应完成之前不能被发送。(#**$}{##**$$}}。
要解决这个问题,需要使协议变得更加复杂--现在可以将请求和响应拆分成更小的块,以及沿TCP连接交错的来自多个请求或响应的块,以便在另一端重新组装。
而且,有时客户端发起的事情,服务器坐着等待模型是不够的。客户端应用程序可能会请求某些信息,并希望服务器在该信息发生更改时立即更新该信息。如果只有客户端可以发起操作,那么它必须不断询问服务器是否有任何更新以获得响应。理想情况下,两端都应该能够向另一端发送消息或等待响应的请求。在最初简单的请求/响应模型下,这是不可能实现的--但是如果我们已经开始使用分块来实现并发请求和响应,那么要使连接成为对称的,这只是沧海一粟。
已经有一些尝试在TCP之上标准化有用的协议。在实践中,人们使用HTTP和WebSockets:HTTP作为成帧协议来表示消息的开始和结束,使用并发请求和分块的多路复用,并使用WebSockets来建立反向连接,以便服务器可以随意向客户端发送消息。这是一个实现起来相当复杂的堆栈,而且在网上发送的数据方面也相当浪费,因为要将最初设计为超文本传输协议(Hyper-Text Transport Protocol)的东西改编成适合此任务所需的各种变通方法和兼容性黑客技术,这是相当浪费的。
BEEP是一种从头开始的尝试;目标看起来相当不错(虽然它是在XML很酷的时候做的,所以有一种讨厌的XML味道),但它并没有起飞;我从来没有遇到过实现,该项目的网站自撰写本文时的2016年以来就没有更新过。
与此相关的是,通过连接向下发送消息的应用程序通常具有具有不同需求的各种消息。想象一下一个在线游戏--如果玩家按下游戏中的一系列按钮,那么这些按钮必须以正确的顺序到达服务器。但同时,服务器可能正在用所有其他播放器的最新位置流回消息;如果其中一个丢失,当流中充满包含更新的播放器位置的消息,从而使丢失的消息完全过时时,停止整个流以等待其重新传输是浪费的。但是,同样,由于TCP不知道应用程序的消息边界,所以它不能以不同方式处理不同的消息。
如果用于打开TCP连接的三次握手的一部分在网络中丢失,客户端会发现,因为它没有在合理的时间范围内取回SYN/ACK,然后重试。它会一直尝试一段时间,然后才会放弃。无论哪种方式,如果网络出现故障或不可靠,网络堆栈可能会有几秒钟的延迟,然后才会向客户端程序报告它有一个工作正常的连接,或者它无法获得连接。
而且三次握手(以及用于关闭连接的FIN数据包)可能是浪费时间。如果要通过TCP连接发送的整个内容正好可以放在一个数据包中,我们最终将执行以下操作:
发送六个数据包,发送一个数据包!我们本可以这么做的:
服务器-客户端:ACK(这样客户端就知道它已到达,不必重新发送)。
最重要的是,这些短连接将得不到流量控制;TCP的每个连接的流量控制甚至不会启动。
由于这种小连接代价,HTTP中已经做了大量工作,以使其对多个逻辑HTTP请求重用单个TCP连接,但这有两个缺点:
它使TCP协议(如HTTP)变得更加复杂,以解决TCP的不足;如果您的应用程序本身只发出单个HTTP请求,它将使用自己的TCP连接,即使您的应用程序的数千个实例可能在同一台计算机上并行运行。
很多TCP连接很快就会关闭,因为发送的第一位数据有问题-密码不正确,发送的请求消息有错误,或者接收服务器有问题,或者因为接收服务器正忙而重定向到另一台服务器作为响应。这通常会排除任何其他HTTP请求,这些请求可能会在不久的将来到达该服务器;它们要么从未尝试过,要么被重定向到另一台服务器。
TCP将校验和放在数据包中,因此如果数据包在通过网络传输时损坏,则当数据包到达时,接收方的TCP堆栈可以拒绝该数据包,并将其重新传输。
然而,校验和相当弱,而且在实践中,TCP无法检测到相当多的错误。TCP之上的应用程序通常假设任何错误都会被捕获和处理,并且会盲目地相信数据到达时没有受到干扰。很难说有多少数据以这种方式被破坏,但值得庆幸的是,TCP之上的加密层,如SSH和TLS,在识别不良数据方面相当出色,因此情况正在改善-但这并不归功于TCP!
在广域组播中提供有序、可靠的传送是不切实际的。当许多人从YouTube流传输相同的实时视频流时,他们每个人都有自己的TCP连接到最近的节点,并且他们可以传输相同的数据。
然而,支持可伸缩的广域多播是可行的:IP多播做到了,尽管在公共Internet上对它的支持很差。但是,因为TCP是有序的和可靠的,或者说是失败的,要想获得IP多播,你需要把婴儿和洗澡水一起扔掉,并使用一个完全不同的协议。
顺便说一句,在这样的多播流环境中进行流量控制是很有趣的。您不能因为接收方或中间链接速度较慢而减慢数据包的发送速度,因此您可以使用丢弃优先级:在质量层中编码视频流,使用包含最低质量流的基本层,然后在下面的层之上添加包含额外数据以提高质量的层。然后,您可以用丢弃优先级标记组成流的不同数据包。当链路过载,或者最终目的地因忙碌而无法处理时,具有较高丢弃优先级的数据包将首先被丢弃-因此您将最低的丢弃优先级放在基本层,然后在不断增加的质量层上增加丢弃优先级。因此,如果您没有足够好的连接或足够快的处理器来处理4k高清数据流,那么网络将丢弃使其成为4k的比特,而仍然保留使其成为1080p流的比特。
是的,我们可以!。各种网络协议以不同的方式改进了TCP。那些熟悉我的软件设计风格的人无疑不会对我的计划感到惊讶,因为我有一个计划,通过灵活的方式,将他们使用的技巧合并到一个可以满足各种需求的协议中……。让我们来看看我从各种协议中收集到的想法吧!
RDP和RUDP都是基于消息的协议,处理丢失数据包的重传、有序传送和流量控制。但是由于是基于消息的,即使在相同的逻辑连接中,也可以为不同的消息请求不同类型的服务。
混沌是一个长期失效的协议,但它有一些有趣的特性值得研究。我在之前的一篇博客文章中讨论过它,但它包括无需任何三次握手即可执行简单请求的能力、在建立连接时包含请求详细信息以便直接拒绝连接而无需等待握手完成的能力,以及在无连接和面向连接的通信之间的选择。
TCP试图创建单个字节流的假象,而NetBLT则完全是为了移动数据块。这很有用,因为大多数时候,TCP应用程序尝试移动的是数据块,而不是字节流-无论是大块文件上传/下载,还是小请求。
它不会尝试提供按顺序交付;当数据块到达接收方时,它们会被直接提供给接收应用程序。如果应用程序想要将它们组装到内存的缓冲区中,直到它们都在那里为止,它可以自由地这样做,但是如果它想要在每个片段到达时对它们做一些事情-比如将块写入文件,或者显示图像的某些部分,或者处理随机访问的数据文件的某些部分-无论它们到达的顺序如何,从而释放内存并提供更快的响应,它都可以做到。
但NetBLT还有一个比TCP更智能的流量控制系统。它不是摆弄滑动窗口大小(亲爱的读者,我为您节省了TCP流量控制的一个可怕细节),而是选择发送数据包的速率,
..