当前位置: 首页 > news >正文

【项目】自主实现HTTP服务器:从Socket到CGI全流程解析

00 引言

​ 在构建高效、可扩展的网络应用时,理解HTTP服务器的底层原理是一项必不可少的技能。现代浏览器与移动应用大量依赖HTTP协议完成前后端通信,而这一过程的背后,是由网络套接字驱动的请求解析、响应构建、数据传输等一系列机制所支撑。为了深入掌握这些关键技术,本项目以“自主实习HTTP服务器”为目标,期望能够带你从零实现一个能够处理基本GET和POST请求的多线程HTTP服务端程序。

​ 整个实现过程中,我们不仅会涉及C/C++语言的系统级编程,还将涵盖网络套接字、线程管理、CGI通信、单例模式以及HTTP协议本身的各项细节。如果你希望通过实战方式扎实掌握这些底层知识,那么这个项目将是一次非常适合入门和拓展的实践机会。

项目源代码地址:https://github.com/Kutbas/LightHttp#

01 理解网络协议与HTTP通信机制

01.1 网络协议栈与数据传输流程

​ 在网络通信中,协议栈是实现数据可靠传输的关键。它采用分层的结构设计,每一层各司其职,共同完成数据的发送与接收。最上层的应用层负责具体业务的数据处理;传输层则保证数据的可靠传输;网络层解决数据应发送到哪儿的问题;链路层则是数据真正被发送和接收的地方。

​ 当我们发送数据时,它会从应用层开始,逐层向下封装,每一层都会附加特定的报头信息,形成完整的数据包。而接收端正好相反,数据自底向上依次被拆解,每一层剥离自己的报头信息,直到还原出原始数据。这种过程称为“封装与分用”。

​ 在我们的项目中,核心任务是处理客户端发来的HTTP请求:提取请求中的报头内容,分析请求数据,处理后再加上响应报头,返回给客户端。虽然我们说“接收请求”和“发送响应”,但实际上传输过程涉及到协议栈中下三层的配合。我们工作的重点位于应用层,和传输层直接进行交互,而数据的真正发送由系统完成。同层之间的通信在逻辑上也可以看作是“直接”的。

01.2 HTTP协议概览

​ 关于HTTP协议,它作为Web通信的基础,具备几个重要特点:

  • 它遵循客户端-服务器模式,通信总是一端请求,另一端响应;
  • 协议设计简单,通信快速;
  • 灵活性强,可传输任意类型的数据,通过Content-Type字段来标识;
  • 是无连接的,即每次请求处理完毕后,连接就会关闭;
  • 也是无状态的,服务器不会自动记住前一次请求的状态。

​ 不过,HTTP无状态的特性也带来了问题,比如无法识别用户是否登录。因此,引入了Cookie技术来维护用户状态,再通过Session机制增强安全性。这也是现代网站实现用户认证的重要基础。

​ 值得一提的是,虽然早期的HTTP/1.0每次请求都断开连接,但后来HTTP/1.1支持了“长连接”(Keep-Alive),减少了重复连接带来的资源消耗。不过我们当前项目实现的是1.0版本,因此不涉及这一特性。

​ 继续来看HTTP的相关格式和用法。URL(统一资源定位符)是我们浏览网页时常见的网址,它用于标识和定位互联网上的资源。URL通常包括协议名(如http://)、服务器地址(如域名)、端口号、资源路径、参数等多个部分。通常情况下,端口号和部分字段可以省略,浏览器会自动补全。

​ 更广义地说,URL只是URI(统一资源标识符)的一种,它不仅标识资源,还能说明如何访问资源。URN则是通过名字标识资源的另一种URI。例如,mailto:example@example.com 就是一个URN。简而言之,URL和URN是URI的两个子集。

​ 关于URI,还有“绝对”和“相对”之分。像URL那样能独立标识资源的,是绝对URI;而依赖环境的资源路径(如浏览器中的请求路径),就是相对URI。

​ 在通信过程中,HTTP请求和响应的数据格式是规范化的:

  • 请求包括请求行(方法+路径+版本)、请求头、空行和请求体;
  • 响应包括状态行(版本+状态码+描述)、响应头、空行和响应体。

​ 常用的HTTP方法有:

  • GET:请求资源(常用于查询);
  • POST:提交数据(比如表单);
  • PUT/DELETE:对资源进行修改或删除;
  • 还有像HEADOPTIONSTRACE等用于特定场景。

​ GET和POST最常见,区别在于参数传递方式:GET通过URL,参数长度有限;POST通过请求体,能传更多数据。

​ 状态码是HTTP的重要反馈机制,分为五类:

  • 1xx:处理中;
  • 2xx:成功(如200 OK);
  • 3xx:重定向(如301、302);
  • 4xx:客户端错误(如404 Not Found);
  • 5xx:服务器错误(如500 Internal Server Error)。

​ 这些状态码帮助开发者快速判断请求处理的结果和原因。

​ 最后是HTTP头部字段,它们承载了请求和响应的各种元信息。常见的包括:

  • Content-Type:指明数据类型;
  • Content-Length:正文长度;
  • Host:请求的主机地址;
  • User-Agent:客户端信息;
  • Referer:来源页面;
  • Location:配合重定向使用;
  • Cookie:维持客户端状态。

​ 理解以上内容,是开发Web服务或HTTP应用的基础,也为我们处理网络请求、调试响应提供了清晰的结构框架。

02 CGI 机制介绍

02.1 CGI 的概念

​ 在了解了网络协议和HTTP通信机制之后,我们可以进一步探讨浏览器与服务器之间是如何实现更复杂的数据交互的。日常上网时,我们不仅仅是打开网页、浏览图片,很多时候还会在网站上登录、提交表单、上传文件、搜索信息……这些操作背后,其实都涉及了服务器对用户数据的接收与处理。

​ 这就引出了**CGI(通用网关接口)**机制的作用。CGI就像是服务器和后台程序之间沟通的桥梁,它定义了一种通用的数据交换方式,使得Web服务器可以将收到的数据转交给外部程序进行处理,再将结果返回给用户。特别是在处理用户提交的信息时,CGI机制发挥着至关重要的作用。

​ 所以现在我们需要知道的是,浏览器向服务器提交数据后,HTTP协议本身并不对这些数据进行处理,而是将它们交由上层的CGI程序来完成相应操作。CGI程序可以使用任何编程语言编写,部署在服务器上,专门负责接收数据、处理请求,并将结果交回服务器,由服务器进一步构建响应返回给浏览器。

​ 比如,用户提交搜索关键词,服务器接收到请求后会调用相应的CGI程序完成搜索工作,再将搜索结果反馈给浏览器,最终展示给用户。整个过程中,HTTP协议仅作为中介,而实际业务逻辑是由CGI程序处理的。

02.1 服务器调用 CGI 程序的方式

​ 为了实现CGI机制,服务器在收到需要处理的请求后,会通过创建子进程的方式调用对应的CGI程序。由于直接使用线程可能会影响主服务器进程的稳定性,因此通常做法是先用 fork 创建一个子进程,再由子进程调用 exec 执行CGI程序。这就要求我们提前建立好用于通信的管道,因为父进程需要向CGI程序发送数据,CGI程序也要把处理结果反馈回来。

​ 考虑到 exec 调用会替换子进程的代码和数据,但不会改变打开的文件描述符,我们可以将通信管道的读写端重定向到标准输入输出,这样CGI程序无需感知底层管道的具体文件描述符,只需要通过标准输入读取数据,通过标准输出返回结果。

​ 根据不同的请求方法(如GET或POST),数据的传递方式也会有所不同。GET方法中的参数通过URL传递,通常会在程序替换前被放入环境变量,CGI程序通过读取环境变量获取参数。而POST方法的数据包含在请求正文中,需要由父进程写入管道供CGI程序读取,同时通过环境变量告知其数据长度和请求方法。

​ 因此,CGI程序启动后会先读取环境变量确定当前请求的类型,再选择从标准输入或环境变量中读取数据。处理完成后,结果写入标准输出返回服务器,再由服务器生成HTTP响应发送给客户端。

​ CGI机制的核心意义在于实现了业务逻辑与服务器逻辑的解耦。服务器专注于处理请求与响应,而具体的业务交由CGI程序负责。这种分工不仅提高了系统的灵活性,也使得CGI程序开发者无需了解HTTP服务器的内部实现,就可以通过标准输入输出与用户进行数据交互,实现面向用户的功能。

日志实现

​ 在服务器运行过程中,会产生大量日志,用于记录各类事件,帮助我们了解系统的运行状态与排查问题。本项目中采用统一的日志格式,包含日志级别、时间戳、日志信息、出错文件和具体行号。其中日志级别分为四种:INFO 表示系统正常运行;WARNING 意味着出现了风险但不影响继续运行;ERROR 则说明发生错误但服务还能继续;而 FATAL 是最严重的错误,通常会导致程序终止。

​ 为了便于记录,我们可以设计一个 Log 函数,它接收日志等级、描述信息、文件名和行号作为参数,并输出标准格式的日志内容。时间戳使用 time(nullptr) 获取,因此调用时无需额外传参。

#pragma once
#include <iostream>
#include <string>
#include <ctime>#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4void Log(std::string level, std::string message, std::string file_name, int line)
{std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << ']' << "[" << file_name << "]" << "[" << line << "]" << std::endl;
}

​ 为了简化使用,每次调用时我们不希望手动传入 __FILE____LINE__,于是使用宏来实现自动插入。定义宏 LOG(level, message) 后,预处理器会自动把调用的位置文件名和行号补充进 Log 函数中,使调用更加简洁:

#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)

​ 此外,我们通过将日志级别如 INFOWARNING 定义为宏,并用 # 操作符将其转换为字符串,进一步简化日志的调用方式。这样,我们只需调用:

LOG(INFO, "This is a log");

​ 就能自动输出带有时间戳、文件位置和日志级别的标准日志信息,既方便又清晰。需要日志时只管调用 LOG 宏,剩下的交给编译器和预处理器处理即可。

TcpServer类实现

​ 为了构建一个高效、可复用的 TCP 服务端,我们定义一个 TcpServer 类,并采用单例设计模式,从而确保在程序中只存在一个 TcpServer 实例。

头文件及宏定义

​ 首先引入所需头文件和宏:

#pragma once
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"#define BACKLOG 5

类定义及构造函数

​ 为了避免创建多个服务器实例,TcpServer 采用单例模式。构造函数被设为私有,拷贝构造和赋值操作也被禁止,确保外部无法复制对象。类中维护了一个静态指针,首次调用 GetInstance 时,创建并初始化唯一的服务器实例。

class TcpServer
{
private:int port;int listen_sock;static TcpServer *svr;TcpServer(int _port): port(_port), listen_sock(-1){}TcpServer(const TcpServer &s) {}

​ 为了线程安全,调用 GetInstance 时使用了双重检查锁定机制,从而避免不必要的加锁开销。

public:static TcpServer *GetInstance(int port){static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;if (nullptr == svr){pthread_mutex_lock(&lock);if (nullptr == svr){svr = new TcpServer(port);svr->InitServer();}pthread_mutex_unlock(&lock); }return svr;}

​ 初始化服务器时,会依次创建套接字、绑定地址和端口,并开始监听客户端连接。监听套接字可通过 Sock() 函数获取。当服务关闭时,监听套接字也会被正确关闭以释放资源。需要注意的是,如果服务器运行在云环境中,绑定 IP 时可直接使用 INADDR_ANY,让系统自动选择合适的网卡;由于它本质是0,也无需进行网络字节序转换。

void InitServer()
{Socket();Bind();Listen();LOG(INFO, "tcp_server init ... success");
}
// 创建套接字
void Socket()
{listen_sock = socket(AF_INET, SOCK_STREAM, 0);if (listen_sock < 0){LOG(FATAL, "socket error!");exit(1);}int opt = 1;setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));LOG(INFO, "create socket ... success");
}
// 绑定端口
void Bind()
{struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0){LOG(FATAL, "bind error!");exit(2);}LOG(INFO, "bind socket ... success");
}
// 监听连接
void Listen()
{if (listen(listen_sock, BACKLOG) < 0){LOG(FATAL, "listen socket error!");exit(3);}LOG(INFO, "listen socket ... success");
}

