1.TCP基本认识
2.TCP连接建立
3.TCP连接断开
4.socket编程
TCP头部格式
序列号:在建立连接时,由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次,就累加一次该数据字节的大小。 (用来解决网络包乱序问题)
问题:初始序列号为什么要随机初始化呢?
答:保证网络安全 ,如果初始序列号不是以随机的方式产生,那么攻击者则很容易获取到你与其他主机之间通信的初始序列号,并且伪造出序列号进行攻击。(常见的网络攻击手段)
确认应答号: 指的是 下一次期望收到的数据的序列号 ,发送端在收到这个确认应答之后可以认为在这个序号以前的数据都已经被正常接收。(用来解决不丢包的问题)
控制位:
为什么需要TCP协议?TCP协议工作在哪一层?
IP 层是 【不可靠的】,它不保证网络包的按序交付、也不保证网络包中数据的完整性。
如图可看见 OSI七层模型和 TCP/IP 分层模型的关系
IP协议位于:网络层
TCP协议位于: 传输层
如果需要保障传输网络数据包的可靠性,那么就需要上层传输层的TCP协议来负责。
因为 TCP 是一个工作在 传输层 的可靠数据传输的服务,它能确保接收端接收的网络数据包是 无损坏 、无间隔、非冗余和按序的。
什么是TCP?
TCP 是 面向连接的,可靠的,基于字节流的传输层通信协议。
什么是TCP连接?
连接: 用于保证 可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小被称为连接。
所以,建立一个TCP连接是需要 客户端和服务器端 达成这三个信息的共识。
如何唯一确定一个TCP连接?
TCP四元组 可以确定唯一的 确定一个连接,四元组包括:
源地址 和 目的地址 的字段(32位)是在 IP 头部中,作用是通过 IP协议发送报文给对方主机。
源端口 和 目的端口 的字段(16位) 是在 TCP 头部中,作用是 告诉 TCP 协议应该把报文发给哪个进程。
有一个 IP 的服务器监听了一个端口, 它的TCP的最大连接数是多少?
服务器 通常固定在本地某个端口上监听,等待客户端的连接请求。
因此,客户端的 端口 和 IP 是可变的,其理论值计算公式如下:
-IPv4来说, 客户端的 IP 数最多为 2^32 , 客户端的 端口数 最多为 2^16,所以服务器单机最大 TCP 连接数 为 2^ 48。
但是,服务端最大并发 TCP 连接数远不能 达到理论上限:
TCP 和 UDP 有什么区别呢? 分别的适用场景是?
UDP 不提供复杂的通信机制,依靠 IP协议 提供面向【无连接】的通信服务。
先来看看UDP的头部:
TCP 和 UDP 的区别
1.连接
2.服务对象
3.可靠性
4.拥塞控制和流量控制
5.首部开销
TCP 和 UDP 的 适用场景
1.由于TCP是 面向连接的,能保证数据的可靠性,所以经常用于:
2.由于UDP 是面向无连接的,可以随时发送数据,且UDP本身处理简单又高效,经常用于:
为什么TCP头部有【首部长度】字段,而 UDP头部 没有呢
原因:TCP头部长度可变, 有 可变长的【选项】字段;而UDP 头部长度是不会变的,无需多一个字段去记录UDP的首部长度。
为什么UDP 头部有【包长度】字段,而 TCP 没有呢?
先看一下:TCP如何计算负载数据长度:
IP总长度 和 IP 首部长度 在IP首部格式是已知的,TCP 首部长度 则是在TCP首部格式 是已知的。以此可得 TCP数据长度。
而:
为了网络设备硬件设计和处理方案,首部长度需要是 4 字节的 整数倍。
如果去掉UDP【包长度】字段的话,那么UDP首部长度就不是 4 字节的整数倍了。
TCP三次握手过程 及 状态变迁
TCP 是面向连接的协议,所以在使用TCP传输数据前,必须在先建立连接,而连接就是依靠 三次握手 而进行的:
客户端会随机初始化序号(client_isn),将此序列号 置于 TCP的 首部的 【序列号】字段中,并且将 SYN 标志位置 置为 1,表示SYN 报文。 接着把 第一个 SYN 报文段发送给 服务端,表示向 服务端 发起连接请求, 该报文不包含 应用层数据,之后客户端处于一个 SYN_SENT 状态。
服务端 收到客户端的 SYN 报文之后,首先服务器也随机初始化自己的 序号(server_isn),将次序号填入TCP首部的【序号】字段中,其次把 TCP首部的【确认应号】字段填入 client_isn +1。接着 把 SYN 和 ACK 的标志位都置为1,最后把该报文段发送给 客户端,该报文也不包含 应用层的数据,之后服务器一直处于 SYN_RCVD 状态。
客户端收到服务器发来的报文后,还要向服务器回应最后一个 应答报文,首先将该应答报文 TCP 首部的 ACK标志置为 1, 其次 【确认应答号】字段填入 server_isn+1,最后把报文发送给 服务端,这次的报文可以携带 客户到服务器的数据,之后客户端处于一个 ESTABLISHED 状态。
从上面过程可看出: 前两次握手是不可以携带数据的,第三次握手是可以携带数据的。
一旦完成三次握手,双方都处于一个 ESTABLISHED 状态,此时连接就已经建立完成了,客户端和服务器端就可以相互发送数据了。
如何在Linux下查看 TCP状态?
TCP的 连接状态查看,在linux可以通过 netstat-napt 命令查看。
为什么是三次握手、而不是两次、四次?
在前面我们得知什么是TCP连接:
重要的是:为什么三次握手才可以初始化 Socket、序列号和窗口大小并建立TCP连接?
三个方面分析:
原因一:避免历史重复连接
网络环境是错综复杂的,往往并不是我们所期望的那样,先发送的数据包,就先到达目标主机。而是可能因为网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么在这种情况下,TCP三次握手是如何避免的呢?
客户端连续多次发送SYN 建立连接的报文,在网络拥堵情况下:
如果是两次握手,就不能判断当前连接是否是历史连接,三次握手则可以在 客户端(发送方)在发送第三次报文的时候,客户端有足够的上下文来判断 当前连接是不是 历史连接:
原因二:同步双方的初始化序列号
TCP协议的通信双方,都必须维护一个【序列号】,序列号是可靠传输的一个关键因素,它的作用是:
当客户端发送携带【初始序列号】的 SYN 报文给服务器,服务器回应一个 ACK 报文,表示客户端的SYN报文已经被服务端成功接收。
当服务器发送【初始序列号】给客户端的时候,依然也要得到客户端的确认应答。
一来一回,才能确保双方的初始序列号能被可靠的同步
如图:四次握手也可以,但第二步和第三步其实可以优化成一步,所以成了【三次握手】
二次握手只保证了一方的初始序列号能够被对方成功接收,而不能保证双方的初始化序列号都能够被确认接收。
原因三:避免资源浪费
如果只有【两次握手】,当客户端的【SYN】请求连接在 网络中堵塞,客户端没有接收到服务器的【ACK】报文,那么就会重新发送【SYN】,由于没有第三次握手,服务器不清楚客户端到底是否接收到自己发送的建立连接的【ACK】确认信号,所以每收到一个【SYN】就只能先建立一个连接,这会造成什么情况呢?
如果客户端的【SYN】在网络中堵塞了,重复发送多次【SYN】报文,那么服务器在收到请求后就会建立多个冗余的无效连接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务器重复接收无用的连接请求【SYN】报文,而造成重复分配资源。
小结:
TCP建立连接时,通过三次握手 能防止历史连接的建立、能减少双方不必要的资源开销、能帮助双方同步初始化序列号。 序列号能保证数据包的不重复,不丢失和按序传输。
不使用【两次握手】 和 【四次握手】的原因:
三次握手中,服务器一直接收不到客户端的ACK会发生什么?
服务器会给每个待完成的半连接都设置一个定时器,如果超过时间还未收到客户端的ACK消息,则重新发送一次ACK+SYN 消息给客户端,直到重试超过一定次数后才会放弃,这个时候,服务器虚分配内核资源来维护半连接。
为什么客户端 和 服务器端的 初始序列号 ISN 是不一样的?
因为网络中的报文会延迟,会复制重发,也有可能丢失,这样会造成的不同连接之间产生互相影响,所以为了避免互相影响,客户端和服务器端的初始化序列号是随机切不同的。
初始序列号是如何随机产生的?
RFC1948 中 提出了一个初始化序列号的 ISN随机生成算法。
ISN = M + F(localhost,localport,remotehost,remoteport)
既然IP层会分片,那为什么TCP还需要MSS呢?
我们先来认识一下:MTU 和 MSS
如果TCP 的整个报文(头部 + 数据)交给IP层 进行分层,会出现什么异常呢?
当IP层有一个超过 【MTU】大小的数据(TCP头部+TCP数据)要发送,那么IP层就要进行分层,把数据分成若干片,保证每一片分片都小于【MTU】。把一份IP数据报进行分片之后,由目标主机的 IP层来进行重新组装之后,再交给上一层TCP传输层。
如此看起来很棒,但是是存在隐患的,如果在此过程中,有一个IP分片丢失,那整个IP报文的所有分片都得重传。
因为IP层 本身是没有超时重传机制,它由传输层的TCP来负责 超时和重传。
当接收方发现TCP报文(头部+数据)的某一片丢失后,就不会发送 响应ACK 给对方,那么发送方的 TCP 在超时后,就会重发【整个TCP报文(头部+数据)】。
因此,可以得知由 IP层进行分片传输,是非常没有效率的。
所以,为了达到最佳的传输效率,TCP协议在建立连接时通常要协商双方的MSS值,当TCP 层发现数据超过 MSS 时,就会先进行分片,当然由它形成的 IP包的长度也就不会大于 MTU,自然也就不会IP分片了。
经过TCP 分片后,如果一个TCP分片丢失后, 进行重发也是以 MSS 为单位重发,而不是重发所有的 数据分片。大大增加了重传的效率。
三次握手哪个阶段会出现异常?
第二个阶段会出现异常,如果服务器相应的端口没有打开,会回复RST报文,此次握手失败。此外,listen创建的监听队列到达上限,也可能失败。
什么是 SYN攻击?如何避免SYN攻击?
SYN攻击 就是攻击客户端。攻击者伪造大量的虚假IP地址,不断的向服务器发送 SYN报文,服务器每接到一个SYN报文,就会回复确认包(ACK+SYN)并等待客户端的确认回应,但是因为那些IP地址都是虚假的,不存在的,无法得到未知IP地址主机的ACK应答,当不断重发乃至超时后,这些伪造的SYN包被长时间的占据未连接队列,正常的SYN请求被丢弃,服务器资源耗尽,使得服务器不能为正常用户提供服务,严重这甚至引起网络堵塞或者系统瘫痪。
避免SYN攻击方式一:
通过修改linux内核参数,控制队列的大小,和当队列满时应做什么处理。
net.core.netdev_max_backlog
net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_abort_on_overflow
还有:
滑动窗口是什么? 为什么要滑动窗口?
在确认应答机制中,对于每一个发送的数据,服务器都会回应一个 ACK 确认应答,等收到ACK后,再次发送下一个数据。这样虽然保证了可靠性,但会导致性能很差,尤其是在数据往返时间很长的时候。
那么我们如何改变优化呢————滑动窗口
所谓的流量控制 ——就是让发送方的发送速率不要太快,让接受方来得及接收。
利用滑动窗口机制可以很方便的控制对发送方的流量控制。
窗口类型
如下图,在此次发送的数据中设置滑动窗口,窗口大小设置为4000:
然后:
窗口大小为4000,也就是说1001到5000的数据,可以一次性发送,而不用等待ACK响应。当发送方发送完4000 大小的数据后,此时等待,等到ACK响应中 确认接收到 1001-2000的数据时,窗口右移。
依次类推,滑动窗口中:
滑动窗口怎么处理丢包问题
情况一:假设是发送端发送的数据丢包:
这里1001-2000 的数据包丢失了,但是发送端并不知道,继续发送,直到连续三次接收到服务器发来的 ACK确认(TCP协议规定,收到三条重复的ACK响应就触发重传),于是重新发送1001-2000的数据,重传完毕后,等待接收端的响应。
此时接收端的ACK响应中下一条为 6001的数据报文,也就是说2000-6000 的数据都接收到了,那么这段报文就放在接收端的接收缓冲区中。
窗口继续移动,发送端继续发送。
收到三条重复的ACK响应,即重传响应所要求的数据报文的机制就是——快重传机制
情况二:接收端的ACK响应丢包
就算连续三条的ACK响应都丢失,那么第四条ACK响应成功到达,就不会影响发送端的发送。
因为此条ACK确认的是 4001的数据包,说明前面的数据都成功接收到了。
所以、接收端的ACK响应丢包不会对发送端的发送造成大影响,后续的ACK会解决好之前的问题。
什么是拥塞控制?意义?
拥塞控制——考察当前的网络情况,动态的调整窗口大小
意义:
提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。
拥塞控制包括什么?
拥塞控制包括四部分:
慢启动是指:以最小的拥塞窗口按照指数方式增长,达到门限值后,就进入到拥塞避免阶段。
拥塞避免阶段,是以拥塞避免算法,以线性方式增大拥塞窗口。(递增间隔为一个往返时间RTT)
在上述过程中,无论是窗口大小指数递增还是线性递增,当发送拥塞现象,则门限值更新为当前窗口大小的一半,拥塞窗口大小变为最小值,重复上述递增过程。
快速重传:当发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发
假设发送端发送的报文段分别为:M1、M2、M3、M4、M5、M6…
当接收端接收了M1,M2后,M3数据包丢失,接着对于收到的M4,M5,M6都不做任何处理,而是每次收到一个数据报就对M2重复确认,发送方收到三次连续的ACK响应,就知道M2数据包丢失,接着立即重传M2,不用等待超时时间到达。当快速重传之后,立即进入快速恢复阶段。
快速恢复:将慢启动门限值设为原来的一半,然后将拥塞窗口设置为现在的慢启动门限值,不再执行慢启动算法,而是直接进入拥塞避免阶段,使用拥塞避免算法,使得 窗口呈 线性方式递增。
【不连续的数据段会严重影响TCP的传输效率。而快速修复这种不连续,会释放掉占用的空间,加快发送方的传输效率】
TCP 四次挥手过程 和状态变迁
关闭连接的过程叫做 四次挥手,由于TCP的全双工的通讯,所以在每个方向上都必须单独进行关闭,当一方完成它的数据发送任务后就会发送一个【FIN】报文来终止这个方向上的连接,收到一个【FIN】意味着 这一方向上没有数据流动。
注意:
每一个方向上都有一个 【FIN】和一个【ACK】
主动关闭连接的,才有 TIME_WAIT 状态
为什么挥手需要四次?
由此,可见服务器端通常需要等待完成数据的发送和处理,所以服务器端的【ACK】和【FIN】一般会分开发送,从此比三次握手导致多了一次。
那可不可以是三次挥手呢?
在某些情况下,三次握手是可以完成挥手的。当本端关闭连接后,恰好也收到 了对方发来的【FIN】报文,此时可以把自己的【FIN】 和 给对端的【ACK】应答报文 一起发送,就变成了三次挥手。
为什么建立连接时三次握手,而关闭是四次挥手呢?
原因:
【FIN】报文是由调用 COLSE 才发送的,而【ACK】是直接由内核发送。所以【ACK】和【SYN】报文在发送的时间上是分开的,不能够同步发送。
但是,三次握手时,发送【SYN】是由内核直接完成的,所以在时间上可以达到一个 同步发送的情况。
为什么需要 TIME_WAIT状态?
time_wait状态是: 主动发送方 在接收到 对方发来的FIN报文 并且将 ACK 报文发出后处于的一个状态。
原因一:防止旧连接的数据包
假设time_wait 没有等待时间或者时间过短,被延迟的数据包 抵达后会发生什么呢?
所以,TCP 设计机制——2MSL 时间
等待 2MSL 时间后,足以让两个方向上的数据包都被丢弃,使得原本连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证连接正确关闭
假设 TIME_WAIT 没有等待时间或者 等待时间过短,连接断开会造成什么问题呢?
如果TIME_WAIT 等待时间足够长,那么就会遇到两种情况:
为什么等待时间是 2MSL呢?
MSL: 报文最大生存时间
是任何报文在网络中存在的最长时间,超过这个时间报文 就会被丢弃。
科普小知识:
TCP报文 是基于IP协议的,IP头中有一个【TTL】字段,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器,此值就会减一, 当此值为0 时, 数据报就会被丢弃,同时发送 ICMP报文通知源主机。
MSL 与 TTL的区别: MSL的单位是时间,TTL的单位是 经过路由跳数。 MSL 应该是要大于 TTL消耗到为0的时间,以确保报文被自然消亡。
TIME_WAIT 等待 2倍的 MSL: 网络中存在来自发送方的数据包,当这些数据包被接收方处理后,又会向对方发送响应,所以一来一回 需要等待2倍的时间。
比如: 如果被动关闭方,没有收到断开连接的最后的那个ACK报文,就会触发超时重传,重发FIN报文,另一方接受到FIN后,会重发ACK 给被动关闭方,一来一去,2MSL。
2MSL的时间 是从客户端收到 FIN 后发送ACK开始计时的, 如果在TIME_WAIT 时间内,因为客户端的ACK 没有传输到 服务器,客户端又收到了 服务器重发的 FIN 报文,那么 2MSL时间将 重新计时。
linux系统下,2MSL 默认为 60 秒。一个为 30秒。
TIME_WAIT 过多有什么危害?
端口资源是有限的,一般可以开启的端口为 32768~61000。如果TIME_WAIT 状态 过多,占满了所有的端口资源,则会导致无法创建新连接。
那如何优化TIME_WAIT 呢?
打开 net.ipv4.tcp_tw_reuse 和 tcp_timetamps 选项;(引入时间戳)
net.ipv4.tcp_max_tw_buckets
这个值被默认为18000, 当系统中处于TIME_WAIT 的连接一旦超过这个值,系统就重置所有TIME_WAIT 状态。(过于暴力,弊大于利,不推荐)
程序中使用 SO_LONGER, 应用强制性使用 RST 关闭。(危险)
什么是 CLOSE_WAIT 状态? 有什么影响??
CLOSE_WAIT是处于被动关闭的一端在接收到 对端关闭请求(FIN报文段)并且将 ACK 发送出去后所处于的状态。 此状态表示:收到了对端关闭的请求,但是半端还没有完成工作,还未关闭。
影响:
服务器长时间处于 CLOSE_WAIT状态, 也就是意味着 服务器的代码没有调用 close,并没有发送【FIN】结束报文段,也就是说 系统分配的文件描述符并没有关闭并归还。 大量的 CLOSE_WAIT 存在的话,会造成资源的泄漏,到最后可能就没有可分配的文件描述符了,导致一些客户端无法连接。
如果已经建立了连接,客户端突然出现故障了怎么办?
TCP 有一个机制是 保活机制,定一个保活计时器
定义一个时间段,这个时间段内,如果没有任何的连接相关的活动,TCP保活机制就开始起作用。
正常情况下,服务器每收到一次客户端的请求后都会复位这个计时器,时间间隔为2小时。
但是如果两小时内,没有收到客户端的任何数据,服务器就每隔一段时间会发送一个探测报文段,该探测报文段内的数据很少,每隔75分钟就发一次,若连发十个10探测报文仍没有任何回应,那么就判定客户端出现故障,接着就关闭连接。
如果开启了TCP保活,需要考虑以下几种情况:
针对TCP应该如何 Socket编程?
注意:
服务器端调用【accept】时,连接成功后会返回一个已完成连接的【socket】,后续用来传输数据。
所以、用来监听的【socket】和真正用来传输数据的【socket】是 两个 socket,一个叫做【监听socket】,另一个叫做【已完成连接socket】。
成功建立连接之后,双方开始通过【read】和【write】函数来读写数据,就像往一个文件流里面来写东西一样。
listen中的 第二个参数是什么?(面试会重点问到)
linux内核会维护两个队列:
int listen(int socketfd,int backlog)
早期: 在linux内核中,backlog 为 SYN 队列大小,未完成的队列大小。
linux 内核2.2后: backlog 变成Accept,也就是完成建立连接的队列长度。
在一些Unix系统中,还会被表示:两个队列长度之和。
accept 发生在 三次握手的哪一步?