一些经典操作系统和计算机的网络的知识
计算机网络
OSI七层模型,TCP/IP四层模型与五层结构
OSI(Open System Interconnection)七层参考模型是一个网络架构模型,由国际标准化组织(ISO)提出,用于描述和标准化各种计算机网络的功能和过程。这七层从高到低分别是:
- 应用层:最靠近用户的层,负责处理特定的应用程序细节。这一层提供了网络服务与用户应用软件之间的接口。例如,Web 浏览器、FTP 客户端和服务器、电子邮件客户端等。
- 表示层:确保从一个系统发送的信息可以被另一个系统的应用层读取。它负责数据的转换、压缩和加密。例如,确保数据从一种编码格式转换为另一种,如 ASCII 到 EBCDIC。
- 会话层:管理用户的会话,控制网络上两节点间的对话和数据交换的管理。它负责建立、维护和终止会话。例如,建立一个会话令牌,以便在网络上的两个节点之间传递。
- 传输层:提供端到端的通信服务,保证数据的完整性和正确顺序。这一层包括 TCP 和 UDP 等。
- 网络层:负责在多个网络之间进行数据传输,确保数据能够在复杂的网络结构中找到从源到目的地的最佳路径。这层使用的是 IP(Internet Protocol)协议。
- 数据链路层:在物理连接中提供可靠的传输,负责建立和维护两个相邻节点间的链路。包括帧同步、MAC(媒体访问控制)。
- 物理层:负责在物理媒介上实现原始的数据传输,比如电缆、光纤和无线信号传输。涉及的内容包括电压、接口、针脚、电缆的规格和传输速率等。
TCP/IP 四层模型是互联网通信的核心,定义了一系列协议和标准,确保设备间可以可靠地进行数据传输。
①、应用层(Application Layer):直接面向用户和应用程序,提供各种网络服务。它包含了用于特定应用的协议和服务,如 HTTP(HyperText Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)等。
示例:当在浏览器中输入一个 URL 并访问一个网页时,浏览器使用 HTTP 协议从 Web 服务器请求页面内容。
②、传输层(Transport Layer):提供端到端的通信服务,确保数据可靠传输。它负责分段数据、流量控制、错误检测和纠正。常见的传输层协议有 TCP 和 UDP。
示例:当发送一封电子邮件时,TCP 协议确保邮件从你的客户端可靠地传输到邮件服务器。
③、网际层:或者叫网络层(Internet Layer),负责在不同网络之间路由数据包,提供逻辑地址(IP 地址)和网络寻址功能。用于处理数据包的分组、转发和路由选择,确保数据可以从源端传输到目标端。
常见协议:IPv4、IPv6、ICMP(Internet Control Message Protocol)。
示例:当访问一个网站时,网络层协议(如 IPv4)将你的请求从你的计算机通过多个路由器传输到目标服务器。
④、网络接口层(Network Access Layer):或者叫链路层(Link Layer),负责将数字信号在物理通道(网线)中准确传输,定义了如何在单一网络链路上传输数据,如何处理数据帧的发送和接收,包括物理地址(MAC 地址)的解析。
常见协议:以太网(Ethernet)、Wi-Fi。
示例:在一个局域网(LAN)中,计算机通过以太网连接交换机,链路层协议负责数据帧在网络设备间的传输
五层体系结构是对 OSI 和 TCP/IP 的折衷,它保留了 TCP/IP 的实用性,同时提供了比四层模型更细致的分层,便于教学和理解网络的各个方面。
- 应用层:作为网络服务和最终用户之间的接口。它提供了一系列供应用程序使用的协议,如 HTTP(网页)、FTP(文件传输)、SMTP(邮件传输)等。使用户的应用程序可以访问网络服务。
- 传输层:提供进程到进程的通信管理,这一层确保数据按顺序、无错误地传输。主要协议包括 TCP 和 UDP。
- 网络层:负责数据包从源到目的地的传输和路由选择,包括跨越多个网络(即互联网)。它使用逻辑地址(如 IP 地址)来唯一标识设备。路由器是网络层设备。
- 数据链路层:确保从一个节点到另一个节点的可靠、有效的数据传输。交换机、网桥是数据链路层设备。
- 物理层:电缆、光纤、无线电频谱、网络适配器等。
常见网络协议

数据在各层之间如何传输的
对于发送方而言,从上层到下层层层包装,对于接收方而言,从下层到上层,层层解开包装。
- 发送方的应用进程向接收方的应用进程传送数据
- 应用先将数据交给本主机的应用层,应用层加上本层的控制信息 H5 就变成了下一层的数据单元
- 传输层收到这个数据单元后,加上本层的控制信息 H4,再交给网络层,成为网络层的数据单元
- 到了数据链路层,控制信息被分成两部分,分别加到本层数据单元的首部(H2)和尾部(T2)
- 最后的物理层,进行比特流的传输

从浏览器地址栏输入URL到显示网页
这个过程包括多个步骤,涵盖了 DNS 解析、TCP 连接、发送 HTTP 请求、服务器处理请求并返回 HTTP 响应、浏览器处理响应并渲染页面等多个环节。
- DNS 解析:浏览器会发起一个 DNS 请求到 DNS 服务器,将域名解析为服务器的 IP 地址。
- TCP 连接:浏览器通过解析得到的 IP 地址与服务器建立 TCP 连接。这一步涉及到 TCP 的三次握手,用于确保双方都已经准备好进行数据传输了。
- 发送 HTTP 请求:浏览器构建 HTTP 请求,包括请求行、请求头和请求体;然后将请求发送到服务器。
- 服务器处理请求:服务器接收到 HTTP 请求后,根据请求的资源路径,经过后端处理,生成 HTTP 响应消息;响应消息包括状态行、响应头和响应体。
- 浏览器接收 HTTP 响应:浏览器接收到服务器返回的 HTTP 响应数据后,开始解析响应体中的 HTML 内容;然后构建 DOM 树、解析 CSS 和 JavaScript 文件等,最终渲染页面。
- 断开连接:TCP 四次挥手,连接结束。
DNS的解析过程
DNS 解析(Domain Name System Resolution)是将人类可读的域名(如 www.example.com)转换成计算机可识别的 IP 地址(如 192.0.2.1)的过程
- 客户端首先会发出一个 DNS 请求,问 www.server.com 的 IP ,并发给本地 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址)。
- 本地域名服务器收到客户端的请求后,如果缓存里的表格能找到 www.server.com,则它直接返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大, 能告诉我 www.server.com 的 IP 地址吗?” 根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。
- 根 DNS 收到来自本地 DNS 的请求后,发现后置是 .com,说:“www.server.com 这个域名归 .com 区域管理”,我给你 .com 顶级域名服务器地址给你,你去问问它吧。”
- 本地 DNS 收到顶级域名服务器的地址后,发起请求问“老二, 你能告诉我 www.server.com 的 IP 地址吗?”
- 顶级域名服务器说:“我给你负责 www.server.com 区域的权威 DNS 服务器的地址,你去问它应该能问到”。
- 本地 DNS 于是转向问权威 DNS 服务器:“老三,www.server.com对应的IP是啥呀?” server.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
- 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
- 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。
Websocket和Socket区别
- Socket 其实就是等于 IP 地址 + 端口 + 协议。
具体来说,Socket 是一套标准,它完成了对 TCP/IP 的高度封装,屏蔽网络细节,以方便开发者更好地进行网络编程。
- WebSocket 是一个持久化的协议,它是伴随 H5 而出的协议,用来解决 http 不支持持久化连接的问题。
- Socket 一个是网络编程的标准接口,而 WebSocket 则是应用层通信协议
应用层协议常见端口
| 服务 | 端口 |
|---|---|
| FTP | 21 |
| SSH | 22 |
| Telnet | 23 |
| DNS域名解析服务 | 53 |
| HTTP | 80 |
| HTTPS | 443 |
| Socks | |
| MySQL | 3306 |
平常抓包吗
平常使用最多的就是 chrome 浏览器自带的 network 面板了,可以看到请求的时间、请求的信息,以及响应信息。更专业的还有 fidder、charles、wireshark 等工具。
HTTP
HTTP常用的状态码及其含义
HTTP 状态码用于表示服务器对请求的处理结果,可以分为 5 种:
- 1xx 服务器收到请求,需要进一步操作,例如 100 Continue。
- 2xx 请求成功处理,例如 200 OK。
- 3xx 重定向:需要进一步操作以完成请求;例如 304 Not Modified 表示资源未修改,客户端可以使用缓存。
- 4xx 客户端错误:请求有问题,例如 404 Not Found 表示资源不存在。
- 5xx 服务器错误,例如500 Internal Server Error 表示服务器内部错误。
200 OK:请求成功
201 Created:创建成功(POST/PUT)
204 No Content:成功,但无返回体
301和302区别
- 301:永久性移动,请求的资源已被永久移动到新位置。服务器返回此响应时,会返回新的资源地址。
- 302:临时性性移动,服务器从另外的地址响应资源,但是客户端还应该使用这个地址。
- 304:协商缓存(E-tag,if-modified) 命中
400,401和403区别
400:请求参数错误 401:当前请求需要认证 403:没有权限 拒绝执行
429:请求频率超限
500,502,503
500:服务器内部错误 502:网关、代理出错 503:服务不可用 504 Gateway Timeout:网关超时
HTTP请求方式
HTTP 协议定义了多种请求方式,用以指示请求的目的。常见的请求方式有 GET、POST、DELETE、PUT。
- GET:请求检索指定的资源。应该只用于获取数据,并且是幂等的,即多次执行相同的 GET 请求应该返回相同的结果,并且不会改变资源的状态。
- POST:向指定资源提交数据,请求服务器进行处理(如提交表单或上传文件)。数据被包含在请求体中。可能会创建新的资源或修改现有资源。
- DELETE:删除指定的资源。
- PUT:用于替换指定的资源。如果指定的资源不存在,创建一个新资源。
- HEAD:类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头。可以用于检查资源是否存在,验证资源的更新时间等。
- OPTIONS:用于获取服务器支持的 HTTP 请求方法。通常用于跨域请求中的预检请求(CORS)。
- TRACE:回显服务器收到的请求,主要用于测试或诊断。但由于安全风险(可能暴露敏感信息),很多服务器会禁用 TRACE 请求。
- CONNECT:建立一个到目标资源的隧道(通常用于 SSL/TLS 代理),用于在客户端和服务器之间进行加密的隧道传输。
GET和POST差别
GET 请求主要用于获取数据,参数附加在 URL 中,存在长度限制,且容易被浏览器缓存,有安全风险;而 POST 请求用于提交数据,参数放在请求体中,适合提交大量或敏感的数据。
另外,GET 请求是幂等的,多次请求不会改变服务器状态;而 POST 请求不是幂等的,可能对服务器数据有影响。
幂等操作可以重复执行而不会改变系统状态。
GET的长度限制
HTTP 中的 GET 方法是通过 URL 传递数据的,但是 URL 本身其实并没有对数据的长度进行限制,真正限制 GET 长度的是浏览器。
例如 IE 浏览器对 URL 的最大限制是 2000 多个字符,大概 2kb 左右,像 Chrome、Firefox 等浏览器支持的 URL 字符数更多,其中 FireFox 中 URL 的最大长度限制是 65536 个字符,Chrome 则是 8182 个字符。这个长度限制也不是针对数据部分,而是针对整个 URL
HTTP请求的过程和原理
HTTP 是基于 TCP/IP 协议的应用层协议,它使用 TCP 作为传输层协议,通过建立 TCP 连接来传输数据。
HTTP 遵循标准的客户端-服务器模型,客户端打开连接发出请求,然后等待服务器返回的响应
- 在浏览器输入 URL 后,浏览器首先会通过 DNS 解析获取到服务器的 IP 地址,然后与服务器建立 TCP 连接。
- TCP 连接建立后,浏览器会向服务器发送 HTTP 请求。
- 服务器收到请求后,会根据请求的信息处理请求。
- 处理完请求后,服务器会返回一个 HTTP 响应给浏览器。
- 浏览器收到响应后,会根据响应的信息渲染页面。然后,浏览器和服务器断开 TCP 连接。
客户端发送一个请求到服务器,服务器处理请求并返回一个响应。这个过程是同步的,也就是说,客户端在发送请求后必须等待服务器的响应。在等待响应的过程中,客户端不会发送其他请求。
怎么利用多线程来下载一个数据呢
可以采取分块下载的策略。首先,通过 HEAD 请求获取文件的总大小。然后根据文件大小和线程数,将文件进行切割。每个线程负责下载一个特定范围的数据。
可以通过设置 HTTP 请求头的 Range 字段指定下载的字节区间。例如,Range: bytes=0-1023 表示下载文件的前 1024 字节。
如果只要下载数据的前十个字节,只需要设置 Range 字段为 Range: bytes=0-9 即可.
HTTP的请求报文结构
请求报文由请求行、请求头部、空行和消息正文组成。如下所示:1
2
3
4GET /index.html HTTP/1.1
Host: www.javabetter.cn
Accept: text/html
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
①、请求行包括请求方法、请求 URL 和 HTTP 协议的版本。例如:GET /index.html HTTP/1.1。
②、请求头部包含请求的附加信息,如客户端想要接收的内容类型、浏览器类型等。例如:
Host: www.javabetter.cn,表示请求的主机名(域名)Accept: text/html,表示客户端可以接收的媒体类型User-Agent: Mozilla/5.0,表示客户端的浏览器类型- Range:用于指定请求内容的范围,如断点续传时表示请求的字节范围。
③、请求头部和消息正文之间有一个空行,表示请求头部结束。
④、消息正文是可选的,如 POST 请求中的表单数据;GET 请求中没有消息正文。
说下 HTTP 响应报文结构?
1 | HTTP/1.0 200 OK |
①、状态行
包括 HTTP 协议的版本、状态码(如 200、404)和状态消息(如 OK、NotFound)。例如:HTTP/1.0 200 OK。
②、响应头部
包含响应的附加信息,如服务器类型、内容类型、内容长度等。也是键值对,例如:
Content-Type: text/plain,表示响应的内容类型Content-Length: 137582,表示响应的内容长度Expires: Thu, 05 Dec 1997 16:00:00 GMT,表示资源的过期时间Last-Modified: Wed, 5 August 1996 15:55:28 GMT,表示资源的最后修改时间Server: Apache 0.84,表示服务器类型
③、空行
表示响应头部结束。
④、消息正文(可选)
响应的具体内容,如 HTML 页面。不是所有的响应都有消息正文,如 204 No Content 状态码的响应。
URI和URL的区别
- URI,统一资源标识符(Uniform Resource Identifier, URI),标识的是 Web 上每一种可用的资源,如 HTML 文档、图像、视频片段、程序等都是由一个 URI 进行标识的。
- URL,统一资源定位符(Uniform Resource Location),它是 URI 的一种子集,主要作用是提供资源的路径。
它们的主要区别在于,URL 除了提供了资源的标识,还提供了资源访问的方式。
这么比喻,URI 像是身份证,可以唯一标识一个人,而 URL 更像一个住址,可以通过 URL 找到这个人——人类住址协议://地球/中国/北京市/海淀区/xx 职业技术学院/14 号宿舍楼/525 号寝/张三.男
HTTP1.0,1.1,2.0区别
HTTP1.0 默认是短连接,HTTP 1.1 默认是长连接,HTTP 2.0 采用的多路复用。
HTTP1.0
- 无状态协议:HTTP 1.0 是无状态的,每个请求之间相互独立,服务器不保存任何请求的状态信息。
- 非持久连接:默认情况下,每个 HTTP 请求/响应对之后,连接会被关闭,属于短连接。这意味着对于同一个网站的每个资源请求,如 HTML 页面上的图片和脚本,都需要建立一个新的 TCP 连接。可以设置
Connection: keep-alive强制开启长连接。
HTTP1.1
- 持久连接:HTTP 1.1 引入了持久连接(也称为 HTTP keep-alive),默认情况下不会立即关闭连接,可以在一个连接上发送多个请求和响应。极大减轻了 TCP 连接的开销。
- 流水线处理:HTTP 1.1 支持客户端在前一个请求的响应到达之前发送下一个请求,以提高传输效率。
HTTP pipeline 是 HTTP/1.1 中允许在单个长连接上连续发送多个请求而无需等待响应的机制,服务端按顺序返回响应。也就是服务端还是要按顺序处理。大多数浏览器都不支持
- 存在队头阻塞(Head-of-Line Blocking):一个请求慢,后面全堵
- 明文传输,不安全
- 头部冗余大,无压缩
HTTP2.0
- 二进制协议:HTTP 2.0 使用二进制而不是文本格式来传输数据,解析更加高效。
二进制分帧不再是文本协议,拆成更小的帧(frame),流(stream)多路复用
- 多路复用:一个 TCP 连接上可以同时进行多个 HTTP 请求/响应,解决了 HTTP 1.x 的队头阻塞问题。
- 头部压缩:HTTP 协议不带状态,所以每次请求都必须附上所有信息。HTTP 2.0 引入了头部压缩机制,可以使用 gzip 或 compress 压缩后再发送,减少了冗余头部信息的带宽消耗。
- 服务端推送:服务器可以主动向客户端推送资源,而不需要客户端明确请求。
依然基于 TCP,所以仍受 TCP 队头阻塞影响(丢包时整个连接卡住)
HTTP3.0
HTTP/2.0 基于 TCP 协议,而 HTTP/3.0 则基于 QUIC 协议,Quick UDP Connections,直译为快速 UDP 网络连接。基于 TCP 的 HTTP/2.0,尽管从逻辑上来说,不同的流之间相互独立,不会相互影响,但在实际传输的过程中,数据还是要一帧一帧的发送和接收,一旦某一个流的数据有丢包,仍然会阻塞在它之后传输的流数据。
而基于 UDP 的 QUIC 协议可以更彻底地解决这样的问题,让不同的流之间真正的实现相互独立传输,互不干扰。同时,QUIC 协议在传输的过程中就完成了 TLS 加密握手,更直接了。
彻底解决队头阻塞,适应移动网络、弱网
- 底层换成 QUIC 协议(基于 UDP)
- 不再用 TCP
- 由 UDP + 可靠传输 + TLS 组成
- 彻底解决队头阻塞
- 每个流独立,一个流丢包不影响其他流
- 真正无队头阻塞
- 0-RTT / 1-RTT 握手
- 首次连接 1-RTT
- 复用连接 0-RTT,直接发数据,极快
- 连接迁移(Connection Migration)
- 手机切换 Wi‑Fi/4G 不断线
- 用 Connection ID 标识,不依赖 IP + 端口
- 内置 TLS 加密
- HTTP/3 强制加密,没有明文版本
HTTP/1.1:文本、多连接、队头阻塞严重
HTTP/2:二进制、多路复用、头部压缩,仍基于 TCP
HTTP/3:QUIC (UDP)、无队头阻塞、1-RTT 握手、连接迁移
HTTP长连接
在 HTTP 中,长连接是指客户端和服务器之间在一次 HTTP 通信完成后,不会立即断开,而是保留连接以供后续请求复用。这种机制可以减少了频繁建立和关闭连接的开销.可以通过 Connection: keep-alive 实现。在 HTTP/1.1 中,长连接是默认开启的,默认超时时间
- HTTP 一般会有 httpd 守护进程,里面可以设置 keep-alive timeout,当 tcp 连接闲置超过这个时间就会关闭,也可以在 HTTP 的 header 里面设置超时时间
- TCP 的 keep-alive 包含三个参数,支持在系统内核的 net.ipv4 里面设置;当 TCP 连接之后,闲置了 tcp_keepalive_time,则会发生侦测包,如果没有收到对方的 ACK,那么会每隔 tcp_keepalive_intvl 再发一次,直到发送了 tcp_keepalive_probes,就会丢弃该连接
HTTP与HTTPS差别
HTTPS在 HTTP 的基础上加入了 SSL/TLS 协议,确保数据在传输过程中是加密的。HTTP 的默认端⼝号是 80,URL 以http://开头;HTTPS 的默认端⼝号是 443,URL 以https://开头。
HTTPS加密针对HTTP层,包括请求行、请求头部以及请求体,响应行、响应头以及响应体。
但IP地址和端口号
HTTP 是明文传输的,存在数据窃听、数据篡改和身份伪造等问题。而 HTTPS 通过引入 SSL/TLS,解决了这些问题。
SSL/TLS 在加密过程中涉及到了两种类型的加密方法:
- 非对称加密:服务器向客户端发送公钥,然后客户端用公钥加密自己的随机密钥,也就是会话密钥,发送给服务器,服务器用私钥解密,得到会话密钥。
- 对称加密:双方用会话密钥加密通信内容。
密钥怎么来的
- 两端各生成一个随机数:Client Random、Server Random
- 密钥交换得到 Pre-Master Secret
- 一起算出 Master Secret
- 再导出对称加密密钥(AES 等)真正传输 HTTP 用的是对称加密,速度快。

