tcp是一种面向连接的、可靠的、基于字节流的传输层通信协议。是专门为了在不可靠的互联网络上提供一个可靠的端到端字节流而设计的,面向字节流。
保证数据的按序到达
目的:为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。
TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。
TCP将每个字节的数据都进行了编号,即为序列号。
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里开始发。
TCP的确认机制是累计的,只使用一个数字来确认数据。这一数字是自上一次成功接收后的最长字节数。
主机A发送数据给B以后,可能因为网络拥堵等原因,数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发。
但是,主机A未收到主机B发来的确认应答,也可能是因为ACK丢失了:因此主机B会收到很多重复数据,那么TCP协议需要能够识别出哪些包是重复的包,并且将这些重复的包丢弃掉,这时我们就可以利用前面提到的序列号,就可以很容易做到去重的效果。
TCP为了保证无论在何种情况下都能比较高性能的通信,因此会动态计算这个最大超时时间。Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。若重发一次之后仍得不到应答,等待2*500ms后再进行重传;如果仍得不到应答,等待4*500ms进行重传,依次类推, 以指数形式递增 。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
增加发送端的发送效率,说白了就是增加通信的速度。
在任何一个时间,发送端的数据都分为如下四种:
第二类+第三类为滑动窗口的大小。
当某一段报文段丢失以后,发送端会一直收到1001这样的ACK,就像是在提醒发送端“我想要的是1001”一样。
如果发送端主机连续三次收到了同样一个“1001”这样的应答,就会将对应的数据1001~2000重新发送。
这时接收端收到了1001以后,再次返回的ACK就是7001了。因为2001~7000接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。
这种机制被称为“高速重发控制”,也称为“快重传”。
快重传其实是超时重传的一种优化。
接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK端通知发送端。我们在讨论滑动窗口时就已经了解到,窗口大小字段越大,说明网络的吞吐量就越高。
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置为一个更小的值通知给发送端。发送端接收到这个窗口以后,就会减慢自己的发送速度。如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
所谓的拥塞控制,归根结底是TCP协议想尽可能快的将数据传输给对方,但是又要避免给网络造成太大压力而采取的折中方案。
点我学习拥塞控制
每次发送数据包时,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送的窗口。
如果接收数据的主机立即返回ACK应答,此时返回的窗口可能比较小。
窗口越大,网络吞吐量就越大,传输速率就越高。我们的目标就是在保证网络不拥塞的情况下尽量提高传输效率。
假设接收端缓冲区为1M,一次收到了500K的数据,若此时立即应答,则返回的窗口大小为500K。但实际上可能处理端处理的速度非常快,几毫秒内就能将数据从缓冲区消费掉。在这种情况下,接收端处理还远未达到自己的极限,即使窗口大小再大一些,也能处理的过来。如果接收端稍微等一会再应答,比如等待几百毫秒再应答,那么此时返回的窗口大小就是1M。
也不是所有的包都可以延迟应答,这是有数量限制和时间限制的。所谓的数量限制,就是每隔N个包就应答一次;所谓的时间限制,就是超过最大延迟时间就应答一次。而具体的数量和超时时间,依操作系统不同也有差异。一般来说N取2,超时时间取 200ms。
在延迟应答的基础上,我们发现,在很多情况下,客户端服务器在应用层也是“一发一收”的,这就意味着客户端给服务器说了“hello”,服务器也会给客户端回一个“hello”。那么此时ACK就可以搭顺风车,和服务器回应的“hello“一起回给客户端。
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区。
在调用write时,数据会先写入发送缓冲区中。如果发送的字节数过长,就会被拆分成多个TCP的数据包发出。若发送的字节数太短,就会先在缓冲区里等待,等到缓冲区的长度差不多或者其他合适的时机,再发送出去。
接收数据时,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可调用read从接收缓冲区拿数据。
另一方面,TCP的一个连接,既有发送缓冲区,又有接收缓冲区,那么对于这样一个连接,我们既可以读数据,也可以写数据,这个概念就叫做“全双工”。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配。例如:在写100个字节的数据时,既可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。同理,read也是如此。
TCP协议是面向字节流的协议,接收方不知道消息的界限,不知道一次提取多少数据,这就造成了粘包问题。我们可以就生活中的一个例子来帮助理解。在热包子新鲜出炉时,我们免不了会看到这种情况:有些包子粘在了一起,必须通过掰扯才能将它们分开。数据也是如此,“粘包问题”中的包,指的是应用层的数据包。
在TCP的协议头里,没有如同UDP一样的“报文长度”的字段,但是有一个序号的字段。站在传输层的角度,TCP是一个一个报文传过来的,按照序号排好序放到缓冲区中;站在应用层的角度,看到的就只是一串连续的字节数据。那么应用程序看到这一连串的字节数据时,就不知道从哪个部分开始,到哪个部分结束才算是一个完整的应用层数据包。
粘包问题出现的原因:
(1)发送端需要等缓冲区满时才发送出去,造成粘包;
(2)接收端不及时的接收缓冲区内的包,造成多个包接收。
避免粘包问题的方法,归根到底,就是要明确两个包之间的边界:
(1)对于定长的包,保证每次都按固定大小读取即可;
(2)对于变长的包,可以在包头的位置约定一个包总长度的字段,从而就知道了包的结束位置;
(3)对于变长的包,还可以在包和包之间使用明确的分隔符,这个分隔符是由程序员自己来定的,只要保证分隔符不和正文冲突即可。
对于UDP协议来说,就不存在“粘包问题”了,因为:
(1)对于UDP,若还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个将数据交付给应用层的,具有很明显的数据边界;
(2)站在应用层的角度,使用UDP时,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况。
文件描述符的生命周期随进程,所以进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别。
和进程终止的情况相同。
接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使无写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,若对方不在,也会将连接释放掉。
另外,应用层的某些协议,也有一些这样的检测机制,例如HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ掉线之后,也会定期尝试重新连接。
在TCP传输数据流中,存在两种类型的TCP报文段,一种包含成块数据(通常是满长度的,携带一个报文段最多容纳的字节数),另一种则包含交互数据(通常只有携带几个字节数据)。
减小网络负担。
nagle算法的核心思想是允许网络中最多只能有一个小分组被发送,而待发送的其它小分组会被重新分组成一个”较大的”小分组,等收到上一个小分组的应答后再发送。
虽然nagle算法可以减少网络中小分组的个数,但是对于那些需要实时预览的通讯程序而言,客户端可能需要不断发送更新数据并得到服务器的响应,这种情况下nagle算法会造成客户端明显的延迟,所以需要禁用nagle算法。尤其是遇上延迟ACK。
在传输大文件的时候,如果使用这个算法,那么会出现明显的延迟现象,
可以通过TCP_NODALAY方式关闭。
关闭Nagle算法。
如果在一个socket绑定到某一地址和端口之前设置了其SO_REUSEADDR的属性,那么除非本socket与产生了尝试与另一个socket绑定到完全相同的源地址和源端口组合的冲突,否则的话这个socket就可以成功的绑定这个地址端口对。相同地址和端口TIME_WAIT状态下也还可以连接。
这听起来似乎和之前一样。但是其中的关键字是完全。SO_REUSEADDR主要改变了系统对待通配符IP地址冲突的方式。
这里的问题在于操作系统如何对待处于TIME_WAIT阶段的socket。如果SO_REUSEADDR选项没有被设置,处于TIME_WAIT阶段的socket任然被认为是绑定在原来那个地址和端口上的。直到该socket被完全关闭之前(结束TIME_WAIT阶段),任何其他企图将一个新socket绑定该该地址端口对的操作都无法成功。这一等待的过程可能和延迟等待的时间一样长。所以我们并不能马上将一个新的socket绑定到一个刚刚被关闭的socket对应的地址端口对上。在大多数情况下这种操作都会失败。
然而,如果我们在新的socket上设置了SO_REUSEADDR选项,如果此时有另一个socket绑定在当前的地址端口对且处于TIME_WAIT阶段,那么这个已存在的绑定关系将会被忽略。事实上处于TIME_WAIT阶段的socket已经是半关闭的状态,将一个新的socket绑定在这个地址端口对上不会有任何问题。这样的话原来绑定在这个端口上的socket一般不会对新的socket产生影响。但需要注意的是,在某些时候,将一个新的socket绑定在一个处于TIME_WAIT阶段但仍在工作的socket所对应的地址端口对会产生一些我们并不想要的,无法预料的负面影响。但这个问题超过了本文的讨论范围。而且幸运的是这些负面影响在实践中很少见到。
许多人将SO_REUSEADDR当成了SO_REUSEPORT。基本上来说,SO_REUSEPORT允许我们将任意数目的socket绑定到完全相同的源地址端口对上,只要所有之前绑定的socket都设置了SO_REUSEPORT选项。如果第一个绑定在该地址端口对上的socket没有设置SO_REUSEPORT,无论之后的socket是否设置SO_REUSEPORT,其都无法绑定在与这个地址端口完全相同的地址上。除非第一个绑定在这个地址端口对上的socket释放了这个绑定关系。与SO_REUSEADDR不同的是 ,处理SO_REUSEPORT的代码不仅会检查当前尝试绑定的socket的SO_REUSEPORT,而且也会检查之前已绑定了当前尝试绑定的地址端口对的socket的SO_REUSEPORT选项。
SO_REUSEPORT并不表示SO_REUSEADDR。这意味着如果一个socket在绑定时没有设置SO_REUSEPORT,那么同预期的一样,其它的socket对相同地址和端口的绑定会失败,但是如果绑定相同地址和端口的socket正处在TIME_WAIT状态,新的绑定也会失败。当有个socket绑定后处在TIME_WAIT状态(释放时)时,为了使得其它socket绑定相同地址和端口能够成功,需要设置SO_REUSEADDR或者在这两个socket上都设置SO_REUSEPORT。当然,在socket上同时设置SO_REUSEPORT和SO_REUSEADDR也是可行的。
获取绑定套接字的本地地址(不能仅将此选项“设置”为“得到”,因为套接字是在创建时绑定的,所以本地绑定的地址不可更改)。
这个参数用来控制客户端读取socket数据的超时时间,如果timeout设置为0,那么就一直阻塞,否则阻塞直到超时后直接抛超时异常。
这个字段对Socket的close方法产生影响,当这个字段设置为false时,close会立即执行并返回,如果这时仍然有未被送出的数据包,那么这些数据包将被丢弃。如果设置为True时,有一个延迟时间可以设置。这个延迟时间就是close真正执行所有等待的时间,最大为65535。
A : 我想和你握手(SYN=1)
B : 好的,你可以(ACK=1.SYN=1)
A: 那我来了(ACK=1)
如果两次的话,第一次A说了这句话,网速比较差,B一直没有接收上,A有重说了一次,重说的这次呢,网速很快。B接到重说的这次,然后伸出手,两个人友好快乐的握了手。松手后,B又接到刚才A的握手请求,B一个人伸出手,尴尬的在冷风中微笑。
A:我说完了(FIN=1)
B:我知道你说完了,但是我还没有说完(ACK=1)
B:我也说完了(等A回复,防止A没有收到)(FIN=1,ACK=1)
A:好了,那咱们断开吧。(但其实它会等待一会儿再关,因为怕B没有收到,另一方面,怕有些数据延迟)(ACK=1)
无连接的传输层协议,提供面向操作的简单不可靠的非连接传输层服务,面向报文。有丢包,无序,检错可以选择。但是效率高。
因为TCP很多硬件底层不支持端口复用,尽管有这个参数可以修改。所以这里我们使用UDP的NAT穿透。
如果我们要用UDP去实现可靠的传输,则需要解决两个问题:丢包和后发先至(包的顺序)。
解决方法:
1)给数据包编号,按照包的顺序接收并存储;
2)接收端接收到数据包后发送确认信息给发送端,发送端接收确认数据以后再继续发送下一个包,如果接收端收到的数据包的编号不是期望的编号,则要求发送端重新发送。