认识TCP报头
其实协议的形式都是一个结构化的数据,TCP协议也不例外。一起来看看TCP协议的报头是怎么样的。
以上就是TCP报头,实际上是一个结构化的数据,也就是一个结构体。例如:
struct tcp_hdr
{
unsigned int stc_port : 16;
unsigned int desc_port : 16;
unsigned int seq;
unsigned int ack_seq;
....
};
目的端口号&&源端口号
其中16位源端口号,就是你当前发送报文的端口号。目的端口号则是目标主机的端口号。
这也就是我们在用accept时,能够获取连接套接字的同时,还能获取到对端主机的端口号(ip也获取到了,这个在网络层的时候解释)。 而目的端口号也很好理解,因为端口号能够确定一台主机中唯一的一个进程。当你的报文被发送到对端主机的时候,就能够通过端口号去锁定对应的PCB进程。
4位首部长度
4位首部长度代表的是报头占多少大小,这回有人问了,4位不是 0000 - 1111 ,也就是 0 -15这个区间吗?
可是报头的基本大小都20了。 实际上上,4位首部长度 * 4 才等于报头的实际大小。也就 0 - 60区间,报文长度默认是20字节,也就说明4位首部长度默认为5。
如何解包&&分用?
如何解包
还记得我们上面提到的4位首部长度吗? 当TCP层收到TCP报文时,会先提取前20个字节。获得除了选项以外,报文的其他信息。随后再去查看4位首部长度,如果4位首部长度 * 4 大于20 。那么再减去20个字节,把剩余的字节提取出来,那么剩下的不就是我们TCP报文的有效载荷吗?
如何分用?
还记得我们报头上有个目的端口号字段吗?在操作系统里,会存在一张hash表,而port则是对应的key值,value值就是我们的进程PCB。所以操作系统可以通过报文中的目的端口号,去找到该端口号绑定的进程PCB。
找到了进程的PCB之后,PCB内部又有一个files_struct的结构体,里面有fd_arr[]数组,数组存储的是描述文件结构体的指针*file,数组的下标则是对应的文件描述符fd。而此时操作系统会创建一个新的文件,这个新的文件的下标就是我们经常见到的sock。而此时传输层并不知道sock是多少,但是当我们进行read的时候,会把sock传进去。此时传输层就知道了sock是多少,并找到PCB中sock对应的文件。把数据拷贝到该文的缓冲区。
这其中还有很多细节,不过我们大致知道一个流程就行了。所以,数据是通过TCP层拷贝到sock套接字的文件缓冲区的!那么这也意味着,TCP内部也是有缓冲区的! 因为客户什么时候从TCP上读取数据是不确定的!这就意味着TCP必须要有保存数据的能力!
TCP的可靠性
为什么网络传输的时候,会存在不可靠的问题?
很简单,因为距离太远!就好比你和你的室友说话,离的越近他听到的就越清除。如果你和他隔了50米说话,人家还能听清吗?人家听不清甚至听不到!这就是你发送的信息丢失了!而在网络中也一样,随着距离的变长,数据丢失的可能就越大。如果数据丢失了,那么我们是不是要采取相对应的策略。去解决这些问题呢?而解决这些问题就提高了可靠性。
不可靠问题常见的有哪些不可靠的场景?
1. 丢包
顾名思义,你的报文丢失了。也就是你的数据丢失了,这是一种不可靠的场景。
2. 乱序
这个也很好理解,比如你发送了5个报文,分别是以 1, 2 ,3 ,4 ,5的顺序发送的,可是对端接收却以5,2,3,4,1接收的。就好比你和你女神发:你喜欢我不。 结果你女神那边却收到:我不喜欢你。这样你的爱情直接毁于一旦了,所以肯定是不可靠的。
3. 校验错误
就是检查你的报文有没有不合理的地方。
4. 重复
你发送了一条报文,但是你以为对方没收到,然后发送了多条报文。但是对方其实都收到了,就说明你发送了重复的报文,这也是不可靠的一种场景
当然,不可靠的场景还有很多,这里就不一 一细说了。
确认应答机制
抛出一个问题:如果距离长了,存不存在绝对的可靠性?
先说结论,答案是不存在!
为什么呢?
就好比我们的A 和 B 说话,他俩之间隔了500米。A 对 B 说,你吃饭了吗?
这时候B收到了消息,但是A并不知道B有没有收到消息。所以这时候B回了一句: 吃了!
当A收到"吃了"这条消息的说话,A 可以肯定的是,他的上一条消息 "你吃饭了吗?"是被对方收到了的!但此时B并不知道A是否收到自己发送的消息。所以此时A又进行了回复。
A向B发送"吃的什么呀?",此时B也知道,他的上一条消息"吃了"是被A接收到了的!但此时A也并不知道自己新发的消息"吃的什么呀?"是否被B接收到。
再举个例子,你在宿舍叫你的室友。但是他没有一点反应,这就是你没有收到应答,你没有收到应答,是不是你就会认为他没听见?你会认为你的消息丢包了。所以你会再叫一遍。这时候他听到了,点了点头,或者看了你一眼,或者和你说话。这些都是对方听到你消息的一种应答。
通过上面的例子,我们可以得到结论:
1. 我们认为,只有收到了回复,我们才能100%确认对方收到了我们发送的历史消息
2. 双发通信,一定存在最新的数据没有应答!而最新的这一条消息无法保证可靠性!
32位序号与32位确认序号
32位序号代表TCP缓冲区的数组下标。每次发送时,都会把这个下标发给对端,当对端接收时,返回应答时,会填入32位确认序号。32位确认序号填的是这个序号之前的报文,已经全部收到了。
而我们都知道,如果发送发出去的顺序和对端接收的顺序不一致,也就是乱序的。那么这是不可靠的,因为有可能是客户端发送一批报文,服务器再给出一批应答。这种情况TCP是不能保证接收的时候是有序接收的,而这时候就可以通过确认序号对接收来的报文进行排序,这样交给上层时就是有序的。也就是说按什么顺序发,就按什么顺序收。
为什么报文不只设置一个序号?
为什么报文设置2个序号,而不是一个序号? 反正也只是确认一下。 这是因为TCP是全双工的!再保证自己接收数据的情况下也要发送数据!所以这也就意味着仅仅一个序号是不行的!
16位窗口大小
我们都知道,TCP是有缓冲区的。如果上层迟迟不拿数据,或者服务器压力太大。会导致缓冲区的数据不能及时取走,如果最后服务器的接收缓冲区已经被打满了。那么客户端是否还要发送数据呢? 如果服务器的接收缓冲区快满了,那么你的客户端发送多少数据呢?如果服务器此时的缓冲区只剩下5个字节可用了。可是你的这一个TCP报文却发送了100字节。是不是意味着有95字节,没地方放了?所以为了保证合理的传输数据,就引入了16位窗口大小的概念。
而这个16位窗口大小,就是用来告诉对端,自己的接收能力!告诉对端,自己能接收多少数据,所以16位窗口大小填的是自己的接收长度!!然后对端收到这个报文后,知道了你的接收能力!就会根据你的接收能力给你传输适合大小的数据!
6位标志位
我们要先明白6位标志位的意义是什么,我们都知道sock套接字再创建时都要指明它的协议类型,是udp通信还是tcp通信还是…所以我们的TCP报文也不例外,**我们的TCP报文也有类型!**而这个6位标志位就是代表着报文的类型。
ACK标志位(acknowledgment)
ACK是一个确认应答标志位,我们上面说过。TCP最新的一条报文是无法保证可靠性的!所以如果你收到了对方的消息,你要告诉它你收到了!你就需要把ACK标志位由0置为1。再发生给对方后,对方就知道你收到了它的报文(这里会有丢包问题,后面的文章会讲解解决策略)。
SYN标志位(synchronization)
我们都知道TCP是面向连接的,而TCP的连接规则是三次握手。当你客户端向服务器发起连接时,那么TCP报文需要把SYN标志段设置为1。这样服务器接收到你这个报文时,就知道你是想和服务器建立连接。随后服务器会返回一个SYN + ACK都被设置了的报文,证明服务器收到了你的连接请求并和你建立连接。而这个过程就是TCP三次握手过程!具体的三次握手过程以及为什么要三次握手,后面会有详细讲解。
FIN标志位(finish)
我建立连接需要SYN标志位告诉服务器,我要建立连接了!
那么我们如果要断开连接时,是不是也要告诉服务器,我要断开连接了!(直接拔电源除外…)
所以当我们断开连接时,客户端(一般由客户端主动断开连接)会向服务器发送FIN为1的标志位的报文。告诉服务器,客户端要断开连接了!随后服务器给你应答后,也会给你发送一个FIN报文。你再给服务器发送ACK确认后,那么连接就断开了,这个过程也就是四次挥手。四次挥手详细过程后面会有讲解。
URG标志位(urgent)
我们上面说了32位序号可以用来对报文排序,让报文有序的交付给上层。 但是如果有的报文想搞特殊,想插队咋办?那们只需要把TCP报文的URG标志位设置位1。这样TCP收到这个报文不会把这个报文加入排序,而是立即处理这个报文。
而这个报文需要被尽快处理的数据在哪?
TCP报文中还有一个字段,那就是16位紧急指针
这个指针存的是在有效载荷中的偏移量!
那么紧急处理的数据占多少个字节呢?答案是1个字节!而这个数据我们称之为带外数据
用到URG标志位的场景非常的少,很少会用到。 如果你想服务器立马区帮你完成某一件事,你可以用上URG标志位。带外数据设置为 0,则返回服务器的健康状态…,设置为1,则去调用一个进程…等等。
PSH标志位(push)
当对端的接收缓冲区已经满了的时候,而你这边的会定时的发送TCP报文,收到对方的应答之后去查看对方的接收能力。可是试了几次,对方还是无法接收你报文的时候。这边已经忍无可忍了,就会把PSH设置为1,去催促对端让对端把数据赶紧取走。如果你是故意不read的,那没办法,人家也拿你没办法。我们都知道read会等待条件是否就绪,如果条件不就绪就会阻塞。而当收到带有PSH被设置的报文时,就会尽快的让read的等待条件就绪,以至于让上层读取数据后恢复自己的接收能力。这样这边的数据就可以发送了。所以,PSH就是让对端的read条件尽快就绪! 具体细节和多路转接有关,后面的博客会更新多路转接相关的知识。
RST标志位(reset)
我们上面说过,当程序结束时,要断开连接。但是有人不按套路出牌,直接拔电源让电脑关机。这样就没机会发送断开连接的报文。所以就会出现一种场景,就是服务器和客户端已经建立了连接。但是服务器直接操作系统重启了,也没来得及断开连接。而此时客户端也没有给服务器发送消息。等到服务器主机重启后重新运行服务进程时,你的客户端此时还认为是和服务器建立了连接的,但此时服务器刚刚重启,不认得客户端。所以客户端给服务器发送报文时,此时的服务器就很奇怪。对端怎么直接把报文发过来了?不是要先建立连接吗? 所以此时服务器就会在应答报文上设置RST标志位。意思就是连接重置!
还有一种情况也会发送重置RST,那就是三次握手失败的情况!在客户端最后一次ACK没有被服务器收到的时候。客户端认为自己已经和服务器建立了连接,而服务器却认为没有和客户端建立连接。所以此时客户端直接给服务器发送数据报文,服务器也会返回重置了RST的报文应答。