客户端会通过数字证书来验证服务器的身份,数字证书由 CA 签发,包含了服务器的公钥、证书的颁发机构、证书的有效期等。
HTTPS如何建立连接的
HTTPS 的连接建立在 SSL/TLS 握手之上,其过程可以分为两个阶段:握手阶段和数据传输阶段
①、客户端向服务器发起请求 (随机数,支持的TLS版本,加密套件等)
②、服务器接收到请求后,返回自己的数字证书,包含了公钥、颁发机构等信息。(随机数,确定TLS版本,加密套件,证书等)
③、客户端收到服务器的证书后,验证证书的合法性,如果合法,会生成一个随机码,然后用服务器的公钥加密这个随机码,发送给服务器。(验证证书,如果有效发送公钥加密后的随机数并生成对称密钥)
④、服务器收到会话密钥后,用私钥解密,得到会话密钥。(利用随机数生成对称密钥)
⑤、客户端和服务器通过会话密码对通信内容进行加密,然后传输。
如果通信内容被截取,但由于没有会话密钥,所以无法解密。当通信结束后,连接会被关闭,会话密钥也会被销毁,下次通信会重新生成一个会话密钥。

TLS 1.3 把握手压缩到 1-RTT(甚至 0-RTT)
- HTTPS 加密从握手完成后才开始
- 证书只在握手阶段传输,用于验身份、换密钥
- 真正传输数据用对称加密,公钥只在握手用
HTTPS会加密URL吗
HTTPS 通过 SSL/TLS 协议确保了客户端与服务器之间交换的数据被加密,这包括 HTTP 头部和正文。
而 URL 是 HTTP 头部的一部分,因此这部分信息也是加密的。
但因为涉及到 SSL 握手的过程,所以域名信息会被暴露出来。另外,完整的 URL 可能在 Web 服务器的日志中记录,这些日志可能是明文的。还有,URL 在浏览器历史记录中也是可见的。
因此,敏感信息永远不应该通过 URL 传递,即使是在使用 HTTPS 的情况下。
中间人攻击
中间人攻击(Man-in-the-Middle, MITM)是一种常见的网络安全威胁,攻击者可以在通信的两端插入自己,以窃取通信双方的信息。中间人攻击是一个缺乏相互认证的攻击,因此大多数加密协议都会专门加入一些特殊的认证方法,以防止中间人攻击。像 SSL 协议,就是通过验证服务器的数字证书,是否由 CA(权威的受信任的数字证书认证机构)签发,来防止中间人攻击的。
HTTPS怎么保证建立的信道是安全的?
主要通过 SSL/TLS 协议的多层次安全机制,首先在握手阶段,客户端和服务器使用得是非对称加密,生成的会话密钥只有服务器的私钥才能解密,而私钥只有服务器持有。在数据传输阶段,即使攻击者拦截了通信数据,没有会话密钥也无法解密。
HTTPS 能抓包吗?
可以,HTTPS 可以抓包,但因为通信内容是加密的,需要解密后才能查看。其原理是通过一个中间人,伪造服务器证书,并取得客户端的信任,然后将客户端的请求转发给服务器,将服务器的响应转发给客户端,完成中间人攻击。常用的抓包工具有 Wireshark、Fiddler、Charles 等。
客户端如何校验证书合法性
CA证书签发过程
- 首先,CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进⾏ Hash 计算,得到⼀个 Hash 值;
- 然后 CA 会使⽤⾃⼰的私钥将该 Hash 值加密,⽣成 Certificate Signature;
- 最后将 Certificate Signature 添加在⽂件证书上,形成数字证书。
CA证书验证过程
客户端(通常是浏览器,通常会集成 CA 的公钥信息)在校验证书的合法性时,主要通过以下步骤来校验证书的合法性。
- 浏览器会读取证书的所有者、有效期、颁发者等信息,先校验网站域名是否一致,然后校验证书的有效期是否过期;
- 浏览器开始查找内置的 CA,与服务器返回证书中的颁发者进行对比,确认是否为合法机构;
- 如果是,从内部植入的 CA 公钥解密 Certificate 的 Signature 内容,得到⼀个 Hash 值 H2;
- 使⽤同样的 Hash 算法获取证书的 Hash 值 H1,⽐较 H1 和 H2,如果值相同,则为可信赖的证书,否则告警。

假如在 HTTPS 的通信过程中,中间人篡改了证书,但由于没有 CA 机构的私钥,所以无法生成正确的 Signature,因此就无法通过校验。
如果服务端发送给客户端的加密算法是客户端没有的,这种情况会怎样?
如果客户端不支持服务器建议的任何加密算法,那么安全连接将建立失败。
当客户端向服务器发起一个 HTTPS 请求时,它会发送一个 ClientHello 消息。这个消息包含了客户端支持的加密算法列表(称为密码套件),以及其他一些参数。服务器收到 ClientHello 后,会从客户端提供的密码套件中选择一个它也支持的密码套件,并在 ServerHello 消息中返回给客户端。如果服务器无法找到一个双方都支持的密码套件,那么它会发送一个警告消息,表示无法协商出一个共同的加密算法,随后连接将被终止。
如何理解HTTP是无状态的
HTTP 协议是无状态的,这意味着每个 HTTP 请求都是独立的,服务器不会保留任何关于客户端请求的历史信息。
换句话说
- 每个 HTTP 请求都包含了所必须的信息,服务器在处理当前请求时,不依赖于之前的任何请求信息。
- 服务器不会记录任何客户端请求的状态,每次请求都像是第一次与服务器通信。
由于 HTTP 是无状态的,像用户的购物车状态就必须通过其他方式来保持,如在每次请求中传递用户的 ID,或者使用 Cookie 在客户端保存购物车状态。
HTTP 协议本身不 “记住” 任何之前的请求信息。
- 服务器对每个请求都是独立处理的
- 这次请求和上次请求,服务器看不出是同一个人发的
- 服务器不保存任何客户端上下文、身份、状态
那有什么办法记录状态呢?
- Cookies:服务器通过 Set-Cookie 响应头将状态信息存储在客户端,客户端在后续请求中发送该 Cookie 以维持状态。
- Session:服务器生成一个唯一的会话 ID,存储在 Cookie 中,并在服务器端维护与该会话 ID 关联的状态信息。
- Token:使用 JWT(JSON Web Token)等机制在客户端存储状态信息,客户端在每次请求中发送该 Token。
Session和Cookie关系
Session 和 Cookie :
- Cookie 是保存在客户端的文本串的数据。客户端向服务器发起请求时,服务端会向客户端发送一个 Cookie,客户端就把 Cookie 保存起来。在客户端下次向同一服务器再发起请求时,Cookie 被携带发送到服务器。服务端可以根据这个 Cookie 判断用户的身份和状态。
- Session 指的就是服务器和客户端一次会话的过程。它是另一种记录客户状态的机制。不同的是 cookie 保存在客户端浏览器中,而 session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 session。客户端浏览器再次访问时只需要从该 session 中查找用户的状态。
Session 和 Cookie 到底有什么不同呢?
- 存储位置不一样,Cookie 保存在客户端,Session 保存在服务器端。
- 存储数据类型不一样,Cookie 只能保存 ASCII,Session 可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
- 有效期不同,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般有效时间较短,客户端关闭或者 Session 超时都会失效。
- 隐私策略不同,Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
- 存储大小不同, 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie
Cookie
- 一小段键值对数据,由服务器通过
Set-Cookie头发给浏览器 - 保存在客户端(浏览器)
- 每次请求自动带上
- 明文可看、可篡改(不安全)
- 不能存敏感信息
Cookie字段
- Domain:默认是当前域名(不含子域名),比如
a.com设置的 Cookie,b.a.com无法访问;若设为.a.com,则所有子域名都能访问。 Path:默认是
/(全站生效),设为/user则只有请求/user、/user/profile等路径时才携带 Cookie。Secure:必须配合 HTTPS,否则 Cookie 不会被存储 / 发送(现代浏览器强制要求
SameSite=None时必须加 Secure)。- HttpOnly:最关键的防 XSS 字段 ——JS 无法通过
document.cookie读取 / 修改,只能由浏览器自动携带到服务器,避免脚本窃取 Cookie。
- SameSite(跨站控制)
1 | Set-Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9; |
Session
Session 不属于 HTTP 标准 —— HTTP 协议本身只有 “无状态” 的特性,没有任何关于 Session 的定义;Session 是应用层(服务器端)基于 HTTP 特性实现的 “状态保持方案”,本质是开发者 / 框架利用 Cookie(或 URL 参数)+ 服务器存储来弥补 HTTP 无状态的不足。
- 状态数据保存在服务器端(内存 / Redis / 数据库)
- 给客户端一个 SessionID,通过 Cookie 存储
- 客户端每次只传 SessionID,服务器查状态
优点:
- 敏感信息存在服务器,安全
- 可随时销毁、踢人、强制下线
缺点:
- 服务器需要存储,分布式要共享 Session(Redis)
- 跨域麻烦
- 依赖 Cookie
用户第一次请求服务器时,服务器根据用户提交的信息,创建对应的 Session,请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器,浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入 Cookie 中,同时 Cookie 记录此 SessionID 是属于哪个域名。
当用户第二次访问服务器时,请求会自动判断此域名下是否存在 Cookie 信息,如果存在,则自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到,说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
JWT
JWT(JSON Web Token)是一种轻量级、自包含的令牌格式,用于在网络中安全地传递 JSON 格式的信息,核心特点是「无状态」—— 令牌本身包含所有必要的用户身份 / 权限信息,服务器无需存储会话数据,仅通过加密签名验证令牌的合法性。
JWT 由 . 分隔的三部分组成,格式为:Header.Payload.Signature
Header声明令牌类型和签名算法,Payload存储实际的用户信息(不要存敏感信息), Signature验证令牌未被篡改、确由合法服务器签发1
2
3
4签名 = HMACSHA256(
Base64编码(Header) + "." + Base64编码(Payload),
服务器密钥(仅服务端保存)
)
客户端无法伪造 / 篡改 Payload(没有密钥就无法生成合法签名),服务器只需用密钥重新计算签名并对比,即可验证令牌合法性。