类中的其他方法

// 提供对监听套接字的访问方法
int Sock()
{return listen_sock;
}
// 析构函数
~TcpServer()
{if (listen_sock >= 0)close(listen_sock);
}TcpServer *TcpServer::svr = nullptr; // 静态成员必须在类外初始化

HttpServer及其相关类实现

HTTP服务器主体逻辑

​ HTTP服务器的实现建立在 TCP 服务基础之上。我们可以将其封装为 HttpServer 类,在构造时指定端口号,调用 Loop() 函数即可启动服务。运行时,首先从 TcpServer 获取监听套接字,然后循环等待新连接,每当有客户端连入,就创建一个新的线程处理请求。

​ 为了避免连接套接字在传递过程中被覆盖,我们可以使用堆空间分配内存保存该套接字,并传递给新线程。新线程通过回调函数处理客户端的 HTTP 请求,主线程继续等待后续连接。每个线程在创建后立即被分离,这样主线程无需等待它们结束,确保服务器持续运行。

​ 这样一来,主函数只需从命令行读取端口号,创建 HttpServer 对象并调用 Loop() 即可启动服务。

​ 基于以上设计思路,下面是 HttpServer 类的完整实现:

#pragma once
#include <iostream>
#include <pthread.h>
#include <signal.h>
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"#define PORT 8081class HttpServer
{
private:int port;bool stop;public:HttpServer(int _port = PORT): port(_port), stop(false){}void InitServer(){signal(SIGPIPE, SIG_IGN); // 信号 SIGPIPE 需要忽略,如果不忽略,在写入时可能直接崩溃}void Loop(){TcpServer *tsvr = TcpServer::GetInstance(port);LOG(INFO, "loop begin");// int listen_sock = tcp_server->Sock();while (!stop){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(tsvr->Sock(), (struct sockaddr *)&peer, &len);if (sock < 0){continue;}LOG(INFO, "get a new link");Task task(sock);ThreadPool::GetInstance()->PushTask(task);}}~HttpServer(){}
};

HTTP 请求结构设计

​ 在处理 HTTP 请求时,我们设计一个 HttpRequest 类用于封装客户端发送的请求。这个类包含请求的各个组成部分:请求行、请求头、请求正文以及解析后的内容,如请求方法、资源路径、参数等。还包含一个标志位,用于标识该请求是否需要 CGI 处理。

class HttpRequest
{
public:std::string request_line;std::vector<std::string> request_header;std::string blank;std::string request_body;// 解析完毕之后的结果std::string method;std::string uri;std::string version;std::unordered_map<std::string, std::string> header_kv;int content_length;std::string path;std::string suffix;std::string query_string;bool cgi;int size;public:HttpRequest(): content_length(0), cgi(false){}~HttpRequest(){}
};

HTTP 响应结构设计

​ 与请求类似,HTTP 响应也封装为一个 HttpResponse 类,分别记录要发送的响应内容(状态行、响应头、空行、正文)以及生成这些内容所需的数据(如状态码、文件描述符、文件大小和后缀等)。

class HttpResponse
{
public:std::string status_line;std::vector<std::string> response_header;std::string blank;std::string response_body;int status_code;int fd;public:HttpResponse(): blank(LINE_END), status_code(OK), fd(-1){}~HttpResponse(){}
};

EndPoint类实现

EndPoint类结构设计

​ 在处理客户端请求时,我们通常将每一个“通信端”称为一个 EndPoint。在这里,我们可以设计一个 EndPoint 类来表示服务端与客户端建立连接后,对该连接的完整处理流程。这个类主要负责从客户端读取请求内容,处理请求并生成响应,最后将响应结果发送回客户端。

EndPoint 对象内部维护了三个核心成员:与客户端通信的套接字 sock,用于封装和存储请求信息的 http_request 对象,以及构造响应所需的 http_response 对象。此外,还有一个布尔值 stop,用来标志在处理请求时是否中止流程。

​ 在功能设计上,EndPoint 提供了一系列私有成员函数来分阶段地完成整个请求处理流程。包括接收请求行 (RecvHttpRequestLine) 和请求头 (RecvHttpRequestHeader),解析这些信息 (ParseHttpRequestLine, ParseHttpRequestHeader),如果请求包含正文,还会通过 IsNeedRecvHttpRequestBodyRecvHttpRequestBody 来读取正文内容。之后根据请求类型判断是否需要使用 CGI 脚本处理(ProcessCgi)还是处理静态资源(ProcessNonCgi),同时也预留了错误处理接口(HandlerError)。构建响应则交由 BuildOkResponseBuildHttpResponseHelper 实现。

​ 类的公共接口主要包括三个方法:RecvHttpRequest() 负责发起请求的接收,BuildHttpResponse() 用于生成响应内容,SendHttpResponse() 则将结果返回给客户端。当请求处理完毕后,在析构函数中会关闭通信套接字,释放资源。

class EndPoint
{
private:int sock;HttpRequest http_request;HttpResponse http_response;bool stop;private:bool RecvHttpRequestLine();bool RecvHttpRequestHeader();void ParseHttpRequestLine();void ParseHttpRequestHeader();bool IsNeedRecvHttpRequestBody();bool RecvHttpRequestBody();int ProcessCgi();void HandlerError(std::string page);int ProcessNonCgi();void BuildOkResponse();void BuildHttpResponseHelper();public:EndPoint(int _sock): sock(_sock), stop(false){}bool IsStop();void RecvHttpRequest();void BuildHttpResponse();void SendHttpResponse();~EndPoint(){}
};

EndPoint 的具体结构如上所示,相关函数的实现后续介绍。

CallBack类设计

​ 为了配合服务器多线程模型,我们可以设计一个 CallBack 类,用作线程的回调处理逻辑。每当服务器收到一个新的连接,就会创建一个新线程来处理这个连接的请求。在这个线程中,我们会创建一个 EndPoint 实例,通过它依次完成接收请求、构建响应和发送响应的完整流程。

​ 如果在调试模式下,线程也可以直接打印收到的原始HTTP请求内容,以便分析调试。而在正常运行模式中,则采用标准流程处理请求并记录日志。请求处理完后,EndPoint 对象会被销毁,自动关闭对应的客户端连接,整个过程实现了高效且结构清晰的请求响应机制。

​ 具体代码如下所示:

class CallBack
{
public:CallBack() {}~CallBack() {}void operator()(int sock){HandlerRequest(sock);}void HandlerRequest(int sock){LOG(INFO, "handler request begin");// std::cout << "get a new link ..." << std::endl;#ifdef DEBUGchar buffer[4096];recv(sock, buffer, sizeof(buffer), 0);std::cout << "-----begin-----" << std::endl;std::cout << buffer << std::endl;std::cout << "-----end-----" << std::endl;
#elseEndPoint *ep = new EndPoint(sock);ep->RecvHttpRequest();if (!ep->IsStop()){LOG(INFO, "recv no error, start build and send");ep->BuildHttpResponse();ep->SendHttpResponse();}else{LOG(WARNING, "recv error, stop build and send!");}delete ep;
#endifLOG(INFO, "handler request end");}
};

读取HTTP请求

​ 在处理 HTTP 请求的过程中,我们通常将其拆解为若干个步骤来完成解析工作。前面我们提到,在服务端 EndPoint 类中,我们通过 RecvHttpRequest() 函数来整体控制请求的接收流程,依次完成请求行、请求报头与空行的读取,以及请求行和报头的解析,最后根据情况读取请求正文。

void RecvHttpRequest()
{if ((!RecvHttpRequestLine()) && (!RecvHttpRequestHeader())){ParseHttpRequestLine();ParseHttpRequestHeader();RecvHttpRequestBody();}
}

​ 请求行的读取通过 RecvHttpRequestLine() 方法实现,该方法从套接字中读取一整行数据,并存入请求对象中的 request_line 字段。由于不同操作系统下行分隔符可能为 \r\n\r\n,因此不能使用标准的 getline()gets(),而是通过自定义的 ReadLine 工具函数来逐字符读取并判断行尾符号,从而兼容所有常见的行分隔格式。该函数会将整行(包括换行符)存入用户提供的字符串中,因此调用者需根据需要手动去除末尾换行符。

bool RecvHttpRequestLine()
{auto &line = http_request.request_line;if (Util::ReadLine(sock, line) > 0){line.resize(line.size() - 1);LOG(INFO, http_request.request_line);}else{stop = true;}return stop;
}	

​ 在读取请求报头和空行时,系统会不断调用 ReadLine() 读取每一行内容,直到遇到一个仅包含换行符的空行为止。读取到的每行数据会先去除末尾的换行符,再存入请求对象中的 request_header 列表中,用于后续解析。

static int ReadLine(int sock, std::string &out)
{char ch = 'X';while (ch != '\n'){ssize_t s = recv(sock, &ch, 1, 0);if (s > 0){if (ch == '\r'){recv(sock, &ch, 1, MSG_PEEK);if (ch == '\n'){// 窥探成功,把 '\r\n' 转换为 '\n'recv(sock, &ch, 1, 0);}elsech = '\n';}// 走到这里 ch 要么是普通字符要么是 \nout.push_back(ch);}else if (s == 0){return 0;}elsereturn -1;}return out.size();
}

​ 接下来解析请求行。其主要任务是将请求行中用空格分隔的三个字段:请求方法、URI 和 HTTP 版本,依次提取出来并存入对应的字段中。解析时借助 std::stringstream 进行分割,同时通过 std::transform 将请求方法转换为大写,以便后续逻辑统一处理。

void ParseHttpRequestLine()
{auto &line = http_request.request_line;std::stringstream ss(line);ss >> http_request.method >> http_request.uri >> http_request.version;auto &method = http_request.method;std::transform(method.begin(), method.end(), method.begin(), ::toupper);
}

​ 对于请求报头的解析,程序逐行处理之前保存的报头字符串,将每一行用 ": " 作为分隔符切割成键值对,存入 header_kv 哈希表中。为了提高代码复用性,字符串切割逻辑被封装在一个名为 CutString 的工具函数中,其利用 findsubstr 方法实现。

#define SEP ": "void ParseHttpRequestHeader()
{std::string key, value;for (auto &iter : http_request.request_header){if (Util::CutString(iter, key, value, SEP)){http_request.header_kv.insert({key, value});}}
}static bool CutString(std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep)
{size_t pos = target.find(sep);if (pos != std::string::npos){sub1_out = target.substr(0, pos);sub2_out = target.substr(pos + sep.size());return true;}return false;
}

​ 在读取请求正文前,需要判断当前请求是否包含正文。只有 POST 方法可能附带正文,而且必须要在请求报头中找到 Content-Length 字段,明确说明正文长度。如果满足条件,则通过 recv 循环读取指定长度的正文内容并存入 request_body。正文的长度会在之前解析过程中转换为整型,并存入请求对象的 content_length 字段中。

bool IsNeedRecvHttpRequestBody()
{auto &method = http_request.method;if (method == "POST"){auto &header_kv = http_request.header_kv;auto iter = header_kv.find("Content-Length");if (iter != header_kv.end()){http_request.content_length = atoi(iter->second.c_str());return true;}}return false;
}bool RecvHttpRequestBody()
{if (IsNeedRecvHttpRequestBody()){int content_length = http_request.content_length;auto &body = http_request.request_body;char ch = 0;while (content_length){ssize_t s = recv(sock, &ch, 1, 0);if (s > 0){body.push_back(ch);content_length--;}else{stop = true;break;}}LOG(INFO, body);}return stop;
}

​ 至此,从接收请求到解析完毕的整体流程就完成了,每一步都围绕着数据结构的逐步填充和协议格式的严格解析展开,确保后续业务逻辑可以基于准确的请求信息进行处理。

处理HTTP请求

​ 在处理HTTP请求时,服务器可能因为请求方式不合法、资源不存在或内部出错等原因中断操作。为了让客户端了解请求的处理结果,服务器通常会返回一个HTTP状态码。比如,请求成功时返回 200 OK,请求格式有误时返回 400 Bad Request,资源未找到时返回 404 Not Found,服务器内部出错时返回 500 Internal Server Error

​ 本项目中定义的状态码如下:

#define OK 200
#define NOT_FOUND 404
#define BAD_REQUEST 400
#define SERVER_ERROR 500	

​ 服务器处理HTTP请求时,首先会检查请求方法是否合法。如果方法不是GET或POST,则视为无效请求,设置状态码为BAD_REQUEST并终止处理。

​ 当请求方法是GET时,服务器会判断URI中是否带有查询参数。如果URI包含?,说明用户在URL中附带了参数,此时需拆分URI,将?左边作为请求路径,右边作为参数,同时标记当前请求需以CGI模式处理。如果没有携带参数,直接将URI作为资源路径。如果是POST请求,虽然URI直接表示资源路径,但由于请求参数包含在请求体中,因此也需要以CGI方式处理。

​ 接下来,服务器会将请求的路径拼接到Web根目录(项目中为wwwroot)后面。如果路径以/结尾,说明客户端请求的是某个目录,这时默认返回该目录下的首页文件index.html。然后,通过stat系统调用检查拼接后的路径是否指向一个存在的资源。如果资源是一个目录但URI没有以/结尾,服务器会自动补全并尝试加载目录下的index.html。如果资源具有可执行权限,则视为CGI程序,设置cgi=true。同时,获取的资源文件大小也会被保存下来,用于后续响应构建。

​ 服务器还会根据文件名后缀判断响应的内容类型。如果找不到后缀,默认使用.html

​ 最终,服务器根据cgi标志选择执行普通静态页面处理(ProcessNonCgi())或CGI动态处理(ProcessCgi())。无论哪种方式,处理完成后都会调用BuildHttpResponseHelper()生成响应头等信息。

​ 下面是关键函数实现:

#define WEB_ROOT "wwwroot"
#define HOME_PAGE "index.html"void BuildHttpResponse()
{std::string _path;struct stat st;std::size_t found = 0;auto &code = http_response.status_code;if (http_request.method != "GET" && http_request.method != "POST") {LOG(WARNING, "method is not right");code = BAD_REQUEST;goto END;}if (http_request.method == "GET") {size_t pos = http_request.uri.find("?");if (pos != std::string::npos) {Util::CutString(http_request.uri, http_request.path, http_request.query_string, "?");http_request.cgi = true;} else {http_request.path = http_request.uri;}} else if (http_request.method == "POST") {http_request.cgi = true;http_request.path = http_request.uri;}_path = http_request.path;http_request.path = WEB_ROOT + _path;if (http_request.path.back() == '/') {http_request.path += HOME_PAGE;}if (stat(http_request.path.c_str(), &st) == 0) {if (S_ISDIR(st.st_mode)) {http_request.path += "/";http_request.path += HOME_PAGE;stat(http_request.path.c_str(), &st);}if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)) {http_request.cgi = true;}http_request.size = st.st_size;} else {LOG(WARNING, http_request.path + " not found!");code = NOT_FOUND;goto END;}found = http_request.path.rfind(".");http_request.suffix = (found == std::string::npos) ? ".html" : http_request.path.substr(found);code = http_request.cgi ? ProcessCgi() : ProcessNonCgi();END:BuildHttpResponseHelper();
}

​ 当请求需要CGI处理时,服务器会创建两个匿名管道用于父子进程间通信:一个用于父进程读取子进程的输出(input管道),另一个用于父进程向子进程写入数据(output管道)。接着,服务器创建子进程。父进程关闭不需要的管道端口,只保留用于通信的部分。子进程也会关闭无关端口,并将标准输入输出重定向到对应的管道上,使得后续替换为CGI程序后能够从标准输入读取数据、向标准输出写入响应。

​ CGI执行前,服务器会通过环境变量将请求信息传递给CGI程序,比如方法名、参数内容等。如果是POST请求,还会将请求正文写入管道。CGI程序处理完毕后,父进程负责从管道中读取其输出,并将内容保存到HTTP响应体中。最后,等待子进程退出并清理资源。

ProcessCgi()函数如下:

int ProcessCgi()
{LOG(INFO, "process cgi method");int code = OK;auto &method = http_request.method;auto &query_string = http_request.query_string;auto &body_text = http_request.request_body;auto &bin = http_request.path;int content_length = http_request.content_length;auto &response_body = http_response.response_body;std::string method_env = "METHOD=" + method;std::string query_string_env, content_length_env;int input[2], output[2];if (pipe(input) < 0 || pipe(output) < 0) {LOG(ERROR, "pipe error!");return SERVER_ERROR;}pid_t pid = fork();if (pid == 0) {close(input[0]);close(output[1]);putenv((char *)method_env.c_str());if (method == "GET") {query_string_env = "QUERY_STRING=" + query_string;putenv((char *)query_string_env.c_str());} else if (method == "POST") {content_length_env = "CONTENT_LENGTH=" + std::to_string(content_length);putenv((char *)content_length_env.c_str());}dup2(output[0], 0); // stdindup2(input[1], 1);  // stdoutexecl(bin.c_str(), bin.c_str(), nullptr);exit(1);} else {close(input[1]);close(output[0]);if (method == "POST") {write(output[1], body_text.c_str(), body_text.size());}char ch = 0;while (read(input[0], &ch, 1) > 0) {response_body.push_back(ch);}waitpid(pid, nullptr, 0);close(input[0]);close(output[1]);}return code;
}

构建HTTP响应

​ 构建 HTTP 响应的过程主要围绕三个部分展开:状态行、响应报头和响应正文。首先,状态行由 HTTP 版本、状态码以及对应的描述组成,并使用空格分隔。例如 "HTTP/1.0 200 OK\r\n"。在代码中,这一行最终被保存到 http_response.status_line 中。接下来是响应报头的构建,其内容将根据请求是否被正常处理而有所不同。

​ 构建响应的核心函数 BuildHttpResponse 会判断当前请求的类型是否为支持的 GET 或 POST 方法。如果请求非法(即不是 GET 或 POST),将返回 400 状态码。对于 GET 请求,还需检查 URI 中是否带有查询字符串(是否包含 ?),以判断是否为 CGI 请求。而 POST 请求则默认作为 CGI 处理。

void BuildHttpResponse()
{std::string _path;struct stat st;std::size_t found = 0;auto &code = http_response.status_code;if (http_request.method != "GET" && http_request.method != "POST"){// 非法请求LOG(WARNING, "method is not right");code = BAD_REQUEST;goto END;}if (http_request.method == "GET"){// GETsize_t pos = http_request.uri.find("?");if (pos != std::string::npos){Util::CutString(http_request.uri, http_request.path, http_request.query_string, "?");http_request.cgi = true;}else{http_request.path = http_request.uri;}}else if (http_request.method == "POST"){// POSThttp_request.cgi = true;http_request.path = http_request.uri;}else{// Nothing}// 拼接 Web 根目录_path = http_request.path;http_request.path = WEB_ROOT;http_request.path += _path;// std::cout << "debug:" << http_request.path << std::endl;if (http_request.path[http_request.path.size() - 1] == '/'){// 如果请求的是根目录,则返回首页http_request.path += HOME_PAGE;}// std::cout << "debug:" << http_request.path << std::endl;// 检查资源是否存在if (stat(http_request.path.c_str(), &st) == 0){// 资源存在if (S_ISDIR(st.st_mode)){// 请求的资源是个目录,不允许http_request.path += "/";http_request.path += HOME_PAGE;stat(http_request.path.c_str(), &st);}if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)){// 请求的资源是个可执行文件http_request.cgi = true;}http_request.size = st.st_size;}else{// 资源不存在LOG(WARNING, http_request.path + " not found!");code = NOT_FOUND;goto END;}found = http_request.path.rfind(".");if (found == std::string::npos)http_request.suffix = ".html";elsehttp_request.suffix = http_request.path.substr(found);// 构建响应if (http_request.cgi){code = ProcessCgi();}else{// 简单的网页返回code = ProcessNonCgi();}END:BuildHttpResponseHelper(); // 状态行填充
}

​ 无论是哪种请求,都会将 URI 拼接上服务器的 Web 根目录,最终生成实际请求资源的完整路径。如果路径以 / 结尾,则默认返回首页(如 index.html)。随后通过 stat 判断该资源是否存在,并据此设定响应的处理方式:如果是目录,则自动拼接首页;如果文件具备执行权限(可执行脚本),则按 CGI 请求处理;否则视为普通文件返回。

​ 当资源存在时,代码会根据文件扩展名来确定返回内容的类型(如 .html.css 等),并设置 http_request.suffix。若找不到扩展名,则默认使用 .html。在此基础上,如果为 CGI 请求,则调用 ProcessCgi() 处理动态生成内容;否则调用 ProcessNonCgi() 处理静态文件。之后,统一调用 BuildHttpResponseHelper() 来构造状态行。

​ 状态码描述通过 Code2Desc 函数获得,例如 200 对应 “OK”,404 对应 “Not Found”。这一部分也会被添加到状态行中。

//根据状态码获取状态码描述
static std::string Code2Desc(int code)
{std::string desc;switch (code){case 200:desc = "OK";break;case 404:desc = "Not Found";break;// 根据需要逐步添加default:break;}return desc;
}

​ 当请求成功处理后,响应报头中至少包含 Content-TypeContent-Length 两项。前者由 Suffix2Desc 函数根据文件后缀确定 MIME 类型,后者则依据处理方式设定内容长度:若为 CGI,请求内容来自内存中的字符串;否则来自磁盘上的静态文件,其长度已通过 stat 获得。

//根据后缀获取资源类型
static std::string Suffix2Desc(const std::string &suffix)
{static std::unordered_map<std::string, std::string> suffix2desc = {{".html", "text/html"},{".css", "text/css"},{".js", "application/javascript"},{".jpg", "application/x-jpg"},{".xml", "application/xml"},};auto iter = suffix2desc.find(suffix);if (iter != suffix2desc.end()){return iter->second;}return "text/html";
}

​ 如果处理过程中发生错误(如资源不存在),则调用 HandlerError() 处理函数,为客户端返回一个错误页面。此时,响应类型统一为 text/html,文件大小通过 stat 获取。同时需注意将 http_request.cgi 设置为 false,确保后续按非 CGI 模式处理响应正文。

void HandlerError(std::string page)
{http_request.cgi = false;// 返回错误码对应的页面http_response.fd = open(page.c_str(), O_RDONLY);if (http_response.fd > 0){struct stat st;stat(page.c_str(), &st);http_request.size = st.st_size;std::string line = "Content-Type: text/html";line += LINE_END;http_response.response_header.push_back(line);line = "Content-Length: ";line += std::to_string(st.st_size);line += LINE_END;http_response.response_header.push_back(line);}
}

发送HTTP响应

​ 在实际发送 HTTP 响应时,首先通过 send 发送状态行、所有响应报头和空行。随后,根据处理模式决定如何发送响应正文:如果是 CGI 请求,正文内容保存在内存中,直接使用 send 发送字符串;若为非 CGI 或错误处理情况,则响应文件已打开,使用 sendfile 直接从文件描述符发送文件内容,并在发送完毕后关闭该文件描述符。

void SendHttpResponse()
{send(sock, http_response.status_line.c_str(), http_response.status_line.size(), 0);for (auto iter : http_response.response_header)send(sock, iter.c_str(), iter.size(), 0);send(sock, http_response.blank.c_str(), http_response.blank.size(), 0);if (http_request.cgi){auto &response_body = http_response.response_body;size_t size = 0;size_t total = 0;const char *start = response_body.c_str();while (total < response_body.size() && (size = send(sock, start + total, response_body.size() - total, 0)) > 0)total += size;}else{sendfile(sock, http_response.fd, nullptr, http_request.size);close(http_response.fd);}
}

错误处理

​ 为了让服务器更加健壮,我们还需要完善其在处理请求过程中的差错应对机制。尽管目前的服务器整体逻辑已基本跑通,但在运行过程中仍可能发生莫名崩溃。这些问题的根源在于对各种错误的处理还不够完善,尤其是在请求的读取、处理和响应发送这几个关键阶段。

逻辑错误

​ 首先来看逻辑错误,这是服务器在解析和处理HTTP请求时可能遇到的问题,比如请求方法不合法、资源不存在或处理时发生内部错误等。对于这类错误,我们已经实现了相应机制,当检测到这些问题时,服务器会向客户端返回一个对应的错误页面,提示用户出现了问题。

读取错误

​ 而在逻辑错误之前,服务器首先要完成对请求的读取,如果这个阶段出现问题,比如 recv 读取失败或客户端提前关闭了连接,我们称之为读取错误。一旦发生读取错误,就意味着服务器连完整的HTTP请求都没有获取到,自然无法继续处理或响应。我们可以在 EndPoint 类中增加一个布尔变量 stop,用于标记是否应中止当前处理流程。在读取请求行、请求头或请求正文的过程中,一旦发生错误,就将 stop 设为 true,后续流程会根据这个标志决定是否继续执行。

bool RecvHttpRequestLine()
{auto &line = http_request.request_line;if (Util::ReadLine(sock, line) > 0){line.resize(line.size() - 1);LOG(INFO, http_request.request_line);}else{stop = true;}return stop;
}

​ 为了配合 stop 的使用,读取请求的每个子步骤,如 RecvHttpRequestLineRecvHttpRequestHeaderRecvHttpRequestBody 函数都被设计成返回 bool 类型,表示是否出现错误。整个读取过程采用逻辑与(&&)的短路策略,确保只有在前一个步骤成功的前提下才会继续执行下一个步骤。此外,还提供了 IsStop() 接口,使外部线程可以判断是否应该终止处理。

​ 在调用 RecvHttpRequest() 后,工作线程会通过 IsStop() 检查是否继续执行后续流程。如果没有读取错误,则继续调用 HandlerHttpRequest() 处理请求,调用 BuildHttpResponse() 构建响应,然后使用 SendHttpResponse() 发送响应。否则,服务器会直接跳过这些操作,关闭与客户端的连接并清理资源。

写入错误

​ 除了读取错误,写入响应时也可能遇到问题。为了防止因写入错误导致进程崩溃的问题,还需要处理 SIGPIPE 信号。当服务器向一个已关闭连接的客户端发送数据时,会触发该信号,而默认行为是终止进程。为避免这种情况,在 HTTP 服务器初始化时,通过 signal(SIGPIPE, SIG_IGN) 忽略这个信号,从而保障服务器的稳定性。

void InitServer()
{signal(SIGPIPE, SIG_IGN); // 信号 SIGPIPE 需要忽略,如果不忽略,在写入时可能直接崩溃
}

引入线程池

​ 为了提升服务器处理并发连接的效率,我们引入线程池机制,用以优化当前多线程模型中的诸多性能瓶颈。在传统多线程服务器中,每当有客户端连接时,服务器主线程就会新建一个线程负责该连接的处理,任务完成后再销毁这个线程。这种方式简单直观,但随着连接数的增加,会迅速消耗系统资源,线程数量一旦激增,不仅加重CPU线程调度的压力,还会导致响应延迟显著增加。

​ 引入线程池后,我们不再为每个客户端临时创建线程,而是在服务器启动时预先创建好一批工作线程,并维护一个任务队列。服务器主线程在接收到客户端连接后,只需将其封装成一个任务对象并加入队列即可,具体的处理由线程池中的空闲线程来执行。如果没有任务,这些线程会进入休眠状态,直到有新任务到来再被唤醒,从而大大减少了线程的频繁创建与销毁,提高了资源利用率和系统响应能力。

任务类设计

​ 每一个客户端请求被封装为一个 Task 对象,其中包含一个套接字 sock 和一个回调函数 handler。当线程池中的线程从任务队列中取出任务后,会通过 ProcessOn() 方法执行该回调函数,从而处理具体业务逻辑。

class Task {
private:int sock;CallBack handler;public:Task() {}Task(int _sock) : sock(_sock) {}void ProcessOn() {handler(sock);}
};

​ 此处 CallBack 是一个可调用对象(仿函数),之前我们已经介绍过了。它重载了 () 运算符,实际内部调用的是 HandlerRequest 方法,这一方法完成了 HTTP 请求的接收、解析、响应等完整流程。

线程池实现

​ 线程池采用单例设计模式,确保整个服务器生命周期内只有一个线程池实例。其核心组件包括任务队列、线程数量、互斥锁、条件变量等。线程池在初始化时便会创建多个线程,这些线程不断循环执行 ThreadRoutine,从任务队列中取任务处理。

​ 说明几个关键点:

  • ThreadRoutine 是线程的主函数,它必须是静态成员函数,因为 pthread_create 不支持普通成员函数指针。
  • 每个线程启动后会不断尝试从任务队列中取任务,如果队列为空就进入等待状态,直到被新任务唤醒。
  • 所有对任务队列的读写都需要加锁,保证线程安全。
  • 线程使用 pthread_detach 分离,避免资源回收时产生阻塞。
class ThreadPool {
private:int num;bool stop;std::queue<Task> task_queue;pthread_mutex_t lock;pthread_cond_t cond;static ThreadPool* single_instance;ThreadPool(int _num = 6) : num(_num), stop(false) {pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);}ThreadPool(const ThreadPool&) = delete;public:static ThreadPool* GetInstance() {static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;if (single_instance == nullptr) {pthread_mutex_lock(&_mutex);if (single_instance == nullptr) {single_instance = new ThreadPool();single_instance->InitThreadPool();}pthread_mutex_unlock(&_mutex);}return single_instance;}void PushTask(const Task& task) {pthread_mutex_lock(&lock);task_queue.push(task);pthread_mutex_unlock(&lock);pthread_cond_signal(&cond);}void PopTask(Task& task) {task = task_queue.front();task_queue.pop();}static void* ThreadRoutine(void* args) {ThreadPool* tp = static_cast<ThreadPool*>(args);while (true) {Task t;pthread_mutex_lock(&tp->lock);while (tp->task_queue.empty()) {pthread_cond_wait(&tp->cond, &tp->lock);}tp->PopTask(t);pthread_mutex_unlock(&tp->lock);t.ProcessOn();}return nullptr;}bool InitThreadPool() {for (int i = 0; i < num; ++i) {pthread_t tid;if (pthread_create(&tid, nullptr, ThreadRoutine, this) != 0) {LOG(FATAL, "create thread pool error!");return false;}pthread_detach(tid);LOG(INFO, "create thread pool success");}return true;}~ThreadPool() {pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}
};ThreadPool* ThreadPool::single_instance = nullptr;

在服务器中使用线程池

​ 在主服务器逻辑中,每当接收到一个新的连接,只需要将该连接封装成一个 Task 对象,并调用线程池的 PushTask 方法即可,无需再手动创建线程。

class HttpServer {
private:int _port;public:void Loop() {TcpServer* tsvr = TcpServer::GetInstance(_port);int listen_sock = tsvr->Sock();while (true) {struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);if (sock < 0) continue;std::string client_ip = inet_ntoa(peer.sin_addr);int client_port = ntohs(peer.sin_port);LOG(INFO, "new client: " + client_ip + ":" + std::to_string(client_port));Task task(sock);ThreadPool::GetInstance()->PushTask(task);}}
};

测试

​ 至此,我们的 HTTP 服务器核心功能已经全部实现。为了方便测试和展示,需要将所有可供访问的资源文件集中放在一个名为 wwwroot 的目录中。与此同时,将编译生成的服务器可执行文件与该目录放在同一级路径下即可。由于当前服务器尚未对接任何实际的业务逻辑,所以 wwwroot 目录下的页面来自网上找的模板。

首页展示与测试

错误请求测试

​ 可以看到网页展示还有些问题,这可能源于模板内部的素材链接失效登原因导致,将来我们自己设计页面的时候可以直接把资源放在服务器上避免这类情况的出现。

​ 以上便是整个项目的所有内容,从底层的TCP通信支持到对HTTP协议的响应处理,再到CGI程序的数据处理与动态响应生成,均有所阐述。限于篇幅,一些细节没有表达到位的地方,还请见谅。如果感兴趣,可以去我的仓库查看源码,谢谢!

相关文章:

【项目】自主实现HTTP服务器:从Socket到CGI全流程解析

00 引言 ​ 在构建高效、可扩展的网络应用时&#xff0c;理解HTTP服务器的底层原理是一项必不可少的技能。现代浏览器与移动应用大量依赖HTTP协议完成前后端通信&#xff0c;而这一过程的背后&#xff0c;是由网络套接字驱动的请求解析、响应构建、数据传输等一系列机制所支撑…...

AI最新资讯,GPT4.1加入网页端、Claude 3.7 Sonnet携“极限推理”发布在即

目录 一、GPT4.1加入网页端二、Claude 3.7 Sonnet携“极限推理”发布在即三、这项功能的关键特点1、双模式操作2、可视化思考过程3、可控的思考预算4、性能提升 四、Claude制作SVG图像1、Prompt提示词模板2、demo&#xff1a;技术路线图**Prompt提示词&#xff1a;**3、甘特图4…...

Android 中使用通知(Kotlin 版)

1. 前置条件 Android Studio&#xff1a;确保使用最新版本&#xff08;2023.3.1&#xff09;目标 API&#xff1a;最低 API 21&#xff0c;兼容 Android 8.0&#xff08;渠道&#xff09;和 13&#xff08;权限&#xff09;依赖库&#xff1a;使用 WorkManager 和 Notificatio…...

在 Kotlin 中,什么是解构,如何使用?

在 Kotlin 中&#xff0c;解构是一种语法糖&#xff0c;允许将一个对象分解为多个独立的变量。 这种特性可以让代码更简洁、易读&#xff0c;尤其适用于处理数据类、集合&#xff08;如 Pair、Map&#xff09;或其他结构化数据。 1 解构的核心概念 解构通过定义 componentN()…...

apisix透传客户端真实IP(real-ip插件)

文章目录 apisix透传客户端真实IP需求和背景apisix real-ip插件为什么需要 trusted_addresses&#xff1f;安全架构的最佳实践 示例场景apisix界面配置 apisix透传客户端真实IP 需求和背景 当 APISIX 前端有其他反向代理&#xff08;如 Nginx、HAProxy、云厂商的 LB&#xff…...

初学者如何用 Python 写第一个爬虫?

初学者如何用 Python 写第一个爬虫&#xff1f; 一、爬虫的基本概念 &#xff08;一&#xff09;爬虫的定义 爬虫&#xff0c;英文名为 Web Crawler&#xff0c;也被叫做网络蜘蛛、网络机器人。想象一下&#xff0c;有一个勤劳的小蜘蛛&#xff0c;在互联网这个巨大的蜘蛛网中…...

基于MNIST数据集的手写数字识别(CNN)

目录 一&#xff0c;模型训练 1.1 数据集介绍 1.2 CNN模型层结构 1.3 定义CNN模型 1.4 神经网络的前向传播过程 1.5 数据预处理 1.6 加载数据 1.7 初始化 1.8 模型训练过程 1.9 保存模型 二&#xff0c;模型测试 2.1 定义与训练时相同的CNN模型架构 2.2 图像的预处…...

QT6 源(103)篇三:阅读与注释 QPlainTextEdit,给出源代码

&#xff08;10&#xff09;关于文本处理的内容很多&#xff0c;来不及全面阅读、思考与整理。先给出类的继承图&#xff1a; &#xff08;11&#xff09;本源代码来自于头文件 qplaintextedit . h &#xff1a; #ifndef QPLAINTEXTEDIT_H #define QPLAINTEXTEDIT_H#include &…...

yocto5.2开发任务手册-7 升级配方

此文为机器辅助翻译&#xff0c;仅供个人学习使用&#xff0c;如有翻译不当之处欢迎指正 7 升级配方 随着时间的推移&#xff0c;上游开发者会为图层配方构建的软件发布新版本。建议使配方保持与上游版本发布同步更新。 虽然有多种升级配方的方法&#xff0c;但您可能需要先…...

LangPDF: Empowering Your PDFs with Intelligent Language Processing

LangPDF: Empowering Your PDFs with Intelligent Language Processing Unlock Global Communication: AI-Powered PDF Translation and Beyond In an interconnected world, seamless multilingual document management is not just an advantage—it’s a necessity. LangP…...

DDS(数据分发服务) 和 P2P(点对点网络) 的详细对比

1. 核心特性对比 维度 DDS P2P 实时性 微秒级延迟&#xff0c;支持硬实时&#xff08;如自动驾驶&#xff09; 毫秒至秒级&#xff0c;依赖网络环境&#xff08;如文件传输&#xff09; 架构 去中心化发布/订阅模型&#xff0c;节点自主发现 完全去中心化&#xff0c;节…...

TC8:SOMEIP_ETS_029-030

SOMEIP_ETS_029: echoUINT8Array16Bitlength 目的 检查当method echoUINT8Array16BitLength的参数中长度字段为16bit时,SOME/IP协议层是否能对参数进行序列化和反序列化。 对于可变长度的数组而言,必须用长度字段表示数组长度。否则接收方无法判断有效数据。 SOMEIP_ETS_02…...

Elasticsearch索引全生命周期管理指南之一

#作者&#xff1a;猎人 文章目录 一、索引常规操作二、索引mapping和别名管理 一、索引常规操作 索引数据特点&#xff1a; 索引中的数据随着时间&#xff0c;持续不断增长 按照时间序列划分索引的好处&挑战&#xff1a; 按照时间进行划分索引&#xff0c;会使得管理更加…...

本土DevOps革命:Gitee如何撬动中国企业的数字化转型新动能

在数字化浪潮席卷全球的背景下&#xff0c;中国企业正面临前所未有的转型压力与机遇。随着《数据安全法》和《个人信息保护法》的全面实施&#xff0c;以及信创产业政策的深入推进&#xff0c;研发工具链的自主可控已成为关乎企业核心竞争力的战略命题。在这一关键赛道上&#…...

ARM服务器解决方案

ARM服务器解决方案已成为异构计算领域的重要技术路径&#xff0c;其核心优势与多元化场景适配性正加速产业渗透。以下为关键要点分析&#xff1a; 一、核心优势与架构设计 能效比优化‌ ARM架构基于RISC指令集&#xff0c;单节点功耗可控制在15W以下&#xff0c;较x86架构能效…...

【暗光图像增强】【基于CNN的方法】2020-AAAI-EEMEFN

EEMEFN&#xff1a;Low-Light Image Enhancement via Edge-Enhanced Multi-Exposure Fusion Network EEMEFN&#xff1a;基于边缘增强多重曝光融合网络的低光照图像增强 AAAI 2020 论文链接 0.论文摘要 本研究专注于极低光照条件下的图像增强技术&#xff0c;旨在提升图像亮度…...

嵌入式EasyRTC音视频实时通话SDK在工业制造领域的智能巡检/AR协作等应用

一、背景 在数字化浪潮席卷全球的当下&#xff0c;远程监控与驾驶技术已深度渗透至工业巡检、智能交通等核心领域。然而&#xff0c;传统方案普遍面临实时性瓶颈、高延迟传输及交互体验匮乏等痛点&#xff0c;严重制约行业智能化转型。EasyRTC作为前沿的实时音视频通信技术&am…...

uniapp-商城-58-后台 新增商品(属性子级的添加和更新)

前面对父级属性的添加进行了分析&#xff0c;这里再来继续做属性子级的数据添加&#xff0c;包含页面逻辑以及后台处理的逻辑。当然这里还是在前面的云对象的方式进行的。 本文介绍了在云对象green-mall-sku中添加子级属性的实现过程。首先&#xff0c;通过updateChild接口处理…...

基于springboot+vue的机场乘客服务系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat12开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;Maven3.3.9 系统展示 用户管理 航班信…...

npm和nvm和nrm有什么区别

npm 全称&#xff1a;Node Package Manager。 作用&#xff1a; 包管理&#xff1a;用于安装、共享、分发代码&#xff0c;管理项目依赖关系。项目管理&#xff1a;创建和管理 package.json 文件&#xff0c;记录项目依赖和配置信息。脚本执行&#xff1a;运行项目中的脚本&…...

几种排序方式的C语言实现(冒泡、选择、插入、希尔等)

## 分类 存储器类型&#xff1a; - 内排序&#xff08;数据规模小 内存&#xff09; - 外排序&#xff08;数据库 磁盘&#xff09; 是否基于元素之间的比较 - 基数排序 - 其他排序&#xff1a;冒泡、选择、插入、快速、归并、希尔、堆…… 时间复杂度 - O&#…...

【MATLAB例程】线性卡尔曼滤波的程序,三维状态量和观测量,较为简单,可用于理解多维KF,附代码下载链接

本文所述代码实现了一个 三维状态的扩展卡尔曼滤波 (Extended Kalman Filter, EKF) 算法。通过生成过程噪声和观测噪声&#xff0c;对真实状态进行滤波估计&#xff0c;同时对比了滤波前后状态量的误差和误差累积分布曲线。 文章目录 简介运行结果MATLAB源代码 简介 代码分为以…...

芯片测试之X-ray测试

原理&#xff1a; X-ray是利用阴极射线管产生高能量电子与金属靶撞击&#xff0c;在撞击过程中&#xff0c;因电子突然减速&#xff0c;其损失的动能会以X-Ray形式放出。而对于样品无法以外观方式观测的位置&#xff0c;利用X-Ray穿透不同密度物质后其光强度的变化&#xff0c;…...

机器学习中的特征工程:解锁模型性能的关键

在机器学习领域&#xff0c;模型的性能往往取决于数据的质量和特征的有效性。尽管深度学习模型在某些任务中能够自动提取特征&#xff0c;但在大多数传统机器学习任务中&#xff0c;特征工程仍然是提升模型性能的关键环节。本文将深入探讨特征工程的重要性、常用方法以及在实际…...

【学习笔记】机器学习(Machine Learning) | 第七章|神经网络(1)

机器学习&#xff08;Machine Learning&#xff09; 简要声明 基于吴恩达教授(Andrew Ng)课程视频 BiliBili课程资源 文章目录 机器学习&#xff08;Machine Learning&#xff09;简要声明 机器学习之深度学习神经网络入门一、神经网络的起源与发展二、神经元模型&#xff08;…...

反向传播算法:神经网络的核心优化方法,一文打通任督二脉

搞神经网络训练,**反向传播(Backpropagation)**是最核心的算法。 没有它,模型就只能瞎猜参数,训练基本白搭。 这篇文章不整公式推导,不搞花架子,咱就把最关键的几个问题讲明白: 反向传播到底是干啥的? 它是怎么一步步更新参数的? 哪些坑你必须避免? 一、反向传播是…...

neo4j框架:java安装教程

安装使用neo4j需要事先安装好java&#xff0c;java版本的选择是一个犯难的问题。本文总结了在安装java和使用Java过程中遇到的问题以及相应的解决方法。 Java的安装包可以在java官方网站Java Downloads | Oracle 中国进行下载 以java 8为例&#xff0c;选择最后一行的x64 compr…...

基于React的高德地图api教程007:椭圆的绘制、编辑和删除

文章目录 7、椭圆绘制7.1 绘制椭圆7.1.1 设置圆心7.1.2 确定短半轴7.1.3 确定长半轴7.1.4 实时显示椭圆形状7.2 修改椭圆7.2.1 修改椭圆属性信息7.2.2 修改椭圆形状7.3 删除椭圆7.4 定位椭圆7.5 代码下载7.07、椭圆绘制 7.1 绘制椭圆 7.1.1 设置圆心 第一次点击地图设置圆心…...

Python多线程实战:提升并发效率的秘诀

一、前言&#xff1a;为什么需要多任务处理&#xff1f; 在实际开发中&#xff0c;我们经常需要让程序同时执行多个任务&#xff0c;例如&#xff1a; 同时下载多个文件&#xff1b;在后台运行耗时计算的同时保持界面响应&#xff1b;并发处理网络请求等。 Python 提供了多种…...

将嵌入映射到 Elasticsearch 字段类型:semantic_text、dense_vector、sparse_vector

作者&#xff1a; Andre Luiz 讨论如何以及何时使用 semantic_text、dense_vector 或 sparse_vector&#xff0c;以及它们与嵌入生成的关系。 通过这个自定进度的 Search AI 实践学习亲自体验向量搜索。你可以开始免费云试用&#xff0c;或者在本地机器上尝试 Elastic。 多年来…...

RabbitMQ 消息模式实战:从简单队列到复杂路由(四)

模式对比与选择 各模式特点对比 简单队列模式&#xff1a;结构最为简单&#xff0c;生产者直接将消息发送到队列&#xff0c;消费者从队列中获取消息&#xff0c;实现一对一的消息传递。其优势在于易于理解和实现&#xff0c;代码编写简单&#xff0c;适用于初学者和简单业务…...

OpenCV CUDA模块中矩阵操作------归一化与变换操作

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在 OpenCV 的 CUDA 模块中&#xff0c;normalize 和 rectStdDev 函数用于对矩阵进行归一化处理和基于积分图计算矩形区域的标准差。 函数介绍 …...

1Panel应用推荐:Beszel轻量级服务器监控平台

1Panel&#xff08;github.com/1Panel-dev/1Panel&#xff09;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;它致力于通过开源的方式&#xff0c;帮助用户简化建站与运维管理流程。为了方便广大用户快捷安装部署相关软件应用&#xff0c;1Panel特别开通应用商店&am…...

谷歌地图代理 | 使用 HTML 和矢量模式 API 更轻松地创建 Web 地图

在过去的一年里&#xff0c;谷歌对 Maps JavaScript API 进行了两项重要更新&#xff0c;以便更轻松地采用我们最新、最好的地图&#xff1a;HTML 地图和矢量模式 API。今天谷歌地图亚太区最大代理商之一的 Cloud Ace云一 为大家介绍一下更新的具体内容。 联系我们 - Cloud Ac…...

最新开源 TEN VAD 与 Turn Detection 让 Voice Agent 对话更拟人 | 社区来稿

关键词&#xff1a;对话式 AI | 语音智能体 | Voice Agent | VAD | 轮次检测 | 声网 | TEN GPT-4o 所展示对话式 AI 的新高度&#xff0c;正一步步把我们在电影《Her》中看到的 AI 语音体验变成现实。AI 的语音交互正在变得更丰富、更流畅、更易用&#xff0c;成为构建多模态智…...

「Mac畅玩AIGC与多模态40」开发篇35 - 用 Python 开发服务对接 SearxNG 与本地知识库

一、概述 本篇介绍如何使用 Python 构建一个集成本地聚合搜索引擎 SearxNG 与本地知识库的双通道服务接口&#xff0c;返回标准结构化 JSON 数据&#xff0c;并用于对接智能体插件系统。该接口适用于本地 Agent 应用开发与 Dify 插件集成场景。 二、目标说明 使用 Flask 实现…...

【Boost搜索引擎】构建Boost站内搜索引擎实践

目录 1. 搜索引擎的相关宏观原理 2. 正排索引 vs 倒排索引 - 搜索引擎具体原理 3. 编写数据去标签与数据清洗的模块 Parser 去标签 编写parser 用boost枚举文件名 解析html 提取title ​编辑 去标签 构建URL 将解析内容写入文件中 4. 编写建立索引的模块 Index 建…...

记参加一次数学建模

题目请到全国大学生数学建模竞赛下载查看。 注&#xff1a;过程更新了很多文件&#xff0c;所有这里贴上的有些内容不是最新的&#xff08;而是草稿&#xff09;。 注&#xff1a;我们队伍并没有获奖&#xff0c;文章内容仅供一乐。 从这次比赛&#xff0c;给出以下赛前建议 …...

【gRPC】HTTP/2协议,HTTP/1.x中线头阻塞问题由来,及HTTP/2中的解决方案,RPC、Protobuf、HTTP/2 的关系及核心知识点汇总

HTTP/2协议特点 gRPC基于HTTP/2协议&#xff0c;原因&#xff1a; 多路复用&#xff1a;允许在同一个TCP连接上并行传输多个请求和响应&#xff0c;即多个gRPC调用可以通过同一个连接同时进行&#xff0c;避免了HTTP/1.x中常见的线头阻塞问题&#xff0c;减少了连接建立和关闭…...

mac中加载C++动态库文件

前言 需要再mac系统下运行C开发的程序&#xff0c;通过摸索&#xff0c;初步实现了一版&#xff0c;大致记录下 1. 前提准备 安装OpenCV 使用Homebrew安装OpenCV&#xff1a; brew install opencv确认安装路径&#xff1a; brew --prefix opencv默认路径为/opt/homebrew/…...

Apollo Client 1.6.0 + @RefreshScope + @Value 刷新问题解析

问题描述 在使用 Apollo Client 1.6.0 结合 Spring Cloud 的 RefreshScope 和 Value 注解时&#xff0c;遇到以下问题&#xff1a; 项目启动时第一次属性注入成功后续配置变更时&#xff0c;Value 属性会刷新&#xff0c;但总是刷新为第一次的旧值&#xff0c;而不是最新的配…...

大语言模型 09 - 从0开始训练GPT 0.25B参数量 补充知识之数据集 Pretrain SFT RLHF

写在前面 GPT&#xff08;Generative Pre-trained Transformer&#xff09;是目前最广泛应用的大语言模型架构之一&#xff0c;其强大的自然语言理解与生成能力背后&#xff0c;是一个庞大而精细的训练流程。本文将从宏观到微观&#xff0c;系统讲解GPT的训练过程&#xff0c;…...

文本分类任务Qwen3-0.6B与Bert:实验见解

文本分类任务Qwen3-0.6B与Bert&#xff1a;实验见解 前言 最近在知乎上刷到一个很有意思的提问Qwen3-0.6B这种小模型有什么实际意义和用途。查看了所有回答&#xff0c;有人提到小尺寸模型在边缘设备场景中的优势&#xff08;低延迟&#xff09;、也有人提出小模型只是为了开…...

Mysql、Oracle、Sql Server、达梦之间sql的差异

1&#xff1a;分页查询 Sql Server&#xff1a; <bind name"startRow" value"(page - 1) * limit 1"/> <bind name"endRow" value"page * limit"/> SELECT *FROM (SELECT ROW_NUMBER() OVER (<if test"sortZd!…...

STM32外设AD-DMA+定时读取模板

STM32外设AD-DMA定时读取模板 一&#xff0c;方法引入二&#xff0c;CubeMX配置三&#xff0c;变量声明四&#xff0c;代码实现 (单通道) 一&#xff0c;方法引入 轮询法虽然简单&#xff0c;但 CPU 一直在忙着等待&#xff0c;效率太低。为了让 CPU 能在 ADC 转换的同时处理其…...

SQL里where条件的顺序影响索引使用吗?

大家好&#xff0c;我是锋哥。今天分享关于【SQL里where条件的顺序影响索引使用吗&#xff1f;】面试题。希望对大家有帮助&#xff1b; SQL里where条件的顺序影响索引使用吗&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 SQL 查询中&#xff0c;W…...

Java 接口中实现异步处理的方法

在Java中,接口本身不能直接实现逻辑(接口中的方法默认是抽象的,JDK 8+允许通过default方法提供非抽象实现,但通常不用于复杂的异步处理)。异步处理的逻辑需要在实现接口的类中通过多线程、异步框架或回调机制来实现。以下是几种常见的在接口实现类中实现异步处理<...

数值分析证明题

文章目录 第一题第二题第三题第四题第五题第六题第七题第八题第九题 第一题 例 给出 cos ⁡ x \cos x cosx&#xff0c; x ∈ [ 0 ∘ , 9 0 ∘ ] x\in[0^{\circ},90^{\circ}] x∈[0∘,90∘]的函数表&#xff0c;步长 h 1 ′ ( 1 60 ) ∘ h 1 (\frac{1}{60})^{\circ} h1′…...

深入理解二叉树:遍历、存储与算法实现

在之前的博客系列中&#xff0c;我们系统地探讨了多种线性表数据结构&#xff0c;包括顺序表、栈和队列等经典结构&#xff0c;并通过代码实现了它们的核心功能。从今天开始&#xff0c;我们将开启一个全新的数据结构篇章——树结构。与之前讨论的线性结构不同&#xff0c;树形…...

Python web 开发 Flask HTTP 服务

Flask 是一个轻量级的 Web 应用框架&#xff0c;它基于 Python 编写&#xff0c;特别适合构建简单的 Web 应用和 RESTful API。Flask 的设计理念是提供尽可能少的约定和配置&#xff0c;从而让开发者能够灵活地构建自己的 Web 应用。 https://andi.cn/page/622189.html...