8.1 协议
网关是什么?它有什么用?
网关是一种网络设备或者软件,连接着不同类型或者不同协议的网络。在服务器环境中,网关的作用非常关键。当我们的服务器需要和外部网络通信时,比如客户端通过公网访问我们的服务器,数据包可能会先到达一个网关设备,网关会把这些数据包进行协议转换或者地址转换,让不同网络之间能够互相理解对方的数据格式和地址规则。另外,网关还能起到安全防护的作用,它可以对外来的访问请求进行过滤和检查,阻止非法的访问进入内部网络,保护服务器的安全。同时,网关也能进行数据的转发和路由选择,确保数据能准确地从源地址传输到目标地址。
【必问】OSI七层模型和四层模型是什么?
七层模型:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
四层模型:七层模型的简化版。只有应用层、传输层、网络层、网络接口层。一般在应用开发过程中,讨论最多的是四层模型。
网络层和传输层分别是做什么工作的?
网络层主要负责数据包的路由和转发,它确定数据从源主机到目标主机的路径,处理IP地址,比如通过路由器在不同网络间传递数据包,典型的协议就是IP协议。
传输层则更关注端到端的通信,它确保数据可靠传输或高效传输,比如TCP提供可靠连接、流量控制和错误恢复,而UDP则提供无连接的快速传输,传输层让不同主机上的应用进程能够互相通信。
每一层分别有哪些协议?
应用层 常见的协议有 HTTP 协议,FTP 协议。
传输层 常见协议有 TCP/UDP 协议。
网络层 常见协议有IP协议、ICMP协议、IGMP 协议。
网络接口层常见协议有 ARP 协议、RARP 协议。
【必问】从输入网址后敲完回车到浏览器返回页面,中间发生了什么?
当你在浏览器输入网址并按下回车后,整个过程其实涉及多个层次的协作。首先,浏览器会对URL进行解析,判断它是HTTP还是HTTPS,然后提取出域名部分。如果是HTTPS,还会涉及到SSL/TLS的握手准备。
接下来,浏览器会通过DNS解析将域名转换成IP地址。这一步可能会先检查本地缓存,如果没有,就会向配置的DNS服务器发起查询请求,可能经过递归或迭代查询最终拿到IP。
拿到IP后,浏览器会尝试建立TCP连接。如果是HTTPS,还会在这个TCP连接上再进行TLS握手,交换密钥,确保后续通信加密。TCP连接建立后,浏览器会构造HTTP请求报文,包括请求方法(比如GET)、请求头(如User-Agent、Accept等)以及可能的请求体(如果是POST请求)。
服务器收到请求后,会根据请求内容进行处理,可能涉及静态文件直接返回,或者动态请求交给后端应用(比如PHP、Python、Node.js等)处理,数据库查询等操作也可能在这一步发生。服务器处理完请求后,会构造HTTP响应报文,包括状态码(如200 OK)、响应头和响应体(如HTML内容)。
浏览器收到响应后,如果是HTML,会开始解析DOM树,同时遇到CSS、JS等资源会继续发起请求,这些资源可能并行下载。CSS会影响渲染树构建,JS可能会阻塞渲染,除非标记为async或defer。最终,浏览器将DOM和CSSOM结合,生成渲染树,计算布局,绘制页面,可能还会执行JS动态修改DOM,触发重绘或回流。
整个过程涉及网络、操作系统、浏览器引擎等多个层面的协作,每个环节都可能影响最终页面的加载速度和用户体验。
C/S模式 vs B/S模式有什么不同?
C/S模式:传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
B/S模式:浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台 PC都默认配置的浏览器即可完成数据的传输。
网络字节序的大小端是什么?网络数据是大端还是小端?
网络字节序采用大端序,也就是高位字节存放在低地址,低位字节存放在高地址。这是因为网络通信需要统一标准,避免不同主机因为大小端不同导致数据解析错误。像x86架构的CPU通常是小端序,低位字节在低地址,但网络传输时数据会被转换成大端序,也就是网络字节序。比如一个32位的整数0x12345678,在小端机器上内存布局是78 56 34 12,但通过网络发送时会转换成12 34 56 78这样的大端格式。
如何快速判断一台主机是大端序还是小端序?
最简单的方法就是定义一个多字节的变量,比如int或short,然后通过指针访问它的第一个字节,看高位是不是在低地址。比如用一个short变量存0x1234,如果第一个字节是0x12就是大端,是0x34就是小端。我写个简单的代码示例:
#include <iostream>
bool isLittleEndian() {
short num = 0x1234;
char *p = (char *)#
return *p == 0x34; // 如果第一个字节是0x34,说明低位在前,是小端
}
int main() {
if (isLittleEndian()) {
std::cout << "小端序" << std::endl;
} else {
std::cout << "大端序" << std::endl;
}
return 0;
}
网络字节序和主机字节序的转换有哪些库函数?
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // 本地转网络(长)
uint16_t htons(uint16_t hostshort); // 本地转网络(短)
uint32_t ntohl(uint32_t netlong); // 网络转本地(长)
uint16_t ntohs(uint16_t netshort); // 网络转本地(短)
int inet_pton(int af, const char *src, void *dst); // 将字符串 IP 转为二进制
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 将二进制 IP 转为字符串
ARP协议是做什么的?
ARP协议,也就是地址解析协议,它的主要作用是在局域网里把IP地址转换成MAC地址。因为数据链路层通信需要知道对方的MAC地址,但上层比如TCP/IP协议栈传递的是IP地址,这时候就需要ARP来搞定这个映射关系。比如当一台主机要发送数据到同一个局域网内的另一台主机时,它会先查自己的ARP缓存表,如果已经有目标IP对应的MAC地址就直接用,如果没有就会广播一个ARP请求包,问"谁是这个IP的?告诉我你的MAC"。局域网内所有主机都能收到这个请求,但只有目标IP的那台主机会回应一个ARP应答包,把自己的MAC地址告诉请求者。这样发送方就能拿到MAC地址,完成数据帧的封装和发送。ARP缓存会保存这个映射关系一段时间,避免重复查询,提高效率。
MTU是什么?最大多大?如何突破上限?
MTU(Maximum Transmission Unit)是指网络通信中单次传输的最大数据包大小,包括协议头和数据部分。以太网的默认MTU通常是1500字节,这是大多数局域网的标准值。理论上IP层的最大MTU是65535字节,但实际受限于链路层协议,比如以太网就限制在1500字节。
要突破MTU上限,主要有两种方式:一是分片(Fragmentation),IP层会把大数据包拆成多个符合MTU的小包传输,接收方再重组,但分片会降低效率且容易丢包;二是路径MTU发现(PMTUD),通过ICMP消息动态探测路径上的最小MTU,避免分片,但可能被防火墙阻断。
在应用层,我们可以主动控制发送的数据大小,比如TCP协议通过MSS(Maximum Segment Size)协商来避免IP分片,或者像QUIC这样的协议在应用层就处理分包,减少依赖IP分片。
8.2 套接字
8.2.1 套接字
【必问】套接字是什么?套接字的本质是什么?
套接字本质上就是操作系统提供的一个抽象接口,它封装了底层的网络通信细节,让应用程序可以通过简单的API来进行网络通信。
在Linux里套接字其实就是一个文件描述符,本质为内核借助缓冲区形成的伪文件。
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区(两个缓冲区)。
socket的参数有哪些,分别有什么功能?
int socket(int domain, int type, int protocol);
-
domain (地址族/协议域) 指定通信使用的协议族,常见取值:
-
AF_INET:IPv4协议族(最常用) -
AF_INET6:IPv6协议族 -
AF_UNIX:本地进程间通信(Unix Domain Socket)
-
-
type (套接字类型) 定义通信语义,常见取值:
-
SOCK_STREAM:面向连接的可靠字节流(如TCP) -
SOCK_DGRAM:无连接的数据报(如UDP) -
SOCK_RAW:原始套接字(需root权限)
-
-
protocol (协议类型) 通常设为0,让系统自动选择对应domain/type的标准协议。
套接字的bind是做什么的?服务器不使用bind会发生什么?
将套接字与特定IP地址和端口号绑定,建立本地标识。
如果不调用bind,服务器直接调用listen或accept,系统会随机分配一个临时端口(就像客户端connect时那样),这样客户端就不知道该连接哪个端口,通信根本建立不起来。
套接字的listen是做什么用的?他是负责监听并建立连接的吗?
转换为被动监听套接字,设置同时与服务器建立连接的上限数。
不是,accept才是负责监听并建立连接。
套接字的listen参数backlog的含义?
backlog 是一个非常关键的参数,它指定了等待被 accept 的连接请求队列的最大长度,他不是半连接队列,它是全连接队列的长度,当然,由于半连接队列必 <= 全连接队列的长度,所他也能控制半连接队列的长度。
全连接队列指的是三握已完成,但是 accept 还未接收。
套接字的accept是做什么用的?
从已完成连接队列中取出第一个连接,创建新的已连接套接字用于与该客户端通信。若队列为空且套接字为阻塞模式,则进程会休眠等待。
accept发生在三次握手的哪一步?
accept 并不直接发生在三次握手的某一步,它是在三次握手完成之后,由服务端应用程序主动调用去取走已经建立好的连接的一个系统调用。
让我展开说一下:当一个客户端发起连接时,首先会发送 SYN 包,服务端收到后回复 SYN+ACK,然后客户端再回复 ACK,到这里三次握手就完成了,此时从 TCP 协议层面看,这个连接已经建立起来了,内核已经知道有这样一个连接存在,并且它是可以通信的。
但在这个时候,服务端的 C++ 程序(也就是我们的服务器代码)还没有真正“看到”这个连接,因为连接虽然建立了,但还没有被应用程序“取走”。这个已经建立好的连接会被放到一个队列里,通常是前面提到的“已完成连接队列”(accept queue),而这个队列的长度就跟 listen 函数里的 backlog 参数有关。
接下来,当服务端程序调用 accept 函数时,它实际上是从这个队列里取出一个已经建立好的连接,返回一个新的 socket 文件描述符,之后我们就可以通过这个新的 socket 和客户端进行数据读写了。所以 accept 是应用层去“认领”一个已经完成握手的连接的操作,它本身并不参与三次握手的过程,而是发生在握手完成之后、应用程序处理连接之前。
换句话说,三次握手是 TCP 协议层的事情,accept 是应用层通过系统调用从内核中获取已建立连接的动作,两者是不同层面的操作,但必须配合好,比如如果 accept 不够快,队列满了,新的连接可能就建立不了或者被丢弃了。
套接字的connect是做什么用的?
通过现有套接字与远程服务器建立连接(TCP三次握手或UDP连接绑定)。对于TCP套接字,该调用会触发完整的三次握手过程;对于UDP套接字,则只是设置默认目标地址。
recv返回值,什么错误是可接受的?
首先,>0正常,=0对端关闭。
-1就是错误。
其中:
EAGAIN / EWOULDBLOCK:没有数据可读,暂时阻塞(在非阻塞 socket 下不会等),可接受。
EINTR:系统调用被信号中断,可接受。
TCP连接中,接收方不调用recv,会发生什么情况?
-
数据开始堆积。
-
缓冲区爆满。
-
接收缓冲区被填满,导致发送方无法继续发送(流量控制)。
-
连接被关闭或异常终止。
-
写C++socket代码的时候,网络连接断开有几种判定方式?
-
主动检测
-
send()/write()返回值:-
0:对端关闭。
-
-1:
EPIPE、ECONNRESET。
-
-
recv()/read()返回值:-
0:对端关闭。
-
-1:
ECONNRESET。
-
-
-
被动检测
-
心跳包。
-
epoll超时时间。
-
一个最简单的服务器和客户端分别调用套接字的这些API的流程是什么?
服务器:socket→bind→listen→accept→read、write→close。
客户端:socket→connect→read、write→close。
8.2.2 本地套接字
【必问】对进程通信的方式之一本地套接字有了解吗?
和网络通信的套接字有所不同。
-
socket参数:
int socket(int domain, int type, int protocol);参数domain变为:AF_INET-->AF_UNIX/AF_LOCAL,type写SOCK_STREAI/SOCK_DGRAM都可以。 -
绑定的地址结构:
struct sockaddr_in srv_addr-->structsockaddr_unsrv_adrr。bind的第三个参数也并非srv_addr的sizeof,跟sockaddr_un本身以及sun_path的长度有关。 -
为保证
bind成功,避免因残留的套接字文件导致的绑定失败,通常我们在bind之前,可以使用unlink(sun_path)。 -
客户端不能依赖“隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:
client_addr服务于bind(),server_addr服务于connect()。 -
特别注意本地套接字的地址长度计算为sockaddr_un本身的长度+自己设置的套接字文件的文件名长度。
为什么在本地套接字里,客户端也需要bind,不能依赖自动绑定?
如果客户端依赖自动绑定,系统可能随机分配一个临时路径,服务端无法预知该路径,导致通信失败。因此,客户端通常需要显式指定服务端的已知路径来发起连接。
8.3 TCP/IP
8.3.1 TCP/IP协议
IP协议是做什么的?
IP协议主要负责在网络中寻址和路由数据包,它让数据能够在不同的网络之间传输。简单来说,IP协议给每台联网设备分配一个地址(IP地址),就像现实中的门牌号一样,这样数据就知道该往哪里送。当你的电脑要访问一个网站时,IP协议会负责把数据包从你的设备送到目标服务器,中间可能经过多个路由器,IP协议会根据路由表决定下一跳该往哪个方向走。它不关心数据具体内容,只管把数据包可靠地(在IP层看来)从A点送到B点,至于数据能不能送达、顺序对不对这些事情,就交给上面的传输层协议(比如TCP)去处理了。
IP协议的TTL是做什么的?
TTL(Time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了。仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。
IPv4 vs IPv6有什么区别?
IPv4:
地址长度:32 位
地址表示形式:用点分十进制
地址总数:约 42 亿个
地址短缺问题:严重短缺,需要 NAT、私有地址等方式缓解
IPv6:
地址长度:128 位
地址表示形式:用冒号分隔的十六进制
地址总数:几乎无穷
地址短缺问题:极其丰富,可为地球上每一粒沙子分配独立 IP
域名 vs IP有什么关联?
-
一个域名只能绑定一个IP
-
一个IP地址被多个域名绑定
为什么我们需要端口,为什么不能用进程PID来标识每一个数据包要发往的对象?
进程PID是操作系统动态分配的,进程创建、销毁时PID会变化。若用PID标识,数据包发送时可能因PID改变而无法准确送达。
公网IP怎么映射成私网IP?
这通常是通过一种叫 NAT(Network Address Translation,网络地址转换) 的技术实现的。
-
SNAT(源地址转换):内网设备访问外网时,将源 IP(私网)转为公网 IP。
-
DNAT(目标地址转换,也叫端口映射/端口转发):将公网 IP 的某个端口映射到内网某台设备的 IP 和端口,让外网可以访问内网服务。
此外,如果你的服务是 HTTP/HTTPS 类型的(比如网站、API),你也可以通过一台有公网 IP 的服务器(或云主机)做反向代理,将请求代理到你内网的服务器上。
【必问】TCP协议是做什么的?
TCP协议是传输控制协议,它是互联网通信的基础协议之一,主要负责在不可靠的网络上提供可靠的、面向连接的字节流传输服务。简单来说,它就像是打电话时的连接——双方先建立连接(三次握手),然后确保你说的话(数据)能准确无误地按顺序传到对方耳朵里(可靠传输),最后挂断电话(四次挥手)。
TCP协议头最长多少?最短多少?
TCP 协议头最长可以是 4x15=60 字节,如果没有选项字段,TCP 协议头最短 20字节。
TCP协议头的内容,详细说说?
| 字段名称 | 长度(字节, Byte) | 描述 |
| 源端口号 (Source Port) | 2 | 发送方的端口号,标识发送数据的进程。 |
| 目的端口号 (Destination Port) | 2 | 接收方的端口号,标识接收数据的进程。 |
| 序列号 (Sequence Number) | 4 | 本报文段中第一个数据字节的序号,用于保证数据的有序到达与重传。 |
| 确认号 (Acknowledgment Number) | 4 | 期望收到的下一个数据字节的序号,表示已经成功接收该序号之前的所有数据(仅在 ACK 标志置 1 时有效)。 |
| 数据偏移 (Data Offset) | 0.5(实际为 4bit,表示头部长度) | 表示 TCP 头部的长度,单位是 4 字节,即头部占多少个 4 字节块。因此,实际头部长度 = 数据偏移 × 4。最小为 5(20 字节),最大为 15(60 字节)。 |
| 保留位 (Reserved) | 0.75 | 保留为将来使用,必须置为 0。 |
| 标志位 (Flags) | 0.75 | 包含 6 个重要的控制标志位,每个占 1 bit,用于控制连接状态和数据流。详见下表。 |
| 窗口大小 (Window Size) | 2 | 表示接收方的接收窗口大小(单位:字节),用于流量控制。 |
| 校验和 (Checksum) | 2 | 用于校验 TCP 头部、数据和伪首部的完整性,防止传输错误。 |
| 紧急指针 (Urgent Pointer) | 2 | 当 URG 标志置 1 时有效,指向紧急数据的最后一个字节的序号 + 1。 |
| 选项字段 (Options) | 0~5(以 4 字节对齐) | 可选的额外参数,如 MSS(最大报文段长度)、时间戳等。若存在选项,则填充至 32bit 对齐。若无选项则此字段不存在。 |
| 填充 (Padding) | 0~3 字节 | 为了确保整个头部长度是 4 字节的整数倍,可能添加填充字节(通常在选项字段后)。 |
TCP协议头的最前面为什么要安排源端口号在最前面?
TCP协议头把源端口号放在最前面,主要是出于性能和实用性的考虑。因为网络通信中,操作系统内核需要快速定位到对应的套接字进行数据处理,而端口号是套接字标识的关键部分。把源端口和目的端口放在协议头的最前面,可以让网卡和内核协议栈在处理数据包时,用最少的CPU周期就能提取出这两个关键字段,快速完成套接字查找和数据分发。特别是现代网卡支持RSS(接收侧缩放)等技术,能并行处理多个数据流,端口字段靠前能显著提升多核处理网络包的效率。另外,TCP协议设计之初就考虑了头部字段的访问模式,频繁操作的字段放在头部更符合实际使用场景。
TCP数据包怎么到用户态进程的?接受数据包需要几次内核的上下文切换?
-
数据包到达网卡:网卡通过 DMA 将包放入内存,触发中断。❌ 不涉及用户态切换。
-
内核协议栈处理:驱动 → IP → TCP → 放入 socket 接收缓冲区。❌ 内核内部处理。
-
用户调用 recv():发起系统调用,请求数据。第1次:用户→内核。
-
内核拷贝数据到用户空间:从 socket 缓冲区拷贝到用户 buffer。第2次:内核→用户。
8.3.2 三握四挥
【必问】TCP有几次握手?几次挥手?
三次挥手、四次挥手。
-
第一次握手(SYN):客户端向服务器发送一个 SYN(
seq = x) 报文,表示请求建立连接。 -
第二次握手(SYN + ACK):服务器收到 SYN 报文后,会返回一个 SYN + ACK(
seq = y、ack = x + 1) 报文,表示同意建立连接。 -
第三次握手(ACK):客户端收到 SYN + ACK 报文后,会发送一个 ACK (
ack = y + 1)报文,确认服务器的序列号。
-
第一次挥手(FIN)
-
客户端向服务器发送一个 FIN(
seq = u) 报文,表示请求关闭连接。 -
此时客户端处于 FIN_WAIT_1 状态。
-
-
第二次挥手(ACK)
-
服务器收到 FIN 报文后,会返回一个 ACK (
ack = u + 1)报文,表示同意关闭连接。 -
此时服务器处于 CLOSE_WAIT 状态,客户端进入 FIN_WAIT_2 状态。
-
注意:此时服务器可能还有数据需要发送给客户端,但此时客户端不能再发送数据(写缓冲区被关闭,但还可读,还可发送ACK报文)。
-
-
第三次挥手(FIN)
-
当服务器完成数据发送后,会向客户端发送一个 FIN (
seq = v)报文,表示请求关闭连接。 -
此时服务器处于 LAST_ACK 状态。
-
-
第四次挥手(ACK)
-
客户端收到 FIN 报文后,会发送一个 ACK 报文(
ack = v + 1),确认服务器的 FIN。 -
此时客户端进入 TIME_WAIT 状态,等待一段时间(通常为 2MSL,最大报文生存时间)以确保服务器收到 ACK。
-
服务器收到 ACK 后,进入 CLOSED 状态,连接关闭。
-
客户端在 TIME_WAIT 结束后也进入 CLOSED 状态。
-
TCP为什么需要三次握手?两次为什么不行?
TCP需要三次握手主要是为了可靠地建立连接,同时同步双方的初始序列号。两次握手是不够的,因为这样只能保证客户端的发送能力和服务器的接收能力正常,但无法确认服务器的发送能力和客户端的接收能力是否正常。举个例子,如果客户端发送的SYN包因为网络延迟很久才到达服务器,服务器会回复SYN-ACK,这时候如果只有两次握手,客户端收到后就会认为连接已建立。但实际上客户端可能已经放弃了这个连接(比如超时重传了新的SYN包),而服务器却认为连接已建立并开始发送数据,这样就会造成资源浪费和数据错误。三次握手通过让客户端再回复一个ACK,既确认了服务器的发送能力,也确保双方使用的初始序列号是一致的,这样才能保证后续通信的可靠性。
DDos攻击主要攻击哪一次握手?
DDoS攻击最常见的是针对TCP三次握手中的第一次握手,也就是SYN包阶段,也就是所谓的SYN Flood攻击。
攻击者会伪造大量的SYN请求包发送给目标服务器,服务器收到后会回复SYN+ACK并进入半连接状态,等待客户端返回最后的ACK来完成握手。但由于这些请求是伪造的,或者攻击者根本不发送最后的ACK,导致服务器上积累了大量未完成的半连接,占满了半连接队列(它和 listen() 的 backlog 参数不是同一个东西,但backlog作为全连接队列长度也能限制半连接队列长度),耗尽服务器的资源,使得正常的用户无法完成握手建立连接,从而达到拒绝服务的目的。所以,DDoS攻击主要针对的就是TCP三次握手中的第一次握手所触发的资源消耗环节。
DDos攻击如何防御?
方法 1:SYN Cookie(最经典、最有效的软件防御机制)
原理:
-
正常情况下,服务端在收到 SYN 后,会为每个半连接分配内存,保存客户端信息(IP、端口、序列号等),放在半连接队列(SYN Queue)中。
-
使用 SYN Cookie 后,服务端不再为每个 SYN 保存状态信息,而是通过一个特殊的算法,将连接信息(如客户端 IP、端口、时间戳等)编码进返回的 SYN-ACK 报文中的初始序列号(ISN)中,即 “Cookie”。
-
只有当客户端真正返回了有效的 ACK(第三次握手)时,服务端才通过解析 ACK 中的序列号,还原出之前的连接信息,才真正创建连接。
也就是说:服务端在没收到 ACK 之前,完全不保存任何状态,避免了资源浪费。
方法 2:限速与黑名单(网络层/传输层防护)
原理:
-
利用防火墙、iptables、DDoS 防护设备或云服务商的安全组功能,对来自某个 IP 的 SYN 包进行速率限制,比如:
-
限制每秒只能发送 N 个 SYN;
-
超过阈值的 IP 认为是恶意来源,直接丢弃或拉黑;
-
RST是做啥的?TCP第三次握手丢失,但是客户端向服务端发送信息了会怎样?
-
当客户端发来的数据到达服务端时,服务端的内核协议栈会认为这是一个非法的、未建立的连接上收到的数据。
-
服务端会回复一个 RST(Reset)包,表示“这个连接我根本不认识,或者没建立好,你不能发数据过来”。
-
客户端收到 RST 后,就会知道连接出问题了,通常会关闭 socket,并可能进行重连。
结论:服务端会拒绝处理该数据,发回 RST,客户端会感知到连接异常。
TCP为什么需要四次挥手?
-
半关闭:TCP 是全双工通信,客户端和服务器可以同时发送和接收数据。因此,一方发送 FIN 只能关闭自己的发送通道,另一方可能还有数据需要发送。
-
防止数据丢失:通过四次挥手,确保双方都确认连接已关闭,避免数据丢失。
客户端发送数据(Data)这个阶段有时候还会连同一个ACK一起发给服务器,为什么会有这样的设计?
防止三次握手的时候,最后一次ACK发给服务器失败,所以会捎带一个ACK。如果确定服务器收到,则该ACK可以不发。
8.3.3 TCP状态
【必问】TCP状态转换?
开始:客户端CLOSE,服务器LISTEN
客户端向服务器发送SYN:客户端CLOSE→SYN SEND
服务器向客户端发送SYN+ACK:服务器LISTEN→SYN RCVD
客户端向服务器发送ACK:客户端SYN SEND→ESTABLISHED,服务器SYN RCVD→ESTABLISHED
客户端向服务器发送FIN:客户端ESTABLISHED→FIN_WAIT1
服务器向客户端发送ACK:客户端FIN_WAIT1→FIN_WAIT2,服务器ESTABLISHED→CLOSE_WAIT
服务器向客户端发送FIN:服务器CLOSE_WAIT→LAST_ACK
客户端向服务器发送ACK:客户端FIN_WAIT2→TIME_WAIT,服务器LAST_ACK→CLOSE
经历2MSL后:客户端TIME_WAIT→CLOSE(大约40秒),只有主动关闭连接方,会经历该状态
【必问】为什么需要2MSL时长?
-
确保最后一个ACK可靠到达。若没到达,被动方会继续发FIN过来,然后你再发ACK回去。当然也不能让被动方无止境发下去,所以设置了大概40秒的时间。
-
防止旧连接的延迟数据包干扰新连接。2MSL的等待时间确保旧连接的残留数据包已从网络中消失,避免干扰新连接。
同时关闭(同时向对方发FIN)会发生什么?
会进入CLOSING状态。双方都会收到对方的 FIN,并各自回复一个 ACK。其中一方(通常是先收到 ACK 的一方)会进入 TIME_WAIT 状态,等待 2MSL 时间后关闭连接。另一方 则直接进入 CLOSED 状态(因为它的 FIN 已经被确认,且不需要再等待 ACK)。
FIN_WAIT1和FIN_WAIT2状态有什么区别?为什么要区分这两个状态?
之所以要区分这两个状态,是因为TCP是全双工的,关闭连接需要双方各自关闭自己的发送通道。FIN_WAIT1到FIN_WAIT2的转变意味着主动方已经完成了自己的"发送关闭"部分,但还要等待对方完成它的"发送关闭"。这样设计可以确保连接双方都能有序地释放资源,避免出现数据丢失或连接异常终止的情况。如果直接合并这两个状态,可能会导致主动方在对方还没准备好时就关闭连接,造成数据传输不完整。
【必问】大量CLOSE_WAIT的原因是什么?
-
程序没有正确关闭 socket(忘记调用 close())
-
程序卡住 / 阻塞(例如一直在循环,没考虑recv() == 0),导致没有执行到 close()
8.3.4 可靠性
【必问】TCP如何保证传输可靠性?
-
重传机制
-
滑动窗口
-
流量控制
-
拥塞控制
TCP重传机制是为了解决什么问题?
对方不给我回ACK,数据就丢失了。
TCP重传机制的具体实现?
序列号+应答机制。
超时重传:内置定时器,超过一定时间就重新发包。
快速重传:如果发送方收到 3 个或更多重复的 ACK(Dup ACK),就认为那个缺失的报文段可能丢失了,于是立即重传该数据段(不用等 RTO 超时),这称为 快速重传。
TCP快速重传机制是传丢失的那个数据包还是全部重传?
TCP 的 快速重传机制(Fast Retransmit) 只会重传那个被认为丢失的数据包(即缺失的那个报文段),而不会重传所有后续未确认的数据包。
如果 TCP 启用了 SACK(Selective Acknowledgment,选择性确认,RFC 2018),接收方可以告诉发送方:
“我已经收到了 Seg4 和 Seg5,但没收到 Seg3”
这样发送方就可以更精确地只重传丢失的 Seg3,而不用猜测。SACK 大大增强了快速重传的准确性。
TCP 还有一个 DSACK 机制,把重复发送的 SACK 重传包告知发送方,用于分析只是返回的 ACK 丢失、网络慢、还是数据包真的丢了。
TCP有超时重传为什么还需要快速重传机制?
TCP 需要快速重传机制,是因为超时重传的响应太慢,在高延迟或高吞吐网络环境下会导致性能严重下降。快速重传能更早、更高效地检测并重传丢失的数据包,显著提升 TCP 的传输效率和响应速度。
TCP滑动窗口是为了解决什么问题?
如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会丢失数据。
滑动窗口的大小最大多大?
滑动窗口是16位,最大65536。
TCP滑动窗口的具体实现?
发多个包过去,只需要回一个ACK。无需对每一个包应答。
TCP流量控制是为了解决什么问题?
这个问题=TCP滑动窗口是为了解决什么问题?
TCP流量控制的具体实现?
收缩滑动窗口+收缩缓冲区。
流量控制的一个特殊情况:窗口为 0
TCP 引入了零窗口探测(Zero-Window Probe)机制,即发送方会定期发送一个小的探测包,询问接收方窗口是否重新开放
TCP拥塞控制是为了解决什么问题?
当网络情况不好的时候,发送方继续发送大量数据,就会导致延迟和丢包。如果配合重传机制,延迟和丢包越来越大。
TCP拥塞控制的具体实现?
-
设置一个拥塞窗口cwnd,发送窗口 = min(cwnd, rwnd)。
-
即:发送方的实际发送窗口大小,是由 拥塞窗口(cwnd) 和 接收窗口(rwnd) 两者取最小值决定的。
-
-
如何判断网络情况如何?慢启动!
-
一开始发的包很小,每收到一个 ACK,发的包指数级变大。
-
到某个界限时,发的包线性增大。
-
-
窗口大小设置具体方法?
-
超时重传:网络没救了,发送的包大小斩半,再重新慢启动。
-
快速重传:网络可能还有救,适度降低发送速率,然后尝试继续传输,而不是完全回退到慢启动。
-
心跳包是什么?为什么需要心跳包?
在 TCP 网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测査询链接状态。常用的解决方法就是在程序中加入心跳机制。
心跳包的实现方式有哪些?
-
Heart-Beat线程:这个是最常用的简单方法。在接收和发送数据时个人设计一个守护进程(线程),定时发送Heart - Beat包,客户端/服务器收到该小包后,立刻返回相应的包即可检验对方是否实时在线。
-
在
setsockopt函数内,设置SO_KEEPALIVE属性使得我们在2小时以后发现对方的TCP连接是否依然存在。 -
UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测,而是在业务层以心跳包做检测比较好,也方便控制。
心跳包的最小粒度是什么?为什么心跳包粒度需要较小?
心跳包的最小粒度通常取决于网络协议和应用需求,但理论上它可以小到一个单独的网络数据包,比如一个简单的 TCP ACK 或自定义的 UDP 消息。比如在 TCP 中,我们可以利用 Keepalive 机制,它本质上是发送一个空的 ACK 包来检测连接状态;而在 UDP 中,心跳包可能就是一个几十字节的自定义消息。
心跳包粒度需要较小的原因主要有两个:第一,较小的包可以减少网络带宽的占用,尤其是在高频发送时,避免对网络造成不必要的负担;第二,较小的包能更快地被发送和接收,降低检测延迟,确保连接状态的实时性。比如在高并发场景下,如果心跳包太大,可能会拖慢整个系统的响应速度,而小粒度的心跳包可以更高效地完成保活检测。
【必问】TCP粘包是什么?怎么造成的?
TCP粘包主要是因为TCP是面向字节流的协议,没有消息边界概念,多个小包可能被合并发送,或者一个大包可能被拆分接收。
怎么造成的?
-
发送的时候,内核进行了优化,数据到达一定量发送一次。
-
网络环境不好,有延时。
-
接收方接收数据频率低,一次性读到了多条客户端发送的数据。
【必问】TCP粘包有哪些解决方案?
-
发送的时候,强制缓冲区数据被发送出去 -> flush
-
在发送数据的时候每个数据包添加包头
-
包头: 一块内存,存储了当前这条消息的属性信息
-
属于谁 -> char[12]
-
有多大 -> int
-
-
TCP的保序性如何实现?
TCP 的保序性是通过给每个字节的数据都分配一个序列号(sequence number)来实现的。发送方在发送数据时,会给每个字节编号,比如第一个字节的序号是某个初始值,后续字节依次递增。接收方根据这些序列号来判断数据包的到达顺序。
当数据包在网络中传输时,由于路由不同等原因,它们可能不会按照发送顺序到达接收端。接收方收到数据后,会根据序列号将数据包重新排序,确保按照正确的顺序将数据交给上层应用。如果某个包还没到,即使后面的包先到了,接收方也会先把后面的包暂存起来,等前面的包到了再按序交付。
还有延迟 ACK ,接收方在收到数据后,并不会立即发送 ACK,而是会稍微等一会儿(通常是几十毫秒),看看有没有数据要回传,或者能不能和下一个数据包一起捎带确认,以此减少网络上的 ACK 数量,提高效率。
Nagle算法是什么?Nagle算法为什么游戏服务器常常要关闭?
Nagle算法是TCP协议里一个优化网络传输的小机制,TCP会先把这些小数据包攒起来,等收到上一个包的ACK或者攒够一定量再一起发出去。
游戏服务器通常要关掉Nagle算法,就是因为游戏需要极低的延迟。比如FPS游戏里玩家按W键前进,这个指令必须立刻发到服务器处理,如果被Nagle算法缓冲了几百毫秒,玩家就会感觉“卡顿”。
8.3.5 半关闭
服务器主动关闭导致必须要等待2MSL怎么办?
设置端口复用解决这个问题的方法是使用 setsockopt()设置 socket 描述符的选项 SO_REUSEADDR为 1,表示允许创建端口号相同但 IP 地址不同的多个 socket 描述符。
半关闭状态是什么?如何用代码实现半关闭?
A可以接收B发送的数据,但是A已不能再向B发送数据(FIN_WAIT_2)。
使用shutdown函数。shutdown不考虑描述符的引用计数,直接关闭描述符,其他重定向dup到这个文件的文件描述符也被关闭。也可选择中止一个方向的连接,只中止读或只中止写。
TCP半连接队列是干啥的?怎么指定大小?
TCP 半连接队列(SYN Queue 或 SYN_RECV Queue),也叫 SYN 队列,是 TCP 三次握手过程中,用于存放那些已经收到了 SYN 包(客户端发起连接请求),但还没有完成 三次握手 的连接的一个队列。
只有当客户端返回了 ACK,完成三次握手后,这个连接才会从半连接队列中移除,并转移到“全连接队列”(Accept Queue),等待应用程序调用 accept() 获取。
为什么需要半连接队列?
-
TCP 是面向连接的,服务器必须记住哪些连接已经收到了 SYN,但还没完成握手。
-
如果没有这个队列,服务器收到 SYN 后若立即回复 SYN-ACK,但还没记录这个连接状态,那么当客户端发来 ACK 时,服务器就无法识别这个连接,导致连接失败。
-
半连接队列就是用来暂存这些 “已收到 SYN,但还未完成握手” 的连接信息,保证连接的可靠性。
在 Linux 系统中,半连接队列(SYN Queue)的大小不是单独一个参数控制的,而是由以下 两个内核参数共同决定:
-
net.ipv4.tcp_max_syn_backlog -
somaxconn和backlog
连接关闭的判断有几种?
-
一般IO
-
read == 0 客户端读端关闭
-
write == -1 && errno == EPIPE 客户端写端关闭
-
-
Reactor模型
-
IO多路复用
-
EPOLLRDHUP 读端关闭
-
EPOLLHUP 读写端都关闭
-
-
8.4 异步网络
8.4.1 select/poll/epoll
为什么我们需要IO多路转接?
多路 IO 转接服务器也叫做多任务 IO 服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
select的各个参数?
-
nfds:要监听的最大文件描述符值 + 1。 -
*readfds:用于监听可读事件的文件描述符集合。如果传入 NULL,则不关心可读事件。 -
*writefds:用于监听可写事件的文件描述符集合。如果传入 NULL,则不关心可写事件。 -
*exceptfds:用于监听异常事件的文件描述符集合。 -
*timeout:设置select()的超时时间,控制select()阻塞行为。
select的返回值是什么的数量?
出现在返回集合中的、真正有写、读或异常响应的文件加起来的个数。
select如何设置要监听的文件描述符?
fd_set是一个只有0和1的位图,被监听的文件的位设为1。
fd_set需要使用专有的一组函数操作。
【必问】select的优点和缺点?
缺点:
监听上限受文件描述符限制。最大 1024。
检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。
时间复杂度 O(n):每次调用 select 都需要遍历所有文件描述符,检查哪些是就绪的。
优点:
跨平台:win、linux、macOS、Unix、类Unix、mips。
select的数组改进版改进了什么?还存在什么问题?
优化了轮询机制,但是不改变本质。
select的本质?底层原理?
当用户调用 select 函数时,内核会将用户传入的 fd_set 集合复制到内核空间,并开始轮询检查这些文件描述符的状态;如果没有任何文件描述符就绪,内核会让调用进程进入阻塞状态(或根据系统配置进行忙等待),直到至少一个文件描述符变为就绪状态(例如套接字接收到数据、连接建立完成等)或者超时时间到达;当有文件描述符就绪时,内核会更新 fd_set 集合中的相应标志位,并唤醒等待的进程,最终用户程序通过检查返回的 fd_set 来确定哪些文件描述符已经准备好进行 I/O 操作。
poll的优点和缺点?
优点:
-
传入、传出事件分离。无需每次调用时,重新设定监听事件。
-
文件描述符上限,可突破 1024 限制。能监控的最大上限数可使用配置文件调整。
缺点:
-
不能跨平台。 Linux特供。
-
无法直接定位满足监听事件的文件描述符,编码难度较大。
poll如何突破文件描述符上限?
可以使用 cat 命令查看一个进程可以打开的 socket 描述符上限。
cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。每往上调一次,需要注销一次用户。
从底层原理来说,为什么poll可以突破文件描述符的上限?
从底层原理来看,poll能突破文件描述符上限主要是因为它不像select那样依赖固定大小的位图(fd_set)来存储监控的fd。select的fd_set通常在编译时就被定义成固定大小,比如常见的1024位,这就直接限制了它能监控的最大fd数量。而poll改用了一个pollfd结构体的数组,这个数组是动态分配的,理论上只要系统内存允许,就可以分配足够大的数组来监控任意数量的fd。
pollfd结构体本身很简单,就是一个fd加上事件掩码和返回事件,每次调用poll时,用户态把整个数组传给内核,内核再遍历这个数组检查每个fd的状态。由于它不依赖固定大小的位图,所以不会像select那样被硬编码的上限卡住。不过,虽然poll在fd数量上更灵活,但它仍然需要把整个数组从用户态拷贝到内核态,而且每次调用都要全量遍历,这在fd数量特别大时性能还是会下降。
不过,poll每次调用均需线性扫描所有 fd,时间复杂度为 O(n),效率低于 epoll 的 O(1) 事件通知机制。
epoll的各个参数?
-
epoll_create:-
size:无作用,只是为了兼容性。
-
-
epoll_ctl:-
epfd:epoll 实例的 fd。 -
op:操作类型,表示你要对这个 fd 做什么:添加、修改、删除 -
fd:你要操作的目标文件描述符,比如一个 socket fd。 -
*event:指定你要监听的事件类型以及用户数据。
-
-
epoll_wait:-
epfd:epoll 实例的 fd。 -
*events:用于接收就绪事件的数组,由用户分配内存。 -
maxevents:events数组最多能存放多少个事件(必须 > 0)。 -
timeout:超时时间(毫秒),控制epoll_wait的阻塞行为。
-
【必问】为什么epoll效率显著高于select/poll?从他的底层角度说说?
从底层实现来看,epoll效率显著高于select和poll,主要归功于三个核心机制:红黑树管理fd、就绪链表通知机制、以及边缘触发(ET)模式。
首先,epoll用红黑树来存储监控的fd,而不是像select/poll那样每次调用都要传入整个fd数组。红黑树的插入、删除和查找时间复杂度都是O(log n),这使得epoll_ctl添加/删除fd的操作非常高效。而select/poll每次都要全量传递fd集合,不仅内核要重新遍历,用户态和内核态之间的数据拷贝开销也很大。
其次,epoll采用就绪链表来管理活跃fd。当内核检测到某个fd就绪时,会直接把它挂载到就绪链表上,而不是像select/poll那样每次调用都要遍历所有fd。epoll_wait只需要检查就绪链表是否为空,如果非空就直接返回就绪的fd,避免了无效遍历。这种机制在连接数很大但活跃连接很少时(比如长连接服务),性能优势特别明显。
最后,epoll支持边缘触发(ET)模式,而select/poll只有水平触发(LT)。ET模式下,内核只会在fd状态变化时通知一次,除非应用主动读完/写完数据,否则不会重复触发。这减少了不必要的事件通知,让程序可以更高效地批量处理数据,而不是频繁被唤醒。
所以,epoll的高效本质上是数据结构优化(红黑树)、事件通知机制优化(就绪链表)、以及触发模式优化(ET)共同作用的结果,而select/poll受限于固定fd集合和全量遍历,在高并发场景下性能差距会越来越大。
【必问】epoll的操作函数主要有哪三个?都是做什么的?
epoll_create:创建epoll文件描述符以及监听红黑树。
epoll_ctl:操作epoll红黑树,包括添加fd、修改fd、取消fd监听。
epoll_wait:阻塞监听。
(追问)struct epoll_event *event和struct epoll_event *events是一样的类型吗?
-
epoll_ctl里的参数epoll_event是一个结构体指针。区别于poll和epoll_wait,这不是数组。 -
epoll_wait里的参数events是传出参数,它是一个epoll_event数组!代表满足监听条件的那些 fd 结构体。
epoll_wait等待占用CPU吗,分几种情况?
-
情况 1:
timeout = -1(阻塞等待,无限等待)或timeout > 0(有限时间等待)-
如果没有注册的 fd 上有事件发生,调用线程会被挂起(进入睡眠状态),不会消耗 CPU。
-
CPU 占用:不占用 CPU(线程休眠中)。
-
-
情况 2:
timeout = 0(非阻塞模式,立即返回)-
CPU 占用:如果你在循环里反复调用
epoll_wait(..., 0),而实际上又一直没有事件,那么就会造成忙等待(busy-looping),进而 100% 占用 CPU!
-
-
情况 3:LT模式使用不当
-
没有一次性读取/写入完所有数据,导致事件一直触发、程序不断被唤醒,循环处理,可能造成高 CPU 占用。
-
【必问】ET vs LT有什么区别?
Edge Triggered(ET):边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered(LT):水平触发只要有数据都会触发。
epoll的ET模式可以设置非阻塞,那么ET模式下不设置非阻塞,或者LT模式下设置了非阻塞,分别会发生什么?
-
ET 模式下强烈建议将 fd 设置为非阻塞模式。如果 fd 是阻塞的,在某些情况下可能导致程序阻塞(卡死),从而影响整个服务的响应和性能。
-
ET 模式的特点是:它只会在文件描述符的状态发生变化时通知一次(比如从不可读变为可读,或从不可写变为可写),之后即使数据未处理完,也不会再次通知,除非状态再次变化。
-
举个例子,假设你监听一个 socket 的可读事件,当有数据到达时,
epoll_wait返回该 fd 可读。但在 ET 模式下,它只会通知你一次,不管当前缓冲区里有多少数据。 -
如果你此时调用
read(),但 没有一次性把数据读完(比如只读了一部分),而且 fd 是阻塞的,那么:-
如果数据没有全部读完,但暂时没有更多数据到达(比如对端还没发完),下一次
epoll_wait不会再通知你该 fd 可读(因为 ET 只在状态变化时通知); -
如果你继续调用
read()希望读剩下的数据,但此时已经没有数据可读,而 fd 是阻塞的,那么read()将一直阻塞,直到有新的数据到来 —— 这就导致你的线程/进程卡在这里,无法处理其他 fd。 -
👉 这就是为什么在 ET 模式下,必须将 fd 设为非阻塞的:
-
你可以安全地循环调用
read()或write(),直到返回EAGAIN或EWOULDBLOCK(表示暂时没数据可读/没空间可写); -
这样你就能确保在事件触发后,尽可能地处理完所有数据,而且不会因为无数据可操作而阻塞。
-
-
-
LT 模式下设置 fd 为非阻塞是完全合法且常见的做法,不会导致问题,而且通常是推荐做法,尤其是配合非阻塞 I/O 编程模型时。
-
LT 模式的特点是:只要文件描述符处于就绪状态(比如有数据可读、可写),
epoll_wait就会持续通知你,直到你将该状态消费掉(比如读完数据或写完缓冲区)。 -
在 LT 模式下:
-
即使你将 fd 设置为非阻塞,也是完全没问题的;
-
每次
epoll_wait返回该 fd 可读/可写时,你可以调用read()/write(); -
如果当前没有足够的数据可读(或缓冲区已满不可写),并且你使用的是非阻塞 fd,那么相应的系统调用(如 read/write)会立即返回 -1,并设置 errno 为 EAGAIN / EWOULDBLOCK,这是正常现象;
-
你可以根据这个返回值决定是否稍后再试,或者做其他处理。
-
👉 LT 模式本身不强制要求 fd 是阻塞还是非阻塞,但:
-
如果你使用非阻塞 fd,你需要正确处理 EAGAIN/EWOULDBLOCK 错误;
-
如果你使用阻塞 fd,那么在数据未完全就绪时,
read()/write()可能会阻塞,这可能会影响程序的并发性能,尤其是在高并发场景下。
-
-
epoll ET模式如何一次性把数据读完?
首先,我会用一个循环来读取数据,直到返回EAGAIN或EWOULDBLOCK错误。因为ET模式下,只要fd可读,epoll_wait就会通知一次,但如果只读了一部分数据,剩下的数据不会再次触发通知,所以必须确保这次通知处理完所有可用数据。比如,用read()或recv()循环读取,直到返回值小于请求的字节数(非阻塞模式下表示数据已读完)或者遇到EAGAIN错误(表示当前没有更多数据可读)。
其次,如果数据量很大,比如超过了一个缓冲区的大小,我会确保缓冲区足够大,或者动态扩展缓冲区,避免数据截断。当然,实际开发中,我们一般不会一次性分配超大缓冲区,而是分批读取,但关键是要保证在ET模式下,这次通知处理期间必须把所有数据读完,否则下次epoll_wait可能不会触发。
另外,如果是写事件,ET模式同样要求一次性写完。如果write()返回EAGAIN,说明内核缓冲区已满,这时候不能继续写,必须等下次可写事件通知再来处理剩余数据。
【必问】epoll reactor(反应堆)思想是什么?
epoll reactor(反应堆)思想本质上是一种事件驱动的编程模型,它的核心在于用epoll高效监听大量fd的事件,再通过回调机制解耦事件检测和处理,从而实现高并发的网络编程。
简单来说,reactor模式就像一个“事件调度中心”——它用epoll_wait监听多个fd(比如socket)上的读写事件,当某个fd就绪时,epoll返回这个fd和对应的事件类型,reactor不会直接处理业务逻辑,而是通过预先注册的回调函数(比如读回调、写回调)交给对应的模块去处理。这样就把“事件检测”和“事件处理”分离开来,程序结构更清晰,扩展性也更强。
这种设计的好处是“事件来了才处理”,避免了轮询的资源浪费,而且通过回调机制,业务逻辑和处理流程解耦,代码更灵活。像Nginx、Redis这些高性能服务器都用了类似的reactor模型,只不过可能结合了多线程或多进程进一步优化。
当然,reactor也有单线程、多线程、多进程等变种,但核心思想不变——用epoll高效管理事件,用回调驱动业务,这也是现代高并发网络编程的基石之一。
epoll reactor的具体实现?
struct epoll_event的data共用体里面的*ptr可以设置为一个结构体指针,结构体内可以包含一个回调函数。当事件发生时,可以将事件分发到对应的回调函数处理。
创建epoll红黑树 → epoll_wait阻塞监听 → epoll_wait有事件返回 → cfd从监听红黑树上摘下 → 触发回调函数 → (可能)修改这个fd以及回调事件 → 这个新cfd又被放上监听红黑树 → epoll_wait继续阻塞监听。
reactor模型是如何处理网络IO的?
监听谁?reactor内部使用epoll来监听多个文件描述符(sockets)。
监听什么事件?epoll的监听和分发事件:比如连接请求(accept)、读(read)、写(write)等 I/O 事件。
如何监听?reactor通常运行在一个主循环中,不断调用多路复用器等待事件发生,然后分发处理,因此也常被称为 "事件循环"。
监听到了事件之后?当某个 I/O 事件就绪时,Reactor 会调用与该事件关联的回调函数或处理器(Handler)来处理具体的业务逻辑,比如读取数据、解析协议、业务处理、写回响应等。
epoll的惊群是什么?如何解决?
epoll 的惊群问题,指的是当多个进程或线程在同时监听同一个监听 socket(比如通过 epoll_wait 等待新连接到来)时,一旦有一个新的客户端连接进来,内核会唤醒所有正在等待该 socket 的进程或线程,但实际上,这个新连接只会被其中一个进程或线程通过 accept 成功取走,其他被唤醒的线程或进程会发现 accept 没有新连接可拿,白白被唤醒,这就是所谓的“惊群效应”。
惊群会导致不必要的 CPU 资源竞争和上下文切换,影响性能,尤其是在高并发场景下,如果大量线程被无意义地唤醒,会明显降低服务器的整体效率。
在早期的 Linux 内核中,这个问题是比较明显的,比如多个进程通过 accept 同一个监听 socket,或者多个线程通过 epoll_wait 监听同一个 socket 时,新连接到来会触发所有等待者被唤醒。
针对这个问题,Linux 内核和编程方式都有相应的优化和解决方案:
-
使用
EPOLLEXCLUSIVE标志(Linux 4.5+) 这是解决 epoll 惊群的一个较新的机制。当你在调用epoll_ctl添加监听 socket 到 epoll 实例时,可以加上EPOLLEXCLUSIVE这个标志。它的意思是:当有事件发生时,只唤醒 epoll_wait 队列里的某一个等待者,而不是全部。这样可以有效避免多个线程或进程同时被唤醒。不过要注意,这个标志是后来内核才支持的,并不是所有环境都默认可用。 -
每个进程/线程独立监听不同的 socket(比如通过 accept_mutex 或 SO_REUSEPORT) 另一种常见的做法是不要让多个线程/进程监听同一个 socket。比如在使用多进程模型时,可以让每个子进程都
accept同一个监听 socket(传统方式,但可能仍有惊群),或者使用 SO_REUSEPORT 选项(Linux 3.9+),让多个进程或线程各自绑定一个相同的端口,内核会自动把连接负载均衡地分发给它们,这样每个 socket 只被一个进程监听,就不存在多个线程抢一个连接的问题,从根本上避免了惊群。 -
使用单线程 accept + 任务队列分发 在一些设计中,会采用单线程负责 accept 新连接,然后将连接任务放入队列,再由工作线程池处理具体业务逻辑。这样连接建立的过程本身不会引发惊群,因为只有一个线程在监听和接收新连接。
总结来说,epoll 惊群就是多个线程/进程被无意义地同时唤醒去处理一个新连接,而实际上只有一个能成功。解决方式包括使用 EPOLLEXCLUSIVE、SO_REUSEPORT 让每个线程独立监听、或者采用单线程 accept 的架构来避免多个线程竞争同一个 socket。在实际项目中,根据你的服务器模型和内核版本,选择合适的方案即可。
epoll的线程安全分析?
epoll_wait 的线程安全性:安全。
epoll_ctl 的线程安全性:不安全。
解决方法:
-
一个专门的线程负责管理 epoll(包括 epoll_ctl 和 epoll_wait),
-
其他线程只负责业务逻辑,通过线程安全队列等方式与 I/O 线程通信。
8.4.2 boost.ASIO库
boost.ASIO是做什么的?
Boost.Asio 是一个跨平台的 C++ 库,用于网络和底层 I/O 编程,它提供了异步 I/O 操作的强大支持,被广泛应用于开发高性能、可扩展的网络应用和分布式系统。
boost.ASIO做异步IO的底层实现是什么?如何做到异步?
与操作系统底层异步机制对接:
Boost.Asio 根据不同的操作系统,使用其原生的异步 I/O 接口,主要包括:
-
Windows:IOCP (I/O Completion Ports)。
-
Linux:epoll + 非真正的异步 I/O。由于缺乏完善的异步网络 I/O 支持,Boost.Asio 通常使用 epoll/kqueue(Reactor) 监听 socket 状态,当 socket 可读/可写时通知应用,再由应用完成数据读写,对用户表现为异步,但本质是同步 I/O + 事件通知机制。
-
macOS / BSD:kqueue。
使用boost.ASIO进行异步编程的通用流程是什么?
-
创建
io_context对象 -
创建 socket(如
tcp::socket) -
发起异步操作(如
async_connect,async_read,async_write) -
提供回调函数(lambda、std::bind、成员函数等)来处理异步完成事件
-
调用
io_context.run()启动事件循环,处理异步回调 -
(可选)在回调中发起下一个异步操作,形成异步操作链
boost.ASIO的定时器是同步的还是异步的?你用过吗?
ASIO 的定时器是 异步的,也就是说它不会阻塞线程,而是通过回调函数在时间到达时通知你,非常适合用在事件驱动模型中。
-
asio::steady_timer(推荐,最常用):基于 单调时钟(monotonic clock),不受系统时间调整的影响,适合用于 间隔/超时计时。推荐在大多数情况下使用。 -
asio::system_timer:基于系统实时时钟(可能受用户修改时间影响),适合需要与实际日历时间相关的定时。 -
asio::high_resolution_timer:高精度定时器(具体实现依赖平台)
boost.ASIO的定时器底层数据结构是什么?如何调度?
ASIO 默认使用 “最小堆(Min-Heap)” 来管理定时器。
具体来说:
-
ASIO 内部维护了一个 定时器队列(Timer Queue)
-
这个队列中的每个元素代表一个 待触发的定时任务(包含触发时间 + 回调 handler)
-
ASIO 使用 最小堆(通常是
std::multimap或类似的有序容器)来高效地获取“最早要触发的那个定时器”
boost.ASIO的strand是什么?如何无锁地让线程串行执行?
在 Boost.Asio(或 C++20 的 std::asio)中,strand(中文常译为“执行绪串”或“执行序串”) 是一个用于 保证特定处理程序(handlers)以序列化(即顺序、不并发)方式执行 的机制。
如果没有同步机制,多个 handler 可能同时运行在多个线程上,就会造成 数据竞争(data race),这是未定义行为,可能导致程序崩溃或数据不一致。
一种常见的解决办法是使用 互斥锁(mutex) 来保护共享数据,但锁会带来复杂性和性能开销,也容易引发死锁。
strand 提供了一种更优雅的解决方案:它不使用锁,而是保证相关的 handlers 永远不会并发执行,而是按顺序执行。
8.4.3 IOCP
IOCP是什么?
IOCP(I/O Completion Ports,I/O 完成端口) 是 Windows 操作系统提供的一种高性能、可伸缩的 异步 I/O 模型,主要用于处理大量并发的网络或磁盘 I/O 操作,尤其在服务器端编程中非常常见。
IOCP做异步IO的底层实现是什么?如何做到异步?
虽然 Windows 没有公开 IOCP 的全部内核实现细节,但根据资料与实践可以总结如下:
-
内核为每个支持异步 I/O 的设备(如文件、Socket)维护异步上下文
-
当应用调用
ReadFile(..., &OVERLAPPED),如果设备支持异步,内核将 I/O 请求加入设备队列,立即返回 -
设备的驱动程序在后台完成 I/O 操作(不占用应用线程)
-
I/O 完成后,内核将完成信息(如传输字节数、状态、OVERLAPPED 指针等)放入与该句柄关联的 IOCP 完成队列中
-
应用线程通过
GetQueuedCompletionStatus从该队列中获取完成事件,进而处理结果
也就是说,真正的数据读写过程是由操作系统内核异步执行的,与应用线程并发,当操作完成时,通过内核队列通知应用,这就是真正的异步 I/O。
使用IOCP进行异步编程的通用流程是什么?
-
创建 IOCP 对象
-
使用
CreateIoCompletionPort函数创建一个 I/O 完成端口。
-
-
将 I/O 句柄绑定到 IOCP
-
比如将一个 socket 或文件句柄与 IOCP 关联起来,依然使用
CreateIoCompletionPort函数。 -
这样当该句柄上的异步 I/O 操作完成时,系统就会向关联的 IOCP 投递一个“完成包”。
-
-
发起异步 I/O 操作
-
比如使用
WSARecv/WSASend(对于网络)或ReadFile/WriteFile(对于文件)等异步 API 发起 I/O。 -
这些调用会立即返回,真正的 I/O 操作在后台进行。
-
-
等待并处理完成通知
-
应用程序线程调用
GetQueuedCompletionStatus函数在 IOCP 上阻塞等待。 -
当有 I/O 完成时,系统会将完成信息(如传输的字节数、错误码等)放入队列,线程被唤醒并处理这些完成包。
-
IOCP中的overlapped是什么?
OVERLAPPED(重叠结构)是 Windows 异步 I/O 操作的核心数据结构,用于标识和跟踪每一个异步 I/O 请求。在 IOCP 模型中,它使得操作系统能够在 I/O 操作完成后,将结果正确关联到对应的请求,从而实现异步通知与 I/O 管理。
IOCP vs epoll有什么区别?
IOCP:
-
操作系统:Windows(微软专属)
-
I/O 模型类型:异步 I/O + 完成通知模型(基于 Overlapped I/O)
-
线程模型:通常配合少量工作线程,通过 GetQueuedCompletionStatus 获取完成通知,由系统调度
-
事件通知机制:基于 I/O 完成通知(告诉你 I/O 已经完成,直接处理数据)
-
文件 I/O 支持:支持异步文件 I/O(如 ReadFile/WriteFile + IOCP)
-
IOCP一次投递请求,完成一次
epoll:
-
操作系统:Linux(主流 Linux 发行版)
-
I/O 模型类型:事件驱动的 I/O 多路复用 模型(仍基于非阻塞 I/O)
-
线程模型:通常一个或多个线程调用 epoll_wait 等待事件,然后处理就绪的 fd
-
事件通知机制:基于 I/O 就绪通知(告诉你 fd 可读/可写了,但你要自己读写)
-
文件 I/O 支持:通常不支持(Linux 的文件 I/O 异步支持较弱,一般不用 epoll 处理文件)
-
epoll一次投递请求,可通知多次
【必问】Proactor vs Reactor有什么区别?
Reactor(反应器)模式:
-
定义:一种 事件驱动 的设计模式,应用程序通过监听一组 I/O 事件(如可读、可写),当事件发生时,由应用程序主动执行相应的 I/O 操作(如读/写数据)。
-
核心特点:
-
同步非阻塞 I/O + 事件通知机制
-
应用注册感兴趣的事件 → 事件就绪后通知 → 应用自己执行读写
-
Proactor(前摄器)模式:
-
定义:一种 异步 I/O 的设计模式,应用程序发起 I/O 操作后,由操作系统完成实际的 I/O(如磁盘/网络读写),并在完成后通知应用程序结果。
-
核心特点:
-
真正的异步 I/O(内核完成 I/O)
-
应用发起异步操作 → 操作系统完成 I/O → 通知应用 I/O 已完成,直接处理数据
-
8.4.4 io_uring
IO是什么?网络IO是什么?文件IO是什么?
IO(Input/Output,输入/输出) 是计算机中数据在计算机内部与外部之间传输的过程。简单来说:
-
输入(Input):将数据从外部(如键盘、鼠标、网络、文件等)读入到程序(内存)中。
-
输出(Output):将程序中的数据写入到外部(如屏幕、文件、网络等)。
IO 可以分为多种类型,常见的有:
-
标准 IO(Standard IO):比如使用
cin和cout进行控制台输入输出。 -
文件 IO(File IO):对磁盘上的文件进行读写操作。
-
网络 IO(Network IO):通过网络连接与其他计算机进行数据交换。
-
内存 IO:比如对共享内存、缓冲区的读写。
文件 IO(File Input/Output) 是指程序与存储设备上的文件(如硬盘上的文本文件、二进制文件等)之间进行数据的读取和写入操作。
网络 IO(Network Input/Output) 是指程序与网络上其他计算机(或服务)之间通过网络协议(如 TCP/IP)进行数据的接收和发送。
在Linux系统上,一般是网络IO更慢还是文件IO更慢?
一般来说,在 Linux 系统上,网络 IO(Network IO)比文件 IO(File IO)要慢,而且通常慢得多。
-
文件 IO:读写的是本地磁盘(如 SSD、HDD)上的文件,数据通过计算机内部的总线、存储控制器直接访问,不经过网络。
-
网络 IO:数据需要通过网卡 → 网线/无线信号 → 路由器/交换机 → 另一台机器的网卡,可能跨越局域网甚至互联网,物理距离更远,中间环节更多。
-
但也有一些“例外情况”:文件 IO 访问的是机械硬盘(HDD),而网络 IO 是本地高速局域网(如万兆网卡 + RDMA)。
fio、iops、iodepth都是什么?
fio(Flexible I/O Tester,灵活的 IO 测试工具) 是一个在 Linux/Unix 等系统上广泛使用的、功能强大的 IO 性能压测工具,用来模拟各种 IO 负载,测试磁盘、文件系统、存储设备的性能,比如吞吐量、IOPS、延迟等。
IOPS(Input/Output Operations Per Second,每秒输入输出操作数) 是衡量存储设备(如硬盘、SSD)性能的关键指标之一,表示存储设备每秒钟能够处理的 IO 请求数量。
iodepth(I/O Depth,IO 深度) 是指在异步 IO 模型下,同时发起的未完成(in-flight)IO 请求数量,也就是IO 队列的深度。
io_uring是什么?
io_uring 是 Linux 内核自 5.1 版本引入的一种高性能异步 I/O(Asynchronous I/O)框架,旨在提供比传统异步 I/O 机制(如 aio)更高效、更灵活的 I/O 操作方式。它通过减少系统调用次数、提高并发性和降低延迟,显著提升了 I/O 密集型应用程序的性能。
【必问】io_uring做异步IO的底层实现是什么?如何做到异步?
-
零拷贝(Zero-copy)支持
-
通过共享内存环(ring buffer)在用户态和内核态之间传递 I/O 请求和完成通知,减少数据拷贝和上下文切换。
-
-
批量提交与批处理完成
-
允许一次性提交多个 I/O 请求,并批量获取完成事件,显著降低系统调用开销。
-
-
灵活的提交/完成队列
-
使用两个环形缓冲区(Submission Queue 和 Completion Queue):
-
Submission Queue (SQ):用户态向内核提交 I/O 请求。
-
Completion Queue (CQ):内核向用户态返回 I/O 完成结果。
-
-
支持无锁设计,用户态和内核态通过内存屏障同步,避免锁竞争。
-
-
支持多种 I/O 场景
-
文件读写(
readv/writev)、网络套接字(send/recv)、内存映射(mmap)等。
-
SQ和CQ是什么?
-
Submission Queue (SQ):用户态程序向其中提交 I/O 请求(即 SQE,Submission Queue Entry),告诉内核要执行什么操作(如读、写、accept 等)。
-
Completion Queue (CQ):内核将 I/O 操作的完成结果(即 CQE,Completion Queue Entry)放入其中,用户态程序从中获取执行结果。
SQ 和 CQ 都是基于连续内存的环形缓冲区(Ring Buffer)。
-
它们不是链表,而是固定大小的、由数组实现的环形队列(circular array)。
-
它们被映射到用户和内核共享的内存区域,通过
mmap实现,避免了用户态和内核态之间大量的数据拷贝。 -
SQ 和 CQ 的元信息(如头尾指针、大小等)也存放在共享结构体中,用户和内核通过原子操作来同步读写位置(head/tail)。
SQ的entry与CQ的entry有什么区别?
-
SQ(Submission Queue)的 entry 是用户程序用来 描述和提交一个异步 IO 请求 的数据结构(如
struct io_uring_sqe),它包含 IO 的类型(如读/写)、目标 fd、缓冲区地址、长度等信息。 -
CQ(Completion Queue)的 entry 是内核在异步 IO 完成后,用来 通知用户程序 IO 执行结果 的数据结构(如
struct io_uring_cqe),它包含请求的完成状态、返回值(如读取的字节数)、以及对应的请求标识(user_data),以便用户匹配是哪个请求完成了。
他们共用同一块内存,但是是同一块内存的两个独立区域,彼此互不干扰。
使用io_uring进行异步编程的通用流程是什么?
-
设置三个结构体:
struct io_uring ring、struct io_uring_sqe *sqe、struct io_uring_cqe *cqe -
执行原本的
socket、bind、listen -
初始化io_uring:
io_uring_queue_init -
把ring和sqe绑定:
io_uring_get_sqe -
异步accept:
io_uring_prep_accept绑定各种参数,io_uring_sqe_set_data64设定唯一请求token标识该请求,io_uring_submit确定提交-
异步接收,把ring和cqe绑定:
io_uring_wait_cqe、获取单个cqe唯一请求token标识:io_uring_cqe_get_data64 -
亦或者获取一整个cqe队列:
io_uring_peek_batch_cqe,再io_uring_cqe_get_data64循环里一个一个取出来唯一请求token标识 -
遍历完记得清空cqe
-
io_uring vs epoll有什么区别?
io_uring:
-
I/O 模型类型:支持真正的异步 IO(async IO),包括文件和网络,不需要等待 IO 完成
-
系统调用次数:可通过批量提交 IO 请求 + 批量获取完成事件,极大减少系统调用次数
-
事件通知机制:提交-完成模型(Submit & Completion Queue):你提交 IO 请求,后续异步获取完成结果,真正的“IO 和程序执行分离”
-
文件 I/O 支持:通用异步 IO(包括网络 IO 和文件 IO,如读写磁盘文件)
epoll:
-
I/O 模型类型:事件驱动的 I/O 多路复用 模型(仍基于非阻塞 I/O)
-
系统调用次数:每次监听用
epoll_wait,但每个实际 IO(如 read/write)仍需独立系统调用 -
事件通知机制:基于 I/O 就绪通知(告诉你 fd 可读/可写了,但你要自己读写)
-
文件 I/O 支持:通常不支持(Linux 的文件 I/O 异步支持较弱,一般不用 epoll 处理文件)
io_uring vs IOCP有什么区别?
io_uring:
-
操作系统:Linux(高级 Linux 发行版)
-
系统调用次数:可通过批量提交 IO 请求 + 批量获取完成事件,极大减少系统调用次数
-
事件通知机制:提交-完成模型(Submit & Completion Queue):你提交 IO 请求,后续异步获取完成结果,真正的“IO 和程序执行分离”(用户态实现)
IOCP:
-
操作系统:Windows(微软专属)
-
系统调用次数:较多(每个异步操作可能需要独立系统调用)
-
事件通知机制:基于 I/O 完成通知(告诉你 I/O 已经完成,直接处理数据)(内核态实现)
io_uring是Reactor模式还是Proactor模式?
io_uring 是典型的 Proactor 模式实现。 但它相比传统 Proactor 模式(如 Windows IOCP)更加灵活和高效,因为它通过用户态直接控制提交与完成队列,实现了更高性能的异步 IO 操作。
不过,io_uring 本身并不强制绑定某种模式,它的设计足够底层,也可以被用来构建 Reactor 模式,但从其原生使用方式和设计哲学来看,它更贴近 Proactor 模式。
8.4.5 libevent
libevent的整个事件框架是什么样的?
-
创建event_base
-
创建事件event
-
常规事件
-
带缓冲区的事件
-
-
将事件添加到base上
-
循环监听事件满足
-
释放 event_base
event的未决和非未决是什么?
-
刚创建:非未决->add到base底座:未决
-
base开始循环dispatch、对应事件被触发:未决->激活
-
回调函数被调用:激活->被处理
-
被处理:被处理->非未决
-
已经设置EV_PERSIST,或继续add到base底座:非未决->未决
bufferevent和普通event有什么区别?什么时候该用bufferevent?
bufferevent有两个缓冲区:也是队列实现,读走没,先进先出。文件描述符被包装进bufferevent内部。
读:有数据 --> 读回调函数被调用-->使用 bufferevent_read()--> 读数据。
写:使用 bufferevent_write()-->向写缓冲中写数据 --> 该缓冲区有数据自动写出->写完,回调函数被调用(鸡肋)。
libevent TCP服务器、客户端的实现流程?
服务器:
-
创建event_base。
-
evconnlistener_new_bind创建监听服务器。当有客户端成功连接时,内部回调函数会被调用。
-
创建bufferevent事件对象。bufferevent_socket_new()。
-
使用bufferevent_setcb()函数给 bufferevent的read_cb、write_cb、event_cb 设置回调函数。当监听的事件满足时,cb会被调用,在其内部 bufferevent_read()读(或者写)。
-
read_cb
-
write_cb
-
-
设置enable读写缓冲区。
-
-
启动循环监听。
-
释放连接。
客户端:
-
创建 event base
-
使用 bufferevnet_socket_new()创建一个用跟服务器通信的 bufferevnet 事件对象
-
使用 bufferevnet socket_connect()连接 服务器
-
使用 bufferevent_setcb()给 bufferevnet 对象的 read、write、event 设置回调。
-
设置 bufferevnet 对象的读写缓冲区 enable/disable
-
接收、发送数据 bufferevent_read()/bufferevent_write()
-
释放资源
8.5 UDP
8.5.1 高性能UDP
UDP协议是做什么的?
UDP协议是用户数据报协议,它和TCP最大的区别就是不保证可靠传输,但换来了更低的延迟和更简单的实现。你可以把它想象成寄平信——你扔出去就不管了,对方可能收到也可能收不到,顺序也可能乱掉,但胜在速度快、开销小。
一个UDP包最多能装多少数据?
UDP包能装的数据量受两个关键限制:协议本身的头部设计和网络MTU(最大传输单元)。
从协议层面看,UDP头部固定占8字节,IP头部固定占20字节(不考虑选项字段),所以理论上IPv4下UDP数据部分最大是 65535(IP包总长) - 20(IP头) - 8(UDP头) = 65507字节。但这是极端理论值,实际根本用不了。
真正决定因素是网络的MTU,也就是路由器能转发的最大帧大小。以太网默认MTU通常是1500字节,扣除IP头20和UDP头8后,单UDP包实际能装的有效数据最多约1472字节(1500-20-8)。如果超过这个值,IP层会分片传输,但分片会增加丢包风险(只要丢一个分片整个包就废了),而且UDP本身不分片重组,得应用层自己处理,麻烦又容易出问题。
【必问】UDP vs TCP的优缺点?
TCP(传输控制协议)
特性
-
面向连接,提供可靠的数据包传输
-
针对不稳定的网络层,采用完全弥补策略(如丢包重传)
优点
-
稳定性高:确保数据完整、流量稳定、传输速度恒定、顺序正确
缺点
-
效率较低:传输速度慢、资源占用高、协议开销大
适用场景
-
对数据完整性要求高的场景,效率为次要考虑
-
大数据传输(如文件下载)
-
文件传输(如FTP、邮件附件)
-
UDP(用户数据报协议)
特性
-
无连接,提供不可靠的数据报传递
-
针对不稳定的网络层,采用完全不弥补策略(直接传递原始网络状况)
优点
-
高效性高:传输速度快、资源占用低、协议开销小
缺点
-
稳定性差:数据可能丢失、流量/速度不稳定、顺序无法保证
适用场景
-
对时效性要求高,稳定性其次的场景
-
实时交互应用(如在线游戏)
-
流媒体传输(如视频会议、视频电话)
-
【必问】TCP做了什么使得它比UDP更稳定呢?
首先,TCP有连接管理,通信前必须通过三次握手建立连接,双方确认彼此的初始序列号和能力,这避免了UDP那种“盲发”的不可控性。通信结束后还要四次挥手释放连接,确保双方都正常终止,不会留下“僵尸状态”。
其次,TCP通过序列号和确认应答机制保证数据不丢不乱。每个字节都有唯一序号,接收方收到数据后必须回ACK确认,发送方如果超时没收到ACK就会重传。而UDP发出去就不管了,丢包只能靠上层应用自己发现。
然后,TCP有流量控制和拥塞控制。流量控制通过滑动窗口让接收方告诉发送方“我能处理多少数据”,避免接收方缓冲区溢出。拥塞控制更厉害,比如慢启动、拥塞避免、快重传这些算法,会根据网络状况动态调整发送速率,防止网络拥塞导致的大面积丢包。UDP完全不管这些,使劲发可能直接把网络堵死。
还有,TCP是面向字节流的,会把应用层的数据拆成多个段传输,接收方再按序组装,即使网络抖动导致乱序也能正确重组。UDP则是面向报文的,发多少收多少,乱序了也没办法。
最后,TCP还有错误检测,每个报文段都有校验和,如果数据损坏会被直接丢弃并触发重传。UDP也有校验和但可选,很多实现为了性能直接关了。
(相似问题)如何在应用层添加数据校验协议,弥补UDP的不足?
-
数据校验与完整性验证。确保数据在传输过程中未被篡改或损坏。 添加校验和:UDP头部自带简单的16位校验和,但可扩展为更强大的校验算法(如CRC32、SHA-1等)。 添加哈希校验:使用SHA-256等算法生成数据指纹,接收方比对验证完整性。
-
丢包重传机制。检测并重传丢失的数据包。 添加序列号:为每个数据包分配唯一序号,接收方通过序号检测丢包并请求重传。 设置超时重传:设置定时器,超时未收到ACK则重传。
-
乱序重组。处理因网络抖动导致的数据包乱序问题。 接收方维护缓冲区,按序号排序后重组数据流。
-
流量控制与拥塞避免。防止网络过载导致丢包加剧。 滑动窗口协议:动态调整发送窗口大小,类似TCP的流量控制。 自适应速率控制:根据网络延迟和丢包率动态调整发送速率。
实际应用案例:
-
Google的QUIC协议基于UDP实现了类似TCP的可靠性,包含:多路复用(避免队头阻塞)、前向纠错(FEC)、0-RTT快速连接。
-
实时音视频(WebRTC):使用FEC(前向纠错)和NACK(否定确认)减少重传延迟。
-
在线游戏(如《英雄联盟》):关键数据(如位置)用可靠UDP传输,非关键数据(如特效)用普通UDP。
UDP为什么快?
-
传输速度:更快(协议头部更小,开销小,无确认重传)
-
传输延迟: 低(无连接、无握手、无重传等待)
-
适用场景:实时性要求高、允许少量丢包
在编写UDP服务器时,使用的socket的API有哪些变化?
-
不需要三握四挥建立连接,
accpet()、connect()不被需要。 -
socket的第二个参数从
SOCK_STREAM变为SOCK_DGRAM。 -
listen()函数也用不上了,可有可无。 -
由于没有连接、没有
cfd,一般的read()和recv()无法使用,需要使用UDP专用的recvfrom()。同理write()被替换为sendto()。 -
客户端
connect()不被需要,直接使用recvfrom()、sendto()即可。
为什么UDP普通服务器和客户端就已经可以实现并发访问?
UDP是无连接的协议,普通服务器和客户端能实现并发访问主要有以下原因:首先,UDP无需建立和维护连接,服务器可同时接收多个客户端的数据包,每个数据包独立处理,不存在连接状态的限制;其次,服务器可在一个端口监听,不同客户端的数据包通过IP和端口号区分,能并行处理来自多个客户端的请求,从而实现并发访问。简言之:没限制,代码里设了IP和端口就可以访问服务器。
为什么UDP客户端设置的bind函数无用?
因为UDP是无连接的,客户端不绑定端口时,操作系统会在发送数据时自动分配一个临时端口作为源端口。若手动bind,会固定客户端的源端口,限制其灵活性,可能因端口被占用导致绑定失败,且多数场景下自动分配端口就能满足需求,所以一般不进行bind操作。
UDP客户端的recvfrom的后两个参数为什么可以写NULL和0?
调用者不关心数据包的来源地址,服务器的IP和端口本来就是固定的。
(追问)那为什么UDP的服务端recvfrom和sendto都需要客户端的地址?
因为先接收了之后是向指定的客户端发数据(sendto),并非不关心sendto的发送地址。而客户端的接收是可以不关心的,服务器的IP和端口本来就是固定的。
客户端分别通过TCP和UDP发三个包,服务端可能会收到几个?
TCP会粘包拆包,所以会收到1~N个,但不会是0个,因为有重传机制。
UDP过大也只会在IP层拆包,最终收到前会重组,所以收到的数量不会增加。但是没有重传,可能会丢包,所以最终收到0~3个。
8.5.2 KCP
KCP是什么?
KCP 是一个运行在 UDP 之上的应用层协议,目标是实现快速、可靠的数据传输,尤其适用于对延迟敏感、要求高吞吐量的网络通信场景。
它并不是一个独立的传输层协议(不像 TCP 或 UDP 那样内置于操作系统内核),而是一个用户态协议,需要开发者自己集成到应用程序中,通常配合 UDP 一起使用。
KCP vs UDP做了哪些提升?
-
KCP 就是在 UDP 基础上,手动实现了可靠传输机制,包括:
-
数据重传
-
滑动窗口
-
流量控制
-
快速重传
-
保序性
KCP vs TCP有什么不同?
KCP:
-
更灵活,可自定义:他不是传输层协议,而是在用户态,基于 UDP
-
拥塞控制没那么复杂:可配置
-
低延迟:TCP丢包时可能主动降速,重传等待时间长。而KCP快速重传、可配置策略
8.5.3 QUIC
QUIC是什么?
QUIC是一个基于 UDP 的、现代化的传输协议,由 Google 提出并开发,目标是解决传统 TCP + HTTPS/TLS 在 Web 和网络通信中的性能瓶颈,同时提供更安全、更快速、更可靠的网络连接。
QUIC = 可靠 + 安全 + 高效 + 多路复用 + 0-RTT 快速连接,全部跑在 UDP 上!
【必问】QUIC vs UDP做了哪些提升?
-
可靠的传输
-
每个数据包都有序号,丢了会重传,保证数据不丢、不乱。
-
-
加密与安全
-
所有 QUIC 数据默认都使用 TLS 1.3 加密,保证通信安全,防止中间人攻击和窃听。
-
-
多路复用
-
一条 QUIC 连接上可以并行传输多个数据流(Stream),不同 Stream 之间互不阻塞,解决了 HTTP/2 在 TCP 上的队头阻塞问题。
-
-
0-RTT / 1-RTT 连接
-
首次连接可能需要 1-RTT(Round-Trip Time),但后续连接(比如同一个网站)可以做到 0-RTT,即无需等待直接发送数据,极大提升访问速度。
-
-
连接迁移
-
传统 TCP 连接与 IP 地址绑定,一旦你从 Wi-Fi 切换到 4G,连接就会断掉。
-
QUIC 使用 连接 ID 而非 IP+端口来标识连接,因此即使 IP 变了,只要连接 ID 不变,连接依旧可以保持,用户体验无缝。
-
-
改进的拥塞控制
-
QUIC 将拥塞控制算法实现在用户态,可以更灵活地选择或自定义算法(如 Cubic、BBR 等),而不像 TCP 一样被操作系统限制。
-
QUIC vs TCP有什么不同?
传统 TCP + TLS(如 HTTP/1.1 / HTTP/2):
-
传输层协议:TCP
-
连接建立速度:慢(TCP 握手 + TLS 握手,通常 1~3 RTT)
-
多路复用:HTTP/2 支持,但受限于 TCP(队头阻塞)
-
队头阻塞问题:TCP 层和 HTTP/2 都可能存在队头阻塞
-
加密与安全:TLS 是额外的协议层(如 TLS 1.2/1.3)
-
连接迁移:IP 变化(如 Wi-Fi 切换 4G)会导致连接断开
-
拥塞控制:依赖操作系统实现的 TCP 拥塞控制算法
-
传输控制:由内核控制,难以定制
特性:QUIC(基于 UDP)
-
传输层协议:UDP
-
连接建立速度:极快(支持 0-RTT 或 1-RTT)
-
多路复用:天然支持多路复用,无队头阻塞
-
队头阻塞问题:完全解决了传输层和应用层的队头阻塞
-
加密与安全:加密内建,QUIC 默认强制使用 TLS 1.3
-
连接迁移:支持连接迁移(IP 变了连接依然存活)
-
拥塞控制:可在用户态灵活实现多种拥塞控制算法
-
传输控制:完全在用户空间实现,灵活可优化
QUIC vs KCP有何相同?有何不同?
相同:
-
协议层次:应用层(基于 UDP)
-
是否可靠:是
-
传输控制:用户态控制,灵活可优化
不同:
-
KCP:
-
是否加密:否(需自己实现)
-
连接速度:中等(依赖配置)
-
多路复用:一般不涉及
-
队头阻塞:一般不涉及
-
连接迁移:不支持
-
-
QUIC:
-
是否加密:是(内置 TLS 1.3)
-
连接速度:快(支持 0-RTT / 1-RTT)
-
多路复用:无队头阻塞
-
队头阻塞:完全解决
-
连接迁移:支持(基于 Connection ID)
-
QUIC的应用?
-
Web 浏览(HTTP/3):QUIC 是 HTTP/3 的唯一官方传输协议,用于替代 TCP + TLS,让网页加载更快、更稳。
-
实时通信 / 视频会议:低延迟、抗丢包、快速重连,非常适合 VoIP、直播、视频会议类应用。
-
游戏、IM、P2P:高效、灵活、可定制的传输控制,适合对网络性能要求高的应用。
8.6 HTTP
8.6.1 HTTP
【必问】HTTP是什么?
HTTP(HyperText Transfer Protocol,超文本传输协议) 是一种用于在互联网上传输超文本(如网页)的应用层通信协议。它是现代 Web(万维网,World Wide Web)的基石,主要用于客户端(如浏览器)和服务器之间的数据通信。
-
应用层协议:HTTP 属于 OSI 模型中的应用层,建立在 TCP/IP 协议之上,通常运行在 TCP 的 80 端口(HTTP)或 443 端口(HTTPS)。
-
无状态(Stateless):HTTP 默认不记录请求之间的状态,即服务器不会记住你之前访问过什么,每次请求都是独立的。(可以通过 Cookie、Session 等机制实现有状态)
-
基于请求-响应模型:通信总是由 客户端发起请求,服务器返回响应,不能反过来。
HTTP为什么默认接受TCP连接?UDP不行吗?
该问题等价于TCP的优点、UDP的缺点。
例外情况:HTTP over UDP(如QUIC)
说几个HTTP的请求方法?
-
GET:请求指定的页面信息,并返回实体主体。
-
POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
-
PUT:从客户端向服务器传送的数据取代指定的文档的内容。
-
DELETE:请求服务器删除指定的页面。
【必问】GET vs POST有什么不同?
-
设计目的
-
GET:用于请求获取资源,即从服务器获取数据,不应该对服务器上的数据产生副作用(如修改、删除等)。
-
POST:用于向服务器提交数据,通常会导致服务器状态发生变化,比如创建新资源、提交表单、上传文件等。
-
-
数据位置
-
GET:数据通过 URL 的查询字符串(query string) 传递,例如:/search?q=http
-
POST:数据放在 请求体(request body) 中发送给服务器
-
-
安全性
-
GET:不安全(不是指加密,而是指数据暴露在 URL 上,容易被窃取或缓存)
-
POST:相对更安全(数据不在 URL 中,但依然需要 HTTPS 才真正安全)
-
POST的四种方式?
数据发送出去,还要服务端解析成功才有意义,服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。
-
application/x-www-form-urlencoded
-
application/json
-
text/xml
-
multipart/form-data
说一下HTTP常用状态码?
状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
-
1xx:指示信息--表示请求已接收,继续处理
-
2xx:成功--表示请求已被成功接收、理解、接受
-
3xx:重定向--要完成请求必须进行更逃一步的操作
-
4xx:客户端错误--请求有语法错误或请求无法实现
-
5xx:服务器端错误--服务器未能实现合法的请求
【必问】HTTP有哪些版本?各有什么不同?
-
HTTP/1.0:第一个正式版本。支持请求头、响应头、状态码、多种方法(GET/POST等),但每个请求独立连接。
-
HTTP/1.1:最广泛使用的版本。支持持久连接、管道化(pipelining)、Host 头、缓存控制等,但存在队头阻塞问题。
-
HTTP/2:二进制协议,多路复用。二进制传输、多路复用、头部压缩、服务器推送,解决 HTTP/1.1 队头阻塞,但仍在 TCP 上。
-
HTTP/3:基于 QUIC 的新协议。基于 UDP 的 QUIC 协议,彻底解决队头阻塞,更快的连接建立,更强的安全性与性能。
长连接和短链接是什么?
短连接(Short Connection)
-
每次通信完成后,客户端与服务器就断开连接。
-
如果下次还需要通信,必须重新建立连接。
优点:
-
实现简单,管理方便。
-
适用于偶发、低频访问的场景。
缺点:
-
频繁建立和断开连接开销大(尤其是 TCP,握手/挥手的延迟和资源消耗不可忽视)。
-
不适合高并发、频繁请求的场景。
长连接(Long Connection / Persistent Connection)
-
客户端与服务器之间建立一次连接后,保持该连接一段时间甚至长期不关闭,在此期间可以进行多次数据交互。
-
只有在空闲超时或者不再需要时,才主动断开连接。
优点:
-
减少 TCP 连接建立和断开的开销,提升性能和响应速度
-
更适合频繁交互、实时性要求高的场景
-
能够更好地利用网络资源与服务器连接资源
缺点:
-
连接需要维护,可能占用服务器资源(如文件描述符、内存、线程等)
-
需要处理连接保活、超时断开、异常重连等问题
-
实现相对复杂,需要考虑更多网络异常、连接状态管理
如何实现一个长连接?
实现长连接通常从两个层面考虑:传输层(如 TCP) 和 应用层协议设计。
传输层:保持 TCP 连接不断开
TCP 本身是面向连接的,只要双方都不主动关闭连接,并且网络稳定,那么这个 TCP 连接就可以一直保持(长连接)。
-
设置合理的超时时间
-
使用 心跳机制(Heartbeat)
-
连接保活(Keepalive)
应用层:协议设计与实现
自己定义即可,只需支持上述的保活TCP即可。
如果不想自己定义应用层,说几个支持长连接的应用层协议?
-
HTTP 长连接(Keep-Alive)
-
HTTP/1.1 默认启用
Connection: keep-alive,即多个 HTTP 请求可以复用一个 TCP 连接。 -
HTTP/2 和 HTTP/3 更进一步支持多路复用、服务推送等特性,也是基于长连接。
-
-
WebSocket
-
是一种基于 TCP 的应用层协议,通过 HTTP 协议升级建立,支持全双工、长连接、实时通信。
-
-
MQTT
-
MQTT 是物联网中广泛使用的轻量级发布/订阅协议,基于 TCP 长连接。
-
8.6.2 WebSocket
WebSocket是什么?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的应用层协议,它使得客户端和服务器之间可以建立持久的连接,并实现实时的双向数据传输。它的诞生主要是为了解决 HTTP 在实时通信方面的局限性。
【必问】WebSocket vs HTTP有什么优势?我们为何需要WebSocket?
-
双向通信:HTTP 1.x 通常是客户端发起请求(request),服务器才能响应(response),服务器不能主动向客户端推送数据。这就导致了实时性差,如果想要实现“服务器主动推送消息”,需要使用一些“曲线救国”的方案,比如:轮询(Polling)、长轮询(Long Polling)。而 WebSocket 实现了双向通信:客户端和服务器可以互相主动发送数据。相比轮询等方式,极大地减少了网络流量与延迟。
-
长连接:一次握手,建立持久连接,减少重复建立连接的开销。
-
低冗余:没有冗余的 HTTP 头信息(WebSocket 帧很小,只传有效数据)。
WebSocket实现这些优势的底层原理是什么?
-
双向通信:在握手完成后,通信双方通过 WebSocket 帧(Frame)格式 在 TCP 连接上直接收发数据;帧可以由客户端发起,也可以由服务器发起;没有“请求-响应”的约束,双方可以随时推送数据。
-
长连接:首先,TCP 协议支持应用层协议长连接。其次,WebSocket 在建立之初,通过一次 HTTP 握手,将连接从 HTTP “升级”为 WebSocket,之后就复用这条 TCP 连接进行全双工通信,不再断开。
-
低冗余:跟 WebSocket 帧有关。
使用WebSocket的通信流程是什么?
-
握手阶段(建立连接):客户端通过 HTTP 协议发起一个特殊的请求,请求升级为 WebSocket 协议。
-
如果服务器支持 WebSocket,会返回
101 Switching Protocols,表示同意升级协议。 -
一旦完成这个握手,HTTP 协议就“升级”成了 WebSocket 协议,之后的通信就不再使用 HTTP,而是使用 WebSocket 自有的帧格式进行全双工的数据传输。
-
-
数据通信阶段:连接建立后,客户端和服务器可以随时主动发送数据给对方,数据以帧(frame)的形式传输,支持文本、二进制数据等。
Sec-WebSocket-Key和Sec-WebSocket-Accept字段是什么?
这是 WebSocket 握手中的安全验证机制,用来确认双方都愿意并能够进行 WebSocket 通信。
-
客户端在请求中发送一个随机的
Sec-WebSocket-Key(Base64 编码字符串)。 -
服务器收到后,将该 key 拼接上固定字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算 SHA-1 哈希值,再做 Base64 编码,得到Sec-WebSocket-Accept,并返回给客户端。 -
如果客户端验证这个值匹配,就认为握手成功。
这个过程是标准规定的,目的是防止 HTTP 代理等误将 WebSocket 请求当作普通 HTTP 处理。
WebSocket的帧格式是什么?协议的header多大?
FIN:1 bit,是否是消息的最后一帧
RSV1, RSV2, RSV3:各 1 bit,保留位,一般为 0
Opcode:4 bits,帧类型,如 0x1 文本,0x2 二进制,0x8 关闭连接,0x9 Ping,0xA Pong
Mask:1 bit,表示是否对负载数据进行掩码处理(客户端到服务器必须为 1)
Payload length:7 / 7+16 / 7+64 bits,表示数据的长度,可能占用 1~9 字节
Masking-key:0 或 4 bytes,如果 Mask=1,则存在 4 字节的掩码 key
Payload data:可变长度 实际传输的数据内容(文本、二进制等)
header:1bit+3bits+4bits+1bit+7bits(+16+64)+0bit(4)=4字节~13字节。
8.6.3 断点续传
断点续传是什么?
断点续传是指在文件传输(多数是下载)过程中,如果传输中断(比如用户暂停、网络断开等),下次可以 从上次已下载的位置继续传输,而不是重新下载整个文件。
它依赖于 HTTP 协议的 Range 请求头,让客户端可以告诉服务器:“我只需要从第 X 字节到第 Y 字节的数据”。
HTTP的Range请求是什么?
-
通过发送 HTTP HEAD 请求或 GET 请求(不下载 body),读取响应头中的
Content-Length,确认服务端文件总大小。
-
同时检查是否支持 Range 请求(返回状态码
206 Partial Content,即允许分段下载,以及存在Accept-Ranges: bytes响应头)。
若支持 Range 请求,构造 HTTP 请求时,添加请求头:Range: bytes=local_size-,表示“我要从 local_size 开始到文件末尾的所有数据”。
文件的多线程下载如何实现?
多线程下载(也称为分段下载或并发下载)的核心思想是:
将一个大文件分成多个部分(通常是按字节范围),然后使用多个线程同时下载不同的部分,最后将这些部分合并成一个完整的文件。
-
获取文件大小:通过
Content-Length响应头,以及看是否有Accept-Ranges: bytes,表示支持HTTP Range。 -
划分下载区间:假设文件总大小为
file_size,你想使用N个线程下载,则每个线程负责下载file_size / N。 -
每个线程下载指定范围的数据:每个线程创建一个独立的 HTTP 连接,请求指定的字节范围,然后将数据写入文件的对应位置(需要支持随机访问,比如用
std::ofstream的seekp定位,C语言的可以使用mmap)。
(追问)文件的多线程下载有线程安全问题吗?
没有,已经提前划分每个线程写入哪块数据,每个线程写的是不同区域,因此不需要加锁(只要不重叠)。
wmem和rmem是什么?
wmem 和 rmem 是与 套接字(socket) 相关的 缓冲区大小选项,主要用于控制 发送缓冲区(send buffer)和接收缓冲区(receive buffer) 的大小,在网络编程中非常重要,尤其是在高性能、高吞吐量的场景下,如文件下载、视频流等。
在大文件下载 / 上传场景中,合理设置这些值可以显著提升性能:需要更大的缓冲区来容纳更多数据,减少系统调用和网络交互次数。
【必问】断点续传如何实现?
-
检查本地是否已有部分下载的文件:比如文件名为
file.zip,我们可以先检查磁盘上是否已经存在该文件,以及它的大小是多少。 -
获取远程文件的总大小:通过
Content-Length响应头,以及看是否有Accept-Ranges: bytes,表示支持HTTP Range。 -
设置断点位置:如果本地文件存在,假设其大小为
local_size,那么可以从local_size字节处继续下载。 -
以追加模式写入文件:打开本地文件时使用 追加模式(std::ios::app 或 seekp 定位后写入),将新下载的数据追加到文件末尾。
-
支持暂停与恢复:用户可以随时暂停(比如关闭程序),下次启动时重复上述逻辑即可。
文件的多线程下载+断点续传如何实现?
多线程断点续传 = 多线程 + 断点续传
将文件分成多个块,每个线程负责一个块,支持每个块都独立断点续传,从而实现高效、可恢复的大文件下载。
假设文件总大小为 file_size,使用 N 个线程,则:
-
每个线程负责下载一个区间
-
最后一个线程处理剩余部分(处理边界)
-
每个线程:
-
检查自己负责的区间中,已下载了多少字节(断点位置)
-
从断点位置开始,发起 HTTP Range 请求:
bytes=start+已下载字节-1 -
将数据写入文件的 正确偏移位置
-
记录当前已下载的字节数(可保存到本地进度文件或内存)
-
断点续传你用的是哪个C++库?
由于直接使用原生 Socket 处理 HTTP 协议较复杂,推荐使用 libcurl 库,它对 HTTP Range、断点续传等支持非常友好。
8.7 网络安全
8.7.1 密码学
密码学三要素是什么?
-
明文/密文
-
明文 -> 原始数据
-
密文 -> 加密之后的数据
-
-
秘钥
-
定长的字符串
-
对称加密 -> 自己生成
-
非对称加密 -> 有对应的算法可以直接生成
-
-
算法
加密算法有哪两类?
对称加密
非对称加密
对称加密有哪些特点?
-
秘钥只有一个:加密解密使用的秘钥是相同的
-
特点:
-
加密的效率高
-
加密强度相对较低
-
秘钥分发困难,因为秘钥要保空不能泄露,秘钥不能直接在网络环境中进行发送
-
对称加密有哪些算法?3DES?AES?
DES/3DES:
DES -> 已经被破解了, 不安全
-
秘钥长度8byte
-
对数据分段加密,每组8字节
-
得到的密文和明文长度是相同的
3DES->3重des
-
安全的,效率比较低
-
对数据分段加密,每组8字节
-
得到的密文和明文长度是相同的==8字节
-
-
秘钥长度24字节,在算法内部会被平均分成3份,每份8字节
-
看成是3个秘钥
-
每个8字节
-
AES:
-
最安全,效率最高的公开的對称加密算法
-
秘钥长度:16字节,24字节,32字节
-
秘钥越长加密的数据越安全,效率越低
-
-
分组加密,每组长度16字节
-
每组的密文和明文的长度相同 == 16byte
非对称加密有哪些特点?
-
秘钥有两个,所有的非对称加密算法都有生成密钥对的函数
-
这两个秘钥对保存到不同的文件中,一个文件是公钥(比较小),一个是私钥(比较大)
-
公钥 -> 可以公开的
-
私钥 -> 不能公开
-
-
加解密使用的秘钥不同
-
如果使用公钥加密,必须私钥解密
-
如果使用私钥加密,必须公钥解密
-
-
特点:
-
效率低
-
加密强度相对较
-
秘钥可以直接分发 -> 分发的公钥
-
非对称加密有哪些算法?
RSA。
密码学中的哈希算法一般有哪些?MD5?SHA?
MD4/MD5:
-
散列值长度:16字节
-
抗碰撞性已经被破解
SHA:
-
SHA-1
-
散列值长度: 20字节
-
抗碰撞性已经被破解
-
-
SHA-2
-
sha224
-
散列值长度: 224bit / 8 = 28byte
-
-
sha256
-
散列值长度: 256bit / 8 = 32byte
-
-
sha384
-
散列值长度: 384bit / 8 = 48byte
-
-
sha512
-
散列值长度: 512bit / 8 = 64byte
-
-
-
SHA3-224/SHA3-256/SHA3-384/SHA3-512
Base64编码是什么?
Base64是一种基于64个可打印字符来表示二进制数据的表示方法。在Base64中的可打印字符包括字母 A-Z、a-z、数字 0-9,这样共有62个字符,加上+和/,共64个字符。
-
普通的文本数据也可以使用base64进行编解码
-
Base64编解码的过程是可逆的
-
Base64不能当做加密算法来使用
Base64编码有哪些应用?
-
它可用来作为电子邮件的传输编码。
-
邮件传输协议只支持 ASCII 字符传递,因此如果要传输二进制文件:图片、视频是无法实现的。
-
-
HTTP 协议
-
HTTP 协议要求请求行和请求头都必须是 ASCII 编码
-
-
数据库数据读写 - blob(大文件块)
-
数据库中存储中文
-
消息认证码是什么?
作用:
-
在通信的时候,校验通信的数据有没有被篡改
-
没有加密的功能
使用:
-
消息认证码的本质是一个散列值
-
hash(原始数据 + 秘钥)= 消息认证码
-
最关键的数据: 秘钥
-
消息认证码的流程?
-
消息认证码生成
-
需要先假设对称秘钥 X 已经生成(其实这个秘钥分发就已经很复杂了,所以这个步骤导致消息认证码使用受限)
-
A端
-
计算散列值:hash_num_A = hash(原始数据 + X)
-
发送:原始数据 + hash_num_A
-
-
-
消息认证码校验
-
AB端使用的哈希算法是相同的
-
B端
-
接收数据:原始数据 + hash_num_A
-
计算散列值:hash_num_B = hash(原始数据 + X)
-
比较散列值:散列值 hash_num_B 和 hash_num_A 是不是相同
-
相同:没篡改
-
不同:被修改了
-
-
-
数字签名是什么?
-
校验数据有没有被篡改(完整性)
-
鉴别数据的所有者
-
不能对数据加密
数字签名的流程?
-
签名生成
-
A端
-
生成一个非对称加密的密钥对,公钥 A_pub、私钥 A_pri
-
把公钥 A_pub 发给B
-
计算散列值:hash_num_A = hash(原始数据)
-
私钥加密:Cry_num = A_pri(hash_num_A)
-
发送:原始数据 + Cry_num
-
-
-
签名校验
-
B端
-
假设已收到公钥 A_pub
-
接收数据:原始数据 + Cry_num
-
计算散列值:hash_num_B = hash(原始数据),和签名的时候使用的哈希函数相同(必须相同)
-
公钥解密:hash_num_A = A_pub(Cry_num)
-
比较散列值:散列值 hash_num_B 和 hash_num_A 是不是相同
-
相同: 数据的所有者确实是A,并且数据没有被篡改
-
不同: 数据的所有者不是A或者数据被篡改了
-
-
-
密钥分发是什么?数据的加密整体流程分别在什么时候用到对称加密,什么时候用到非对称加密?
-
假设我有数据需要发送
-
这时候就需要加密,使用非对称加密,效率比对称加密高
-
但是对方不知道你非对称加密的秘钥,你直接把秘钥发过去又有风险
-
所以第一次通信时需要使用非对称秘钥把对称秘钥发过去,这样从第二次开始就可以使用对称秘钥通信
-
这个“用非对称加密发送对称加密的秘钥”的过程就是秘钥分发
密钥分发的流程?
-
A端
-
生成一个非对称加密的密钥对,公钥 A_pub、私钥 A_pri
-
发送:A_pub
-
-
B端
-
接收数据:A_pub
-
在客户端生成一个随机字符串 X,这就是对称加密需要使用的密钥
-
公钥加密:Cry_num = A_pub(X)
-
发送:Cry_num
-
-
A端
-
接收数据:Cry_num
-
私钥解密:X = A_pri(Cry_num)
-
接下去,双方就可以使用同一秘钥进行对称加密通信
-
为什么秘钥分发是公钥加密私钥解密,而数字签名是私钥加密公钥解密?
-
密钥分发 / 安全通信:保证消息的机密性(Confidentiality),即只有预期的接收者能够读取消息内容。因为私钥只有接收方持有,所以即使消息在传输过程中被截获,攻击者也无法解密。
-
数字签名:只有私钥能确认是本人发送。中间人确实可以截获并解密,但是其内容就一个签名,没意义的。
OpenSSL是什么?
OpenSSL 是一个安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。
SSL 是 Secure Sockets Layer(安全套接层协议)的缩写,可以在 Internet 上提供保密性传输。Netscape 公司在推出第一个 Web 浏览器的同时,提出了 SSL 协议标准。其目标是保证两个应用间通信的保密性和可靠性,可在服务器端和用户端同时实现支持。已经成为 Internet 上保密通讯的工业标准。
OpenSSL中的BIO链是什么?
BIO 是 OpenSSL 中的 Basic I/O(基本输入输出)抽象层,它是 OpenSSL 提供的一种通用 I/O 接口,用于封装各种数据源或数据目的地,BIO 提供了一种统一的、可组合的方式来处理数据的读写,使得开发者不用关心底层数据的具体来源或去向。
BIO 链就是将多个 BIO 对象按照一定顺序串联(链接)起来,形成一个处理数据的流水线(pipeline)。你可以把它想象成一系列处理模块连接在一起,数据流经它们,每经过一个模块就被处理一次。
BIO 链的常见用途:
-
SSL/TLS 通信:比如将一个网络 BIO(如 socket)和一个 SSL BIO 组合成一个链,实现加密通信。
-
加密/解密过滤:将一个明文数据流通过一个加密 BIO,变成密文后再传输。
-
缓冲与优化:使用缓冲 BIO 来提高读写效率。
-
组合功能:比如先从文件读取,然后加密,再通过网络发送。
8.7.2 认证和授权
认证 vs 授权有什么区别?
认证(Authentication):认证是验证用户身份的过程,即确认“你真的是你声称的那个人”。
授权(Authorization):授权是在用户通过认证后,确定该用户被允许访问哪些资源或执行哪些操作的过程,即确认“你有权限做这个吗?”
一句话总结:认证是证明你是谁,授权是决定你能做什么。
【必问】Cookie vs Session vs Token之间有什么区别?
Cookie:由服务器发送到用户浏览器并保存在本地的一小段数据,浏览器之后每次请求都会自动携带(特定条件下)。常用于保存会话标识或用户偏好。
Session:一种服务端机制,用于跟踪用户状态。服务器为每个用户创建一个唯一的 Session ID,通常将其存储在 Cookie 中传给浏览器,实际数据保存在服务端(如内存、数据库)。
Token(常见如 JWT):一种无状态的令牌,由服务器生成并返回给客户端(通常保存在前端,如 localStorage 或 Cookie),客户端在后续请求中将该 Token 发回给服务器进行身份验证。不依赖服务端存储会话数据。
服务端存一个session,这个session该如何存,存在哪?
Session 存入 Redis:最常用,高性能、支持持久化。
Cookie+Session如何做认证?
-
用户登录后,服务器创建一个 Session(有有效期限),把用户信息存在服务端(如 Redis 或内存)。
-
服务器生成一个 Session ID,通过 Cookie 发送到浏览器。
-
之后每次请求,浏览器都会自动带上这个 Cookie(含 Session ID),服务器根据 ID 找到对应的 Session 数据,确认用户身份。
Token+JWT如何做认证?
-
用户登录后,服务器生成一个 Token(如 JWT),返回给客户端。
-
客户端将 Token 保存(如在 localStorage 或 Cookie 中),并在后续请求的 HTTP Header(如 Authorization: Bearer ) 中发送。
-
服务器收到 Token 后,验证其签名和有效性,无需查询服务端存储,即可确认用户身份和权限。
Cookie+Session vs Token+JWT各自优缺点?
Cookie+Session:
优点:安全性较高(数据在服务端)。
缺点:服务端需要存储大量 Session 数据,不易扩展;服务器一旦挂掉session也没了。
Token+JWT:
优点:无状态、适合分布式系统、便于跨域、适合 API 调用。
缺点:Token 一旦签发,在过期前无法轻易废止(除非使用黑名单等额外机制);若 Token 泄露且无 HTTPS,可能被滥用。
JWT由哪三部分组成?
-
Header(头部):描述 JWT 的元数据,比如使用的加密算法和 token 类型。
-
Payload(载荷 / 负载):存放实际要传递的数据(即“声明 Claims”),可以包括用户信息、权限等。
-
Signature(签名):用于验证消息在传输过程中没有被篡改,确保 Token 的完整性和真实性。
为什么Cookie无法防止CSRF攻击,而Token可以?
CSRF(Cross-Site Request Forgery,跨站请求伪造)的定义:
CSRF 是一种 Web 安全攻击,攻击者诱导用户在已登录某个网站的情况下,在不知情的情况下向该网站发送恶意请求,利用用户的身份执行非预期的操作。
举个例子:
假设你登录了银行网站 https://bank.com,该网站使用 Cookie 来维持登录状态。此时你又访问了一个恶意网站 https://evil.com,该网站偷偷向 https://bank.com/transfer?to=attacker&amount=1000 发送了一个请求。
因为你已经在 bank.com 登录过,浏览器会自动带上你的 Cookie(比如 sessionid),银行服务器看到这个 Cookie,就会认为是你发起的合法请求,于是就把钱转给了攻击者!
为什么 Cookie 无法防止 CSRF 攻击?
原因核心:浏览器会自动在每次请求中附带 Cookie。
为什么 Token(如 JWT)可以防止 CSRF 攻击?
客户端在每次向服务器发送请求时,手动将该 Token 放在 HTTP 请求头中。
关键点在于:浏览器不会自动在请求中附加这个 Token!
分布式架构下,Session共享有什么方案?
-
集中式 Session 存储
-
不再把 Session 存在单个服务器的内存中,而是存储在外部统一的存储系统中,所有服务器节点都从这个地方读取和写入 Session。
-
常见存储介质:Redis。
-
-
粘性会话 / 会话绑定
-
通过负载均衡策略,让同一个用户的请求始终被路由到同一台服务器上,从而保证他访问的服务器上保存着他的 Session。
-
但是某些服务器可能负载过高,某些可能闲置。
-
-
使用 Token 如 JWT 替代 Session
-
完全抛弃服务端 Session,改用客户端存储 Token(如 JWT、OAuth Token)的方式,实现无状态认证。
-
8.7.3 单点登录
单点登录是什么?
单点登录(SSO) 是一种身份验证机制,允许用户使用 一组凭据(如用户名和密码)登录一次,之后就可以 无需再次输入凭据而访问多个相互信任的应用程序或系统。
简单来说:你只需要登录一次,就能访问多个关联系统,不用为每个系统都单独登录。
为什么我们需要单点登录?
-
提升用户体验
-
增强安全性
-
简化账号管理
-
降低开发与维护成本
SAML是什么?有哪些适用场景?
SAML 是一种基于 XML 的开放标准协议,用于在不同的安全域(比如企业内不同的系统或企业与第三方之间)之间 交换身份验证和授权数据。
-
用户访问一个服务提供者(SP),比如公司内部的某个系统。
-
SP 发现用户未登录,将用户重定向到身份提供者(IdP),比如公司的统一认证系统。
-
用户在 IdP 登录。
-
IdP 生成一个 SAML 断言(Assertion)(包含用户身份信息的 XML 文档),并用加密方式传回给 SP。
-
SP 验证该断言,确认用户身份后,允许其访问。
适用场景:
-
企业内部多个系统之间的 SSO(如 SAP、Salesforce、Workday 等)。
-
企业与合作伙伴系统之间的身份认证。
OIDC是什么?有哪些适用场景?
OpenID Connect (OIDC) 是在 OAuth 2.0 基础上构建的 身份认证层,专门用于 实现 SSO 和用户身份信息的传递。
-
用户访问一个应用(客户端),比如一个网站或移动 App。
-
应用发现用户未登录,将其重定向到 身份认证服务器(如 Google、Microsoft、自建 IdP)。
-
用户在该认证服务器登录。
-
认证服务器返回一个 ID Token(JWT 格式,含用户身份信息) 和可能的 Access Token 给客户端。
-
客户端验证 ID Token,确认用户身份,完成 SSO 登录。
适用场景:
-
第三方登录(如“用微信登录”、“用 Google 账号登录”)。
-
现代 Web 应用、移动 App 的 SSO。
-
云服务、SaaS 平台的统一登录。
Kerberos是什么?有哪些适用场景?
Kerberos 是一种 网络认证协议,旨在通过 密钥加密技术 在不安全的网络中实现 安全的用户身份认证,主要用于 Windows 域环境 或类 Unix 系统。
-
用户登录时,客户端向 KDC(Key Distribution Center) 请求一个 Ticket Granting Ticket(TGT)。
-
之后访问具体服务时,再通过 TGT 换取对应服务的 服务票据(Service Ticket)。
-
服务端验证票据,确认用户身份。
适用场景:
-
一般是局域网内,或是公司内网。
LDAP是什么?有哪些适用场景?
LDAP 是一种 轻量级的目录访问协议,通常用于 存储和查询用户身份信息(如用户名、密码、邮箱、所属组等),但它本身 不是 SSO 协议,而是常被 SSO 系统用作 用户信息的后台存储或认证源。
-
通常配合其他 SSO 协议(如 Kerberos、SAML、OIDC)一起使用。
适用场景:
-
企业内部用户账号的集中存储与认证。
8.7.4 HTTPS
HTTPS是什么?
HTTPS 是在 HTTP 协议的基础上加入了 SSL/TLS(Secure Sockets Layer / Transport Layer Security)加密层,用于在客户端(如浏览器)和服务器之间建立安全的通信连接,保障数据在传输过程中不被窃听、篡改或伪造。
HTTPS安全的三个手段?
-
数据加密(Encryption)
-
使用 SSL/TLS 对传输的数据进行加密,防止第三方(比如黑客、网络运营商等)通过抓包工具窃取用户与网站之间传输的敏感信息,例如:
-
用户名和密码
-
支付信息(银行卡号、密码等)
-
个人隐私数据
-
-
-
身份验证(Authentication)
-
通过数字证书验证服务器的身份,确保用户访问的是真正的官方网站,而不是仿冒的钓鱼网站。
-
这通常依靠由受信任的证书颁发机构(CA, Certificate Authority)签发的 SSL 证书来实现。
-
-
数据完整性(Data Integrity)
-
防止数据在传输过程中被篡改。TLS 机制可以检测出数据是否在传输途中被人恶意修改。
-
HTTPS 的工作原理简述?
-
客户端发起 HTTPS 请求(比如访问一个网站);
-
服务器返回 SSL/TLS 证书,证明自己的身份;
-
客户端验证证书(是否由可信 CA 签发,是否过期,是否匹配域名等);
-
双方协商加密算法和生成会话密钥(通过非对称加密交换对称密钥);
-
建立安全连接后,双方使用对称加密算法加密数据进行通信,保证传输安全。
【必问】HTTP vs HTTPS的区别?
HTTP:
-
协议:明文传输
-
端口:80
-
安全性:不安全,易被窃听/篡改
-
证书:不需要
-
URL 前缀:http://
HTTPS:
-
协议:加密传输(通过 SSL/TLS)
-
端口:443
-
安全性:安全,防止窃听、篡改和伪造
-
证书:需要 SSL/TLS 数字证书
-
URL 前缀:https://
SSL/TLS是什么?
-
SSL(Secure Sockets Layer,安全套接层) 是最早由网景公司(Netscape)在1990年代提出的加密通信协议,用于保护网络通信。
-
TLS(Transport Layer Security,传输层安全协议) 是 SSL 的升级版和继任者,目前广泛使用的是 TLS 1.2 和 TLS 1.3,SSL 本身已不再推荐使用(存在安全漏洞)。
-
虽然我们常说 “SSL 证书”,但实际上现代使用的都是 TLS 协议,只是习惯上仍称 “SSL/TLS”。
TLS 1.3 vs TLS 1.2有什么区别?
-
减少了握手步骤(通常只需要 1-RTT,甚至支持 0-RTT)
-
移除了不安全的加密算法(如 RSA 密钥交换、一些弱加密套件)
-
默认使用前向保密(Forward Secrecy)
-
提升了性能和安全性
SSL/TLS之加密解密如何做到?
-
非对称加密(Asymmetric Encryption)
-
使用一对密钥:公钥(Public Key) 和 私钥(Private Key)
-
公钥可以公开,任何人都可以用它加密数据;但只有对应的私钥才能解密。
-
常见算法:RSA、ECC(椭圆曲线加密)、Diffie-Hellman 等
-
用途:用于安全地传递密钥(如预主密钥)、身份验证(证书验证)
-
-
对称加密(Symmetric Encryption)
-
加密和解密使用同一个密钥
-
速度比非对称加密快得多,适合加密大量数据
-
用途:在握手完成后,用于加密实际的 HTTP 请求和响应内容
-
常见算法:AES(高级加密标准)、ChaCha20 等
-
-
哈希算法(Hash Function)
-
用于生成数据的摘要,确保数据完整性
-
常见算法:SHA-256、SHA-3 等
-
在数字签名和密钥派生中都会用到
-
SSL/TLS之身份认证如何做到?
-
数字签名(Digital Signature)
-
用于验证证书的合法性,确保它确实是由受信任的 CA 签发的,且未被篡改
-
利用私钥对数据的哈希值进行加密生成签名,公钥可验证该签名
-
数字证书是什么?
数字证书是一种由权威机构签发的电子文件,用于证明某个网站(或服务器、个人等)的身份,并绑定其公钥。
在 HTTPS 的场景下,数字证书主要用于:
-
证明服务器的身份(比如你访问的是
www.example.com,而不是冒牌网站); -
提供服务器的公钥,用于后续的 TLS 加密通信(密钥交换);
-
建立用户与网站之间的信任关系。
数字证书的组成,里面有什么?
最核心的东西是:公钥 + 身份信息 + CA 的签名。
数字证书签发的一般流程?
-
网站拥有者生成密钥对
-
生成一对密钥:私钥(自己保存,绝对不能泄露) 和 公钥(交给 CA,用于绑定到证书中)
-
-
提交 CSR(Certificate Signing Request,证书签名请求)给 CA
-
CSR 中包含:
-
你的公钥
-
你的网站信息(如域名、公司名等)
-
-
CA 会验证你的身份(比如验证你是否真的拥有该域名,是否是合法企业等)
-
-
CA 验证身份
-
对于普通网站,常见的是 DV(Domain Validation,域名验证),CA 会通过邮件、DNS 或文件验证你拥有该域名;
-
对于企业级网站,可能需要 OV(Organization Validation) 或 EV(Extended Validation),验证更严格的身份和组织信息。
-
-
CA 签发证书
-
验证通过后,CA 会使用自己的 私钥 对你的公钥 + 身份信息进行数字签名,生成最终的 数字证书,并返回给你;
-
-
服务器安装证书
-
你将证书部署到你的 Web 服务器(如 Nginx、Apache、Tomcat 等)上;
-
当用户访问时,服务器会把证书发给浏览器,用于验证身份和建立加密连接。
-

被折叠的 条评论
为什么被折叠?