优点
- 无状态易扩展:分布式系统中无需同步 Session,降低服务器存储压力;
- 跨域友好:不依赖 Cookie,适合前后端分离、跨域 API 调用;
- 自包含:令牌自带用户信息,减少数据库查询。
缺点
- 无法主动作废:令牌签发后,有效期内无法手动吊销(除非服务器维护黑名单);
- Payload 可解码:Base64 是编码而非加密,不能存密码、手机号等敏感信息;
- 令牌变长:Payload 内容越多,令牌越长,增加网络传输开销。
JWT 的安全最佳实践
- 必加过期时间(exp):有效期不宜过长(比如 1 小时),过期后重新登录;
- 用非对称加密(RS256):签发用私钥,验证用公钥,避免密钥泄露;
- 敏感信息不进 Payload:仅存用户 ID、角色等非敏感标识;
- HTTPS 传输:防止令牌被中间人窃取;
- 客户端安全存储:优先用 HttpOnly Cookie 或移动端安全存储,避免 LocalStorage 被 XSS 窃取。
问题1:分布式环境下 Session 怎么处理
分布式环境下,客户端请求经过负载均衡,可能会分配到不同的服务器上,假如一个用户的请求两次没有落到同一台服务器上,那么在新的服务器上就没有记录用户状态的 Session。
这时候可以使用 Redis 等分布式缓存来存储 Session,在多台服务器之间共享。
问题2:客户端无法使用 Cookie 怎么办?
有可能客户端无法使用 Cookie,比如浏览器禁用 Cookie,或者客户端是安卓、IOS 等等。
这时候SessionID 可以使用客户端的本地存储,比如浏览器的 sessionStorage。
传输可以通过:
- 拼接到 URL 里:直接把 SessionID 作为 URL 的请求参数
- 放到请求头里:把 SessionID 放到请求的 Header 里,比较常用。
TCP
TCP的三次握手机制
TCP(传输控制协议)的三次握手机制是一种用于在两个 TCP 主机之间建立一个可靠连接的过程。这个机制确保了两端的通信是同步的,并且在数据传输开始前,双方都准备好了进行通信。
w
握手过程+状态变化+目的+为什么是三次
①、第一次握手:SYN(最开始都是 CLOSE,之后服务器进入 LISTEN)
- 发起连接:客户端发送一个 TCP 报文段到服务器。这个报文段的头部中,SYN 位被设置为 1,表明这是一个连接请求。同时,客户端会随机选择一个序列号(Sequence Number),假设为 x,发送给服务器。
- 目的:客户端通知服务器它希望建立连接,并告知服务器自己的初始序列号。
- 状态:客户端进入 SYN_SENT 状态。
②、第二次握手:SYN + ACK
- 确认并应答:服务器收到客户端的连接请求后,如果同意建立连接,它会发送一个应答 TCP 报文段给客户端。在这个报文段中,SYN 位和 ACK 位都被设置为 1。服务器也会选择自己的一个随机序列号,假设为 y,并将客户端的序列号加 1(即 x+1)作为确认号(Acknowledgment Number),发送给客户端。
- 目的:服务器告诉客户端,它的连接请求被接受了,并通知客户端自己的初始序列号。
- 状态:服务器进入 SYN_RCVD 状态。
③、第三次握手:ACK
- 最终确认:客户端收到服务器的应答后,还需要向服务器发送一个确认。这个 TCP 报文段的 ACK 位被设置为 1,确认号被设置为服务器序列号加 1(即 y+1),而自己的序列号是 x+1。
- 目的:客户端确认收到了服务器的同步应答,完成三次握手,建立连接。
- 状态:客户端进入 ESTABLISHED 状态,当服务器接收到这个包时,也进入 ESTABLISHED 状态
目的:
- 确认双方发送能力、接收能力都正常
- 协商初始序列号(ISN),为可靠传输做准备
- 避免失效的连接请求报文导致服务器资源浪费
SYN 不仅确保了序列号的同步,使得后续的数据能够有序传输,还能防止旧的报文段被误认为是新连接。

为什么不是2次?
为了防止 “已失效的连接请求报文段” 突然到达服务器,导致服务器错误打开连接、浪费资源。
场景:
- 客户端发了一个 SYN,网络阻塞迟迟没到服务器
- 客户端超时重传,新 SYN 成功建立连接并关闭
- 旧的 SYN 这时才到达服务器
- 如果是两次握手:服务器收到 SYN 就直接建立连接,而客户端根本不知道,服务器会白白维持一个无效连接。
- 三次握手:客户端收到第二次握手后会发现异常,不会发送第三次 ACK,服务器不会进入 ESTABLISHED。
两次握手无法保证客户端的接收 / 发送能力正常,也无法防止历史重复连接。
为什么不是四次:三次握手已经足够创建可靠的连接了,没有必要再多一次握手。
三次握手可以携带数据吗?
- 第 1、2 次握手:不能携带数据
- 第 3 次握手:可以携带数据(因为此时客户端认为连接已建立)
SYN 攻击是什么?
泛洪攻击(SYN Flood Attack)是一种常见的 DoS(拒绝服务)攻击,攻击者会发送大量的伪造的 TCP 连接请求,导致服务器资源耗尽,无法处理正常的连接请求。
SYN Flood 是一种典型的 DDos 攻击,它在短时间内,伪造不存在的 IP 地址, 向服务器发送大量 SYN 报文。当服务器回复 SYN+ACK 报文后,不会收到 ACK 回应报文,那么 SYN 队列里的连接旧不会出对队,久⽽久之就会占满服务端的 SYN 接收队列(半连接队列),使得服务器不能为正常⽤户服务。
攻击者大量发送 SYN 但不回复第三次 ACK,导致服务器维持大量 SYN_RCVD 半连接,耗尽资源。
所谓的半连接就是指在 TCP 的三次握手过程中,当服务器接收到来自客户端的第一个 SYN 包后,它会回复一个 SYN-ACK 包,此时连接处于“半开”状态,因为连接的建立还需要客户端发送最后一个 ACK 包。
在收到最后的 ACK 包之前,服务器会为这个尚未完成的连接分配一定的资源,并在它的队列中保留这个连接的位置。
防御:SYN Cookie、缩短超时、增大半连接队列。
SYN Cookie
不维护半连接队列,把 SYN+ACK 的序列号做成 “Cookie”,让客户端帮服务器存状态。
具体流程:
- 服务器收到 SYN,不分配资源、不入半连接队列;
- 根据客户端 IP、端口、服务器 IP、端口、时间戳,计算一个加密序列号(SYN Cookie);
- 服务器把这个 Cookie 当作 seq=y 发给客户端(SYN+ACK);
- 客户端正常回复 ACK = y+1;
- 服务器收到 ACK 后,用 y+1-1 还原出 Cookie,验证合法后,才真正建立连接、分配资源。
优点:
- 不占半连接队列,从根源防御 SYN 洪水攻击;
- 不需要服务器存储任何半连接状态。
SYN Proxy 防火墙:服务器防火墙会对收到的每一个 SYN 报文进行代理和回应,并保持半连接。等发送方将 ACK 包返回后,再重新构造 SYN 包发到服务器,建立真正的 TCP 连接。
初始序列号 ISN 为什么是随机的?
防止被攻击者猜测,避免被伪造报文段劫持连接。
三次握手中每一次如果没有收到会发生什么
- 第一次握手服务端未收到 SYN 报文
服务端不会进行任何的动作,而客户端由于一段时间内没有收到服务端发来的确认报文,等待一段时间后会重新发送 SYN 报文,如果仍然没有回应,会重复这个过程,直到发送次数超过最大重传次数限制,就会返回连接建立失败。
- 第二次握手客户端未收到服务端响应的ACK报文
客户端会继续重传,直到次数限制;而服务端此时会阻塞在 accept()处,等待客户端发送 ACK 报文
事实上客户端与服务端都会考虑进行重传
- 第三次握手服务端未收到客户端发送过来的 ACK 报文
服务端同样会采用类似客户端的超时重传机制,如果重试次数超过限制,则 accept()调用返回-1,服务端建立连接失败;而此时客户端认为自己已经建立连接成功,因此开始向服务端发送数据,但是服务端的 accept()系统调用已经返回,此时不在监听状态,因此服务端接收到客户端发送来的数据时会发送 RST 报文给客户端,消除客户端单方面建立连接的状态。
第二次握手传回了 ACK,为什么还要传回 SYN?
ACK 是为了告诉客户端传来的数据已经接收无误。而传回 SYN 是为了告诉客户端,服务端响应的确实是客户端发送的报文。
第三次握手可以携带数据吗?
第 3 次握手是可以携带数据的。
此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,它已经建立连接成功,并且确认服务端的接收和发送能力是正常的。
第一次握手不能携带数据是出于安全的考虑,因为如果允许携带数据,攻击者每次在 SYN 报文中携带大量数据,就会导致服务端消耗更多的时间和空间去处理这些报文,会造成 CPU 和内存的消耗。
TCP半连接状态是什么
TCP 半连接指的是在 TCP 三次握手过程中,服务器接收到了客户端的 SYN 包,但还没有完成第三次握手,此时的连接处于一种未完全建立的状态。
如果服务器回复了 SYN-ACK,但客户端还没有回复 ACK,该连接将一直保留在半连接队列中,直到超时或被拒绝。
半连接队列
TCP 进入三次握手前,服务端会从 CLOSED 状态变为 LISTEN 状态, 同时在内部创建了两个队列:半连接队列(SYN 队列)和全连接队列(ACCEPT 队列)。
半连接队列存放的是三次握手未完成的连接,全连接队列存放的是完成三次握手的连接。
- TCP 三次握手时,客户端发送 SYN 到服务端,服务端收到之后,便回复 ACK 和 SYN,状态由 LISTEN 变为 SYN_RCVD,此时这个连接就被推入了 SYN 队列,即半连接队列。
- 当客户端回复 ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入 ACCEPT 队列,即全连接队列。
TCP四次挥手过程
TCP 连接的断开过程被形象地概括为四次挥手。
第一次挥手:客户端向服务器发送一个 FIN 结束报文,表示客户端没有数据要发送了,但仍然可以接收数据。客户端进入 FIN-WAIT-1 状态。
第二次挥手:服务器接收到 FIN 报文后,向客户端发送一个 ACK 报文,确认已接收到客户端的 FIN 请求。服务器进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。
第三次挥手:服务器向客户端发送一个 FIN 报文,表示服务器也没有数据要发送了。服务器进入 LAST-ACK 状态。
第四次挥手:客户端接收到 FIN 报文后,向服务器发送一个 ACK 报文,确认已接收到服务器的 FIN 请求。客户端进入 TIME-WAIT 状态,等待一段时间以确保服务器接收到 ACK 报文。服务器接收到 ACK 报文后进入 CLOSED 状态。客户端在等待一段时间后也进入 CLOSED 状态。

TCP挥手为什么需要四次
因为 TCP 是全双工通信协议,数据的发送和接收需要两次一来一回,也就是四次,来确保双方都能正确关闭连接。
- 第一次挥手:客户端表示数据发送完成了,准备关闭,你确认一下。
- 第二次挥手:服务端回话说 ok,我马上处理完数据,稍等。
- 第三次挥手:服务端表示处理完了,可以关闭了。
- 第四次挥手:客户端说好,进入 TIME_WAIT 状态,确保服务端关闭连接后,自己再关闭连接
因为 TCP 是全双工的,每个方向必须独立关闭:
- 主动方发 FIN,关闭它的发送方向
- 被动方先回 ACK(第二次挥手)
- 但被动方可能还有数据要发,不能立刻发 FIN
- 等被动方数据发完,再单独发 FIN(第三次挥手)所以 ACK 和 FIN 不能合并,必须是四次。
只有当被动方恰好没有数据要发时,ACK 和 FIN 才可能合并,变成 “三次挥手”,但标准是四次。
主动关闭方:经历 FIN_WAIT1 → FIN_WAIT2 → TIME_WAIT
被动关闭方:经历 CLOSE_WAIT → LAST_ACK
TIME_WAIT 是什么?为什么要等待 2MSL?
MSL = Maximum Segment Lifetime(报文最大生存时间)
两个根本原因:
确保最后一个 ACK 能到达对方
如果第四次挥手的 ACK 丢失,服务器会重传 FIN,客户端在 TIME_WAIT 期间可以再次回复 ACK,保证服务器正常关闭。
防止本连接的过期报文干扰新连接
等待 2MSL 可以让网络中所有残留报文自然消失,避免被下一个相同四元组的连接收到。
TIME_WAIT 过多会有什么问题?怎么解决?
问题:
- 占用大量端口(一个 TIME_WAIT 占用一个端口)
- 高并发下端口耗尽,无法新建连接
解决方法(面试常问):
- 调整内核参数:
net.ipv4.tcp_tw_reuse(允许复用 TIME_WAIT 端口) net.ipv4.tcp_tw_recycle(快速回收,但 NAT 环境有坑,慎用)- 增大端口范围
- 让服务端成为主动关闭方(因为服务端通常不担心端口耗尽,客户端才会)
CLOSE_WAIT 过多是什么原因?
经典 Bug 题:
CLOSE_WAIT 过多一定是服务器代码 Bug。
原因:被动关闭方收到 FIN 后进入 CLOSE_WAIT,但是没有调用 close () /shutdown (),所以不会发 FIN,一直卡在 CLOSE_WAIT。
第四次挥手的 ACK 丢失了会怎样?
- 主动关闭方进入 TIME_WAIT,等待 2MSL
- 被动关闭方(LAST_ACK)收不到 ACK,会超时重传 FIN
- 主动关闭方收到重传的 FIN,会再次回复 ACK
FIN_WAIT1 和 FIN_WAIT2 区别?
- FIN_WAIT1:发了 FIN,还没收到 ACK
- FIN_WAIT2:收到了 ACK,但还没收到对方的 FIN
可以只有三次挥手吗?
可以。如果被动关闭方立刻没有数据要发送,可以将 ACK + FIN 合并成一个报文发送,看起来就是三次挥手。但这不是标准,只是优化。
TCP四次挥手过程中,为什么需要等待2MSL才进入CLOSED状态
MSL (Maximum Segment Lifetime):报文在网络中最大生存时间,超过这个时间报文会被路由器丢弃。
1. 为了保证客户端发送的最后一个 ACK 报文段能够到达服务端。 这个 ACK 报文段有可能丢失,因而使处在 LAST-ACK 状态的服务端就收不到对已发送的 FIN + ACK 报文段的确认。服务端会超时重传这个 FIN+ACK 报文段,而客户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK 报文段。接着客户端重传一次确认,重新启动 2MSL 计时器。最后,客户端和服务器都正常进入到 CLOSED 状态。
2. 防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧的连接请求报文段。
为什么等待的时间是 2MSL?
MSL 是 Maximum Segment Lifetime,报⽂最⼤⽣存时间,它是任何报⽂在⽹络上存在的最⻓时间,超过这个时间报⽂将被丢弃。
TIME_WAIT 等待 2 倍的 MSL,⽐较合理的解释是:⽹络中可能存在来⾃发送⽅的数据包,当这些发送⽅的数据包被接收⽅处理后⼜会向对⽅发送响应,所以⼀来⼀回需要等待 2 倍的时间
两个根本原因
- 确保最后一个 ACK 能到达被动关闭方(ACK 丢失场景)
假设第四次挥手的 ACK 丢失:
- 主动关闭方(Client)发出 ACK,进入 TIME_WAIT。
- 被动关闭方(Server)没收到 ACK,会超时重传 FIN。
- 这个重传的 FIN 从 Server 到 Client 需要 最多 1MSL。
- Client 重发 ACK 回到 Server 又需要 最多 1MSL。
→ 一来一回,总共需要 2MSL 才能确保这个修复流程能完整执行。
如果只等 1MSL,Client 可能已经关闭,重传的 FIN 到达后,Server 会收到 RST 导致异常关闭。
- 防止旧连接的报文干扰新连接(迷之报文场景)
TCP 可能存在延迟、重复的旧报文。
- 旧报文在网络中流浪最多 1MSL 才会消失。
- 为了绝对确保下一个使用相同四元组(源 IP、源端口、目的 IP、目的端口)的新连接,不会收到上一次连接残留的脏数据,必须等待旧报文来 + 回的最大时间 2MSL,让网络彻底 “清空”。
保活计时器
除时间等待计时器外,TCP 还有一个保活计时器(keepalive timer)。
设想这样的场景:客户已主动与服务器建立了 TCP 连接。但后来客户端的主机突然发生故障。显然,服务器以后就不能再收到客户端发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就需要使用保活计时器了。
服务器每收到一次客户端的数据,就重新设置保活计时器,时间的设置通常是两个小时。若两个小时都没有收到客户端的数据,服务端就发送一个探测报文段,以后则每隔 75 秒钟发送一次。若连续发送 10 个探测报文段后仍然无客户端的响应,服务端就认为客户端出了故障,接着就关闭这个连接
CLOSE_WAIT和TIME_WAIT含义
CLOSE_WAIT:服务端收到客户端关闭连接的请求并确认之后,就会进入 CLOSE-WAIT 状态。此时服务端可能还有一些数据没有传输完成,因此不能立即关闭连接,而 CLOSE-WAIT 状态就是为了保证服务端在关闭连接之前将待发送的数据处理完。
TIME-WAIT 发生在第四次挥手,当客户端在发送 ACK 确认对方的 FIN 报文后,会进入 TIME_WAIT 状态。
它存在的意义主要有两个:
- 在 TIME_WAIT 状态中,客户端可以重新发送 ACK 确保对方正常关闭连接。
- 在 TIME_WAIT 持续的 2MSL 时间后,确保旧数据包完全消失,避免它们干扰未来建立的新连接。
补充:MSL(Maximum Segment Lifetime):TCP 报文段在网络中的最大存活时间,通常为 30 秒到 2 分钟
TIME_WAIT状态过多会导致什么
主要危害:端口资源耗尽 + 内存占用
如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器⽅主动发起的断开请求。
过多的 TIME-WAIT 状态主要的危害有两种:
第⼀是内核要维护连接结构,大量 TIME_WAIT 会轻微增加内存开销。
第⼆是对端⼝资源的占⽤,每个 TIME_WAIT 连接会占用一个本地端口,高并发短连接场景下,端口很快被占满,导致无法新建 TCP 连接
怎么解决 TIME_WAIT 状态过多?
- 服务器可以设置 SO_REUSEADDR 套接字来通知内核,如果端口被占用,但是 TCP 连接位于 TIME_WAIT 状态时可以重用端口。
- 还可以使用长连接的方式来减少 TCP 的连接和断开,在长连接的业务里往往不需要考虑 TIME_WAIT 状态。
问题 1:服务端重启报端口占用,该用哪个?
答:用 SO_REUSEADDR(应用层设置,允许重复绑定端口)。
问题 2:高并发短连接导致 TIME_WAIT 过多、端口耗尽,该用哪个?
答:用 net.ipv4.tcp_tw_reuse = 1(内核层,复用 TIME_WAIT 端口)。
TCP报文头部的格式
一个 TCP 报文段主要由报文段头部(Header)和数据两部分组成。头部包含了确保数据可靠传输所需的各种控制信息,比如说序列号、确认号、窗口大小等。

