收包过程:
- 当有网络包到达时,会通过 DMA 技术,将网络包写入到ring buffer的环形缓冲区中,接着网卡向 CPU 发起硬中断,当 CPU 根据中断表,调用中断处理函数。
- 中断处理函数会发起软中断,内核线程 ksoftirqd 处理软中断,会来轮询处理数据。
- ksoftirgd 线程会从 Ring Buffer 中获取数据,作为一个网络包交给网络协议栈进行逐层处理。
- 网络接口层:检查报文的合法性。
- 网络层:确认这个网络包是发给本机还是转发出去。
- 传输层取出 TCP 头或 UDP 头,根据四元组找到socket,把数据放到socket的接受缓冲区中。
- 应用层:调用 Socket 接口,将内核的 Socket 接收缓冲区的数据拷贝到应用层的缓冲区,然后唤醒用户进程。
发包过程与收包过程相反
- 第一次,调用发送数据的系统调用的时候,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk buff 内存,然后入到发送缓冲区。
- 第二次,在使用 TCP 传输协议的情况下,从传输层进入网络层的时候,sk_buff 会被创建副本。副本 sk_buff 会被送往网络层,等它发送完的时候释放掉,然后原始的 sk buf 还保留在传输层,目的是为了实现 TCP 的可靠传输,等收到这个数据包的 ACK 时,才会释放原始的 sk_buff
- 第三次,当IP 层发现 sk_buff 大于 MTU 时才需要进行。会再申请额外的 sk_buff,并将原来的 sk_buff 拷贝为多个小的 sk_buff。
两种,强制缓存和协商缓存。
**强制缓存:**只要浏览器判断缓存没有过期,直接用浏览器的本地缓存,浏览器决定用不用缓存。强缓存利用HTTP 响应头里的字段实现:
Cache-Control
, 相对时间;Expires
,绝对时间;
**协商缓存:**协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。请求的响应码是 304
,就是浏览器可以使用本地缓存的资源。需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。
自己不乱点,正常情况下是安全的。中间的假基站会给客户端发送证书的,浏览器会提示假基站的证书不安全。
但是如果电脑中毒了,被导入了中间人的根证书,那么等中间人的证书是合法的,这种情况下,浏览器是不会弹出证书存在问题的风险提醒的。
-
简单: HTTP 基本的报文格式就是
header + body
,头部信息也是key-value
简单文本的形式 -
灵活和易于扩展: 请求方法、URI/URL、状态码、头字段 都没有被固定死,可以自定义和扩充
-
应用广泛和跨平台: HTTP 的应用范围非常的广泛,pc端浏览器和手机上的APP都用。
-
**和1.0对比: ** 用了长连接
- 没有状态: 不能记录状态信息,可以用cookie解决。
- 明文传输,不安全: 通信使用明文,不验证通信方的身份,不能证明报文的完整性
- 队头阻塞: 服务端在处理 一个请求时耗时比较长,后面的请求的处理会被阻塞住, HTTP/1.1 管道解决了请求的队头阻塞,但是浏览器基本上默认关闭。
- 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
- 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
- 避免发送HTTP请求: 用缓存
- 减少HTTP请求次数: **减少重定向的请求次数:**重定向的工作让代理服务器完成。 **合并请求:**把多个访问小文件的请求合并成一个大的请求,可以减少重复发送HTTP头。 **延迟发送请求:**请求网页的时候,只获取一部分,当用户下滑的时候再请求资源。
- 减少HTTP响应的数据大小: 可以对相应的资源进行压缩,一般有无损压缩和有损压缩。无损压缩常见的有gzip。有损压缩会把不重要的数据舍弃,牺牲一些质量来减少数据量、提高压缩比,经常用于压缩多媒体数据,音频、视频、图片。
- 头部压缩: 用到HPACK算法,就是在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号。静态表只包含了高频出现在头部的字符串,不在静态表里的头部字符串要构建动态表,会在编码解码的时候随时更新。
- 二进制格式: 1.1是纯文本形式的报文,2.0用是二进制格式。头信息和数据体都是二进制,头信息帧和数据帧。
- 并发传输: 用了 Stream 概念,多个 Stream 复用在一条 TCP 连接,不同的 HTTP 请求用 Stream ID 来区分。可以解决一部分队头阻塞问题,但是不能完全解决,因为底层还是TCP。
- 服务器主动推送: 服务端可以主动向客户端发送消息,可以建立stream
- 解决了队头阻塞问题: 因为底层是tcp,就会有队头阻塞问题,HTTP3.0使用了quic协议,是基于udp的。
- 建立连接更快: HTTP3.0的QUIC协议包含了TLS,会携带 TLS 里的内容,如果是TLS1.2,需要1个RTT就能完成建立连接和密钥协商。
- 连接迁移: 基于 TCP 传输协议的 HTTP 协议,是通过四元组确定一条 TCP 连接,切换网络的时候,要断开连接,然后重新建立连接。QUIC协议通过连接ID标记一个连接,切换网络的时候可以复用原连接。
- 二级制格式: 与2.0一样采用二进制帧的结构, HTTP/2 的二进制帧里需要定义 Stream,而HTTP/3 自身不需要再定义 Stream,直接使用 QUIC 里的 Stream。
- 纯裸 TCP 能收发数据,但是是没有边界的数据流,上层需要定义消息格式用于定义消息边界。所以就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。
- RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是能像调用本地方法那样去调用远端的服务方法。
- 很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。
用HTTP定时轮询:登录页面二维码出现之后,前端网页根本不知道用户扫没扫,不断去向后端服务器询问。如果间隔1~2秒发送请求会消耗带宽。所以用长轮询,发出请求后,给服务器留时间去响应,只要服务器收到了扫码请求,就立马返回给客户端网页。如果超时,就立马发起下一次请求。
适用于需要服务器和客户端(浏览器)频繁交互的大部分场景,比如网页/小程序游戏,网页聊天室,以及一些类似飞书这样的网页协同办公软件。
如何建立websocket连接?
浏览器在 TCP 三次握手建立连接之后,使用 HTTP 协议先进行一次通信。
- 如果是普通的 HTTP 请求,那后续双方继续用普通 HTTP 协议进行交互。
- 如果想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头,代表升级协议,然后服务端的响应为101。一来一回两次HTTP握手,websocket就建立完成了
tcp首部固定长度20字节,udp首部8字节。
对 IPv4,客户端的 IP 数最多为 2
的 32
次方,客户端的端口数最多为 2
的 16
次方,也就是服务端单机最大 TCP 连接数,约为 2
的 48
次方。
不能达到理论上限,会受以下因素影响:
-
文件描述符限制
每个 TCP 连接都是一个文件,文件描述符会被占满。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
- 系统级:当前系统可打开的最大数量,通过
cat /proc/sys/fs/file-max
查看; - 用户级:指定用户可打开的最大数量,通过
cat /etc/security/limits.conf
查看; - 进程级:单个进程可打开的最大数量,通过
cat /proc/sys/fs/nr_open
查看;
- 系统级:当前系统可打开的最大数量,通过
-
内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。
- 阻止重复历史连接的初始化(主要原因):两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
- 可以同步双方的初始序列号
- 保证双方具有接受和发送的能力
- 为了防止历史报文被下一个相同四元组的连接接收(主要方面):如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题。
起始 ISN
是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。随机数是会基于时钟计时器递增的。
深入解释:
**公式:**ISN = M + F(localhost, localport, remotehost, remoteport)。
M
是一个计时器,这个计时器每隔 4 微秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
答:如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。一个 IP 分片丢失后,接收方的 IP 层不能组装成一个完整的 TCP 报文(头部 + 数据),不能把数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。
会触发「超时重传」机制,重传 SYN 报文,每次超时时间时上一次的2倍。SYN 报文最大重传次数由 tcp_syn_retries
内核参数控制,这个参数可以自定义的,一般默认是 5。
客户端和服务端都会超时重传。
第二次握手报文里包含了客户端的第一次握手的 ACK 确认报文,客户端认为自己的 SYN 报文丢失了,所以客户端就会触发超时重传机制,重传 SYN 报文。
第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文,如果第二次握手丢失,服务端收不到第三次握手的报文,所以服务端也会发生超时重传。
第三次握手丢失,服务端收不到确认报文,触发超时重传机制,重传第二次握手报文。
就是攻击者短时间伪造不同的IP地址的SYN报文发给服务端,会把 TCP 半连接队列打满,后续再在收到 SYN 报文就会丢弃,导致客户端不能和服务端建立连接。
避免方法:
- 增大TCP半连接队列
- 开启
tcp_syncookies
: 开启syncookies
功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。 - 减少
SYN+ACK
报文的重传次数: 当服务端受到 SYN 攻击时,就会有大量处于SYN_REVC
状态的 TCP 连接,处于这个状态的 TCP 会重传SYN+ACK
,当重传超过次数达到上限后,就会断开连接。减少SYN-ACK
的重传次数,可以快处于SYN_REVC
状态的 TCP 连接断开。
深入解释:
tcp_syncookies
具体过程:
- 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃,而是根据算法,计算出一个
cookie
值; - 将 cookie 值放到第二次握手报文的「序列号」里,然后服务端回第二次握手给客户端;
- 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」。
- 最后应用程序通过调用
accpet()
接口,从「 Accept 队列」取出的连接。
如果第一次挥手丢失了,那么客户端收不到服务端的 ACK 的话,就会触发超时重传,重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
服务端会重发 FIN 报文,重发次数由 tcp_orphan_retrie
s 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
服务端没有收到 ACK 报文前,处于 LAST_ACK 状态。如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数tcp_orphan_retries
参数控制。
没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:
- 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
- close 函数,关闭读和写,如果有多线程共享同一个 socket,如果有一个线程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。
- shutdown 函数,只关闭写而不关闭读。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的线程想要使用该 socket,将会受到影响。
如果客户端是用 close 函数来关闭连接,那么在 TCP 四次挥手过程中,如果收到了服务端发送的数据,由于客户端已经不再具有发送和接收数据的能力,所以客户端的内核会回 RST 报文给服务端,然后内核会释放连接,这时就不会经历完成的 TCP 四次挥手,所以调用 close 是粗暴的关闭。
MSL
是报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN
报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
-
防止历史连接中的数据,被后面相同四元组的连接错误的接收 :序列号和初始化序列号不是无限递增的,会发生回绕为初始值的情况,所以不能根据序列号来判断新老数据,所以需要
TIME_WAIT
状态让就连接的数据自然消失。 -
保证「被动关闭连接」的一方,能被正确的关闭: 等待足够的时间以确保最后的 ACK 能让被动关闭方接收,帮助被动关闭方正常关闭。
- 占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源;
- 占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围。
- 打开
tcp_tw_reuse
和tcp_timestamps
: 可以复用处于TIME_WAIT的socket给新连接用,但是这个功能只能在连接发起方用,开启后在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。 tcp_max_tw_buckets
: 这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。- 程序中使用
SO_LINGER
: 通过设置 socket 选项,来设置调用 close 关闭连接行为。如果linger.l_onoff
为非 0, 且linger.l_linger
值为 0,调用close
后,会发送一个RST
标志给对端,该TCP 连接就会跳过四次挥手,也就跳过了TIME_WAIT
状态,直接关闭。
因为服务器主动断开了很多 TCP 连接
服务端主动断开连接场景
- HTTP 没有使用长连接
- HTTP 长连接超时
- HTTP 长连接的请求数量达到上限
当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
没调用close原因:
- 没有将服务端 socket 注册到 epoll: 这样有新连接到来时,服务端没办法感知这个事件,也就不能获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。
- 有新连接到来时没有调用 accpet 获取该连接的 socket: 导致当有大量的客户端主动断开了连接,然后服务端没机会对这些 socket 调用 close 函数,导致服务端出现很多CLOSE_WAIT 状态的连接。发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。
- 通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll: 导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。
- 当发现客户端关闭连接后,服务端没有执行 close 函数: 可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。
发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是无法感知到客户端宕机这个事件的。TCP有保活机制可以感知。
**TCP保活机制:**TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将 socket 绑定在指定的 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
accept在三次握手之后。
accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。
可以。
accpet 系统调用并不参与 TCP 三次握手过程,它只是从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到已经建立连接的 socket,就可以对该 socket 进行读写操作。
可以。
- 客户端是可以自己连自己的形成连接(TCP自连接),客户端没有执行listen,所以没有全连接队列和半连接队列,但是内核有个全局hash表,可以存放连接信息。
- 也可以两个客户端同时向对方发出请求建立连接(TCP同时打开)。
常见的重传机制:
- 超时重传
- 快速重传: 当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
- SACK:在 TCP 头部「选项」字段里加一个
SACK
的东西,可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。 - D-SACK: 主要使用了SACK 来告诉「发送方」有哪些数据被重复接收。
TCP窗口糊涂综合症产生原因:
- 滑动窗口动态调整机制导致发送方发送了小包。
- 接收方通告了小窗口。
从两方面解决问题:
- 接收方:
- 在接收方这边接受能力过小直接通告窗口为0,一般等到1/2MSS时再通告窗口。
- 延迟确认,让ACK包等等数据,然后一起发送。这样给了消费消息的时间,通告时可以通告更大的窗口。
- 累计确认,收到好几个包之后,回一个ACK
- 发送方:
- 减少小包的发送数量,采用Nagle算法,算法的本质是小于MSS时,进行等待不发送,攒一个大的包合并发送,等待过程中有定时器,如果超时不管大小包立即发送。
MSS是TCP最大报文段
如何绕过三次握手: TCP Fast Open 功能,可以减少 TCP 连接建立的时延。 这个功能在第一次连接时正常三次握手,服务端会返回一个cookie,在下次建立连接的时候,客户端的SYN报文会带上这个cookie和数据,服务端验证cookie有效就会接收数据。减少了握手带来的1个RTT时延。
开启TCP 时间戳:tcp_timestamps
参数是默认开启的,有两个好处,一个是便于精确计算 RTT ,另一个是能防止序列号回绕(PAWS)。
防止序列号绕回序列号方法: 要求连接双方维护最近一次收到的数据包的时间戳,每收到一个新数据包都会读取数据包中的时间戳值跟最近的时间戳值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。
时间戳的大小是 32 bit,所以理论上也是有回绕的可能性的。时间戳回绕的速度只与对端主机时钟频率有关。
解决方案:
- 增加时间戳的大小,由32 bit扩大到64bit
- 将一个与时钟频率无关的值作为时间戳,时钟频率可以增加但时间戳的增速不变。
两种情况:
- 开启 tcp_tw_recycle 参数,并且在 NAT 环境下,造成 SYN 报文被丢弃
- TCP 两个队列满了(半连接队列和全连接队列),造成 SYN 报文被丢弃
深入解释:
tcp_tw_recycle
,如果开启该选项的话,允许处于 TIME_WAIT 状态的连接被快速回收。
开启了 recycle 和 timestamps 选项,就会开启一种叫 per-host 的 PAWS 机制。per-host 是对「对端 IP 做 PAWS 检查」,而非对「IP + 端口」四元组做 PAWS 检查。
但是如果客户端网络环境是用了 NAT 网关,那么客户端环境的每一台机器通过 NAT 网关后,都会是相同的 IP 地址,在服务端看来,就好像只是在跟一个客户端打交道一样,无法区分出来。
当客户端 A 通过 NAT 网关和服务器建立 TCP 连接,然后服务器主动关闭并且快速回收 TIME-WAIT 状态的连接后,客户端 B 也通过 NAT 网关和服务器建立 TCP 连接,注意客户端 A 和 客户端 B 因为经过相同的 NAT 网关,所以是用相同的 IP 地址与服务端建立 TCP 连接,如果客户端 B 的 timestamp 比 客户端 A 的 timestamp 小,那么由于服务端的 per-host 的 PAWS 机制的作用,服务端就会丢弃客户端主机 B 发来的 SYN 包。
两种情况:
- 客户端的 SYN 报文里的端口号与历史连接不相同: 服务端会认为是新的连接要建立,于是就会通过三次握手来建立新的连接。
- 客户端的 SYN 报文里的端口号与历史连接相同: 在收到客户端的SYN报文后,因为这个SYN是乱序的,服务端会回复一个携带了正确序列号和确认好的ACK报文,叫Challenge ACK,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。
在 FIN_WAIT_2 状态时,如果收到乱序的 FIN 报文,会加入到「乱序队列」,并不会进入到 TIME_WAIT 状态。等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,才会进入 TIME_WAIT 状态。
看 SYN 的「序列号和时间戳」是否合法。
- 如果合法: 就会重用此四元组连接,跳过 2MSL 而转变为 SYN_RECV 状态,接着就能进行建立连接过程。
- 如果不合法: 如果处于 TIME_WAIT 状态的连接收到「非法的 SYN 」后,就会再回复一个第四次挥手的 ACK 报文,客户端收到后,发现并不是自己期望收到确认号,就回 RST 报文给服务端。
主机崩溃: 客户端主机崩溃了,服务端是无法感知到的,如果服务端没有开启 TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。如果有tcp保活,报文重传几次后,服务端会自己断开连接。
进程崩溃: TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能和客户端完成 TCP四次挥手的过程。
在,服务端就会触发超时重传机制,重传未得到响应的数据报文。如果在服务端重传报文的过程中,客户端刚好把网线插回去了,就什么都不会发生。
如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去,重传几次后,服务端会断开。
- 开启
tcp_tw_reuse
的同时,也需要开启tcp_timestamps
,可以用时间戳的方式有效的判断回绕序列号的历史报文。但是,对于 RST 报文的时间戳即使过期了,只要 RST 报文的序列号在对方的接收窗口内,也是能被接收的。 - 复用 TIME_WAIT 状态后发送的 SYN 报文被处于 last_ack 状态的服务端收到后,服务端会回复确认号与服务端上一次发送 ACK 报文一样的 ACK 报文,这个 ACK 报文称为Challenge ACK,客户端收到Challenge ACK后,会恢复RST。
正常情况下,「先进行 TCP 三次握手,再进行 TLS 四次握手」。
满足下面条件可以同时握手:
- 客户端和服务端都开启了 TCP Fast Open 功能,且 TLS 版本是 1.3: 开启tcp fast open可以在握手时携带数据, TLS1.3只需要一个RTT。
- 客户端和服务端已经完成过一次通信: 完成过一次通信,tcp fast open才开始生效。
不是
- TCP keepalive:是TCP的保活机制,是在内核态的传输层实现的,作用是在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活。
- HTTP keep-alive: 是HTTP的长连接,是在用户态的应用层实现的,作用是使用同一个TCP进行多次HTTP请求和/应答,避免连接建立和释放的开销。
- 升级 TCP 的工作很困难: 但是 TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。
- TCP 建立连接的延迟: 需要先建立三次握手才能进行数据传输。
- TCP 存在队头阻塞问题: TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据。
- 网络迁移需要重新建立 TCP 连接: 基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。
**可以。**传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。 TCP 和 UDP,在内核中是两个完全独立的软件模块。TCP/UDP 各自的端口号相互独立。
如果两个 TCP 服务进程绑定的 IP 地址不同,而端口相同的话,也是可以绑定成功的。
如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。
当 TCP 服务进程重启时,服务端会出现 TIME_WAIT 状态的连接,TIME_WAIT 状态的连接使用的 IP+PORT 仍然被认为是一个有效的 IP+PORT 组合,相同机器上不能够在该 IP+PORT 组合上进行绑定,那么执行 bind() 函数的时候,就会返回了 Address already in use 的错误。
解决方法:
在调用 bind 前,对 socket 设置 SO_REUSEADDR 属性。
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
TCP 连接是由四元组唯一确认的,只要四元组中其中一个元素发生了变化,就是不同的 TCP 连接的。所以如果客户端已使用端口 64992 与服务端 A 建立了连接,那么客户端要与服务端 B 建立连接,还是可以使用端口 64992 的,因为内核是通过四元祖信息来定位一个 TCP 连接的,并不会因为客户端的端口号相同,导致连接冲突的问题。
要看多个客户端绑定的 IP + PORT 是否都相同,如果都是相同的,那么在执行 bind() 时候就会出错,错误是“Address already in use”。
可以
bind 函数虽然常用于服务端网络编程中,但是它也是用于客户端的。客户端是在调用 connect 函数的时候,由内核随机选取一个端口作为连接的端口。如果想自己指定连接的端口,就可以用 bind 函数来实现:客户端先通过 bind 函数绑定一个端口,然后调用 connect 函数就会跳过端口选择的过程了,使用 bind 时确定的端口。
只要客户端连接的服务器不同,端口资源可以重复使用的
服务端如果只 bind 了 IP 地址和端口,而没有调用 listen 的话,然后客户端对服务端发起了连接建立,服务端会回 RST 报文。
全连接队列是个链表,而半连接队列是个哈希表。
- 全连接里队列,里面放的都是已经建立完成的连接,这些连接正等待被取走。而服务端取走连接的过程中,并不关心具体是哪个连接,只要是连接就行,所以直接从队列头取就行了。这个过程算法复杂度为
O(1)
。 - 半连接队列里的都是不完整的连接,等待第三次握手的。有一个第三次握手来了,需要从队列里把相应IP端口的连接取出,**如果半连接队列还是个链表,就需要依次遍历,才能拿到想要的那个连接,算法复杂度就是O(n)。**而如果将半连接队列设计成哈希表,那么查找半连接的算法复杂度就回到
O(1)
了。因此出于效率考虑,全连接队列被设计成链表,而半连接队列被设计为哈希表。
没有cookies队列,它是通过通信双方的IP地址端口、时间戳、MSS等信息进行实时计算的,保存在TCP报头的seq
里。
cookies
方案虽然能防止 SYN Flood攻击。但是服务端并不会保存连接信息,所以如果传输过程中数据包丢了,也不会重发第二次握手的信息。- 编码解码
cookies
,都是比较耗CPU的,如果攻击者构造大量的第三次握手包(ACK包),同时带上各种乱写的cookies
信息,服务端收到ACK包
后以为是正经cookies,跑去解码,会消耗CPU,最后发现不是正经数据包后才丢弃。
TCP只能保证传输层及以下不会丢包,但是上面还有应用层,如果消息已经从tcp的接收缓冲区读出来了,然后应用层崩溃了,就会产生丢失。
- 公式一:序列号 = 上一次发送的序列号 + len(数据长度)。特殊情况,如果上一次发送的报文是 SYN 报文或者 FIN 报文,则改为 上一次发送的序列号 + 1。
- 公式二:确认号 = 上一次收到的报文中的序列号 + len(数据长度)。特殊情况,如果收到的是 SYN 报文或者 FIN 报文,则改为上一次收到的报文中的序列号 + 1。
- IPv6 可自动配置,没有 DHCP 服务器也可以实现自动分配IP地址。
- IPv6 包头包首部长度采用固定的值
40
字节,去掉了包头校验和,简化了首部结构,减轻了路由器负荷,提高了传输的性能**。 - IPv6 有应对伪造 IP 地址的网络安全功能以及防止线路窃听的功能,提升了安全性。
通过ARP协议可以得到下一跳的MAC地址。
- 主机会通过广播发送 ARP 请求,这个包中包含了想要知道的 MAC 地址的主机 IP 地址。
- 当同个链路中的所有设备收到 ARP 请求时,会去拆开 ARP 请求包里的内容,如果 ARP 请求包中的目标 IP 地址与自己的 IP 地址一致,那么这个设备就将自己的 MAC 地址塞入 ARP 响应包返回给主机。
DHCP 动态获取 IP 地址。
获取IP地址步骤:
- 客户端发起 **DHCP发现报文 ** 的 IP 数据报,因为客户端没有 IP 地址,也不知道 DHCP 服务器的地址,所以使用 UDP 广播通信,目的地址是 255.255.255.255:67 ,源地址0.0.0.0:68 。
- DHCP 服务器收到 DHCP 报文时,给客户端回复DHCP提供报文,使用 IP 广播地址 255.255.255.255,这个报文信息携带服务器提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器和 IP 地址租用期。
- 客户端收到一个或多个服务器的 DHCP 提供报文后,从中选择一个服务器,给选中的服务器发送 DHCP 请求报文进行响应,回显配置的参数。
- 最后,服务端用 DHCP ACK 报文对 DHCP 请求报文进行响应,应答所要求的参数。
一种网络地址转换 NAT 的方法,缓解了 IPv4 地址耗尽的问题。比如两个私有 IP 地址都转换 IP 地址为公有地址,但是以不同的端口号作为区分。
原理:
会生成一个 NAPT 路由器的转换表,可以正确地转换地址跟端口的组合,让客户端 A、B 能同时与服务器之间进行通信,这种转换表在 NAT 路由器上自动生成。
NAT缺点:
由于 NAT/NAPT 都依赖于自己的转换表,因此会有以下的问题:
- 外部无法主动与 NAT 内部服务器建立连接,因为 NAPT 转换表没有转换记录。
- 转换表的生成与转换操作都会产生性能开销。
- 通信过程中,如果 NAT 路由器重启了,所有的 TCP 连接都将被重置
解决方法:
- 改用IPV6
- NAT穿透:让网络应用程序主动发现自己位于 NAT 设备之后,主动获得 NAT 设备的公有 IP,为自己建立端口映射条目,这些都是 NAT设备后的应用程序自动完成的。
互联网控制报文协议,属于网络层。
ICMP
主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。
ICMP 大致可以分为两大类:
- 一类是用于诊断的查询消息,也就是「查询报文类型」
- 另一类是通知出错原因的错误消息,也就是「差错报文类型」
ping是基于ICMP协议工作的。ICMP 报文是封装在 IP 包里面,工作在网络层。
ping的发送和接收过程:
ping 命令执行的时候,源主机会构建一个 ICMP 回送请求消息数据包。
然后,由 ICMP 协议将这个数据包交给 IP 层。IP 层将协议字段设置为 1
表示是 ICMP
协议,再加上一些其他控制信息,构建一个 IP
数据包。
对端接收数据包后,将有用的信息提取后交给 ICMP 协议,然后 会构建一个 ICMP 回送响应消息数据包,回送响应数据包的类型字段为 0
,然后再发送回去。
127 开头的都属于回环地址
能
发现目标IP是回环地址时,就会选择本地网卡。
本地网卡,其实就是个**"假网卡",它不像"真网卡"那样有个ring buffer
什么的,"假网卡"会把数据推到一个叫 input_pkt_queue
的 链表中。这个链表,其实是所有网卡共享的,上面挂着发给本机的各种消息。消息被发送到这个链表后,会再触发一个软中断**。
专门处理软中断的ksoftirqd
内核线程,它在收到软中断后就会立马去链表里把消息取出,然后顺着数据链路层、网络层等层层往上传递最后给到应用程序。
ping
回环地址和 ping
本机地址,是一样的,走的是"假网卡",都会经过网络层和数据链路层等逻辑,最后在快要出网卡前, 会将数据插入到一个链表后就软中断通知 ksoftirqd 来进行收数据的逻辑,压根就不出网络。所以断网了也能 ping
通回环地址。
localhost
会默认解析为127.0.0.1
,所以没什么区别。
0.0.0.0
在listen的时候可以表示本机上所有的IPV4地址。