Util工具类功能设计与类设计

目录

  • 前言
  • [一、Util 的设计理念](#一、Util 的设计理念)
  • [二、Util 类的简单设计](#二、Util 类的简单设计)
    • [2.1 YAGNI 原则下的 Util 设计](#2.1 YAGNI 原则下的 Util 设计)
    • [2.2、使用 class + static](#2.2、使用 class + static)
    • [2.3、Util 需要实现的几个功能](#2.3、Util 需要实现的几个功能)
  • [三、Util 类的实现](#三、Util 类的实现)
    • [3.1 分割字符串 Split](#3.1 分割字符串 Split)
    • [3.2 读取文件 ReadFile](#3.2 读取文件 ReadFile)
    • [3.3 写入文件 WriteFile](#3.3 写入文件 WriteFile)
    • [3.4 URL编码](#3.4 URL编码)
    • [3.5 URL解码](#3.5 URL解码)
    • [3.6 响应状态码与文件 MIME 的信息获取](#3.6 响应状态码与文件 MIME 的信息获取)
    • [3.7 判断一个文件的属性](#3.7 判断一个文件的属性)
    • [3.8 HTTP 请求的资源路径有效性判断](#3.8 HTTP 请求的资源路径有效性判断)
    • [3.9 Util 使用场景速查表](#3.9 Util 使用场景速查表)
  • 结语

前言

在前面的文章中,我们围绕网络库的核心链路,从零完成了一个完整的多线程 Reactor 服务器框架:从负责数据缓冲的 Buffer,到管理套接字生命周期的 Socket;从事件驱动的 ChannelPoller,到串联一切的 EventLoop;再到连接管理的 AcceptorConnection,以及支撑多线程并发的 LoopThreadPool,最终通过 TcpServer 将所有组件整合为一个可运行的服务器。

但我们的最终目标,是实现一个支持 HTTP 协议的高并发服务器。

HTTP(HyperText Transfer Protocol,超文本传输协议)是工作在应用层的请求-响应协议:客户端根据需求向服务器发送请求,服务器处理完毕后返回响应,一次通信即告完成。值得注意的是,HTTP 本质上运行在 TCP 之上,这意味着 HTTP 服务器就是一个 TCP 服务器,只不过在应用层需要按照 HTTP 格式组织与解析数据,以明确客户端意图并完成业务处理。

因此,实现 HTTP 服务器可以概括为四个步骤:

  1. 搭建 TCP 服务器,接收客户端请求;
  2. 按照 HTTP 协议格式解析请求数据,明确客户端目的;
  3. 根据请求目的提供对应服务;
  4. 将响应结果按 HTTP 协议格式组织并回传给客户端。

一个带有协议支持的高性能 Reactor 服务器可以划分为两大模块:

  • SERVER 模块:实现 Reactor 模型的 TCP 服务器;

  • 协议模块:为 Reactor 服务器提供应用层协议支持。

此前的代码正是对 SERVER 模块的实现,已基本完成。接下来,我们将进入协议模块,为当前的 Reactor 服务器赋予 HTTP 协议处理能力。


在之前的实现中,我们的注意力集中在网络通信这一核心主题上,力求把 Reactor 模型的每一环都讲清楚。但当你真正动手去写、去测试、去完善这个网络库时,很快会意识到一个现实问题:支撑业务运转的,从来不只是那些光鲜的核心模块。

你会在日志系统里反复编写 time()localtime()strftime() 的样板代码;你会在解析配置或 HTTP 报文时,无数次调用 find()substr() 来处理字符串;你会在读取静态资源文件时,面对路径拼接、文件存在性判断、二进制读取等琐碎操作;你还会在调试时,需要格式化输出十六进制内存、获取线程 ID、进行大小端转换......

这些代码不直接参与网络 IO,却像基础设施一样无处不在。如果任由它们散落在各个业务文件的角落,代码很快会充斥大量重复、臃肿、难以维护的噪音。

这正是 Util(工具类) 登场的意义。

与前面那些"主角"不同,Util 模块不会出现在架构图的中心,它更像是整个项目的基础设施(Infrastructure) 。它不解决"如何收发数据"的问题,而是解决"如何优雅地处理字符串"、"如何安全地操作文件"、"如何高效地进行编解码"等问题。它的设计哲学也与众不同:Util 不是凭空设计出来的,而是从代码复用中提炼出来的------当你发现同样的逻辑写了第三遍时,它就是下一个 Util 接口。

在本节中,我们将为网络库补齐这块基础设施,从实际痛点出发,逐步设计和实现几个最常用、最不可或缺的工具接口:字符串分割、文件读写、URL 编解码、HTTP 状态码与 MIME 查询、文件属性判断,以及资源路径安全校验。它们将成为支撑上层业务最坚实的底座。

下面是实现 HTTP 服务器的整体流程:
#mermaid-svg-ISlLAsuxr3ZNpg5n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ISlLAsuxr3ZNpg5n .error-icon{fill:#552222;}#mermaid-svg-ISlLAsuxr3ZNpg5n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ISlLAsuxr3ZNpg5n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .marker.cross{stroke:#333333;}#mermaid-svg-ISlLAsuxr3ZNpg5n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ISlLAsuxr3ZNpg5n p{margin:0;}#mermaid-svg-ISlLAsuxr3ZNpg5n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster-label text{fill:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster-label span{color:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster-label span p{background-color:transparent;}#mermaid-svg-ISlLAsuxr3ZNpg5n .label text,#mermaid-svg-ISlLAsuxr3ZNpg5n span{fill:#333;color:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .node rect,#mermaid-svg-ISlLAsuxr3ZNpg5n .node circle,#mermaid-svg-ISlLAsuxr3ZNpg5n .node ellipse,#mermaid-svg-ISlLAsuxr3ZNpg5n .node polygon,#mermaid-svg-ISlLAsuxr3ZNpg5n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .rough-node .label text,#mermaid-svg-ISlLAsuxr3ZNpg5n .node .label text,#mermaid-svg-ISlLAsuxr3ZNpg5n .image-shape .label,#mermaid-svg-ISlLAsuxr3ZNpg5n .icon-shape .label{text-anchor:middle;}#mermaid-svg-ISlLAsuxr3ZNpg5n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .rough-node .label,#mermaid-svg-ISlLAsuxr3ZNpg5n .node .label,#mermaid-svg-ISlLAsuxr3ZNpg5n .image-shape .label,#mermaid-svg-ISlLAsuxr3ZNpg5n .icon-shape .label{text-align:center;}#mermaid-svg-ISlLAsuxr3ZNpg5n .node.clickable{cursor:pointer;}#mermaid-svg-ISlLAsuxr3ZNpg5n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .arrowheadPath{fill:#333333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ISlLAsuxr3ZNpg5n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ISlLAsuxr3ZNpg5n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ISlLAsuxr3ZNpg5n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster text{fill:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n .cluster span{color:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ISlLAsuxr3ZNpg5n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ISlLAsuxr3ZNpg5n rect.text{fill:none;stroke-width:0;}#mermaid-svg-ISlLAsuxr3ZNpg5n .icon-shape,#mermaid-svg-ISlLAsuxr3ZNpg5n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ISlLAsuxr3ZNpg5n .icon-shape p,#mermaid-svg-ISlLAsuxr3ZNpg5n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ISlLAsuxr3ZNpg5n .icon-shape .label rect,#mermaid-svg-ISlLAsuxr3ZNpg5n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ISlLAsuxr3ZNpg5n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ISlLAsuxr3ZNpg5n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ISlLAsuxr3ZNpg5n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 搭建 TCP 服务器
解析 HTTP 请求
根据请求提供对应服务
组织并回传 HTTP 响应
完成一次通信

一、Util 的设计理念

在实际工程中,很少有接口是凭空想出来的。标准库也好,第三方库也罢,其函数设计往往经历了多轮需求迭代:痛点先行,提取在后

当你把一段代码反复写了多次,就应该注意到它的高度复用性。此时,将其封装为一个独立的函数接口,不仅能消除重复,还能通过命名自解释其功能,让代码从"指令堆砌"变成"积木拼装"。

对于那些功能常规、泛用性强、不依附于特定业务对象的辅助接口------就像工具箱里的扳手与螺丝刀------我们可以把它们集中收纳在一个类中,这便是 Util(工具类)。顾名思义,其内部没有业务状态,只有各种静态方法,充当项目的通用工具箱。

二、Util 类的简单设计

在正式动手实现之前,有必要先对 Util 模块的定位与设计边界做一个清晰的交代。毕竟,工具类往往是新手最容易"用力过猛"的地方------要么一上来就设计得过于庞大,恨不得把所有能想到的辅助功能全塞进去;要么过于简陋,导致后续业务代码里到处充斥着重复的逻辑。我们需要在这两者之间找到一个务实的平衡点

2.1 YAGNI 原则下的 Util 设计

首先要明确的是,本节实现的 Util并不追求大而全 。我们不会去封装一个能媲美 Boost 或 Abseil 的通用工具库,也不会按照严格的正交性原则把字符串、文件、网络、时间等维度拆分成独立的命名空间模块。原因很简单:我们的目标是为当前这个高并发网络服务器项目提供"刚好够用"的基础设施,而不是编写一个可复用的标准库扩展

在软件工程中有一条著名的 YAGNI 原则(You Aren't Gonna Need It) ------不要为你当前不需要的功能提前买单。对于一个基础的高并发服务器来说,我们真正频繁遇到的痛点其实非常集中:读取静态资源文件、解析 HTTP 请求路径、处理 URL 编码、判断文件类型等。这些需求加起来,一只手就能数得过来。如果我们为了"架构优雅"而强行拆分成 StringUtilFileUtilHttpUtilTimeUtil 等多个子模块,虽然理论上更规范,但对于当前这个体量的项目而言,反而会增加不必要的头文件管理和认知负担。因此,我们采用一种集中式的、扁平化 的组织方式:一个 Util 类,若干静态方法,仅此而已。

当然,这种"大杂烩"式的做法在大型生产项目中并不推荐。它更像是一个教学项目与原型系统 中的务实选择------先用最短的代码路径解决问题,等到业务复杂度真正膨胀、工具函数突破二十个、横跨多个领域时,再按照正交性原则进行拆分才是更合理的重构时机。现在,我们优先保证的是可用性简洁性

2.2、使用 class + static

现代 C++ 更推崇命名空间 + 自由函数 的方式来组织工具(例如 StringUtil::Split(...) 作为一个自由函数存在,而非封装在类中)。那为什么不直接采用这种更现代的做法呢?

这里存在一个教学与工程实践的权衡 。对于熟悉面向对象编程的开发者来说,class Util 配合 static 方法的形式在视觉上更为直观:所有的工具能力都挂载在一个明确的类型之下,通过 Util::xxx() 调用时,IDE 的自动补全和代码提示也更加友好。更重要的是,这种写法与项目里其他核心类(如 BufferSocketConnection)的风格保持了一致,降低了阅读和理解的心智负担。

但我们必须为此设置一条不可逾越的红线

使用 class + static 方式设计 Util 时,该类绝对不允许拥有任何成员变量,所有接口必须是静态方法。

一旦 Util 类里出现了非静态成员变量,它就不再是一个工具集合,而变成了一个隐式的状态容器 。这意味着你每次调用 Util::xxx() 时,背后可能隐藏着对某个共享状态的读写,从而引入线程安全问题、单测困难、以及跨模块的隐式耦合。静态方法保证了每个函数都是自包含的 ------输入参数确定,输出结果就确定,不依赖任何外部上下文。这是 Util 类能够被称为"工具"而非"服务"的底线。

换句话说,我们的 Util 类本质上只是一个**"函数的命名容器"**。它借用了类的语法外壳来组织代码,但内核必须保持纯粹的功能性。如果你发现某个工具函数需要维护配置状态、缓存结果、或者依赖业务对象的上下文,那它就不该属于 Util,而应该被提升为一个独立的业务组件。

2.3、Util 需要实现的几个功能

基于上述定位,我们提炼出当前服务器最迫切需要的10个工具接口。这 10 个功能覆盖了文件 IO、字符串处理、HTTP 协议辅助、文件系统元信息查询四个维度,已经足以支撑一个基础 Web 服务器的完整运转:

  1. 字符串分割Split
    将需要被分割的字符串按照传入的某个标记字符进行分割,将分割后得到的各个子串放到 arry 中,最终返回子串的数量
  2. 读取文件内容(ReadFile
    服务器需要提供静态资源服务(如 HTML、CSS、图片)。该接口负责将磁盘文件一次性读入内存缓冲区,避免在业务代码里重复编写 ifstream 的打开、定位、读取、关闭等样板代码。
  3. 向文件写入数据(WriteFile
    用于日志落盘、临时缓存或上传文件的持久化。提供原子化的写入能力,屏蔽文件打开模式和错误处理的细节。
  4. URL 编码(UrlEncode
    HTTP 请求中,URL 路径和查询参数可能包含空格、中文、特殊符号等。RFC3986 规定这些字符必须编码为 %HH 格式。该接口确保服务器生成的重定向链接、表单回显等数据在协议层面合法。
  5. URL 解码(UrlDecode
    与编码对应,用于解析客户端提交的请求 URI。例如将 %20 还原为空格,将 %2F 还原为 /,是路由匹配和参数提取的前置步骤。
  6. 响应状态码描述信息获取(StatusDesc
    构造 HTTP 响应报文时,状态行需要包含状态码对应的文本描述(如 200 OK404 Not Found)。通过查表方式快速获取标准描述,避免在业务层硬编码字符串。
  7. 根据文件后缀名获取 MIME 类型(ExtMime
    HTTP 响应头中的 Content-Type 字段决定了浏览器如何渲染返回体。通过文件扩展名(.html.css.png)映射到对应的 MIME 类型,是静态资源服务的关键一环。
  8. 判断文件是否为目录(IsDirectory
    在提供目录浏览服务或进行路径安全校验时,需要区分目标路径是文件夹还是普通文件。基于 stat 系统调用封装,屏蔽平台差异。
  9. 判断文件是否为普通文件(IsRegular
    防止服务器误将设备文件、管道、符号链接等非常规文件作为资源返回给客户端,避免安全风险和不可预期的读取行为。
  10. HTTP 请求资源路径有效性判断(ValidPath
    安全防御的核心接口。通过解析路径中的 ...,计算目录层级深度,确保客户端无法通过构造特殊 URI(如 /../../etc/passwd)逃出服务器的资源根目录,实现最基本的沙箱隔离。

这 10 个接口看似零散,实则构成了一个最小可用的工具闭环 :文件读写解决数据从哪里来、到哪里去的问题;URL 编解码解决 HTTP 协议层面的字符安全;状态码与 MIME 解决响应报文的语义正确性;文件属性与路径校验则解决资源访问的安全边界。对于实现一个基础的高并发服务器而言,这套组合已经完全够用。后续如果业务扩展(例如需要 JSON 解析、正则匹配、时间格式化等),再按需追加即可,无需在一开始就过度设计。


最后需要强调 Util 的边界感 。以下类型的代码,即便看起来像是"辅助功能",也绝对不该出现在 Util 类中:

  • 依赖业务上下文的函数 :例如 GetCurrentConnectionCount()ParseHttpRequest(),它们依赖 ConnectionHttpRequest 对象,属于业务逻辑而非工具。

  • 需要初始化和生命周期的模块:例如数据库连接池、线程池、配置管理器,这些带有状态和资源管理职责的组件,应该作为独立的服务类存在。

  • 与特定领域强耦合的算法:例如自定义的业务协议解析、加密解密流程,应该放在对应的协议模块中。

Util 应该是一个可以被直接复制到另一个无关项目中依然能编译通过的代码集合。守住这条边界,它才能长期保持干净和可用。

接下来,我们将逐一实现上述几个接口,并在实现过程中展示如何规避常见的陷阱(如路径遍历攻击、空字符串死循环、二进制读取的换行符转换等)。


这 10 个接口的职责划分如下图所示:
#mermaid-svg-VeqplpIaZuhT1932{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VeqplpIaZuhT1932 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VeqplpIaZuhT1932 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VeqplpIaZuhT1932 .error-icon{fill:#552222;}#mermaid-svg-VeqplpIaZuhT1932 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VeqplpIaZuhT1932 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VeqplpIaZuhT1932 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VeqplpIaZuhT1932 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VeqplpIaZuhT1932 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VeqplpIaZuhT1932 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VeqplpIaZuhT1932 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VeqplpIaZuhT1932 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VeqplpIaZuhT1932 .marker.cross{stroke:#333333;}#mermaid-svg-VeqplpIaZuhT1932 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VeqplpIaZuhT1932 p{margin:0;}#mermaid-svg-VeqplpIaZuhT1932 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VeqplpIaZuhT1932 .cluster-label text{fill:#333;}#mermaid-svg-VeqplpIaZuhT1932 .cluster-label span{color:#333;}#mermaid-svg-VeqplpIaZuhT1932 .cluster-label span p{background-color:transparent;}#mermaid-svg-VeqplpIaZuhT1932 .label text,#mermaid-svg-VeqplpIaZuhT1932 span{fill:#333;color:#333;}#mermaid-svg-VeqplpIaZuhT1932 .node rect,#mermaid-svg-VeqplpIaZuhT1932 .node circle,#mermaid-svg-VeqplpIaZuhT1932 .node ellipse,#mermaid-svg-VeqplpIaZuhT1932 .node polygon,#mermaid-svg-VeqplpIaZuhT1932 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VeqplpIaZuhT1932 .rough-node .label text,#mermaid-svg-VeqplpIaZuhT1932 .node .label text,#mermaid-svg-VeqplpIaZuhT1932 .image-shape .label,#mermaid-svg-VeqplpIaZuhT1932 .icon-shape .label{text-anchor:middle;}#mermaid-svg-VeqplpIaZuhT1932 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VeqplpIaZuhT1932 .rough-node .label,#mermaid-svg-VeqplpIaZuhT1932 .node .label,#mermaid-svg-VeqplpIaZuhT1932 .image-shape .label,#mermaid-svg-VeqplpIaZuhT1932 .icon-shape .label{text-align:center;}#mermaid-svg-VeqplpIaZuhT1932 .node.clickable{cursor:pointer;}#mermaid-svg-VeqplpIaZuhT1932 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VeqplpIaZuhT1932 .arrowheadPath{fill:#333333;}#mermaid-svg-VeqplpIaZuhT1932 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VeqplpIaZuhT1932 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VeqplpIaZuhT1932 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VeqplpIaZuhT1932 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VeqplpIaZuhT1932 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VeqplpIaZuhT1932 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VeqplpIaZuhT1932 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VeqplpIaZuhT1932 .cluster text{fill:#333;}#mermaid-svg-VeqplpIaZuhT1932 .cluster span{color:#333;}#mermaid-svg-VeqplpIaZuhT1932 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VeqplpIaZuhT1932 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VeqplpIaZuhT1932 rect.text{fill:none;stroke-width:0;}#mermaid-svg-VeqplpIaZuhT1932 .icon-shape,#mermaid-svg-VeqplpIaZuhT1932 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VeqplpIaZuhT1932 .icon-shape p,#mermaid-svg-VeqplpIaZuhT1932 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VeqplpIaZuhT1932 .icon-shape .label rect,#mermaid-svg-VeqplpIaZuhT1932 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VeqplpIaZuhT1932 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VeqplpIaZuhT1932 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VeqplpIaZuhT1932 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文件系统元信息
IsDirectory 目录判断
IsRegular 普通文件判断
ValidPath 路径校验
HTTP 协议辅助
UrlEncode URL 编码
UrlDecode URL 解码
StatusDesc 状态码描述
ExtMime MIME 类型
字符串处理
Split 字符串分割
文件 IO
ReadFile 读取文件
WriteFile 写入文件

三、Util 类的实现

此前实现的 BufferSocket 等类都包含在 server.hpp 中。为了防止重复包含,记得使用 #ifndef 管理。

我们在 server.hpp 同路径下创建一个新目录 http/,并在其中新建头文件 http.hpp,用于存放 HTTP 协议相关的代码。


3.1 分割字符串 Split

该接口接收三个参数:待分割的原字符串 src、分隔符 sep、以及存放结果的 arry 数组。其中 arry 是输出型参数,使用指针传递。

实现逻辑很清晰:从起始位置开始不断查找分隔符,找到后将片段截取出来,调整偏移量后继续查找,直到遍历完毕。

需要注意的是各种边界情况:传入空分隔符、以分隔符开头或结尾、连续出现多个分隔符等。建议实现后自行构造用例测试,确保没有遗漏。

c++ 复制代码
// 字符串分割函数
    static size_t Split(const std::string& src, const std::string sep, std::vector<std::string> *arry)
    {
        if (sep.empty() || arry == nullptr) // 如果传入的sep是一个"",或者传进来的指针是个空指针,就直接返回0,表示啥也没干就走了
        {
            return 0;
        }

        size_t offset = 0; // 这是我们的查找的起始位置

        while (offset < src.size()) // 不管怎么说下标都不可能大于等于数量
        {
            // 从offset开始向后查找,到第一个sep处停下,find返回的是sep的下标
            size_t pos = src.find(sep, offset);
            if (pos == std::string::npos) // 没找到,把从起始位置开始的剩下字符全部充当子串
            {
                arry->push_back(src.substr(offset));
                return arry->size();
            }
            if (offset == pos) // 这说明是,,,这种情况,,为sep,所以第一次找到之后,offset会跑到下标1去,此时从1开始查找到的第一个sep的下标就是本身1
            {
                offset += sep.size();
                continue;
            }
            // 除此之外都是正常情况

            arry->push_back(src.substr(offset, pos - offset));
            offset = pos + sep.size();
        }
        return arry->size();
    }

3.2 读取文件 ReadFile

文件读写是非常常见的操作。如果不是经常使用,可能会对某些接口感到陌生,建议直接查阅相关文档。

读取文件需要外界传入完整路径,结果存入 string 输出参数。读取流使用 ifstream,并以 binary 模式打开,确保原样读取文件内容,不做任何换行符转换。

ifstream 是 C++ 标准库中用于文件输入 (读取文件)的类,全称是 Input File Stream (输入文件流)。它继承自 istream,第一个参数是文件路径,第二个是打开模式

参数 含义
std::ios::in 打开文件用于读取(ifstream 默认,可以省略)
std::ios::binary 二进制方式打开
std::ios::ate 打开后立即定位到文件末尾
组合使用 用 `
由于事先不知道文件大小,我们先通过 seekg 将读写位置跳转到文件末尾,再用 tellg 获取偏移量,从而得到文件大小。随后将位置重置到开头,调用 read 一次性读取全部内容。
c++ 复制代码
 // 读取文件的所有内容,将读取的内容放到一个Buffer中
    static bool ReadFile(const std::string &filename, std::string *buf)
    {
        std::ifstream ifs(filename, std::ios::binary); // 找到文件后以二进制方式打开
        if (ifs.is_open() == false)
        {
            printf("OPEN %s FILE FAILED!!", filename.c_str());
            return false;
        }

        size_t fsize = 0;      // 存储文件的大小
        ifs.seekg(0, ifs.end); // 跳转到文件末尾
        fsize = ifs.tellg();   // 获取当前读写位置相对于起始位置的偏移量,从末尾偏移刚好就是文件大小

        ifs.seekg(0, ifs.beg); // 再跳转到开头
        buf->resize(fsize);    // 调整空间大小,预留出足够空间
        ifs.read(buf->data(), fsize);//这里要使用c++17及以上编译才行,如果是c++11还是建议ifs.read(&(*buf)[0], fsize);
        if (ifs.fail())
        {
            printf("READ %s FILE FAILED!!", filename.c_str());
            ifs.close();
            return false;
        }
        ifs.close();
        return true;
    }

这里 ifs 提供的各类接口(seekgtellgread 等),多使用几次就能熟练掌握。读取时调用 buf->data() 而非 c_str(),因为 data() 返回的指针可用于写入(C++11 起 std::string 保证连续存储)。

关于获取 buf 的首地址,根据不同的 C++ 编译版本可以选择不同的写法。


文件读取失败的常见原因与排查建议

在实际运行中,ReadFile 返回 false 的情况并不少见。下面列出最常见的几类原因,并给出对应的排查思路与错误处理策略:

常见原因 现象 排查建议 错误处理策略
路径不存在 is_open() 返回 false,日志打印 OPEN ... FILE FAILED 检查传入的 filename 是否为绝对路径,确认文件确实存在于该路径下;可用 ls -lstat 命令验证 返回 false 并记录日志;在 HTTP 层映射为 404 Not Found
权限不足 文件存在但 is_open() 失败,或 read()fail() 置位 ls -l 查看文件权限,确认运行服务器的用户对文件拥有读权限 ;检查父目录是否具备 x(执行)权限 返回 false 并记录日志;在 HTTP 层映射为 403 Forbidden
文件被占用 文件被其他进程以独占方式打开(如 Windows 下),或读取过程中被截断 lsof(Linux)或资源监视器(Windows)查看占用进程;确认没有其他线程在并发写入同一文件 返回 false 并记录日志;可考虑重试一次,仍失败则返回 503 Service Unavailable
路径是目录 is_open() 可能成功,但 read() 失败或读取到异常内容 读取前先用 IsDirectory 判断目标是否为目录,若是则直接拒绝 返回 false 并记录日志;在 HTTP 层映射为 403 Forbidden
文件过大 buf->resize(fsize) 抛出异常或内存不足 检查文件大小是否超出可用内存;对大文件可考虑分块读取或改用流式传输 捕获异常并返回 false;在 HTTP 层映射为 500 Internal Server Error

错误处理的核心原则ReadFile 作为工具函数,只负责"尽力读取并返回成败",不应当 在内部决定返回给客户端的 HTTP 状态码。正确的做法是------工具函数返回 false 并打印日志,由上层业务根据失败场景(路径不存在、权限不足、文件被占用等)自行决定映射为 404403 还是 500。这样既保持了 Util 类的纯粹性,也让错误处理策略与具体业务解耦。

3.3 写入文件 WriteFile

写入文件操作与从文件中读取是差不多的原理,唯一的区别就是我们使用的是 ofstream,并且,我们要求写入的时候并不是追写,而是覆盖的方式:

c++ 复制代码
// 向文件写入数据
    static bool WriteFile(const std::string &filename, const std::string &buf)
    {
        std::ofstream ofs(filename, std::ios::binary | std::ios::trunc); // 表示二进制写入并覆盖
        if (ofs.is_open() == false)                                      // 如果打开文件流失败
        {
            printf("OPEN %s FILE FAILED!!", filename.c_str());
            return false;
        }
        ofs.write(buf.c_str(), buf.size()); // 写入的时候是const char*无所谓
        if (ofs.good() == false)
        {
            ERR_LOG("WRITE %s FILE FAILED!", filename.c_str());
            ofs.close();
            return false;
        }
        ofs.close();
        return true;
    }

3.4 URL编码

URL(Uniform Resource Locator)在设计上只允许使用有限的字符集。根据 RFC3986 标准,URL 中不经过编码就能直接出现的字符只有:

  • 英文字母A-Z a-z
  • 数字0-9
  • 极少数特殊符号- _ . ~

问题来了:如果 URL 里出现了"不允许"的字符怎么办?

比如你想通过 GET 请求搜索 "C++ 教程"

plain 复制代码
GET /search?q=C++ 教程 HTTP/1.0

这行请求在协议层面是非法且危险的,因为 URL 中包含了:

  1. 空格 ------ URL 中不允许出现空格;
  2. + ------ 在 query string 中有特殊含义(通常代表空格);
  3. 中文字符 ------ 超出 ASCII 范围,不同编码环境下解析结果不一致。

如果不处理,服务器、浏览器、代理网关对这条 URL 的解析会产生歧义,甚至直接报错。此时就需要进行 URL 编码:处理非 ASCII 字符,消除歧义,区分"数据"与"定界符"

编码规则如下:

  • 将特殊字符的 ASCII 值转换为两个十六进制字符,前缀 %,例如 C++C%2B%2B
  • RFC3986 规定 . - _ ~ 以及字母、数字属于绝对不编码字符
  • W3C 标准规定,查询字符串中的空格可以编码为 +(解码时反向处理)。

函数参数为一个 string,外加一个 bool 控制是否启用 W3C 的空格转 + 规则:

c++ 复制代码
// URL编码,避免URL中资源路径与查询字符串中的特殊字符与HTTP请求中特殊字符产生歧义
    // 编码格式:将特殊字符的ascii值,转换为两个16进制字符,前缀%   C++ -> C%2B%2B
    //   不编码的特殊字符: RFC3986文档规定 . - _ ~ 字母,数字属于绝对不编码字符
    // RFC3986文档规定,编码格式 %HH
    // W3C标准中规定,查询字符串中的空格,需要编码为+, 解码则是+转空格
    static std::string UrlEncode(const std::string &url, bool convert_plus_to_space)
    {
        std::string res; // 返回字符串
        for (auto &c : url)
        {
            //当 URL 包含中文时,c 的值可能是 -28(即 0xE4),传给 isalnum 属于未定义行为(C 标准库要求传入 unsigned char 值或 EOF)。
            unsigned char uc = static_cast<unsigned char>(c);
            if (c == '.' || c == '-' || c == '_' || c == '~' || isalnum(uc))
            {
                res += c;
                continue;
            }
            if (c == ' ' && convert_plus_to_space)
            {
                res += '+';
                continue;
            }
            // 剩下的字符都是需要编码成为 %HH 格式
            char tmp[4] = {0};
            // snprintf 与 printf比较类似,都是格式化字符串,只不过一个是打印,一个是放到一块空间中
            snprintf(tmp, 4, "%%%02X", uc); //%%转义为%,%02X表示以16进制形式,保留2位,如果有空位以0填充
            res += tmp;
        }
        return res;
    }

3.5 URL解码

解码是编码的逆过程,发生在服务器接收请求后、处理业务前

它主要用来还原用户的原始输入。

当服务器收到:

plain 复制代码
GET /search?q=C%2B%2B+%E6%95%99%E7%A8%8B HTTP/1.0

我们就必须先解码,才能知道用户真正想搜的是 "C++ 教程",而不是一串百分号。

解码完成之后,获得的 URL 就可以用来进行路由匹配与路径校验。

一个 ValidPath 工具函数判断路径是否合法,必须在解码之后才能判断。因为攻击者可能用编码绕过检查:

plain 复制代码
原始攻击路径:/../../etc/passwd
编码后:/%2e%2e/%2e%2e/%2e%2e/etc/passwd

如果先校验再解码,编码后的路径看起来可能"合法",解码后却逃出根目录。所以安全校验必须在解码之后进行

我们解码的逻辑也很简单,基本上只要遇见了 % 符号,就代表我们需要去解码了,当然,要判断 % 后两个字符是否存在。

c++ 复制代码
// URL 解码过程中的辅助函数,作用是把单个十六进制字符转换成对应的整数值。
    static char HEXTOI(char c) // 即把a,A转化为10这些
    {
        if (c >= '0' && c <= '9')
        {
            return c - '0';
        }
        else if (c >= 'a' && c <= 'f')
        {
            return c - 'a' + 10;
        }
        else if (c >= 'A' && c <= 'F')
        {
            return c - 'A' + 10;
        }
        return -1;
    }
    // URL解码
    static std::string UrlDecode(const std::string &url, bool convert_space_to_plus)
    {
        // 遇到了%,则将紧随其后的2个字符,转换为数字,第一个数字左移4位,然后加上第二个数字  + -> 2b  %2b->2 << 4 + 11
        std::string res;

        for (int i = 0; i < url.size(); ++i)
        {
            if (url[i] == '+' && convert_space_to_plus)
            {
                res += ' ';
                continue;
            }
            if (url[i] == '%' && (i + 2) < url.size())
            {
                char v1 = HEXTOI(url[i + 1]);
                char v2 = HEXTOI(url[i + 2]);
                char v = (v1 << 4) + v2; // 等价于v1*16+v2
                res += v;
                i += 2;
                continue;
            }
            res += url[i];
        }
        return res;
    }

这里编写了辅助函数 HEXTOI,用于将单个十六进制字符转换为对应的整数值。由于这一逻辑在解码过程中被反复使用,提取为独立函数可以避免代码臃肿,也让意图更加清晰。


URL 解码与安全校验的完整流程如下:
服务器 客户端 服务器 客户端 #mermaid-svg-hsYqrVXWIh5wcTQu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hsYqrVXWIh5wcTQu .error-icon{fill:#552222;}#mermaid-svg-hsYqrVXWIh5wcTQu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hsYqrVXWIh5wcTQu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hsYqrVXWIh5wcTQu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hsYqrVXWIh5wcTQu .marker.cross{stroke:#333333;}#mermaid-svg-hsYqrVXWIh5wcTQu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hsYqrVXWIh5wcTQu p{margin:0;}#mermaid-svg-hsYqrVXWIh5wcTQu .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hsYqrVXWIh5wcTQu text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hsYqrVXWIh5wcTQu .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hsYqrVXWIh5wcTQu .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hsYqrVXWIh5wcTQu #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hsYqrVXWIh5wcTQu .sequenceNumber{fill:white;}#mermaid-svg-hsYqrVXWIh5wcTQu #sequencenumber{fill:#333;}#mermaid-svg-hsYqrVXWIh5wcTQu #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hsYqrVXWIh5wcTQu .messageText{fill:#333;stroke:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hsYqrVXWIh5wcTQu .labelText,#mermaid-svg-hsYqrVXWIh5wcTQu .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .loopText,#mermaid-svg-hsYqrVXWIh5wcTQu .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hsYqrVXWIh5wcTQu .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hsYqrVXWIh5wcTQu .noteText,#mermaid-svg-hsYqrVXWIh5wcTQu .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hsYqrVXWIh5wcTQu .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hsYqrVXWIh5wcTQu .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hsYqrVXWIh5wcTQu .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hsYqrVXWIh5wcTQu .actorPopupMenu{position:absolute;}#mermaid-svg-hsYqrVXWIh5wcTQu .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hsYqrVXWIh5wcTQu .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hsYqrVXWIh5wcTQu .actor-man circle,#mermaid-svg-hsYqrVXWIh5wcTQu line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hsYqrVXWIh5wcTQu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 路径合法 路径非法(如路径遍历攻击- ) 发送编码后的请求 URI UrlDecode 解码请求路径 ValidPath 校验路径合法性 路由匹配并处理业务 返回正常响应 返回 403 / 404 拒绝访问

3.6 响应状态码与文件 MIME 的信息获取

响应状态码的描述信息获取与文件的 MIME 获取是两个函数,但是具体实现都差不多,所以这里就一起说了。对于文件的 MIME,我们的想法就是从后向前,通过 . 来截取子串,随后将这个后缀进行一个查找操作。而对于相应状态码的描述信息,就直接通过状态码来查找就行了。

但是从哪里查找呢,这里可以是一个哈希表,关于一个文件后缀名对应的 MIME 信息,以及一个状态码的描述信息,都是可以在官方文档中查到的。

我们可以创建两个哈希表,初始化的时候放入这些信息。

c++ 复制代码
// 响应状态码的描述信息获取
    static std::string StatuDesc(int statu)
    {
        auto it = _statu_msg.find(statu);
        if (it != _statu_msg.end()) // 找到了
        {
            return it->second;
        }
        return "Unknown";
    }
    // 根据文件后缀名获取文件 MIME
    static std::string ExtMime(const std::string &filename)
    {
        size_t pos = filename.find_last_of('.');

        if (pos == std::string::npos)
        {
            return "application/octet-stream";
        }

        std::string ext = filename.substr(pos);
        auto it = _mime_msg.find(ext);
        if (it == _mime_msg.end())
        {
            return "application/octet-stream";
        }
        return it->second;
    }

在类的外面定义两个哈希表,并将这些信息传进去(未截全):


3.7 判断一个文件的属性

在 HTTP 协议的使用中,我们经常会检查一个文件的属性,比如检查一个文件是文件夹,还是一个普通文件,这里就一起说了。

主要的方法就是通过 stat 函数获取该文件的属性,随后通过 stat 结构体来判断。

struct stat 是 Linux / Unix 中用来描述一个文件或目录属性信息的结构体

它通常和 stat()fstat()lstat() 这些系统调用一起使用。

struct stat 就是一张"文件信息表",里面保存了文件的大小、权限、类型、时间、inode 等信息。

c++ 复制代码
// 判断一个文件是否是一个目录
    static bool IsDirectory(const std::string &filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(), &st); // 把属性存到st中
        if (ret < 0)
        {
            return false;
        }
        return S_ISDIR(st.st_mode); // 是否是文件夹
    }
    // 判断一个文件是否是一个普通文件
    static bool IsRegular(const std::string &filename)
    {
        struct stat st;
        int ret = stat(filename.c_str(), &st);
        if (ret < 0)
        {
            return false;
        }
        return S_ISREG(st.st_mode); // 是否是普通文件
    }

3.8 HTTP 请求的资源路径有效性判断

路径合法性校验是安全防御的关键。我们将问题转化为目录层级是否越过根目录

HTTP 请求的资源路径可能包含 ...,其中真正危险的是 ..,因为它代表"返回上一级目录"。因此,判断路径是否安全,本质上就是判断:不断执行 .. 之后,是否走出了服务器规定的资源根目录。

我们不需要操作真实文件系统,而是进行纯逻辑判断 。用一个 level 变量模拟目录层级变化:初始为 0,遇到普通目录加 1,遇到 .. 减 1,一旦 level 变为负数,说明路径已逃逸出根目录,直接拒绝。

c++ 复制代码
// http请求的资源路径有效性判断
    static bool ValidPath(const std::string &path)
    {
        // 思想:按照/进行路径分割,根据有多少子目录,计算目录深度,有多少层,深度不能小于0
        std::vector<std::string> subdir;
        Split(path, "/", &subdir);
        int level = 0;
        for (auto &dir : subdir)
        {
            if (dir == ".")
            {
                continue;
            }

            if (dir == "..")
            {
                level--; // 任意一层走出相对根目录,就认为有问题
                if (level < 0)
                    return false;
                continue;
            }
            level++;
        }
        return true;
    }

ValidPath 的路径校验逻辑如下:
#mermaid-svg-UxCnZnqkPw9RujiV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UxCnZnqkPw9RujiV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UxCnZnqkPw9RujiV .error-icon{fill:#552222;}#mermaid-svg-UxCnZnqkPw9RujiV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UxCnZnqkPw9RujiV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UxCnZnqkPw9RujiV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UxCnZnqkPw9RujiV .marker.cross{stroke:#333333;}#mermaid-svg-UxCnZnqkPw9RujiV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UxCnZnqkPw9RujiV p{margin:0;}#mermaid-svg-UxCnZnqkPw9RujiV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UxCnZnqkPw9RujiV .cluster-label text{fill:#333;}#mermaid-svg-UxCnZnqkPw9RujiV .cluster-label span{color:#333;}#mermaid-svg-UxCnZnqkPw9RujiV .cluster-label span p{background-color:transparent;}#mermaid-svg-UxCnZnqkPw9RujiV .label text,#mermaid-svg-UxCnZnqkPw9RujiV span{fill:#333;color:#333;}#mermaid-svg-UxCnZnqkPw9RujiV .node rect,#mermaid-svg-UxCnZnqkPw9RujiV .node circle,#mermaid-svg-UxCnZnqkPw9RujiV .node ellipse,#mermaid-svg-UxCnZnqkPw9RujiV .node polygon,#mermaid-svg-UxCnZnqkPw9RujiV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UxCnZnqkPw9RujiV .rough-node .label text,#mermaid-svg-UxCnZnqkPw9RujiV .node .label text,#mermaid-svg-UxCnZnqkPw9RujiV .image-shape .label,#mermaid-svg-UxCnZnqkPw9RujiV .icon-shape .label{text-anchor:middle;}#mermaid-svg-UxCnZnqkPw9RujiV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UxCnZnqkPw9RujiV .rough-node .label,#mermaid-svg-UxCnZnqkPw9RujiV .node .label,#mermaid-svg-UxCnZnqkPw9RujiV .image-shape .label,#mermaid-svg-UxCnZnqkPw9RujiV .icon-shape .label{text-align:center;}#mermaid-svg-UxCnZnqkPw9RujiV .node.clickable{cursor:pointer;}#mermaid-svg-UxCnZnqkPw9RujiV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UxCnZnqkPw9RujiV .arrowheadPath{fill:#333333;}#mermaid-svg-UxCnZnqkPw9RujiV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UxCnZnqkPw9RujiV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UxCnZnqkPw9RujiV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UxCnZnqkPw9RujiV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UxCnZnqkPw9RujiV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UxCnZnqkPw9RujiV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UxCnZnqkPw9RujiV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UxCnZnqkPw9RujiV .cluster text{fill:#333;}#mermaid-svg-UxCnZnqkPw9RujiV .cluster span{color:#333;}#mermaid-svg-UxCnZnqkPw9RujiV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UxCnZnqkPw9RujiV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UxCnZnqkPw9RujiV rect.text{fill:none;stroke-width:0;}#mermaid-svg-UxCnZnqkPw9RujiV .icon-shape,#mermaid-svg-UxCnZnqkPw9RujiV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UxCnZnqkPw9RujiV .icon-shape p,#mermaid-svg-UxCnZnqkPw9RujiV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UxCnZnqkPw9RujiV .icon-shape .label rect,#mermaid-svg-UxCnZnqkPw9RujiV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UxCnZnqkPw9RujiV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UxCnZnqkPw9RujiV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UxCnZnqkPw9RujiV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





遍历结束
输入请求路径
按 / 分割路径
遍历每个路径段
路径段是 . ?
路径段是 .. ?
level 减 1
level < 0 ?
路径非法,拒绝访问
level 加 1
路径合法,允许访问

3.9 Util 使用场景速查表

为了方便大家快速回顾与查阅,这里将 Util 类中的 10 个工具接口整理成一张速查表,列出每个接口的名称、功能描述以及典型调用场景:

接口 功能描述 典型调用场景
Split 按指定分隔符分割字符串,将子串存入数组并返回数量 解析 HTTP 请求行,按空格或 / 拆分出方法、路径与版本
ReadFile 以二进制模式将磁盘文件一次性读入内存缓冲区 读取静态资源(HTML、CSS、图片)并作为响应体返回
WriteFile 以覆盖方式将数据写入文件 日志落盘、临时缓存、上传文件的持久化
UrlEncode 将 URL 中的特殊字符编码为 %HH 格式 生成重定向链接、表单回显,确保 URL 在协议层面合法
UrlDecode %HH 还原为原始字符,+ 还原为空格 解析客户端提交的请求 URI,还原用户真实输入
StatusDesc 根据状态码查表获取对应的文本描述 构造响应状态行,如 200 OK404 Not Found
ExtMime 根据文件后缀名映射到对应的 MIME 类型 设置响应头 Content-Type,决定浏览器渲染方式
IsDirectory 判断目标路径是否为目录 目录浏览服务、路径安全校验前的类型区分
IsRegular 判断目标路径是否为普通文件 防止将设备文件、管道、符号链接等作为资源返回
ValidPath 校验请求路径是否越出资源根目录 安全防御,拦截 /../../etc/passwd 等路径遍历攻击

这张表可以作为日常开发时的快速索引:当你在业务代码中遇到字符串处理、文件读写、URL 编解码或路径安全校验等需求时,直接对照上表找到对应的工具接口即可,无需重复造轮子。

结语

至此,我们完成了 Util 工具类的全部设计与实现。

回顾整个过程,Util 没有引入任何复杂的底层机制,它不负责网络 I/O,不直接操作套接字,也不参与事件循环。它所做的事情,是把散落在业务代码中的高频重复逻辑 ------字符串分割、文件读写、URL 编解码、MIME 查询、路径安全校验------提炼为一组纯粹、无状态、可复用的静态接口。这些接口像螺丝刀与扳手一样,不引人注目,却支撑起了整个上层建筑的运转。

在实现过程中,我们也展示了工具类设计中的几个关键意识:防御性编程 (空分隔符、非法编码、路径遍历的防护)、边界感 (什么该放进 Util,什么不该放)、以及务实的 YAGNI 精神(不为不需要的功能提前买单)。

希望能够对大家有所帮助!

相关推荐
塔望品牌咨询1 小时前
食品品牌战略预算的决策框架:如何根据经营瓶颈安排研究、产品、渠道与传播
大数据·人工智能·塔望消费战略·食品
小此方1 小时前
Linux网络(十一):HTTP重定向与请求方法详解:从301/302状态码到GET/POST,再认识Fiddler抓包
linux·网络·http
羊羊小栈1 小时前
基于「YOLO目标检测 + 多模态AI分析」的公共场所暴力安全智能检测分析预警系统
人工智能·算法·面试·毕业设计·大作业
小王的创意工坊1 小时前
C++ 在线判题平台(OJ)测试报告
c++·可用性测试
KANGBboy1 小时前
doris+kafka安装部署(单机+集群)六安装部署
linux
vipjx11 小时前
百度网盘下载慢解决思路:2026最新PanDownload与直链工具对比
网络·智能路由器
weixin_435208161 小时前
pi agent 扩展与 hook 机制浅析
人工智能·agent
土星云SaturnCloud1 小时前
ResNet-50 图像分类算法在边缘微服务器上的部署与性能评测
服务器·人工智能·算法·边缘计算·resnet-50
QYRdata1 小时前
基于ANPR数据分析平台市场增长预测(2026-2032年复合增长率4.3%)
大数据·人工智能