- 源端口号(Source Port):16 位(2 个字节),用于标识发送端的应用程序。
- 目标端口号(Destination Port):也是 16 位,用于标识接收端的应用程序。
- 序列号(Sequence Number):32 位,用于标识从 TCP 发送者发送的数据字节流中的第一个字节的顺序号。确保数据按顺序接收。
- 确认号(Acknowledgment Number):32 位,如果 ACK 标志被设置,则该字段包含发送确认的序列号,即接收 TCP 希望收到的下一个序列号。
- 数据偏移(Data Offset):4 位,表示 TCP 报文头部的长度,用于指示数据开始的位置。
- 保留(Reserved):6 位,为将来使用预留,目前必须置为 0。
- 控制位(Flags):共 6 位,包括 URG(紧急指针字段是否有效)、ACK(确认字段是否有效)、PSH(提示接收端应该尽快将这个报文段交给应用层)、RST(重置连接)、SYN(同步序号,用于建立连接)、FIN(结束发送数据)。
- 窗口大小(Window):16 位,用于流量控制,表示接收端还能接收的数据的字节数(基于接收缓冲区的大小)。
- 校验和(Checksum):16 位,覆盖整个 TCP 报文段(包括 TCP 头部、数据和一个伪头部)的校验和,用于检测数据在传输过程中的任何变化。
- 紧急指针(Urgent Pointer):16 位,只有当 URG 控制位被设置时才有效,指出在报文段中有紧急数据的位置。
TCP为什么可靠
TCP 首先通过三次握手和四次挥手来保证连接的可靠性,然后通过校验和、序列号、确认应答、超时重传、滑动窗口等机制来保证数据的可靠传输。
- 超时重传:如果发送方发送的数据包超过了最大时间,接收方还没有收到,发送方会重传数据包以保证丢失数据重新传输。
- 序列号/确认机制:TCP 将数据分成多个小段,每段数据都有唯一的序列号,以确保数据包的顺序传输和完整性。同时,发送方如果没有收到接收方的确认应答,会重传数据。按序列号排序后再交给应用层。重复的序列号报文直接丢弃,保证数据不重复、按序到达。
- 校验和:TCP 报文段包括一个校验和字段,用于检测报文段在传输过程中的变化。如果接收方检测到校验和错误,就会丢弃这个报文段。
- 超时重传:如果发送方发送的数据包超过了最大生存时间,接收方还没有收到,发送方会重传数据包以保证丢失数据重新传输
- 流量控制:接收方会发送窗口大小告诉发送方它的接收能力。发送方会根据窗口大小调整发送速度,避免接收方缓冲区溢出。
- 拥塞控制:四个核心算法:慢启动、拥塞避免、快重传、快恢复。TCP 会采用慢启动的策略,一开始发的少,然后逐步增加,当检测到网络拥塞时,会降低发送速率。在网络拥塞缓解后,传输速率也会自动恢复。避免网络崩溃,保证整体传输稳定。
流量控制:端到端,防止接收方缓冲区溢出;
拥塞控制:全局,防止网络本身拥塞崩溃。
TCP 通过序列号确认、超时重传、校验和、流量控制、拥塞控制、按序去重六大机制,保证数据传输的可靠性、有序性、无差错性,因此是可靠传输协议
TCP的流量控制
TCP 提供了一种机制,可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是流量控制。
TCP 流量控制是让发送方根据接收方的实际接收能力,控制发送速率,防止接收方缓冲区溢出而丢包的机制。核心目的匹配发送方速度 ≈ 接收方处理速度。
接收方在 ACK 报文中携带自己的 rwnd(接收窗口大小);
发送方严格遵守已发送但未确认的数据量接收窗口为 0 时,发送方停止发送,直到接收方更新窗口。
窗口为 0 后重新开启:
发送方停止发送
发送方会启动持续计时器(persistence timer),
- 定时发送窗口探测报文,防止接收方的窗口更新报文丢失。
TCP的滑动窗口
TCP 发送一个数据,如果需要收到确认应答,才会发送下一个数据。这样的话就会有个缺点:效率会比较低。为了解决这个问题,TCP 引入了窗口,它是操作系统开辟的一个缓存空间。窗口大小值表示无需等待确认应答,而可以继续发送数据的最大值。
TCP 头部有个字段叫 win,也即那个 16 位的窗口大小,它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度,从而达到流量控制的目的。
TCP 滑动窗口分为两种: 发送窗口和接收窗口。发送端的滑动窗口包含四大部分,如下:
- 已发送且已收到 ACK 确认(左边可滑走)
- 已发送但未 ACK(窗口内)
- 允许发送但尚未发送(窗口内)
- 不允许发送(窗口外)

发送方不能发送超过窗口大小的数据。发送窗口 (swnd) = min (接收窗口 rwnd, 拥塞窗口 cwnd)
- 接收窗口 rwnd:接收方给的,来自 流量控制
- 拥塞窗口 cwnd:发送方自己算的,来自 拥塞控制
- 发送方真正能发多少,取两者较小值
接收方的滑动窗口包含三大部分,存在于接收方,是接收缓冲区的空闲大小,接收方通过 ACK 报文把 rwnd 带给发送方.作用:流量控制,防止接收方缓冲区溢出,如下:
- 已成功接收并确认
- 未收到数据但可以接收
- 未收到数据并不可以接收的数据

