TCP协议详解(1)
TCP协议
TCP全称为 "传输控制协议(Transmission Control Protocol"). 就像是它的名字一样,要对数据的传输进行一个详细的控制
TCP不像UDP它是有完整的接收缓冲区和发送缓冲区的!
之所以叫传输控制,是因为所有的发送函数(write,send....)的这些函数本质都是拷贝!都是将数据拷贝到传输层缓冲区里面!
当数据拷贝到传输层,数据什么时候发,发多少,出错了怎么办!完全都是由TCP协议自主控制!——我们不用管!操作系统自己会做!
TCP协议格式
每一行有32个bit位,选项暂时不考虑,有5行长字节!
保留6位是指不知道要干什么用的,所以先保留下来!
我们下面不讲选项,如果有兴趣可以看下面这个文章
数据——就是指从上层拷贝下来的应用层的报文(包含报头和有效载荷)!——==这个是UDP协议的有效载荷==
==UDP协议的报头==:
源端口号和目的端口号——和上面的UDP协议的作用是一致的!
4位首部长度是什么作用的呢?——我们这里就要说明一下TCP协议的解包和分用了!
TCP协议的封装解包和分用——4位首部长度
tcp协议是有标准长度的!——就是20!所以先读取20字节!先不要怕管选项和数据总之我们知道前20字节是一定存在的!
前20字节我们可以转化为一个结构化的数据!我们可以立马提取标准报头中的4位首部长度
==4位首部长度是指——**TCP报头的总长度!**总长度和标准长度是要区分的!标准长度是20字节!但是如果tcp协议带选项的话那么可能是40,60字节!tcp报头是一个带有自描述字段的变长报头!==
首部长度只有4位——那么·就是[0000,1111]——也就是[0,15]说明报头最短0字节,最长15字节,但是根据我们现在的理解!这是不对的!因为标准长度都20了!
==所以其实tcp报文的总长度 == 4位首部长度*4字节!——所以长度范围是[0,60],但是因为标准长度都有20了!所以最终的范围是[20,60]==
所以如果我们的报头长度是标准报头长度!那么我们的4位首部长度是5——0101
==这样我们就能知道剩下报头的剩余大小!4位首部长度 - 20就是剩下的报头长度!如果是大于0就继续读取!==
只要将tcp报头处理完毕!剩下的就是有效载荷了!我们就可以将有效载荷扔到上层,给上层读取
问题——我们tcp里面报头没有有效载荷的长度!那么我们如何读取tcp的有效载荷?我们该如何读取!
==很简单!tcp是字节流的!有字节流了数据的可靠性就可以得到保证!是按序到达的!——那么我们只要获取下一个报头的开始我们不就得到了上一个报头的结尾吗?==
tcp的封装就是解包的逆向过程!
==那么tcp是如何分用呢?——tcp顶上也有各种各样的应用层协议!我们如何将有效载荷交给上层哪一个协议呢?我们报头已经包含了目的端口号!我们依旧可以使用目的端口号来找到应用层的进程了!数据就可以被交付给进程了!==
目的端口号的作用——端口号
我们收到一个报文后,是如何找到曾经bind的特定的port进程的?
系统有很多的场景需要我们快速定位一个进程的!内核里面系统是将进程以单链表的形式将进程管理起来!——但是不仅如此!单链表可以保证我们不丢失,方便管理!
但是有时候我们需要快速的定位进程!——所以我们还要将进程的pcb放进其他的数据结构里面!——那就是hash表!
内核里面进程间关系是十分的复杂的!都是由很多的数据结构来一起维护的!
网络协议栈和文件又有什么关系呢?
==也就是说当我们当我们在进行数据正常读写的时候,底层是可以找到对应进程,找到对应的pcb,找到对应的文件,找到对应的缓冲区然后将数据放入缓冲区里面!然后上层就可以使用文件的方式来统一的读取数据!==
TCP报头的理解
TCP的确认应答机制(可靠性)
首先我们要搞明白一个问题——为什么网络传输的时候,会存在不可靠的问题!
在冯诺依曼体系结构体下,有CPU,有内存,还有键盘,鼠标,网卡等各种各样的外设!
所以的这个设备看起来都是独立的!但是经验告诉我们我们可将键盘的数据放到内存里,将内存的数据放到CPU里面,将CPU里的数据,经内存写到网络里面——那么我们就能明白所有的设备不是我们想象的那样孤立!
==这些设备都是通过我们计算机中的“线”连接起来的!——而我们一般内存和外设连接的"线"——就是IO总线!内存的CPU连接的时候用的“线”——叫做系统总线!==
不可靠问题常见都有哪些场景呢?
丢包就是最常见的不可靠的场景
乱序,就是指我们先发的数据后到
校验错误,例如:比特位翻转导致的校验失败,从而导致报文被丢弃
重复,就是报文实际没有丢,但是发送方以为它丢了!发送方于是又发送了一份,导致接收方收到两份报文!
那么TCP可靠性如何保证?
首先我们以一个问题作为切入点——如果距离长了,存不存在决定的可靠性呢?
老师在网络上给学生直播上课,老师能不能保证老师听到的话一定会被学生听到呢?——不能!那么什么情况下能保证知道?——学生给老师进行答复!这样子老师才能知道他说的话被学生听到了!
TCP的工作模式——32位序号
因为上面我们说的,请求后如果只发一个收到了那么效率其实就有点不高了!
所以我们一般会在确认的后面加上我们的请求!
==就像是上面的AB对话,A说:吃了吗?B此时就不简单的回答说吃了,而是回答:吃了,你吃了什么?——既包含了确认应答,也包含了请求!==
但是无论什么工作模式是C-->S,还是S-->C都是需要进行应答!
但是如果是像上面的第一种工作模式,对方只给了一个应答!那么我们就不需要进行回应!==我们需要的是对正常的数据段进行应答!==
无论Clienti端和Serveri端在应用层是如何的**!但是在传输层tcp中地位就是对等的!** 所以在传输层就不会说什么请求响应!而是说数据段!
就像是在现实中,我们用快递送东西,无论你是什么身份,在快递员看来,都是快递 所以我们只需要直到一个方向到另一个方向的通信流程!我们就知道双方的通信流程!
==但是我们上面说过真实情况是服务端会连续的发送多个请求!然后服务端一次性的批量处理!==
那么数据到达对面的顺序一定是和发送的顺序是一样的么?——==不一定的!==
因为我们先发的数据可能因为阻塞,而后发的数据传输通畅导致了后面的数据先到达!
就好比我们去淘宝买东西,先买的东西不一定先到!因为运快递的路的路况是不一样的!
而客户端如何知道发送过来的确认和曾经的请求的对应关系是什么样子的?
例如:我们一次发送了4个请求,而服务器发送回4个确认,但是客户端怎么知道这个4个确认和请求是谁跟谁对应的?
还有就是如果我们发送了4个消息,但是服务端只给我们发送回3个确认,客户端要怎么知道为什么只有3个确认,哪一个的确认是丢失的
==这就注定了tcp数据段就需要有方式表示数据段本身!——这就是我们上面tcp协议结构中的32位序号!==
32位序号/32位确认序号
为什么要怎么定义呢?——按照我们正常的思维,我们只要对报文本身做确认就可以了!为什要确认历史的报文呢?——这个和滑动窗口的有关系!这个确认序号是要支持滑动的线性右移的!
充当正常报文的时候,那个序号就是表明当前报文的序号!而充当确认报文的时候,那个序号表明的就是确认序号——==但是为什么要有两组序号==
为什么我们不能把这两组压缩成一个序号?——当我们请求的时候我们就正常填序号即可,应答的时候也在这个里面填入确认序号不就行了?
==因为TCP是全双工的!我们一直都在讨论的是从C——>S,但是现实中,除了C——>S,还可能同时有从S——>C,所以Server也要有自己的序号!一组的序号是搞不定的!==
从C——>S,S要响应所以填入的是确认序号!
从S——>C,S要请求所以填入的是序号!
正确认识序号
==我们上面说的序号其实是为了方便理解!其实真实的排序方式是这样的!==
==主机A和主机B发送送数据,肯定是要有报头的!报头里面的序号填入的其实是我们**发送的数据的最大的字节数!**就像是我们发送的数据是1000字节!那么确认应答是1001!正确的序号是跳跃式的!这是为了接收到每一个字节!==
16位窗口大小
tcp协议格式中有一个16位窗口大小!这个是什么?
==那么当我们发送报文的时候这个16位窗口大小填的是我们自己的接收缓冲区大小还是对方的接收缓冲区大小呢?——肯定是自己的!因为我怎么知道对方是多少!我们是通过接收对方的报头来知道对方的接收缓冲区的大小!==
==填自己的目的,就是因为我们要构建TCP报文!我们构建的所有TCP报文都是要给对方发送的!这个规则对于客户端和服务端都是同样适用的!这样子就达到了交换接收能力的目的!==、
这样子就可以达成流量控制!
==16位窗口大小就是为了来支撑流量控制的功能!==
TCP标志位/16位紧急指针/如何发送接收带外数据!
首先我们要明白为什么要有TCP标记位
我们的传输的时候数据报文有的是正常的数据段,有的是确认报文!——由此我们可以得出一个概念==TCP报文也是有类型的!==
==我们知道TCP是面向连接的!——所以就要有三次握手和四次挥手!然后才会进行各种IO通信!==
对于服务器来说,一般面对的都不止一个客户端!而每一个客户端都会发过来不同的请求!——例如:可能和一个客户端读取数据,同时又和另一个客户端close连接等等,==这就说明服务端一定会收到各种类型数据报文!==
就好比一个餐厅店老板,来的客户可能有的就是这里点餐吃饭,有的可能打包带走,有的也可能是外卖小哥来这里拿外卖!这个老板处理来自不同类别的人的各种各样的请求!
==服务器会接收各种不同类型TCP报文!——然后要根据这些不同类型的报文要执行不同高达动作!==
还是上面的例子:如果来的客户是点餐店里吃饭!那么老板就要给这位客户准备餐盘,装备筷子然后把食物放上去,如果是打包的客户,那么老板就要准备一次性饭盒一次性筷子,然后把饭菜都封装好,如何是外卖小哥,那么老板就要问外卖小哥的单号是多少,然后告诉外卖小哥东西在哪里!——==针对不同类型的人,动作都是不一样的!==
==同理如果今天服务器接收到一个建立连接请求的报文,那么服务器要做的根本不是读取数据,或者四次挥手什么的!而是要发起连接方进入三次握手的流程!如果对方是进行常规的数据发送!那么这边就要将其放入接收缓冲区中!如果是断开连接的报文!那么就要进入四次挥手!而不是进行读数据,或者三次挥手!==
==为了区分不同的类型的报文!于是有了标记位!==
==ACK/FIN/SYN标志位==
==PSH标记位==
PSH——就是PUSH
这个标记位有什么作用呢?
我们上面说TCP是有接收缓冲区的!
客户端,将数据发送,然后服务端这边接收然后放在接收缓冲区里面!然后与文件关联!上层应用进行读取!一一这是正常情况下!
但是如果在上层非常忙碌的情况下呢?一一上层很长时间都无法从缓冲区里面将数据读取走!缓冲区里面的数据会越来越多!空间也会越来越少!
那么如果这时候客户端将服务端的接收缓冲区打满了!例如:一个最简单的情况!客户端一直发送数据!但是服务端不调用rad函数读取!虽然我们底层已经接收到了!这样子接收缓冲区就会被打满了!
如果接收缓冲区被打满!那么客户端就不能再发送了!因为接收缓冲区就已经被写满了!但是客户端只能一直等待吗?——如果上层一直不将数据拿走!
不可以!客户端也不会一直等!等到一定时间后!发送方就会发送一个询问报文!(就是一个TCP但是不携带数据!)然后服务端就会ACK,告知客户端16位窗口大小,进行轮询!否则万一有空间了!还一直等待也不是事但是如果一直返回的是16位窗口大小都是0!那么客户端就会在发送一次报文!但是这一次会将PSH标记位置为1这就是在催促服务端,赶快将上层服务处理好!
==PSH的作用就是催促接收方,赶快读走接收缓冲区的数据!腾出空间!那么什么是催促?怎么催促?一一又不像我们现实中,可以直接上人家门去像是我们使用read和recv的时候,如果缓冲区没有数据,就会阻塞!==
==而PSH就是尽快让read和recv的读取条件满足!==
==URG标志位与16位紧急指针==
客户端给服务器一次性发送了很多数据(报文),那么服务器也要对这些数据进行应答!
所以服务器也发回了很多的应答(报文)但是这里就有一个问题,假如客户端发送了1,2,3,4的序号的报文,但是到达的时候报文却不一定是按顺序到达达!数据对于接收方,乱序本身就是不可靠的表现!
为了保证数据的可靠性,==TCP接收方就一定要对收到的数据进行排序==!保证数据的按序达到!
那么我们要依靠什么排序呢?一如何知道数据段和如何端直接的先后顺序呢?==报文是有序号的!我们只要按照序号进行排序即可!==
这样子我们就可以保证上层肯定是先到达的先取走!——所以我们也可以看出来接收缓冲区的本质就是一个队列!
因为数据是按序到达的!那么这就产生了一个问题!==如果我们的数据想要插队呢?==
因为总有一些数据是高优先级数据需要被紧急处理!
==为了满足这个需求所以有了一个紧急指针和URG标记位!==
即如果我们的数据里面有需要被尽快读取的数据,那么就将URG置为1,置为1就说明这个数据的数据段里面是有紧急数据的!!
记住==不是所有的数据都是紧急数据==!URG只是表示这些数据里面存在==一些紧急数据!==
**为了找到这些紧急数据在哪里!于是有了16位紧急指针来进行标识一紧急指针的本质是一个偏移量!**不是真的指针!例如:我们我们写紧急指针为20那么就说明从有效数据第20开始是紧急数据!
我们发现了一个问题!我们好像不知道紧急数据的大小啊!我们只知道紧急数据的位置!
难道是从当前位置开始到偏移量结束么?一一不是!
我们TCP协议规定了紧急指针的开始指向的位置**只能读取一个字节!也就是说只有一个字节的大小是紧急数据!**因为正常的数据都是要排队的!但是URG的数据却没有!所以我们一般将其称之为==带外数据!==
但是一般情况下(99.999.%的情况),我们一般是根本用不上这个的带外数据!
这个功能不是TCP回去主动使用的!而是在正常情况下!由上层去主动去检测有没有带外数据!那么怎么检查呢?
我们使用recv函数的时候会发现比read函数一个flag的选项!
==我们可以直接使用MEG_OOB选项从而来直接读取带外数据!==
那么我们怎么发送带外数据呢?——我们可以用send!send也比write多了一个参数选项!
==我们就可以使用MSG_OOB选项来发送带外数据!==
那么什么情况下会用到带外数据呢?
我们写了一个应用层服务!这个服务的作用非常简单!有人给它发送数据,那么它就响应!
==但是有时候我们想要知道这个服务挂掉了没有!——我们会使用轮询!==
服务器会首先检查OOB,如果没有那么就正常读取!
但是如果这个时候服务器已经爆满了!我们发起轮询,那么如果按部就班的读取上层读取到就可能已经花了很多时间了!
但是如果我们以带外数据的方式来进行发送!因为服务器会以OOB的方式进行读取!当发现带外数据后,就会直接读取!——因为只有一个字节!我们就设定1为询问对方状态!
然后服务器也可以使用带外数据的方式来发送!我们这边也可以通过读取这一个字节的方式来判断服务器的状态,例如CPU负载很高,内存要满了.....根据不同的数字,来表示不同的状态!
==RST标志位==
REST——reset
==客户端和服务端在通信之前,首先要进行三次握手!==
但是这里有一个问题,三次握手一定能保证握手成功么?——不一定!我们通信的时候对方随时都有可能挂掉!尤其是三次握手的时候最后一次是没有应答的!
同样的四次挥手也是一样的!不能保证成功的!
即便是连接建立成功了!我们通信过程,有没有可能有链接单方面出问题了呢?——有!例如:服务器和客户端正在通信!有人把服务端的电源拔了,那么这个服务器就没有时间进行四次挥手
假设我们今天就将服务器拔电重启!服务是意识不到曾经还有连接的!==可是客户端不知道服务器曾经重启过!不知道!因为服务器又没有发起四次挥手!==
==这种客户端认为连接还存在,而服务器认为连接不存在!——那么此时就会出现客户端直接发报文!但是这时候服务器觉得很奇怪,因为服务器认为双方是没有建立连接的!得先握手!——那么服务器就会响应一个报文!这个报文里面就含有RST标志位!==
==这个标志位就是告诉客户端!不要发送了!我们连接出异常了!关闭连接!重新建立连接!——当双方中有一方觉得建立连接不一致的时候,在后续通信时用来处理连接认知不一致的问题!==
这种情况就是发生了RST!有可能是因为,服务器太忙了!导致了我们刚刚建立握手成功后,服务器又使用了某些策略强制清除掉了我们的连接!
导致了连接不一致!