目录
- 协议段格式
- 确认应答(ACK)机制
- 序号
- 标志位
- 超时重传
- 连接管理机制
- 延迟应答
- 捎带应答
- 流量控制
- 滑动窗口
- 拥塞控制
- 面向字节流
- 粘包问题
- 异常情况
- 小结
- 文件和socket的关系
- 基于tcp应用层协议
- udp和tcp对比
- udp实现可靠传输
全称为“传输控制协议(Transmission Control Protocol),可以对数据的传输详细的控制
在应用层定义的缓冲区是用户级缓冲区,每创建一个连接,tcp创建发送缓冲区和接收缓冲区,保证可以在发送的同时接收,做到全双工。应用层只需要将需要发送的数据交付给传输层,至于数据什么时候发送?发多少?出错怎么办?都由tcp自主决定。发送数据其实和read,write文件io操作一样,将数据拷贝到缓冲区内
tcp是面向连接的、可靠的、基于字节流,有接收和发送缓冲区全双工的通信,可以对数据控制的传输层通信协议
1. 协议段格式
-
源/目的端口号:表示数据从哪个进程来,到哪个进程去
-
32位序号/32位确认号
-
4位tcp报头长度:表示该tcp头部有多少个32bit(有多少4字节),所以头部最大长度15*4=60
-
16位窗口大小:暂时省略
-
16位校验和:发送端填充,CRC校验,接收端校验不通过,则认为数据有问题,校验和不光包含tcp首部,也包含tcp数据部分
-
16位紧急指针:标识哪部分数据时紧急数据
-
40字节头部选项:暂时忽略
报头和有效载荷分离,采取固定长度+自描述字段的方式,4位的首部长度的单位是4字节,所以范围是0-60,表示整个报头的长度,除过选项固定长度是20字节,减去就得到了选项的长度
双方在发送数据的过程中,即使没有内容至少有完整的tcp报头,也就是一个完整的报文
tcp保证可靠性的一个重要的策略就是确认应答机制
2. 确认应答(ACK)机制
当发出一个报文后,不知道对方有没有收到,一个方法就是对方收到发一个答复,就像写信一样。每发出一个报文,就得到一个应答,就是ack
图中发送是斜线,是因为数据的发送接收需要经过网络,纵轴是时间,有一定的时间差
每一个ack都带有对应的确认序列号,意思是告诉发送者,已经收到了哪些数据,下一次从哪里开始发
不存在百分百可靠的网络协议,收到应答表示最近的一条数据对方收到了,但这条应答有没有被收到,是不能知晓的。没有应答的数据无法保证可靠性,所以无法保证百分百可靠,因为总有最新的一条是没有应答的
但在局部上可以可靠。在实际通信中,双方关注的是数据有没有收到,当客户端发送的tcp数据得到应答,可以保证从客户端到服务端通信的可靠性,而不需要对应答做应答。同理,服务端到客户端的数据可靠,就保证了tcp双方通信的可靠性
客户端怎么知道有没有收到应答?
客户端收到应答就知道自己发送的数据对方收到了,如果没收到就认为数据丢失了。客户端用什么方法确认有没有收到应答,如果数据丢失不能一直等下去。所以客户端会维持一段时间定时任务,如果没有收到应答就认为数据丢失,重发(下面内容)。
3. 序号
发一个数据收到一个应答,只有在收到应答后再发送下一个数据,这样的效率是低下的。实际通信过程中都是一次性发送一批报文,只要这些报文每一个都有应答,也可以保证可靠性。但数据是有顺序的,在发送到对方的过程中,由于网络情况,路由线路等原因造成了乱序收到,就不能保证数据可靠性了,怎么解决这种情况
32位序号
只需要对每一个数据添加序号,收到后按序号排序。这就是首部32位序号的作用之一,具体怎么填?
在缓冲区中,数据都是按顺序存放的,在数组中天然每个字节都有自己的编号,就是数组下标。每一个报文的序号是数据块最后一个字符的下标
确认序号
客户端发送了一批报文,收到ack怎么知道是对哪个报文的应答。这就要用到确认序号,确认序号填充的是收到的序号+1
确认序号的意义:表示确认序号之前的数据,已经全部收到了,下一次发送,从确认序号指定的数字开始
可以只用序号吗?ack只针对序号+1
服务端在捎带应答的时候,可能也会携带自己的数据,就需要填充序号,同时也是对客户端的ack,需要填充确认序号
4. 标志位
通信双方在最开始建立连接的时候需要三次握手,其实就是互相发送数据,接收方怎么分辨是建立连接的报文还是正常数据?
报文是有类型的,不同的类型决定要做不同的动作,标志位存在的意义就是区分不同的报文类型
- 6位标识位:
URG:紧急指针是否有效。
数据都是正常排队处理的,如果想优先处理某个数据,就需要URG,表示16位紧急指针有效,里面记录的是紧急数据的偏移量,每个报文只能有1个字节。想要读取紧急数据,send和recv可以设置MSG-OOB,带外数据选项。当一个服务器响应比较慢时,可以设置处理紧急数据,当长时间得不到回复,发紧急数据可以询问服务器目前处于什么状态,什么原因,返回状态编号得知情况
ACK:确认号是否有效。
标记确认属性是否有效,应答须将这个设置为1。一般都会携带这个标志
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走。
是客户和服务生产消费的同步过程,服务端长时间不将数据读走,缓冲区满了后流量控制,客户端就不会再发送,如果客户端缓冲区也满后就会写阻塞。这个标志位的报文提醒接收方尽快交付数据腾出空间,同时存在定期询问和服务端有空间后通知的两种协商方式,哪种先达成就采取哪种。也存在客户端缓冲区没满为了效率携带psh
RST:对方要求重新建立连接,把鞋带RST标识的称为复位报文段。
tcp虽然是可靠的,但是允许连接建立失败,这个标志位用于连接异常的时候重新建立连接。已建立的连接有很多,需要统一管理,正常通信服务器可能崩溃重启,这些连接就会清空。比如连接建立失败的情况,客户端发出ack答复后就会认为连接已经建立成功,而服务端只有收到ack才会认为建立成功,如果ack丢失,就会存在双方不一致的情况,客户端正常发送数据,服务端收到数据后发现并没有成功建立连接,这时就会携带这个标志,要求重新建立连接
SYN:请求建立连接,把鞋带SYN标识称为同步报文段。
三次握手的建立连接前两个报文会携带
FIN:通知对方,本端要关闭了,我们鞋带FIN标识为结束报文段。
四次挥手会携带
标记位是os设置的,提供了一些函数,例如close关闭连接就会设置FIN标志
联想生活中打电话约朋友吃饭,“去吃饭吗?” 答:“好啊,去哪吃饭。”,而不是答:“好啊”,再问:“去哪吃饭?”。客户端给服务端发送数据需要应答,服务端刚好也需要给客户端发送数据,怎么做
5. 超时重传
超时的两种情况
主机A发送数据给B后,可能因为网络拥堵等原因,无法到达B,如果A在一个特定的时间间隔内没有收到B的确认应答,就会重发
主机A未收到B发来的确认应答,也可能是因为ACK丢失了,但主机A可能认为数据丢失
这两种情况认为数据丢失,需要进行补发,怎么确认数据是丢失了?
超时时间
利用超时时间确定数据丢失,超时时间的确认方式
- 最理想的情况下,找到一个最小的时间,保证“确认应答一定能在这个时间内返回”
- 这个时间的长短,随着网络环境的不同,有差异
- 如果超时时间太长,会影响整体的重传效率
- 如果太短,有可能频繁的发送重复的包
为了保证无论任何环境下都能搞笑的通信,因此会动态计算这个最大超时时间
- linux中(BSD Unix和Windows也是如此)超时以500ms为一个单位控制,每次判定差事重发都是按500ms的整数倍
- 如果重发一次仍然得不到响应,等待2*500ms后再次重发
- 如果仍然没有应答,等待4*500ms重传,以此类推,指数倍递增
- 累积到一定重传次数,认为网络或者对端主机出现异常,强制关闭连接
因此主机B可能会受到很多重复数据,需要把重复的包丢弃掉,这时候就可以利用前面提到的序列号,做到去重的效果
6. 连接管理机制
正常情况下,tcp需要三次握手建立连接,四次挥手断开连接
为什么需要三次握手
一次连接,客户端可能发很多建立连接的请求,服务端都要维护这个连接,会造成SYN洪水
两次连接,服务端再发出ack后建立连接,而这个ack不确认能不能收到,如果一方出问题,维护链接的成本在服务端,服务端需要维护很多设备的连接,所以这种成本应该优先让客户端让步
三次握手,奇数次保证了谁发起连接,成本在谁那,三次是建立有效连接的最短次数,保证了连接失败成本是嫁接在客户端
为什么四次挥手
断开连接意味着没有数据给对方发送了,发送数据是双方都可能发送,所以必须断开2次。客户端发送FIN表示没有数据再发了,进入FIN_WAIT_1状态,ack是管理报文,不属于数据。收到ack后进入FIN_WAIT_2,当服务端也没有数据发时,发送FIN断开请求,客户端收到后TIME_WAIT状态,一会后关闭
过程
connect函数只负责发起握手,其余由os内部完成,成功返回。accept只负责拿出建立了的连接,如果没有就阻塞。注意上面每个阶段状态的情况
三次握手客户端和服务端都至少经历了一次收发的过程,验证了全双工是否通畅。服务端只有最后ack收到时才建立连接
被捆绑了木马病毒的简称肉鸡,可能就会领取到任务同一时间向一个服务器发送大量的连接建立请求,服务器面对这种情况就得设置防火墙
服务端状态变化
[CLOSED->LISTEN] 服务器调用listen后进入LISTEN状态,等待客户端连接
[LISTEN->SYN_RCVD] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并想客户端发送SYN确认报文
[SYN_RCVD->ESTABLISTEND] 服务端一旦收到客户端的确认报文,就进入ESTABLISTEND状态,可以读写数据了
[ESTABLISTEND->CLOSE_WAIT] 当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT
[CLOSE_WAIT->LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据),当服务器真正调用close关闭连接时,会向客户端发送
FIN,此时服务器进入LAST_ACK状态,等待最后一个ack到来(这个ack是客户端确认收到了FIN)
[LAST_ACK->CLOSED] 服务器收到了对FIN的ACK,彻底关闭连接
客户端状态转化
[CLOSED -> SYN_SENT] 客户端调用connect,发送同步报文段
[SYN_SENT ->ESTABLISHED] connect调用成功,则进入ESTABLISHED状态,开始读写数据
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1;
[FIN_WAlT_1-> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段;
[FIN_WAIT_2 ->TIME_WAIT] 客户端收到服务器发来的结束报文段,进TIME_WAIT,并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态.
验证
服务器只停在listen后的阶段。启动服务器后,处于listen状态,就可以查到了
客户端连接后变为了established,连接建立成功和accept没关系
listen的第二个参数backlog
将backlog改为0,表示允许的最大连接数量,建立第二个连接查看
状态变为了SYN_RECV,而客户端已经建立成功,当关闭一个连接时,就会变为建立成功
在os中,对于每个连接都要维护起来,一个连接指向下一个,accept就是在这个队列里取一个连接。listen的第二个参数backlog+1表示底层已经建立好的连接队列的最大长度,这个队列叫全连接队列。从一个状态变到下一个状态必须满足下一个状态的条件,对于SYN_RECV状态的连接,也要维护起来,这个队列称为半连接队列,不会长时间维护syn_recv,过一段时间就会删除。如果恶意大量发送建立不成功的连接,就会导致半连接队列挤满,这时正常连接也无法成功,这就是真正的SYN洪水
全连接队列为什么不能太长?为什么不能没有
全连接队列需要维护成本,当服务器处于非常繁忙的状态还要维护很长的全连接队列会降低效率。如果没有当服务器空闲出来不能及时补上连接,资源得不到充分利用
状态变迁汇总
实现表示客户端的变化,虚线表示服务端的变化,CLOSED是一个假想的起始点,不是真实状态
理解TIME_WAIT状态
做测试,首先启动双端,5秒后服务端关闭连接,此时server进入FIN_WAIT_2状态
当客户端也关闭连接,进入TIME_WAIT状态
主动断开连接的一方,在4次回收完成后会进入time_wait状态,等待若干时长,自动释放
-
tpc规定,主动关闭连接的一方要处于time_wait状态,等待两个MSL(maximum segment lifetime)最大存在时长,后才能回到CLOSED状态。
之所以等待,1.让通信双方的历史数据得以消散。收到数据后丢弃,不影响下次通信。 2.断开连接,4次挥手,有较好的容错性。如果另一端第2次挥手的ack丢失,还可以对其补发,可以正常退出 -
使用crtl+c终止了server,所以数主动关闭连接的一方,在time_wait期间仍然不能再次监听同样的server端口
-
MSL在RFC1122中规定为两分钟,但各操作系统的实现不同,在Centos7默认配置的值是60s,可以通过
cat /proc/sys/net/ipv4/tcp_fin_timeout
查看msl值 -
规定time_wait 的时间请参考UNP2.7节
想一想,为什么是2MSL? -
MSL是tcp报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话就能保证两个传输方向上的尚未被接收货迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)
-
同样也是理论上保证最后一个报文可靠到达(假设最后一个ack丢失,那么服务器会再重发一个FIN,这时虽然客户端的进程不在了,但是tcp连接还在,可以重发LAST_ACK)
解决time_wait状态引起的bind失败的方法
处于这种状态,服务端并未完全关闭,再次重启服务就会出现绑定错误,因为ip端口还在被占用
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒有很大数量的客户端请求)
- 这个时候如果由服务器主动关闭连接(比如某些客户端不活跃,就需要被服务器主动清理掉),会产生大量的time_wait连接
- 由于请求量很大,就肯能导致time_wait连接数很多,每个连接都占用一个通信五元组,其中服务器的ip和端口是固定,如果新来的客户端连接的ip和端口号和time_wait占用的连接重复了,就会出现问题
客户端因为使用的是随机端口,一般不会出现此问题
可以使用setsockopt函数设置套接字属性,让可以立即重启
第二个参数表示设置到套接字这一层,第三个是设置的选项,可以支持新老版本
setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
这时,虽然断开后仍是time_wait状态,但可以立即重启,复用这个ip和端口
理解CLOSE_WAIT状态
将socket的close删除测试
对于大量的close_wait状态,原因是服务器没有正确关闭socket,导致四次挥手没有正确完成,这是一个bug,只需要加上对应的close即可解决
7. 延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小
- 假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K
- 但实际上可能处理端处理的速度很快,10ms内就把500K的数据从缓冲区消费掉了
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来
- 如果接收端稍微等一会再应答,比如等待200ms再应答,这个时候返回的窗口大小就是1M
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
那么所有的包都可以延迟应答吗?肯定不是
- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间久应答一次
具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms
8. 捎带应答
在延迟应答的基础上,很多情况下,客户端在应用层也是“一发一收”,意味着服务端在回复时可以顺带对上一条ack,一起回给客户端
9. 流量控制
接收端处理数据的速度是有限的,如果发的太快,导致接收的缓冲区被打满,这个时候如果发送端继续发送,继而引起丢包重传等一系列连锁反应
因此tcp根据接收端的处理能力,来决定发送端的发送速度,这个机制叫做流量控制(Flow Control)
流量控制的依据是什么?怎么控制?
- 接收端将自己可以接收的剩余缓冲区大小放入tcp报头的“窗口大小”字段,通过ACK端通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知发送端
- 发送端接收到这个窗口之后,就会减慢自己的发送速度
- 如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
接收端如何把窗口大小告诉发送端,tcp首部有一个16位窗口大小字段,存放了窗口大小信息。那么tcp窗口最大是65535字节吗?
实际上,tcp首部选项里还有一个窗口扩大因子M,实际窗口大小就是窗口字段的值左移M位
怎么保证第一次发送数据量是合理的?
三次握手双方也交换了报文,已经协商了双方的接受能力,第三次的时候,可以携带数据
流量控制既属于可靠性,也属于效率。流量控制防止正常丢包,也不会有过多的重传,发送效率也会提升。可靠性为主,效率为辅
10. 滑动窗口
确认应答中了解到数据并不是一个一个发,是一次性发送多条数据,可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
对于已经发出去,但暂时没收到应答的报文,要被tcp暂时保存起来,已经发出去,暂时没有应答可能存在多个。保存在滑动窗口里
关注的问题
滑动窗口在哪里?
是发送缓冲区的一部分
滑动窗口的范围大小?
是对方的接收窗口(目前),这样既可以保证直接发,又不会超出对方的接收能力
如何理解区域划分?
通过指针/下标来进行区分
发送缓冲区分为三个区域,已发送已确认,和未确认和未发送。中间的就是滑动窗口,窗口的大小根据对方的接收能力,维护它的范围用两个指针移动。网卡不支持一次性发很大的包,所以须分批发送
- 窗口大小指无序等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个字段)
- 发送前四个段的时候,不需要等待任何ACK,直接发送
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,以此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉
- 窗口越大,网路吞吐率就越高
滑动情况
滑动窗口只能往右滑,不能往左滑。上图发出2001的报文也收到了,滑动窗口的左侧就滑动到2001
如果丢包了怎么办?
首先明确确认序号的含义:确认序号是x,表示x之前的报文全部收到了
情况1:数据包递达,ack丢失
虽然1001和2001的ack丢失,但4001收到了,根据确认序号的含义,滑动窗口左侧可以直接划到4001,左边的都已经发送收到答复。
确认序号的存在,允许少量的ack丢失,不影响数据的收发
情况2:数据包直接丢了
当1001报文丢失后,发送端会一直收到1001这样的ack,像是在提醒“想要的是1001”,再收到2001,3000等滑动窗口左边只会移动到1001,不会越过丢失的报文,保证了线性的连续向后更新,不会出现跳跃的情况
如果发送端连续三次收到了同样一个"1001",这样的应答,就会将对应数据1001-2000重新发送。
这个时候接收端收到了10001后,再次返回的ack就成了4001,因为2001-4000的数据已经收到了,被放到了接收端os内核的接收缓冲区内,这种快速重发控制也叫“快重传”
快重传是有条件的,需要收到3个同样的序号。超市重传作为兜底
滑动窗口的变化
可以用start和end表示滑动窗口的两边,范围是动态变化的。如果右不变,左移动,表明收到应答但对方再没处理数据,范围缩小。左右都移动,这时候可能变大、变小和不变。如果对方一直不处理数据,滑动窗口大小就会变为0
流量控制就是通过滑动窗口实现的
start=确认序号
end=确认序号+win大小(min (对方窗口大小, 有效数据) )
滑动窗口的界限
滑动窗口只能往右移动,那有没有可能越界呢?
滑动窗口采用的是环状的算法,不会越界
起始序号
time_wait状态可以尽量保证数据消散,但仍有可能数据在网络中滞留,当下次再连接的时候,新发的数据和老数据的序号一样就会被顶掉。
所以tcp每次开始的序号都是随机的,发送的数据就是随机值+数组下标,收到数据后下次发送的数组下标就等于确认序号-随机值,算出下次发送数据在缓冲区的位置。这样,就能让冲突的可能变得很小
11. 拥塞控制
如果发送数据出问题不是双方主机的问题,也可能是网络的问题。出现少量丢包是常规情况,出现大量丢包就需要考虑是网络出了问题,硬件设备问题无法直接解决,也无法直接对网络做修复,但双方可以采取策略。如果是数据量太大引起的阻塞,导致大量的数据超时,通信双方大量数据丢失,tcp就会判断网络出了问题,这时候不能对报文超时重发,这样会加剧网络的拥塞
在很多主机访问服务器的场景中,可以让识别出网路问题的设备先等等或少量发送,而没有识别出来的正常发送,如果网络情况很糟糕,识别出的主机就会越来越多,这样网络的带宽就会随着主机减少发送好起来,这就是一种博概率和自适应的方法,实现了多主机面对网络拥塞的共识,有效解决网络状况
慢启动
虽然tcp有了滑动窗口这个大杀器,能够高效可靠的发送大量数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前的网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的
慢启动,先发送少量的数据,探探路,摸清当前的网络拥堵状况,再决定按照多大的速度传输数据
此处引入一个概念叫拥塞窗口,发送开始的时候,定义为1,每次收到一个ack应答,拥塞窗口+1,每次发送数据,将拥塞窗口和接收端反馈的窗口大小作比较,取较小的值作为实际发送的窗口
上面的滑动窗口end的计算现在可以再次更新,end=确认序号+min(win,有效数据,拥塞窗口)
通过滑动窗口,接收窗口,拥塞窗口结合,拥塞窗口是主机判断网络健康程度的指标,超过拥塞窗口引发拥塞,先发少量的试探,前期慢,增长速度快,网络趋于健康后,尽快恢复正常通信
像上面这样的拥塞窗口增长速度,是指数级的“慢启动”,只是初始时慢,但增长速度很快
为了不增长这么快,因此不能使拥塞窗口单纯的加倍,引入一个叫慢启动的阈值,超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
- 当tcp开始启动的时候,慢启动阈值等于窗口最大值
- 每次超时重发的时候,慢启动阈值会变为原来的一半,同时拥塞窗口置回1
少量的丢包,仅仅是出发超时重传,大量的丢包,认为网络拥塞
tcp开始通信后,网络吞吐量主机上升,随着网络拥堵,吞吐量立刻下降
拥塞控制,归根到底是tcp协议想尽快把数据传输给对方,又要避免给网络造成太大压力的这种方案
慢启动的阈值:最近一次发生拥塞时,拥塞窗口大小/2
上面的慢启动机制只有在引发网络拥塞后才会生效,发送数据的量同时还会考虑对方的接收能力,如果拥塞窗口超过接收能力,以接受能力为准
12. 面向字节流
创建一个tcp的socket,同时在内核创建一个发送缓冲区和接收缓冲区
- 调用wirte时,数据会先写入发送缓冲区,如果字节数太长,会被拆分成多个tcp的数据包发出。如果太短,就先在缓冲区等待,长度差不多了或者其他合适时机发送
- 接受数据的时候,也是从网卡驱动程序到达内核的接收缓冲区,应用程序调用read从接收缓冲区拿数据
- 既有接收缓冲区,也有发送缓冲区,既可以读也可以写,叫全双工
由于缓冲区的存在,读和写不需要一一匹配,例如:
- 写100个字节数据,可以调用一次write写100个,也可以调用100次wrtie,每次写一个字节
- 读100个字节数据时,也完全不需要考虑写的时候怎么写的,既可以一次read100和字节,也可以一次read一个字节,重复100次
在发送的时候,只认识多个字节数据,不在乎是不是完整的请求。将多个字节数据发送到对方缓冲区,tcp不关心上层协议,不关心上层报文格式,只有字节的概念。一次取多少由用户决定,有可能取到多个报文,,也有可能是半个,应用层需要一个个处理,将字节流变为一个一个的请求
所以tcp报头不需要记录长度
13. 粘包问题
面向字节流,如果上层读到一个半,将剩下半个构不成一个报文的直接丢弃,就造成了粘包问题
首先要明确,粘包问题中的“包”,指的是应用层的数据包
在tcp的协议头中,没有如同udp一样的“报文长度”这样的字段,但是有一个序号这样的字段
站在传输层的角度上,tcp是一个一个保温过来的,按照序号排序放在缓冲区中
站在应用层的角度,看到的只是一串连续的字节数据
那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包
那么如何避免粘包问题呢,就是一句话,明确两个包的边界
1.定长报文。对于定长的包,保证每次都按固定大小读即可,例如上面的Request结构,是固定大小的,就从缓冲区从头开始按sizeof(request)依次读取即可
2.使用特殊字符。还可以在饱和包之间使用明确的分隔符(应用层协议,只要保证分隔符不和正文冲突即可)
3.使用自描述字段+定长报头。对于变长的包,可以在报头的位置,约定一个包长度的字符,就知道了结束为止
4.使用自描述字段+特殊字符
思考:对于udp有没有粘包问题?
对于udp来说,如果还没有上层交付数据,报文长度仍然在,udp是一个一个吧数据交付给应用层,就有很明确的数据边界
站在应用层的角度,使用udp,要么收到完整的,要么不收,不会出现半个
14. 异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
机器重启:和进程终止的情况相同。
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,发现连接已经不在,就会进行reset,即使美誉写入操作,tcp也内置了保活定时器,会定期询问对方是否还在,如果不在,会把连接释放
另外,应用层的某些协议,也有一些这样的检测机制,例如HTTP长连接中,也会定期检测对方的状态,例如qq,断线之后会定期尝试重连
15. 小结
tcp为什么这么复杂,要保证可靠性,又尽可能提高性能
可靠性:
- 校验和
- 序列号(按序到达,去重)
- 确认应答(核心)
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
其他:
- 定时器(超市重传,保活,time_wait定时器等)
三次握手:1.建立连接 2.协商起始序号 3.协商双方的接收缓冲区大小
16. 文件和socket的关系
进程的tcb结构里保存了文件数组,文件结构里有文件的操作方法,如果是网络,指向的就是网络相关的。还有一个指针,会指向socket结构
ops是socket的一些方法,上面的file又会指回file机构,双向连接
上面的sock结构里有接收和发送缓冲区
sk_buff_head是一个双链表
sk_buff用四个指针维护空间,封包解包只需要移动指针,往下层交付同样的结构后也只需要移动四个指针
在创建socket的时候会参数会传入套接字类型,udp和tcp会将sock结构转为不同的类型
协议栈本质就是1.特定数据结构表述的协议 2.和特定协议匹配的方法集
维护链接是需要成本的,内部会创建这些数据结构
17. 基于tcp应用层协议
HTTP
HTTPS
SSH
Telnet
FTP
SMTP
也包括其他自定义的应用层协议
18. tcp和udp对比
tcp是可靠连接,那么是不是tcp就一定优先于udp?tcp和udp的优点和缺点,不能简单绝对的比较
- tcp用于可靠传输的情况,应用于文件传输,重要状态更新等场景
- udp用于对告诉传输和实时性要求较高的通信领域,例如:早期的qq,视频传输等,另外udp可以用于广播
归根到底,tcp和udp都是工具,什么时机用,具体怎么用,还是要根据具体的场景去判断
19. udp实现可靠传输
先明确使用场景,然后往tcp靠,应用层实现类似逻辑
- 引入序列号,保证顺序
- 确认应答,超市重传等
- …