Nagle算法和延迟确认
当TCP 报⽂的承载的数据⾮常⼩的时候,例如⼏个字节,那么整个⽹络的效率是很低的,因为每个 TCP 报⽂中都会有 20 个字节的 TCP 头部,也会有 20 个字节的 IP 头部,⽽数据只有⼏个字节,所以在整个报⽂中有效数据占有的比例⾮常低。
常⻅的两种策略,来减少⼩报⽂的传输,分别是:
- Nagle 算法
- 延迟确认
Nagle 算法是 TCP 的小包优化算法,目的是减少网络中小报文段的数量,避免大量微小数据包造成网络拥塞和带宽浪费。
若已发送的数据都已收到 ACK,可以立即发送数据
若还有未确认的数据包,则将后续小数据缓存起来,直到:
- 收到之前数据的 ACK;
- 缓存数据达到MSS 大小;
- 超时触发发送。
需要低延迟的实时交互场景必须禁用 Nagle 算法,例如:
- 游戏(实时操作)
- 实时通信(音视频)
- 高频交易
禁用方法:设置套接字选项 TCP_NODELAY = 1。
大量小包交互的场景(如 telnet、ssh),减少报文数量,提升带宽利用率。
延迟确认
每收到一段数据就立刻发纯 ACK,会产生大量只有头部没有数据的小包,浪费带宽。
延迟确认是接收方的优化策略:不收到数据就立刻发 ACK,而是稍微等一小段时间(通常 40ms~200ms),看看能不能 “顺便” 把 ACK 和回包数据一起发出去,减少网络报文数量。
为了解决 ACK 传输效率低问题,所以就衍⽣出了 TCP 延迟确认。
当没有携带数据的 ACK,它的⽹络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报⽂。TCP 延迟确认的策略:
- 当有响应数据要发送时,ACK 会随着响应数据⼀起⽴刻发送给对⽅
- 当没有响应数据要发送时,ACK 将会延迟⼀段时间,以等待是否有响应数据可以⼀起发送
- 如果在延迟等待发送 ACK 期间,对⽅的第⼆个数据报⽂⼜到达了,这时就会⽴刻发送 ACK
并不是永远都等,遇到下面情况不等,立刻发 ACK:
- 收到满 MSS的数据段
- 收到乱序报文
- 连续收到 2 个数据段
- 延迟计时器超时(通常~200ms)
Nagle 算法和延迟确认不能一起使用,Nagle 算法意味着延迟发,延迟确认意味着延迟接收,两个凑在一起就会造成更大的延迟,会产生性能问题。
Nagle:发送方有未 ACK 数据 → 不发新小包,等 ACK
Delayed ACK:接收方收到小包 → 不立刻发 ACK,等数据捎带
结果:发送方等 ACK,接收方等数据,互相卡死几十毫秒。这就是经典的 Nagle + Delayed ACK 互锁。
TCP拥塞算法
流量控制是为了避免发送⽅的数据填满接收⽅的缓存,但并不能控制整个⽹络。⼀般来说,计算机⽹络会处在⼀个共享的环境。因此也有可能会因为其他主机之间的通信使得⽹络拥堵。
当⽹络出现拥堵时,如果继续发送⼤量数据包,可能会导致数据包延时、丢失等,这时 TCP 就会重传数据,但重传会增加⽹络负担,于是会导致更⼤的延迟以及更多的丢包,就进⼊了恶性循环.
当⽹络发送拥塞时,TCP 会⾃我牺牲,降低发送的数据流。拥塞控制的⽬的就是避免发送⽅的数据填满整个⽹络。拥塞控制:防止网络本身拥塞崩溃,由发送方维护cwnd(拥塞窗口)。
拥塞窗口
拥塞窗⼝ cwnd是发送⽅维护的⼀个的状态变量,它会根据⽹络的拥塞程度动态变化的。
发送窗⼝swnd 和接收窗⼝ rwnd 是约等于的关系,那么由于加⼊了拥塞窗⼝的概念后,此时发送窗⼝的值是 swnd = min(cwnd, rwnd),也就是拥塞窗⼝和接收窗⼝中的最⼩值。
- 慢启动(Slow Start)
- 目标:快速探测网络可用带宽
- 规则:
cwnd指数增长(每个 RTT cwnd 翻倍) - 退出条件:
cwnd >= ssthresh(慢启动门限)或 触发拥塞
- 拥塞避免(Congestion Avoidance)
- 目标:平稳增加发送速率,避免急剧拥塞
- 规则:
cwnd线性增长(每个 RTT cwnd+1) - 退出条件:触发拥塞
- 快重传(Fast Retransmit)
- 触发条件:收到3 个重复 ACK
- 动作:不等超时,立即重传丢失报文
- 意义:大幅提升丢包恢复效率
- 快恢复(Fast Recovery)
- 触发条件:快重传之后
- 规则:
ssthresh = cwnd / 2cwnd = ssthresh + 3- 直接进入拥塞避免阶段
- 关键:只有超时才会回退到
cwnd=1的慢启动,3 个重复 ACK 不回退。
| 信号 | 处理策略 | cwnd 变化 |
|---|---|---|
| 超时重传 | 网络严重拥塞 | ssthresh = cwnd/2,cwnd=1,回到慢启动 |
| 3 个重复 ACK | 局部丢包,网络尚可 | 快重传 + 快恢复,不进入慢启动 |
TCP的重传机制
在发送某个数据后开启一个计时器,如果在一定时间内没有得到发送数据报的 ACK 报文,就重新发送数据,直到发送成功为止。
超时重传机制是 TCP 的核心之一,它能确保在网络传输中如果某些数据包丢失或没有及时到达的话,TCP 能够重新发送这些数据包,以保证数据完整性。
重传包括超时重传、快速重传、带选择确认的重传(SACK)和重复 SACK 四种。
超时重传
触发条件:RTO(重传超时时间)定时器超时,未收到 ACK。重传最早未 ACK 的那个报文段
超时时间如何设置
TCP中的重传超时时间不是一个固定的值,而是动态计算的,目的是为了适应不同的网络条件。RTO 有个标准方法的计算公式,叫 Jacobson / Karels 算法。RTO 基于平滑 RTT(SRTT)和RTT 偏差(RTTVAR)动态计算
(SRTT = (1-\alpha) \times SRTT + \alpha \times RTT)
(RTTVAR = (1-\beta) \times RTTVAR + \beta \times |RTT - SRTT|)
(RTO = SRTT + 4 \times RTTVAR)
超时重传缺点:
- 等待超时时间长,延迟大
- 当报文丢失时,在等待超时的过程中,可能会出现这种情况:后面的报文已经被接收端接收了但却迟迟得不到确认,发送端会认为也丢失了,从而引起不必要的重传。
- 超时判定网络严重拥塞,cwnd 直接砍到 1,吞吐量暴跌
快速重传
收到 3 个重复 ACK,不等超时,立即重传丢失报文。进入快恢复,不回到慢启动。它不以时间驱动,而是以数据驱动。它是基于接收端的反馈信息来引发重传的。
- 比超时重传响应更快、延迟更低
- 避免 cwnd 骤降,性能更好
缺点:只能知道 “前面丢了”,不知道后面哪些收到了。一旦丢包,会盲目重传丢失包之后的所有包,大量带宽浪费,长距离链路效果差。
带选择性确认的重传
为了解决应该重传多少个包的问题,TCP 提供了带选择确认的重传
SACK 机制是在快速重传的基础上,接收方返回最近收到报文段的序列号范围,这样发送方就知道接收方哪些数据包是没收到的。这样就很清楚应该重传哪些数据包。
传统快速重传使用累计确认 ACK,只能告知连续接收的最大序号,无法携带乱序到达的报文信息,因此发送方只知道前面丢包,却不知道后续报文是否已接收,只能保守重传全部,这就是它的最大缺陷。
重复SACK
在 SACK 的基础上做了一些扩展,主要用来告诉发送方,有哪些数据包,自己重复接受了。
DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
DSACK 是重复 SACK,用于接收方告知发送方收到了重复报文,帮助发送方识别乱序导致的假丢包,避免错误触发拥塞控制、降低拥塞窗口。
TCP的粘包和拆包
TCP 的粘包和拆包更多的是业务上的概念。TCP 是面向流,没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。
TCP 是面向字节流的协议,没有消息边界;UDP 是面向数据报的,有明确边界。
- TCP:发送方发的是一串字节流,接收方看到的也是一串字节流,不知道你业务上的 “一条消息” 从哪开始、到哪结束。
- UDP:一个 UDP 数据报就是一条消息,发几次,收几次,不会合并也不会拆分。
为什么会产生粘包和拆包呢?
发送方发了两条独立消息:消息A + 消息B
因为 Nagle 算法或滑动窗口批量发送,接收方一次读到:消息A+消息B连在一起。分不清边界,就是粘包。
- 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包;
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
发送方发一条大消息 消息C因为 MSS / 缓冲区限制,被分成两次到达:消息C前半段 + 消息C后半段
接收方先读到一半,没法用 → 拆包。
- 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包;
- 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。即 TCP 报文长度 - TCP 头部长度 > MSS。
怎么解决呢?
- 发送端将每个数据包封装为固定长度
- 在数据尾部增加特殊字符进行分割
- 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。如:HTTP、Redis、自定义 RPC 都用这种。
TCP 是字节流协议,无消息边界,因此存在粘包拆包,常用长度域方案解决;UDP 是数据报协议,自带消息边界,内核保证:要么收不到,要么收到一整个,不会合并、不会拆分,不存在粘包拆包问题。
一个TCP连接可以发送多少次HTTP请求
一个 TCP 连接可以发送多少次 HTTP 请求,取决于 HTTP 协议的版本。
在 HTTP/1.0 中,每个 HTTP 请求-响应使用一个单独的 TCP 连接。这意味着每次发送 HTTP 请求都需要建立一个新的 TCP 连接。
HTTP/1.1 引入了持久连接(Persistent Connection),默认情况下允许在一个 TCP 连接上发送多个 HTTP 请求。
通过使用 Connection: keep-alive 头部实现,保持连接打开状态,直到明确关闭为止。这极大地提高了效率,因为无需为每个请求都建立新的连接。
此外,HTTP/1.1 支持请求管道化(Pipelining),允许客户端在收到前一个响应之前发送多个请求。
HTTP/2 进一步优化了连接复用,允许在单个 TCP 连接上同时发送多个请求和响应,这些请求和响应被分割成帧并通过流传输。HTTP/2 的多路复用(Multiplexing)机制显著提高了并发性能和资源利用效率。
UDP和TCP的区别
TCP 是面向连接的,而 UDP 是无连接的。在数据传输开始之前,TCP 需要先建立连接,数据传输完成后,再断开连接。这个过程通常被称为“三次握手”、“四次挥手”。
UDP 是无连接的,发送数据之前不需要建立连接,发送完毕也不需要断开,数据以数据报形式发送。
换句话说:TCP 是可靠的,它通过确认机制、重发机制等来保证数据的可靠传输。而 UDP 是不可靠的,数据包可能会丢失、重复、乱序。
- TCP: 适用于那些对数据准确性要求高于数据传输速度的场合。例如:网页浏览、电子邮件、文件传输(FTP)、远程控制、数据库链接。
- UDP: 适用于对速度要求高、可以容忍一定数据丢失的场合。例如:QQ 聊天、在线视频、网络语音电话、广播通信。容忍一定的数据丢失。
如何设计QQ中的网络协议
首先,我们要实现登录功能,这是使用 QQ 的第一步,为了保证账号和密码的安全性,我们可以选择 TCP + SSL/TLS 协议来进行登录。
因为 TCP 协议是一种可靠的传输协议,能够保证数据的完整性,而 SSL/TLS 能够对通信进行加密,保证数据的安全性。
接下来,需要考虑消息传递的实时性,如语音视频通话等,这时候我们可以选择 UDP 协议。UDP 的传输速度更快,对于实时性服务来说,速度是最重要的。
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接,需三次握手建立连接 | 无连接,发送前无需建立连接 |
| 可靠性 | 可靠传输,保证不丢、不重、有序 | 不可靠,不保证到达、有序、不重复 |
| 传输模式 | 面向字节流,无消息边界 | 面向数据报,有明确消息边界 |
| 粘包拆包 | 存在,需手动解决 | 不存在 |
| 重传机制 | 超时重传、快重传、SACK | 无重传机制 |
| 流量 / 拥塞控制 | 有(滑动窗口、cwnd) | 无 |
| 开销 | 头部大(最小 20 字节),开销高 | 头部小(8 字节),开销低 |
| 适用场景 | 文件传输、HTTP/HTTPS、数据库 | 直播、游戏、DNS、视频通话 |
如何保证消息不丢失
对于 TCP 协议来说,如果数据包在传输过程中丢失,TCP 协议会自动进行重传。
而对于 UDP 协议来说,我们可以通过应用层的重传机制来保证消息的不丢失。当接收方收到消息后,返回一个确认信息给发送方,如果发送方在一定时间内没有收到确认信息,就重新发送消息。
同时,每个消息都附带一个唯一的序列号,接收方根据序列号判断是否有消息丢失,如果发现序列号不连续,就可以要求发送方重新发送。这样还可以防止消息重复。
另外,消息持久化也很重要,可以将消息保存在服务器或者本地的数据库中,即使在网络中断或者其他异常情况下,也能从数据库中恢复消息。
为什么QQ采用UDP协议

- 首先,QQ 并不是完全基于 UDP 实现。比如在使用 QQ 进行文件传输等活动的时候,就会使用 TCP 作为可靠传输的保证。
- 使用 UDP 进行交互通信的好处在于,延迟较短,对数据丢失的处理比较简单。同时,TCP 是一个全双工协议,需要建立连接,所以网络开销也会相对大。
- 如果使用 QQ 语音和 QQ 视频的话,UDP 的优势就更为突出了,首先延迟较小。最重要的一点是不可靠传输,这意味着如果数据丢失的话,不会有重传。因为用户一般来说可以接受图像稍微模糊一点,声音稍微不清晰一点,但是如果在几秒钟以后再出现之前丢失的画面和声音,这恐怕是很难接受的。
- 由于 QQ 的服务器设计容量海量级的应用,一台服务器要同时容纳十几万的并发连接,因此服务器端只有采用 UDP 协议与客户端进行通讯才能保证这种超大规模的服务
简单总结一下:UDP 协议是无连接方式的协议,它的效率高,速度快,占资源少,对服务器的压力比较小。但是其传输机制为不可靠传送,必须依靠辅助的算法来完成传输控制。QQ 采用的通信协议以 UDP 为主,辅以 TCP 协议。
UDP协议为什么不可靠
UDP 在传输数据之前不需要先建立连接,远地主机的运输层在接收到 UDP 报文后,不需要确认,提供不可靠交付。总结就以下四点:
- 不保证消息交付:不确认,不重传,无超时
- 不保证交付顺序:不设置包序号,不重排,不会发生队首阻塞
- 不跟踪连接状态:不必建立连接或重启状态机
- 不进行拥塞控制:不内置客户端或网络反馈机制
DNS为什么要用UDP
DNS 既使用 TCP 又使用 UDP。
当进行区域传送(主域名服务器向辅助域名服务器传送变化的那部分数据)时会使用 TCP,因为数据同步传送的数据量比一个请求和应答的数据量要多,而 TCP 允许的报文长度更长,因此为了保证数据的正确性,会使用基于可靠连接的 TCP。
当客户端想 DNS 服务器查询域名(域名解析)的时候,一般返回的内容不会超过 UDP 报文的最大长度,即 512 字节,用 UDP 传输时,不需要创建连接,从而大大提高了响应速度,但这要求域名解析服务器和域名服务器都必须自己处理超时和重传从而保证可靠性。
IP协议的定义和作用
IP 协议用于在计算机网络之间传输数据包,它定义了数据包的格式和处理规则,确保数据能够从一个设备传输到另一个设备,可能跨越多个中间网络设备(路由器)
IP 协议有哪些作用?
①、寻址:每个连接到网络的设备都有一个唯一的 IP 地址。IP 协议使用这些地址来标识数据包的源地址和目的地址,确保数据包能够准确地传输到目标设备。
②、路由:IP 协议负责决定数据包在网络传输中的路径。比如说路由器使用路由表和 IP 地址信息来确定数据包的最佳传输路径。
③、分片和重组:当数据包过大无法在某个网络上传输时,IP 协议会将数据包分成更小的片段进行传输。接收端会根据头部信息将这些片段重新组装成完整的数据包。
IP地址分类
IP 地址 = {<网络号>,<主机号>}。
- 网络号:它标志主机所连接的网络地址表示属于互联网的哪一个网络。
- 主机号:它标志主机地址表示其属于该网络中的哪一台主机。
IP 地址分为 A,B,C,D,E 五大类:
- A 类地址 (1~126):以 0 开头,网络号占前 8 位,主机号占后面 24 位。
- B 类地址 (128~191):以 10 开头,网络号占前 16 位,主机号占后面 16 位。
- C 类地址 (192~223):以 110 开头,网络号占前 24 位,主机号占后面 8 位。
- D 类地址 (224~239):以 1110 开头,保留为多播地址。
- E 类地址 (240~255):以 1111 开头,保留位为将来使用
回环地址:A 类中127.0.0.0/8段,用于本机通信,最常用127.0.0.1。
本机自发自收,数据不会离开本机,不会经过网卡、不会发到网络上。用于本机内部进程通信测试。
私有 IP 地址(RFC1918,非全球唯一)
- A 类:
10.0.0.0 ~ 10.255.255.255 - B 类:
172.16.0.0 ~ 172.31.255.255 - C 类:
192.168.0.0 ~ 192.168.255.255

