TCP 相关机制
TCP 基本特点:有连接、可靠传输、面向字节流、全双工
- 有连接、面向字节流和全双工都能在前面的代码中体现
- 有连接:必须要先调用
accept
建立联系才能处理 - 面向字节流:会拿到
clientSocket
对象的InputStream
和OutputStream
,再来去读写数据 - 全双工:一个
Socket
对象,既可以读,也可以写
而此处的可靠传输在代码层面感知不到,它是在系统内核完成了这里的工作
TCP
最核心的资质就是“可靠传输”,不能做到 100%
送达,只能尽可能的是数据能到达对方方
- 能感知到对方是否收到
- 如果发现对方没有收到,就要进行重试
后发先至
- 如果数据按照左边的传输次序来的,那就不会有歧义
- 虽然女神先发的“好啊好啊”,后发的“滚”,但是在网络传输中,可能存在“后发先至”,对于我们接收方来说,可能会先收到“滚”,后收到“好啊好啊”
- 此时,歧义就产生了,我将会误以为女神答应我了
出现原因
互联网最初是用来防御核弹打击的,即使是遭受到了核弹打击,但是 A~B 之间的通信路径有很多很多条,不会全军覆没,所以仍然可以确保数据能正常传输
- 在正常传输数据的时候,传输数据包不一定走同一条路线
- 因为是不同的路线,每个数据包传输过程中,遇到的状况也都有差别,最终达到目标的时序就可能存在差异了
举个例子:一个婚车队伍,在触发的时候都是按照顺序,一辆一辆跟着走的,但一到大路上,车队就可能会走散
- 本来跟着头车走,结果遇到了红灯,一旦跟不上,就各凭本事了
- 每辆车,各个导航,可能走的路线都不一样,可能有的车还会堵车
- 最后到达目的地的先后顺序改变的情况就是很常见的
1. 确认应答
后发先至的解决方法,TCP 核心机制,感知对方是否收到,就是要靠对方告诉你一声“收到了”
- 后发先至是客观存在的情况,无法改变
若要解决,可以给传输的数据添加“编号”,通过编号,可以区分出数据的先后顺序
- 我收到的应答报文,即使顺序出现错乱,也能识别出来原意
- 由于
TCP
是面向字节流的,是加上这里的编号并非是按照“第一条,第二条”这样的方式来编排的,而是按照“字节“,“第一个字节,第一百个字节”,这样来编排的 - 每个字节都有一个独立的编号,字节和字节之间,编号是连续的、递增的
- 这种按照字节编号的机制,就成为“
TCP
的序号”,在应答报文中,针对之前收到的数据进行对应的编号,称为“TCP
的确认序号”
之后,TCP 就可以针对接收方收到的信息,进行重新排序,确保应用程序 read 到的数据一定是和发送方的数据顺序是一致的
接收方这边调用 read 的时候如果没有数据,就会阻塞等待(前面回显服务器文章中写的是 scanner 读取,本质上就是调用 InputStream.read
)
- 接收方收到的数据信息顺序可能和发送发传输时的顺序不一样
- 此时接收方收到
1001-2000
这个数据到了,但是接收方不会让read
接触阻塞,因为这个数据的起始部分还没到,2001-3000
到了之后也进行阻塞 - 直到
1-1000
这个数据到达之后,read
才会接触阻塞,才会读取到1-1000
,1001-2000
,2001-3000
- 接收方这边,操作系统内核里面,有一段空间,作为“接收缓冲区”,收到的数据就会先在缓冲区中排队等待,直到开头的数据到了,应用程序才能真正读取到里面的数据
前面接亲的例子:等婚车到了女方门口,因为头车还没来,所以不能直接开到新娘门口去接人
- 因此这样的车就得在外等待,必须等头车到
- 等车到了之后,再重新排好队,再一起开到新娘家门口
丢包
丢包的原因有很多种:
- 数据传输的过程中,发生了 bit 翻转,收到这个数据的接收方/中间的路由器什么的,计算校验和,但是发现校验和对不上
发现错误,要及时止损,不能将错就错
- 所以就会把这个数据丢弃掉,不继续往后转发/不交给应用层使用
- 数据传输到某个节点(路由器/交换机),但这个节点的负载太高了,后续传输过来的数据就可能被这个路由器直接丢弃掉
- 负载太高:某个路由器,单位时间只能转发
N
个包,但现在是网络高峰期,这个路由器单位时间需要转发的包超过N
个了,发不过来了
2. 超时重传
发生丢包是完全随机,不可预测的,TCP
再怎么厉害,也不可能避免数据发生丢包。TCP
能做的是:感知到数据是否丢失,如果丢包,就重新再发一次
此时需要通过应答报文来区分
- 收到应答报文,说明数据没丢包
- 没收到应答报文,就说明数据丢包了
- 网络传输是需要消耗时间的,这里的“没收到”是暂时没收到,还是永远都收不到?
- 发送方发送数据之后,会给出一个“超时时间”,如果在这个时间限制之内,没有收到反馈的 ACK(应答报文 ACK 由 0 变为 1),就视为数据丢包了
不管是因为数据丢了导致的丢包还是因为 ACK 丢了导致的,都会重发
- 但如果是因为 ACK 丢了导致的重发,接收方就会收到两份一样的数据,这样是很不好的(一次扣款请求扣两次)
- 所以为了确保应用程序调用
read
读出来的数据是唯一、不重复的,TCP 就会对这种情况进行处理——去重
- 接收方有一个“接收缓冲区”,收到的数据会先进入到缓冲区里,后续再督导数据,就会根据序号,在缓冲区中找到对应的位置(排序)。如果发现当前序号 1-1000 这个数据已经在缓冲区中存在了,就会直接把新收到的这个数据丢弃掉
超时时间的设定
这里的时间不是固定不动的,而是动态变化的
发送方第一次重传,超时时间是 t1
,如果重传之后,仍然没有 ACK
,就会继续重传,第二次重传的超时时间是 t2
,t2>t1
- 每多重传一次,超时时间的间隔就会变大,重传的频次会降低
- 经过一次重传之后,就能让数据到达的概率提升很多
- 反之,如果重传了几次,都没有顺利到达,说明网络的丢包率已经达到了一个非常高的程度——>网络发生了严重故障,大概率没法继续使用了
重传也不会无休止的进行,当重传达到一定次数之后,TCP
不会再重传,就认为这个连接已经挂了 - 先尝试进行“重置/复位连接”,发送一个特殊的数据包“复位报文”,尝试和对方重新进行连接
- 如果网络这会恢复了,复位报文就会重置连接,使通信可以继续进行
- 如果网络还有严重问题,复位报文也没有得到回应,此时
TCP
就会单方面放弃连接(发送方释放掉之前接收方的相关信息,这个连接诶也就没了)
确认应答和超时重传相互补充,共同构建了 TCP 的“可靠传输机制”
- 可靠传输机制不是靠“三次握手和四次挥手保证的”
TCP 报头
首部长度
TCP
报头的长度
UDP
协议报头固定就是8
个字节- 对于
TCP
来说,报头长度是可变的
4 个比特位可表示的范围: 0000~1111
——>0x0~0xF
——>0~15
- 此处的长度单位是 4 字节,不是字节(所以范围是
0~60
字节)
保留(6 位)
虽然现在不用,但是先把这个东西申请下来,以备不时之需。用于考虑未来的可扩展性
- 充分吸取了
UDP
的教训,UDP
的报文长度字段,是没法扩展的 - 如果未来某一天,
TCP
需要新增属性或者谋和属性的长度不够用,就可以把保留位拿出来,进行使用 TCP
的结构不需要发生太大的改变,这样的升级就会容易很多
关于“可扩展性”也是属于编程的时候需要考虑到的一点,毕竟写的代码不可能写一份就能持续地使用。对代码做出调整,做出修改,是非常普遍、常见的情况
但是,
选项
TCP 报头边长的主要原因。四个字节为一个单位
- 可以有, 也可以没有
- 可有一个,也可有多个
通过“首部长度”确定报头有多长,如果是两个四个字节长度就是两个选项,三个四个字节长度就是三个选项,以此类推
序号
由于会出现“后发先至”的情况,所以需要通过编号,区分出数据的先后顺序
序号:表示的就是 TCP
数据报载荷中的第一个字节的序号,由于序号是连续递增,知道了第一个字节的序号,后续每个字节的序号也就知道了
- 32 位/四字节,表示的范围是
0~42亿9千万
(0~4G
) - 因为
TCP
是面向字节流的,所以一个TCP
数据报和下一个TCP
数据报携带的数据,是可以直接进行拼装的 - 比如要传输一个特别大的数据,传输过程中,本身就会通过多个
TCP
数据报来进行携带,这些TCP
数据报彼此之间携带的载荷都是可以在接受方自动拼起来的
- 这样就不像
UDP
存在传输的上限,使用UDP
传输大数据,就需要考虑调用这一次send
操作,参数是否超过了64KB
,超过了就不行 - 使用
TCP
的话就没关系,可以调用一次write
,也可以调用多次write
。无论怎么进行write
,在网络传输和对端接收的角度来看是没有任何差别的 - 如果多次
write
,传输的总数据量超过上述的4G
也没关系,这里的数据序号是可以再从 0 开始重新设置的
确认序号
确认序号的设定方式,和后发先至中发短信的例子,略有差别
- TCP 序号不是按照“一条两条”来编排的,而是按照“字节”来编排的
TCP 的确认序号这里,填写的是 1001
,接收方收到的数据的最后一个字节序号的下一个序号
- 表示的含义是
<1001
的序号的数据都收到了(TCP
序号是连续增长的) - 对于应答报文来说,“确认序号”就会按照收到的数据的最后一个字节序号
+1
的方式来填写 - 并且六个标志位中,第二个标志位(
ACK
)会设为1
- 普通报文的
ACK
为0
,应答报文的ACK
为1
- 如果是普通报文,序号是有效的,确认序号是无效的;如果是应答报文,序号和确认序号都是有效的
- 应答报文的序号是另一套编号体系,和传输数据的序号是不一样的
- 应答报文默认情况下是不携带数据的