早期 ABC 分类太死板,浪费 IP。CIDR 表示法IP / 前缀位数例如:192.168.1.0/24表示:前 24 位是网络位,等价于掩码 255.255.255.0
子网(Subnet)就是把一个大的 IP 网络,按规则切分成多个更小、相互隔离的小网络。子网划分是向主机位借位生成子网,通过公式计算子网数、主机数和步长,将大网段拆分为多个小网段,节约 IP 并缩小广播域。子网划分用于缩小广播域、节约 IP、提升安全与管理效率。
域名和IP的关系?一个IP可以对应的多个域名吗?
- IP 地址在同一个网络中是惟一的,用来标识每一个网络上的设备,其相当于一个人的身份证号
- 域名在同一个网络中也是惟一的,就像是一个人的名字、绰号
假如你有多个不用的绰号,你的朋友可以用其中任何一个绰号叫你,但你的身份证号码却是惟一的。但同时你的绰号也可能和别人重复,假如你不在,有人叫你的绰号,其它人可能就答应了。
一个域名可以对应多个 IP,但这种情况 DNS 做负载均衡的,在用户访问过程中,一个域名只能对应一个 IP。而一个 IP 也可以对应多个域名,是一对多的关系。虚拟主机,一台服务器一个 IP,运行多个网站
浏览器在请求里带上 Host 头,服务器知道你访问哪个站点.这是互联网早期节省公网 IP 的重要方式
IPV4地址不够如何解决
- DHCP:动态主机配置协议,动态分配 IP 地址,只给接入网络的设备分配 IP 地址,因此同一个 MAC 地址的设备,每次接入互联网时,得到的 IP 地址不一定是相同的,该协议使得空闲的 IP 地址可以得到充分利用。
- CIDR:无类别域间路由。CIDR 消除了传统的 A 类、B 类、C 类地址以及划分子网的概念,因而更加有效地分配 IPv4 的地址空间,但无法从根本上解决地址耗尽的问题。
- NAT:网络地址转换协议,属于不同局域网的主机可以使用相同的 IP 地址,从而一定程度上缓解了 IP 资源枯竭的问题,然而主机在局域网中使用的 IP 地址是不能在公网中使用的,当局域网主机想要与公网主机进行通信时,NAT 方法可以将该主机 IP 地址转换为全球 IP 地址。该协议能够有效解决 IP 地址不足的问题。
- IPv6:作为接替 IPv4 的下一代互联网协议,其可以实现 2 的 128 次方个地址,而这个数量级,即使给地球上每一粒沙子都分配一个 IP 地址也够用,该协议能够从根本上解决 IPv4 地址不够用的问题。
ARP协议
ARP是网络通信中的一种协议,主要目的是将网络层的 IP 地址解析为链路层的 MAC 地址。
①、ARP 请求
当主机 A 要发送数据给主机 B 时,首先会在自己的 ARP 缓存中查找主机 B 的 MAC 地址。
如果没有找到,主机 A 会向网络中广播一个 ARP 请求数据包,请求网络中的所有主机告诉它们的 MAC 地址;这个请求包含了请求设备和目标设备的 IP 和 MAC 地址。
②、ARP 应答
网络中的所有主机都会收到这个 ARP 请求,但只有主机 B 会回复 ARP 应答,告诉主机 A 自己的 MAC 地址。并且主机 B 会将主机 A 的 IP 和 MAC 地址映射关系缓存到自己的 ARP 缓存中,以便下次通信时直接使用。
③、更新 ARP 缓存
主机 A 收到主机 B 的 ARP 应答后,也会将主机 B 的 IP 和 MAC 地址映射关系缓存到自己的 ARP 缓存中。
为什么既要有IP地址,又要有MAC地址
MAC 地址和 IP 地址都有什么作用?
- MAC 地址是数据链路层和物理层使用的地址,是写在网卡上的物理地址,用来定义网络设备的位置,不可变更。
- IP 地址是网络层和以上各层使用的地址,是一种逻辑地址。IP 地址用来区别网络上的计算机。
为什么有了 MAC 地址还需要 IP 地址?
如果我们只使用 MAC 地址进行寻址的话,我们需要路由器记住每个 MAC 地址属于哪个子网,不然一次路由器收到数据包都要满世界寻找目的 MAC 地址。而我们知道 MAC 地址的长度为 48 位,也就是最多共有 2 的 48 次方个 MAC 地址,这就意味着每个路由器需要 256T 的内存,显然是不现实的。
和 MAC 地址不同,IP 地址是和地域相关的,在一个子网中的设备,我们给其分配的 IP 地址前缀都是一样的,这样路由器就能根据 IP 地址的前缀知道这个设备属于哪个子网,剩下的寻址就交给子网内部实现,从而大大减少了路由器所需要的内存。
为什么有了 IP 地址还需要 MAC 地址?
- 只有当设备连入网络时,才能根据他进入了哪个子网来为其分配 IP 地址,在设备还没有 IP 地址的时候,或者在分配 IP 的过程中。我们需要 MAC 地址来区分不同的设备。
- IP 地址可以比作为地址,MAC 地址为收件人,在一次通信过程中,两者是缺一不可的。
IP 地址负责跨网络的端到端寻址(我要去哪),MAC 地址负责同一链路上的逐跳交付(下一跳是谁)。二者在 TCP/IP 分层模型中各司其职,缺一不可。
ICMP协议
ICMP(Internet Control Message Protocol) ,网际控制报文协议。
- ICMP 协议是一种面向无连接的协议,用于传输出错报告控制信息。
- 它是一个非常重要的协议,它对于网络安全具有极其重要的意义。它属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。
- 当遇到 IP 数据无法访问目标、IP 路由器无法按当前的传输速率转发数据包等情况时,会自动发送 ICMP 消息。
比如日常使用得比较多的 ping,就是基于 ICMP 的。
ping的原理
网络工具,主要用来测试网络连接的可达性和延迟。Ping 的过程主要基于 ICMP(互联网控制消息协议)实现,其基本过程包括:
①、当执行 Ping 命令,如ping javabetter.cn,Ping 首先解析域名获取 IP 地址,然后向目标 IP 发送一个 ICMP Echo Request 消息。
②、当目标 IP 收到 ICMP Echo Request 消息后,它会生成一个 ICMP Echo Reply 消息并返回,即 Ping 响应消息。
③、发起 Ping 命令的设备接收到 ICMP Echo Reply 消息后,计算并显示从发送 Echo Request 到接收到 Echo Reply 的时间(通常称为往返时间 RTT,Round-Trip Time),以及可能的丢包情况。
Ping 通常会发送多个请求,以便提供平均响应时间和丢包率等信息,以便我们了解网络连接的质量。
网络安全
网络安全攻击主要分为两种类型,被动攻击和主动攻击:
被动攻击:是指攻击者从网络上窃听他人的通信内容,通常把这类攻击称为截获,被动攻击主要有两种形式:消息内容泄露攻击和流量分析攻击。由于攻击者没有修改数据,使得这种攻击很难被检测到。
主动攻击:直接对现有的数据和服务造成影响,常见的主动攻击类型有:
篡改:攻击者故意篡改网络上送的报文,甚至把完全伪造的报文传送给接收方。
恶意程序:恶意程序种类繁多,包括计算机病毒、计算机蠕虫、特洛伊木马、后门入侵、流氓软件等等。
拒绝服务 Dos:攻击者向服务器不停地发送分组,使服务器无法提供正常服务
DNS劫持
DNS 劫持即域名劫持,是通过将原域名对应的 IP 地址进行替换,从而使用户访问到错误的网站,或者使用户无法正常访问网站的一种攻击方式。
域名劫持往往只能在特定的网络范围内进行,范围外的 DNS 服务器能够返回正常的 IP 地址。攻击者可以冒充原域名所属机构,通过电子邮件的方式修改组织机构的域名注册信息,或者将域名转让给其它主持,并将新的域名信息保存在所指定的 DNS 服务器中,从而使用户无法对原域名来进行解析以访问目标地址。
DNS 劫持的步骤是什么样的?
- 获取要劫持的域名信息:攻击者会首先访问域名查询要劫持的站点的域名信息。
- 控制域名响应的 E-Mail 账号:在获取到域名信息后,攻击者通过暴力破解或者专门的方法破解公司注册域名时使用的 E-mail 账号所对应的密码,更高级的攻击者甚至能够直接对 E-Mail 进行信息窃取。
- 修改注册信息:当攻击者破解了 E-Mail 后,会利用相关的更改功能修改该域名的注册信息,包括域名拥有者信息,DNS 服务器信息等。
- 使用 E-Mail 收发确认函:在修改完注册信息后,攻击者 E-Mail 在真正拥有者之前收到修改域名注册信息的相关确认信息,并回复确认修改文件,待网络公司恢复已成功修改信件后,攻击者便成功完成 DNS 劫持。
怎么应对 DNS 劫持?
- 直接通过 IP 地址访问网站,避开 DNS 劫持
- 由于域名劫持往往只能在特定的网络范围内进行,因此一些高级用户可以通过网络设置让 DNS 指向正常的域名服务器以实现对目标网址的正常访问,例如计算机首选 DNS 服务器的地址固定为 8.8.8.8。
XSS攻击是什么 如何避免
跨站脚本攻击(Cross-Site Scripting)恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览网页的时候,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。
用户输入未被正确过滤 / 转义,直接被当作 HTML/JS 渲染到页面上,浏览器无法区分是正常代码还是恶意代码。
存储型 XSS
- 恶意代码存入服务器数据库(如评论、留言)
- 所有访问该页面的用户都会执行攻击
反射型 XSS
- 恶意代码放在 URL 参数中,诱导用户点击
- 代码不存库,只在当前响应中 “反射” 执行
DOM 型 XSS
- 不经过服务器,直接通过前端 JS 修改 DOM 触发
- 完全发生在浏览器端
如何应对XSS攻击
- 输入过滤:对用户输入进行严格校验
- 输出转义:渲染到 HTML 前转义特殊字符(
< > " ' &) - CSP(内容安全策略):限制脚本来源,从根本上阻止恶意 JS
- Cookie 设置 HttpOnly:禁止 JS 读取 Cookie,防止被盗
CSRF攻击是什么
跨站请求伪造是一种挟持用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。
核心原理
- 用户登录目标网站 A,浏览器保存 A 的 Cookie(含 Session)
- 用户被诱导访问恶意网站 B
- B 自动向 A 发送请求(表单 / 图片 / 接口),浏览器自动带上 A 的 Cookie
- 服务端校验 Cookie 通过,误认为是用户本人操作
关键特点
- 不需要窃取 Cookie,利用浏览器自动携带机制
- 必须满足:用户已登录目标站点
- 常见场景:转账、改密、发帖、删除数据等状态修改操作
CSRF防御方式
- 使用 CSRF Token(最主流、Spring Security 默认方案)
- 服务端生成随机 Token,存入 Session/Redis,同时返回前端
- 前端请求时手动携带 Token(Header / 参数)
- 服务端校验 Token 与 Cookie 中的会话是否匹配
- 优势:攻击者无法获取页面中的 Token,防御效果最好
- Cookie 设置 SameSite 属性
SameSite=Strict/Lax:限制第三方站点请求时携带 Cookie- 现代浏览器默认支持,从源头阻断跨站 Cookie 发送
- 验证来源站点(辅助方案)
- 校验 Referer / Origin 请求头,确认请求来自合法域名
- 缺点:请求头可被伪造,仅作为辅助防护
- 敏感操作增加二次验证
- 短信验证码、邮箱验证码、登录密码二次确认
- 即使绕过 CSRF 防护,也无法完成操作
DoS,DDoS,DRDoS攻击是什么
- DOS: (Denial of Service), 翻译过来就是拒绝服务, 一切能引起拒绝 行为的攻击都被称为 DOS 攻击。最常见的 DoS 攻击就有计算机网络宽带攻击、连通性攻击。
- DDoS: (Distributed Denial of Service),翻译过来是分布式拒绝服务。是指处于不同位置的多个攻击者同时向一个或几个目标发动攻击,或者一个攻击者控制了位于不同位置的多台机器,并利用这些机器对受害者同时实施攻击。
主要形式有流量攻击和资源耗尽攻击,常见的 DDoS 攻击有:SYN Flood、ACK Flood、UDP Flood 等。
ACK Flood:发送大量伪造的 TCP ACK 包,耗尽服务器连接状态表与 CPU。攻击者发送大量随机源 IP 的 ACK 包服务器需要查找连接表,发现不存在则回复 RST.大量查找 + 回复导致CPU 爆满、连接表耗尽
- DRDoS: (Distributed Reflection Denial of Service),中文是分布式反射拒绝服务,该方式靠的是发送大量带有被害者 IP 地址的数据包给攻击主机,然后攻击主机对 IP 地址源做出大量回应,从而形成拒绝服务攻击。
如何防范 DDoS?
针对 DDoS 中的流量攻击,最直接的方法是增加带宽,理论上只要带宽大于攻击流量就可以了,但是这种方法成本非常高。在有充足带宽的前提下,应该尽量提升路由器、网卡、交换机等硬件设施的配置。
针对资源耗尽攻击,我们可以升级主机服务器硬件,在网络带宽得到保证的前提下,使得服务器能够有效对抗海量的 SYN 攻击包。也可以安装专业的抗 DDoS 防火墙,从而对抗 SYN Flood 等流量型攻击。负载均衡,CDN 等技术都能有效对抗 DDos 攻击。
对称加密和非对称加密
对称加密:指加密和解密使用同一密钥,优点是运算速度较快,缺点是如何安全将密钥传输给另一方。常见的对称加密算法有:DES、AES 等。
非对称加密:指的是加密和解密使用不同的密钥(即公钥和私钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。常见的非对称加密算法有 RSA。
AES算法采用对称加密的方式,其秘钥长度最长只有 256 个比特,加密和解密速度较快,易于硬件实现。由于是对称加密,通信双方在进行数据传输前需要获知加密密钥。
RSA算法采用非对称加密的方式,采用公钥进行加密,私钥解密的形式。其私钥长度一般较长,由于需要大数的乘幂求模等运算,其运算速度较慢,不合适大量数据文件加密。
操作系统
操作系统是计算机系统中管理硬件和软件资源的中间层系统,屏蔽了硬件的复杂性,并且为用户提供了便捷的交互方式。
操作系统功能:
①、负责创建和终止进程。进程是正在运行的程序实例,每个进程都有自己的地址空间和资源。
②、负责为进程分配资源,比如说内存,并在进程终止时回收内存。
③、提供创建、删除、读写文件的功能,并组织文件的存储结构,比如说目录。
④、通过设备驱动程序控制和管理计算机的硬件设备,如键盘、鼠标、打印机等。
用户态和内核态
内核是一个计算机程序,它是操作系统的核心,提供了操作系统最核心的能力,可以控制操作系统中所有的内容。内存可以分为两大区域:内核空间(Kernel Space)和用户空间(User Space)。这种划分主要用于保护系统稳定性和安全性。
- 内核空间,是操作系统内核代码及其运行时数据结构所在的内存区域,拥有对系统所有资源的完全访问权限,如进程管理、内存管理、文件系统、网络堆栈等。
- ⽤户空间,是操作系统为应用程序(如用户运行的进程)分配的内存区域,用户空间中的进程不能直接访问硬件或内核数据结构,只能通过系统调用与内核通信。
当程序使⽤⽤户空间时,常说该程序在 ⽤户态 执⾏,⽽当程序使内核空间时,程序则在 内核态 执⾏。
用户态和内核态的切换
当应用程序执行系统调用时,CPU 将从用户态切换到内核态,进入内核空间执行相应的内核代码,然后再切换回用户态。

系统调用是应用程序请求操作系统内核提供服务的接口,如文件操作(如 open、read、write)、进程控制(如 fork、exec)、内存管理(如 mmap)等。
用户态切换到内核态的三种方式
- 系统调用(System Call)
- 用户进程主动请求内核服务(open、read、write、malloc 底层等)
- 软中断 / 异常指令进入内核
- 主动切换
- 中断(Interrupt)
- 外设事件:网卡收包、磁盘完成、键盘输入、时钟中断
- CPU 暂停当前用户进程,转去执行中断处理程序
- 被动切换
- 异常(Exception)
- 缺页中断(page fault)
- 除零错误、非法指令、权限错误
- CPU 自动转到异常处理程序
- 被动切换
进程和线程
并行和并发有什么区别
并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器做到的并发,其实是利用时间片的轮转,例如有两个进程 A 和 B,A 运行一个时间片之后,切换到 B,B 运行一个时间片之后又切换到 A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。
并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。
进程上下文切换
上下文切换是操作系统在多任务处理环境中,将 CPU 从一个进程切换到另一个进程的过程。通过让多个进程共享 CPU 资源,使系统能够并发执行多个任务。
进程上下文切换通畅包含以下几个步骤:
- 保存当前进程的上下文:操作系统保存当前进程的 CPU 寄存器,程序状态等关键信息。
- 选择下一个进程:调度程序选择下一个要执行的进程。
- 恢复上一个进程的上下文。
切换到下一个进程。
系统调用 vs 普通函数调用
普通函数:同权限、同地址空间,开销极小,系统调用:跨权限、地址空间切换,上下文保存 / 恢复,开销大
参数传递为什么不用普通栈?
用户栈不可信,内核需校验参数,寄存器传递更快,大量参数才拷贝到内核空间
为什么要通过库函数间接调用?
屏蔽底层陷入指令差异(int 0x80/syscall),简化用户编程,提供统一接口
进程有哪些状态
当一个进程开始运行时,它可能会经历下面这几种状态:
- 运⾏状态(Runing):该时刻进程占⽤ CPU;
- 就绪状态(Ready):可运⾏,由于其他进程处于运⾏状态⽽暂时停⽌运⾏;
- 阻塞状态(Blocked):该进程正在等待某⼀事件发⽣(如等待输⼊/输出操作的完成)⽽暂时停⽌运⾏,这时,即使给它 CPU 控制权,它也⽆法运⾏;
进程还有另外两个基本状态:
- 创建状态(new):进程正在被创建时的状态;
- 结束状态(Exit):进程正在从系统中消失时的状态;
僵尸进程
子进程已经退出(终止),但其父进程还没有调用 wait() 或 waitpid() 来获取子进程的退出状态信息。此时子进程虽然不再运行,但其进程描述符(PCB)仍然残留在进程表中。
状态标志:在 top 或 ps 命令中,僵尸进程的状态显示为 Z (Defunct)。
设计初衷是让父进程有机会知道子进程是怎么死的(退出码是多少)。
危害:有潜在危害。虽然僵尸进程不占用 CPU 和内存,但它占用进程号 (PID)。系统的 PID 数量是有限的,如果大量产生僵尸进程,会导致系统无法创建新进程。
解决方法:
- 父进程调用
wait()。 - 如果父进程不配合,可以杀死父进程。父进程死后,僵尸子进程变成“孤儿”,由 init 进程领养并立即清理。
孤儿进程
父进程先于子进程退出,此时子进程还在运行,它就成了“孤儿”。系统不会让进程无家可归。在 Linux 中,原父进程死后,init 进程(PID 为 1,现在很多系统是 systemd)会自动领养这些孤儿进程,成为它们的新父进程。当孤儿进程运行结束时,由 init 进程负责收集它们的退出状态并释放资源。
进程调度算法
进程调度是操作系统中的核心功能之一,它负责决定哪些进程在何时使用 CPU。这一决定基于系统中的进程调度算法.
先来先服务,进程按照请求 CPU 的顺序进行调度。这种方式易于实现,但可能会导致较短的进程等待较长进程执行完成,从而产生“饥饿”现象。
短作业优先,选择预计运行时间最短的进程优先执行。这种方式可以减少平均等待时间和响应时间,但缺点是很难准确预知进程的执行时间,并且可能因为短作业一直在执行,导致长作业持续被推迟执行。
- 非抢占式 SJF:一旦开始执行就运行到结束。
- 最短剩余时间优先 (SRTF):抢占式版本,新进程剩余时间更短则切切换。
优点:平均等待时间最小。缺点:饥饿现象。如果不断有短进程加入,长进程可能永远得不到执行
优先级调度,在这种调度方式中,每个进程都被分配一个优先级。CPU 首先分配给优先级最高的进程。优先级调度可以是非抢占式的或抢占式的。在非抢占式优先级调度中,进程一旦开始执行将一直运行直到完成;在抢占式优先级调度中,更高优先级的进程可以中断正在执行的低优先级进程。
时间片轮转调度为每个进程分配一个固定的时间段,称为时间片,进程可以在这个时间片内运行。如果进程在时间片结束时还没有完成,它将被放回队列的末尾。时间片轮转是公平的调度方式,可以保证所有进程得到公平的 CPU 时间,适用于共享系统。
高响应比优先综合考虑等待时间和要求服务时间。
$优先权 = \frac{等待时间 + 要求服务时间}{要求服务时间}$
多级反馈队列
设置多个就绪队列,每个队列优先级不同,时间片大小也不同(优先级越高,时间片越小)。
新进程先进入第一级高优先级队列,若时间片内未完成,则降级到下一级队列。
只有当高优先级队列为空时,才调度低优先级队列。
优点:兼顾短作业(快速完成)、I/O 型作业(保持高优先级)和长作业(最终能完成)。
进程间通信方式
进程间通信的方式有 6 种,管道、信号、消息队列、共享内存和套接字。
管道
管道包括匿名管道和命名管道.进程间的管道就是内核中的一串缓存,从管道的一端写入数据,另一端读取。数据只能单向流动,遵循先进先出(FIFO)的原则。
匿名管道:允许具有亲缘关系的进程(如父子进程)进行通信。特点:半双工(数据只能单向流动),只能用于具有亲缘关系的进程(如父子进程)。
原理:在内核中开辟一块缓冲区,一端写,一端读。生命周期随进程结束。
命名管道 (FIFO):
- 特点:可以在无关进程之间通信。
- 原理:在文件系统中有一个路径名,以文件形式存在,但数据存储在内核内存中。
信号
- 原理:一种异步通信机制,用于通知接收进程某个事件已发生。
- 场景:比如你在终端按下
Ctrl+C发送SIGINT信号强制停止进程,或者子进程退出发送SIGCHLD给父进程。
Linux 中常用的信号:
- SIGHUP:当退出终端时,由该终端启动的所有进程都会接收到这个信号,默认动作为终止进程。
- SIGINT:程序终止(interrupt)信号。按
Ctrl+C时发出,大家应该在操作终端时有过这种操作。 - SIGQUIT:和 SIGINT 类似,按
Ctrl+\键将发出该信号。它会产生核心转储文件,将内存映像和程序运行时的状态记录下来。 - SIGKILL:强制杀死进程,本信号不能被阻塞和忽略。
- SIGTERM:与 SIGKILL 不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出
消息队列
存放在内核中的消息链表。每个消息都有一个类型(Type)和优先级。
解耦:发送方不需要等待接收方准备好。结构化:不像管道是无格式的字节流,消息队列支持按类型读取,非常灵活。
缺点:消息体有一个最大长度的限制,不适合比较大的数据传输;存在用户态与内核态之间的数据拷贝开销。
共享内存
两个或多个进程映射同一块物理内存。进程可以直接读写这块内存,不需要内核介入数据拷贝。
优点:速度极快,是最高效的 IPC 方式。
缺点:没有同步机制。如果两个进程同时写,数据会乱。因此通常需要配合信号量一起使用。
信号量
它本质上是一个计数器,用来控制对共享资源的访问数量。
控制信号量的⽅式有两种原⼦操作:
- ⼀个是 P 操作(wait,减操作),当进程希望获取资源时,它会执行 P 操作。如果信号量的值大于 0,表示有资源可用,信号量的值减 1,进程继续执行。如果信号量的值为 0,表示没有可用资源,进程进入等待状态,直到信号量的值变为大于 0。
- 另⼀个是 V 操作(signal,加操作),当进程释放资源时,它会执行 V 操作,信号量的值加 1。如果有其他进程因为等待该资源而被阻塞,这时会唤醒其中一个进程。
套接字Socket
网络通信,允许不同主机上的进程进行通信
进程和线程的联系和区别
进程是一个正在执行的程序实例。每个进程都有自己独立的地址空间、全局变量、堆栈、和文件描述符等资源。线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享进程的地址空间和资源。每个进程在独立的地址空间中运行,不会直接影响其他进程。线程共享同一个进程的内存空间、全局变量和文件描述符。进程切换需要保存和恢复大量的上下文信息,代价较高。线程切换相对较轻量,因为线程共享进程的地址空间,只需要保存和恢复线程私有的数据。
线程的生命周期由进程控制,进程终止时,其所有线程也会终止。
| 特性 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享 |
| 内存开销 | 高 | 低 |
| 上下文切换 | 慢,开销大 | 快,开销小 |
| 通信 | 需要 IPC 机制,开销较大 | 共享内存,直接通信 |
| 创建销毁 | 开销大,较慢 | 开销小,较快 |
| 并发性 | 低 | 高 |
| 崩溃影响 | 一个进程崩溃不会影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
协程是什么
协程是一种用户态的轻量级线程。它的调度完全由用户程序(或者编程语言的运行时,如 Go 语言的调度器)控制,而不是由操作系统内核控制。协程运行在单线程之上。当一个协程遇到 I/O 等待时,它会主动“让出” CPU 控制权,让同一线程下的其他协程运行。
核心关键字:非抢占式。线程是抢占式的(OS 随时可能中断你),而协程是协作式的(必须手动 yield 或遇到挂起点)。
- 用户态轻量级线程,由用户态调度,无内核切换开销
- 一个线程可跑多个协程,协作式调度
为什么需要协程
极致的并发能力,解决 I/O 密集型任务的痛点
线程上下文切换
- 当两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样;
- 当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
所以,线程的上下⽂切换相⽐进程,开销要⼩很多。
线程有哪些实现方式
主要有三种线程的实现⽅式:
内核态线程实现:在内核空间实现的线程,由内核直接管理线程。 Java (JDK 21 之前) 默认采用的方式。一对一模型 (1:1)
实现原理:线程管理的所有工作(创建、调度、回收)都由操作系统内核完成。
映射关系:每个用户线程都对应一个独立的内核调度实体。
优点:
- 真并行:多核 CPU 环境下,多个线程可以真正的同时运行。
- 不阻塞:一个线程阻塞,不影响其他线程。
缺点:
- 昂贵:创建、销毁和上下文切换都需要进行系统调用(System Call),涉及用户态和内核态的转换,开销较大。
- 资源限制:内核线程占用栈空间较大,系统无法支持数十万级别的线程。
⽤户态线程实现:在⽤户空间实现线程,不需要内核的参与,内核对线程无感知。多对一模型 (M:1)
实现原理:由用户空间的线程库(如早期的 Java Green Threads)来管理线程的创建、调度和销毁。
映射关系:多个用户线程对应一个内核线程。
优点:
- 极快:切换线程不需要切换到内核态,开销极小。
- 自定义:可以根据应用需求定制调度算法。
缺点:
- 一堵全堵:如果一个用户线程发起了阻塞 I/O(如等待磁盘读取),由于内核只看到一个进程,整个进程都会被挂起,其他用户线程也无法运行。
- 无法利用多核:内核只给进程分配一个 CPU 核心,无法让多个用户线程在多个核上并行计算。
混合线程实现:现代操作系统基本都是将两种方式结合起来使用。用户态的执行系统负责进程内部线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换。
即同时实现内核态和用户态线程管理。其中内核态线程数量较少,而用户态线程数量较多。每个内核态线程可以服务一个或多个用户态线程。 多对多模型 (M:N)
实现原理:用户空间管理大量的虚拟线程,内核维护一定数量的内核线程。
映射关系:$M$ 个用户线程映射到 $N$ 个内核线程上(通常 $M > N$)。
优点:
- 高性能:用户线程切换快。
- 高并发:支持海量线程。
- 不阻塞:利用内核线程实现并行,且当一个内核线程阻塞时,用户调度器可以将剩余的用户线程切换到其他活跃的内核线程上。
线程之间如何同步
同步解决的是多线程操作共享资源的问题,不管线程之间是如何穿插执行的,最后的结果都是正确的。
在多线程编程中,由于多个线程共享进程的资源(如堆内存、全局变量),如果不同步,就会出现竞态条件(Race Condition),导致数据混乱。线程同步的核心目标是:让多个线程按照预定的先后次序执行。
在操作系统层面,保证线程同步的方式有很多,比如锁、信号量等。
临界区:对共享资源访问的程序片段,希望这段代码是
互斥的,可以保证在某个时刻只能被一个线程执行,也就是说一个线程在临界区执行时,其它线程应该被阻止进入临界区。
互斥锁
这是最基础的同步方式。它保证同一时刻只有一个线程能访问共享资源。
- 原理:线程在进入临界区前尝试“加锁”,如果锁已被占用则阻塞;执行完后“释放锁”。
- Java 示例:
synchronized关键字或ReentrantLock类。
读写锁
因为“读”操作本身不会破坏数据。读写锁允许多个线程同时读,但写操作是排他的。
规则:
- 读-读:不互斥(可以并发)。
- 读-写 / 写-写:互斥。
适用场景:读多写少的场景(如缓存)。
信号量
信号量也可以用于线程同步。
- 原理:维护一个计数器,表示可用资源的数量。
- P (acquire):计数器减 1,若为 0 则阻塞。
- V (release):计数器加 1,唤醒等待线程。
- 场景:控制并发访问的线程上限(如数据库连接池)。
条件变量
条件变量通常与互斥锁配合使用。它允许线程在某个条件不满足时“挂起”,直到另一个线程通知它条件已达成。
- 核心操作:
wait(等待并释放锁) 和signal/notify(唤醒)。 - 经典模型:生产者-消费者模型。
- 消费者发现缓冲区空了,调用
wait睡觉。 - 生产者放了东西,调用
notify叫醒消费者
- 消费者发现缓冲区空了,调用
事件 / 屏障
- 屏障 (CyclicBarrier):让一组线程全部到达某个点后再一起继续执行。就像团建导游说:“等所有人到齐了再开饭”。
- 倒计时锁 (CountDownLatch):一个线程等待其他 N 个线程执行完后再执行。
原子操作
如果你只是想给一个变量加 1,用锁太重了(涉及上下文切换)。
- 原理:利用 CPU 提供的特殊指令(如 CAS, Compare And Swap),在硬件层面保证操作的不可分割性。
- Java 实现:
AtomicInteger,AtomicReference等。
什么是死锁
两个或多个线程互相持有对方需要的资源,并且都在等待对方释放,导致所有相关线程都陷入永久阻塞。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public class DeadlockDemo {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
// 线程 1 尝试先锁 1 再锁 2
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock 1 & 2");
}
}
}).start();
// 线程 2 尝试先锁 2 再锁 1
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock 2 & 1");
}
}
}).start();
}
}
死锁产生的四个必要条件
只有当以下四个条件同时满足时,才会发生死锁
- 互斥条件(Mutual Exclusion):资源不能被多个进程共享,即资源一次只能被一个进程使用。如果一个资源已经被分配给了一个进程,其他进程必须等待,直到该资源被释放。
- 持有并等待条件(Hold and Wait):一个进程已经持有了至少一个资源,同时还在等待获取其他被占用的资源。在此期间,该进程不会释放已经持有的资源。
- 不可剥夺条件(No Preemption):已分配给进程的资源不能被强制剥夺,只有持有该资源的进程可以主动释放资源。
- 循环等待条件(Circular Wait):存在一个进程集合{P1,P2,…} ,其中P1等待P2持有的资源,P2等待P3持有的资源,依此类推,直到Pn等待P1持有的资源,形成一个进程等待环。
解决死锁通常有三种策略:
A. 预防 (Prevention) —— 破坏必要条件
- 破坏“循环等待”:这是最常用的方法。规定所有线程必须按同一顺序申请资源。比如上面的例子,如果线程 2 也先申请
lock1再申请lock2,就不会死锁了。 - 破坏“占有且等待”:要求线程在开始执行前一次性申请所有需要的资源。
B. 避免 (Avoidance) —— 动态分配检查
- 银行家算法 (Banker’s Algorithm):在每次分配资源前,先计算这次分配是否会导致系统进入“不安全状态”。如果可能导致死锁,就不分配。
C. 检测与恢复 (Detection & Recovery)
- 死锁检测:系统定期运行一个算法,检查资源分配图中是否存在环。
- 恢复:一旦发现死锁,强制终止某个进程或剥夺其资源以打破循环。
活锁和饥饿锁
饥饿锁:
饥饿锁,这个饥饿指的是资源饥饿,某个线程一直等不到它所需要的资源,从而无法向前推进,就像一个人因为饥饿无法成长。
活锁:
进程或线程并没有被阻塞,它们状态一直在改变,但由于互相谦让或错误的重试机制,导致程序始终无法向前推进。
两个线程都在尝试获取两个资源,如果发现冲突,就同时释放已占有的资源并重试。由于步调一致,它们永远在“获取-冲突-释放-重试”的怪圈里循环。
| 状态 | 线程表现 | 资源占用 | 根本原因 |
|---|---|---|---|
| 死锁 (Deadlock) | 停止 (Block) | 互相持有并等待 | 循环等待,互不相让 |
| 活锁 (Livelock) | 运行 (Active) | 频繁释放与申请 | 步调一致的“过度谦让”或逻辑错误 |
| 饥饿 (Starvation) | 等待 (Waiting) | 无法获取资源 | 资源分配不公或优先级太低 |
内存管理
物理内存和虚拟内存
物理内存指的是计算机中实际存在的硬件内存。物理内存是计算机用于存储运行中程序和数据的实际内存资源,操作系统和应用程序最终都必须使用物理内存来执行。
虚拟内存是操作系统提供的一种内存管理技术,它使得应用程序认为自己有连续的、独立的内存空间,而实际上,这个虚拟内存可能部分存储在物理内存上,部分存储在 磁盘(如硬盘的交换分区或页面文件) 中。
虚拟内存的核心思想是通过硬件和操作系统的配合,为每个进程提供一个独立的、完整的虚拟地址空间,解决物理内存不足的问题。
①、每个进程都有自己的虚拟地址空间,虚拟内存使用的是逻辑地址,它与实际的物理内存地址不同,必须经过地址转换才能映射到物理内存。
②、操作系统通过 页表(Page Table) 将虚拟地址映射到物理地址。当程序访问某个虚拟地址时,CPU 会通过页表找到对应的物理地址。
③、操作系统将虚拟内存划分为若干个页(Pages),每个页可以被映射到物理内存中的一个页面。如果物理内存不够,操作系统会将不常用的页暂时存储到磁盘的交换区(Swap)中,这个过程叫做页交换(Paging)。
内存分段
程序是由若⼲个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就⽤分段(Segmentation)的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成,段号和段内偏移量。虚拟地址和物理地址通过段表映射,段表主要包括段号、段的界限
内存分页
分⻚是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚(Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB
访问分页系统中内存数据需要两次的内存访问 :一次是从内存中访问页表,从中找到指定的物理页号,加上页内偏移得到实际物理地址,第二次就是根据第一次得到的物理地址访问内存取出数据。
因为分页需要频繁查询页表(这在内存中),会导致 CPU 访问内存的速度减慢。
- TLB (Translation Lookaside Buffer):一块高速缓存,存储最近查过的页表项,极大加快了地址转换。
- 多级页表:为了节省页表本身占用的巨大内存空间,将页表像书的目录一样分层存储。
多级页表
多级页表是一种内存管理技术,用于在虚拟内存系统中高效地管理和转换虚拟地址到物理地址。它通过分层结构减少页表所需的内存开销,以解决单级页表在大地址空间中的效率问题。

多级页表通过将单级页表拆分为多个层级,减少了内存浪费。以两级页表为例:
- 一级页表(页目录):存储二级页表的地址。每个页目录条目(PDE)指向一个二级页表。
- 二级页表(页表):存储实际的页框地址。每个页表条目(PTE)指向一个物理页框。
虚拟地址分为多个部分,每一部分用于索引相应层级的页表。例如,对于一个 32 位地址和 4 KB 页大小的两级页表:
- 高 10 位:一级页表索引(页目录索引)。
- 中 10 位:二级页表索引(页表索引)。
- 低 12 位:页内偏移。
快表
同样利用了局部性原理,即在⼀段时间内,整个程序的执⾏仅限于程序中的某⼀部分。相应地,执⾏所访问的存储空间也局限于某个内存区域。
利⽤这⼀特性,把最常访问的⼏个⻚表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯⽚中,加⼊了⼀个专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。
分页和分段有什么区别
分页 (Paging) 和 分段 (Segmentation) 是两种不同的内存管理技术。最核心的区别在于:分页是出于压力(物理限制),而分段是出于逻辑(程序员需求)。
- 段是信息的逻辑单位,它是根据用户的需要划分的,因此段对用户是可见的 ;页是信息的物理单位,是为了管理主存的方便而划分的,对用户是透明的。
- 段的大小不固定,有它所完成的功能决定;页的大小固定,由系统决定
- 段向用户提供二维地址空间;页向用户提供的是一维地址空间
- 段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制
| 特性 | 分页 (Paging) | 分段 (Segmentation) |
|---|---|---|
| 划分单位 | 页 (Page):固定大小(如 4KB) | 段 (Segment):大小不一,取决于逻辑内容 |
| 划分目的 | 提高内存利用率,离散存储 | 方便程序员按逻辑模块管理、共享和保护 |
| 程序员透明度 | 透明:程序员感知不到页的存在 | 显式:程序员需要划分段(代码段、数据段等) |
| 碎片类型 | 内部碎片:最后一页可能没填满 | 外部碎片:段与段之间产生无法利用的空隙 |
| 地址维度 | 一维:逻辑地址是连续的线性空间 | 二维:由(段号 + 段内偏移)组成 |
| 共享与保护 | 较难:物理切分可能切断逻辑联系 | 容易:按逻辑段(如函数、对象)整体保护 |
什么是交换空间与缺页中断
操作系统把物理内存分成一块一块的小内存,每一块内存被称为页(page)。当内存资源不足时,Linux 把某些页的内容转移至磁盘上的一块空间上,以释放内存空间。磁盘上的那块空间叫做交换空间(swap space),而这一过程被称为交换(swapping)。物理内存和交换空间的总容量就是虚拟内存的可用容量。
用途:
- 物理内存不足时一些不常用的页可以被交换出去,腾给系统。
- 程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去
代价:硬盘(尤其是机械硬盘)的读写速度比内存慢成千上万倍。如果系统频繁读写 Swap,你会感觉到电脑明显的卡顿(这种现象称为抖动 Thrashing)。
现代趋势:在内存很大的现代服务器或带 NVMe SSD 的电脑上,Swap 的重要性有所降低,但在内存压力大时依然是防止系统崩溃(OOM)的最后一道防线
缺页中断(Page Fault)是虚拟内存管理的一个重要概念。当一个程序访问的页不在物理内存中时,就会发生缺页中断。操作系统需要从磁盘上的交换区中将缺失的页调入内存。
处理流程:
- 触发中断:CPU 发现页表项的“存在位”为 0。
- 陷入内核:操作系统接管 CPU,检查该地址是否合法。
- 寻找页框:
- 如果有空闲物理内存,直接把磁盘上的数据读进来。
- 如果物理内存满了,执行页面置换算法,踢走一个老页面。
- 更新页表:修改页表项,指向新的物理地址,并将存在位置为 1。
- 重新执行:回到刚才触发中断的那条指令重新执行。
页面置换算法
页面置换算法的目标是最小化缺页中断的次数,常见的页面置换算法有最佳⻚⾯置换算法(OPT)、先进先出置换算法(FIFO)、最近最久未使⽤的置换算法(LRU)和时钟页面置换算法等。
①、最佳⻚⾯置换算法
基本思路是,淘汰以后不会使用的页面。这是理论上的最佳算法,因为它可以保证最低的缺页率。但在实际应用中,由于无法预知未来的访问模式,OPT 通常无法实现。
②、先进先出置换算法
基本思路是,优先淘汰最早进入内存的页面。FIFO 算法维护一个队列,新来的页面加入队尾,当发生页面置换时,队头的页面(即最早进入内存的页面)被移出。
缺点:Belady 异常。有时给进程分配的物理页面越多,缺页率反而越高。且它不考虑页面使用的频率,可能会踢走经常使用的页面。
③、最近最久未使⽤的置换算法
基本思路是,淘汰最近没有使用的页面。LRU 算法根据页面的访问历史来进行置换,最长时间未被访问的页面将被置换出去。
相对更接近最优算法的效果,因为最近未使用的页面可能在将来也不会被使用。但 LRU 算法的实现需要跟踪页面的访问历史,可能会增加系统的开销。
④、时钟页面置换算法
时钟算法是 LRU 的一种近似和实现简单的形式。它通过一个循环列表(类似时钟的指针)遍历页面,每个页面有一个使用位,当页面被访问时,使用位设置为 1。
当需要页面置换时,时钟指针会顺时针移动,直到找到使用位为 0 的页面进行置换。这个过程类似于给每个页面一个二次机会。算法执行时,会先将使用位从 1 清零,如果该页面再次被访问,它的使用位再次被设置为 1。
每个页面都有一个“引用位”(Reference Bit):
- 1:代表这个页面最近被访问过。
- 0:代表这个页面最近没被访问过。
当发生缺页中断,且内存已满需要置换时,指针开始转动:
- 检查指针指向的页面:
- 如果引用位是 1:说明它最近被用过,我们给它“第二次机会”。将该位清零(置为 0),然后指针移动到下一个页面。
- 如果引用位是 0:说明它最近没被用过,这就是我们要找的“牺牲者”。直接置换该页面,指针移动到下一个位置,停止。
⑤、最不常⽤置换算法
根据页面被访问的频率进行置换,访问次数最少的页面最先被置换。实现较为复杂,需要记录每个页面的访问频率。
硬链接和软链接
硬链接就是在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。删除任意一个条目,文件还是存在,只要引用数量不为 0。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。
软链接相当于重新创建⼀个⽂件,这个⽂件有独⽴的 inode,但是这个⽂件的内容是另外⼀个⽂件的路径,所以访问软链接的时候,实际上相当于访问到了另外⼀个⽂件,所以软链接是可以跨⽂件系统的,甚⾄⽬标⽂件被删除了,链接⽂件还是在的,只不过打不开指向的文件了而已。
零拷贝是什么
假如需要文件传输,使用传统 I/O,数据读取和写入是用户空间到内核空间来回赋值,而内核空间的数据是通过操作系统的 I/O 接口从磁盘读取或者写入,这期间发生了多次用户态和内核态的上下文切换,以及多次数据拷贝。

为了提升 I/O 性能,就需要减少用户态与内核态的上下文切换和内存拷贝的次数
零拷贝技术实现主要有两种:
mmap+write
mmap() 系统调⽤函数会直接把内核缓冲区⾥的数据「映射」到⽤户空间,这样,操作系统内核与⽤户空间就不需要再进⾏任何的数据拷⻉操作。
sendfile
它可以替代前⾯的 read() 和 write() 这两个系统调⽤,这样就可以减少⼀次系统调⽤,也就减少了 2 次上下⽂切换的开销。其次,该系统调⽤,可以直接把内核缓冲区⾥的数据拷⻉到 socket 缓冲区⾥,不再拷⻉到⽤户态,这样就只有 2 次上下⽂切换,和 3 次数据拷⻉。

Kafka、RocketMQ 都采用了零拷贝技术来提升 IO 效率
DMA (Direct Memory Access,直接存储器访问) 是一种硬件机制,它允许外设(如硬盘、网卡、显卡)直接与内存进行数据交换,而不需要 CPU 的全程参与。
阻塞与非阻塞IO、同步与异步IO
阻塞 I/O当⽤户程序执⾏ read ,线程会被阻塞,⼀直等到内核数据准备好,并把数据从内核缓冲区拷⻉到应⽤程序的缓冲区中,当拷⻉过程完成, read 才会返回。阻塞等待的是内核数据准备好和数据从内核态拷⻉到⽤户态这两个过程。
⾮阻塞的 read 请求在数据未准备好的情况下⽴即返回,可以继续往下执⾏,此时应⽤程序不断轮询内核,直到数据准备好,内核将数据拷⻉到应⽤程序缓冲区, read 调⽤才可以获取到结果。
非阻塞的IO多路复用,非阻塞 I/O 有一个问题,应用程序要一直轮询,这个过程没法干其它事情,所以引入了I/O 多路复⽤技术。当内核数据准备好时,以事件通知应⽤程序进⾏操作。
⽆论是阻塞 I/O、还是⾮阻塞 I/O、非阻塞 I/O 多路复用,都是同步调⽤。因为它们在 read 调⽤时,内核将数据从内核空间拷⻉到应⽤程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷⻉效率不⾼,read 调⽤就会在这个同步过程中等待⽐较⻓的时间。
真正的异步 I/O 是内核数据准备好和数据从内核态拷⻉到⽤户态这两个过程都不⽤等待。
发起 aio_read 之后,就⽴即返回,内核⾃动将数据从内核空间拷⻉到应⽤程序空间,这个拷⻉过程同样是异步的,内核⾃动完成的,和前⾯的同步操作不⼀样,应⽤程序并不需要主动发起拷⻉动作。
阻塞 (Blocking) vs 非阻塞 (Non-blocking)
这两个概念描述的是进程在等待 I/O 结果时的状态。
- 阻塞 I/O:
- 表现:你调用一个
read操作,如果数据没准备好,你的线程就会被挂起(休眠),直到数据准备好并拷贝到用户空间。 - 比喻:去餐厅点餐,你在柜台死等,直到饭做好端到你面前,期间你什么都干不了。
- 表现:你调用一个
- 非阻塞 I/O:
- 表现:你调用
read,如果没数据,内核立即返回一个错误(如EAGAIN)。你不会被挂起,而是可以过一会儿再来问。 - 比喻:去点餐,你问“好了吗?”,服务员说“没好”。你先去刷会儿手机,过一分钟再回来问“好了吗?”。这就是轮询。
- 表现:你调用
同步 (Synchronous) vs 异步 (Asynchronous)
这两个概念描述的是消息通知机制,即“谁负责把数据搬到内存”。
- 同步 I/O:
- 关键点:由调用者自己负责将数据从内核空间拷贝到用户空间。在数据拷贝过程中,调用者是无法做别的事的。
- 注意:非阻塞 I/O 也属于同步 I/O。因为虽然轮询时不阻塞,但当数据准备好时,你依然需要亲自坐下来等数据拷贝完。
- 异步 I/O (AIO):
- 关键点:由内核负责把数据拷贝完,然后发个信号通知你。
- 比喻:外卖模式。你下单后该干嘛干嘛,外卖小哥(内核)负责送货上门,甚至帮你放进冰箱(拷贝到用户内存),最后发个短信通知你“货已带到”。
IO多路复用
在传统的 I/O 模型中,如果服务端需要支持多个客户端,我们可能要为每个客户端分配一个进程/线程。
不管是基于重一点的进程模型,还是轻一点的线程模型,假如连接多了,操作系统是扛不住的。
所以就引入了I/O 多路复用 技术。一个进程/线程维护多个 Socket,这个多路复用就是多个连接复用一个进程/线程。
I/O 多路复用三种实现机制:
- select
select 实现多路复⽤的⽅式是:
将已连接的 Socket 都放到⼀个⽂件描述符集合fd_set,然后调⽤ select 函数将 fd_set 集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很粗暴,就是通过遍历 fd_set 的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个 fd_set 拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍历的⽅法找到可读或可写的 Socket,再对其处理。
select 使⽤固定⻓度的 BitsMap,表示⽂件描述符集合,⽽且所⽀持的⽂件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最⼤值为 1024 ,只能监听 0~1023 的⽂件描述符。
select 机制的缺点:
(1)每次调用 select,都需要把 fd_set 集合从用户态拷贝到内核态,如果 fd_set 集合很大时,那这个开销也很大,比如百万连接却只有少数活跃连接时这样做就太没有效率。
(2)每次调用 select 都需要在内核遍历传递进来的所有 fd_set,如果 fd_set 集合很大时,那这个开销也很大。
(3)为了减少数据拷贝带来的性能损坏,内核对被监控的 fd_set 集合大小做了限制,一般为 1024,如果想要修改会比较麻烦,可能还需要编译内核。
(4)每次调用 select 之前都需要遍历设置监听集合,重复工作。
- poll
poll 不再⽤ BitsMap 来存储所关注的⽂件描述符,取⽽代之⽤动态数组,以链表形式来组织,突破了 select 的⽂件描述符个数限制,当然还会受到系统⽂件描述符限制。
但是 poll 和 select 并没有太⼤的本质区别,都是使⽤线性结构存储进程关注的 Socket 集合,因此都需要遍历⽂件描述符集合来找到可读或可写的 Socke,时间复杂度为 O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓。
- epoll
epoll 通过两个⽅⾯,很好解决了 select/poll 的问题。
第⼀点,epoll 在内核⾥使⽤红⿊树来跟踪进程所有待检测的⽂件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加⼊内核中的红⿊树⾥,红⿊树是个⾼效的数据结构,增删查⼀般时间复杂度是 O(logn) ,通过对这棵⿊红树进⾏操作,这样就不需要像 select/poll 每次操作时都传⼊整个 socket 集合,只需要传⼊⼀个待检测的 socket,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
第⼆点, epoll 使⽤事件驱动的机制,内核⾥维护了⼀个链表来记录就绪事件,当某个 socket 有事件发⽣时,通过回调函数,内核会将其加⼊到这个就绪事件列表中,当⽤户调⽤ epoll_wait() 函数时,只会返回有事件发⽣的⽂件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,⼤⼤提⾼了检测的效率。
epoll 的⽅式即使监听的 Socket 数量越多的时候,效率不会⼤幅度降低,能够同时监听的 Socket 的数⽬也⾮常的多了,上限就为系统定义的进程打开的最⼤⽂件描述符个数。因⽽,epoll 被称为解决 C10K 问题的利器
普通内存与一般机械硬盘
机械硬盘,也叫 HDD(Hard Disk Drive),是一种通过磁盘旋转和磁头移动来存储数据的设备,读写速度比较慢,通常比内存的速度慢 10 万倍左右。
- HDD 的访问时间大约在 5-10ms,数据传输速率约为 100 到 200 MB/s。
- 内存,也就是 RAM(Random Access Memory),访问时间大约在 10-100ns,数据传输速率约为数十 GB/s。
固态硬盘(Solid State Drive,SSD),SSD 的读写速度比 HDD 快 200 倍左右,价格也在逐渐下降,已经逐渐取代了 HDD。
