拓冰建站拓冰建站
首页 / 资讯中心 / 正文

QT C++手搓轻量级HTTP服务器:从TCP到RESTful API实战

1. 为什么我要用QT C手搓一个HTTP服务器先说结论这个项目适合那些已经写过一点C、用过QT做界面但一直没搞明白“网络请求到底怎么从浏览器跑到程序里”的人。我当初动手做这件事起因特别简单——公司内网有个小工具需要暴露一个接口给其他系统调用装Nginx太重写Python又不想额外维护运行环境手头正好有个QT项目就想着干脆用QT自带的网络模块撸一个轻量级HTTP服务器出来。你可能会有疑问QT不是做桌面界面的吗怎么还能干服务器的活实际上QT的网络模块QtNetwork封装得相当完整QTcpServer和QTcpSocket这两个类把TCP层面的连接管理、数据收发都处理好了我们只需要在TCP之上按照HTTP协议的格式解析请求、组装响应就行。整个过程不需要引入任何第三方库编译出来的可执行文件也就几MB扔到哪台机器上都能跑。这个实践指南要解决的问题很明确从TCP连接建立开始一步步实现一个能处理GET/POST请求、能返回JSON数据、支持简单路由的RESTful API服务器。我会把我在这个过程中踩过的坑、调过的参数、想明白的原理都摊开来讲。不管你是刚学完C基础想找个项目练手还是已经工作但没接触过网络编程跟着走一遍都能跑通。核心关键词我先摆出来QT、C、HTTP服务器、TCP、RESTful API。这五个词贯穿全文后面每个章节都会围绕它们展开。我用的环境是QT 5.15.2 MinGW编译器Windows和gLinux代码在两个平台上都验证过。QT 6的API有少量变化我会在涉及的地方标注出来。2. 整体设计思路与方案选型2.1 为什么选QT而不是裸写Socket裸写Socket当然可以C标准库加上系统调用socket()、bind()、listen()、accept()一路写下来能跑。但问题在于跨平台。Windows下你要用WSAStartup初始化Linux下不用Windows的closesocket和Linux的close不一样更别说IO多路复用Windows是select和IOCPLinux是epoll。这些差异处理起来非常琐碎。QT的QTcpServer把这些平台差异全部封装掉了。你调用listen(QHostAddress::Any, port)它在Windows下走Winsock在Linux下走BSD Socket对你来说接口完全一致。而且QT的信号槽机制天然适合事件驱动的服务器模型——新连接来了触发newConnection信号数据到了触发readyRead信号你只需要在槽函数里处理就行不需要自己写事件循环。另一个考虑是部署便利性。QT编译出来的程序在Windows上可以用windeployqt打包依赖在Linux上链接动态库或者静态编译都行。相比之下如果你用Boost.Asio写还得额外处理Boost的依赖用Python写目标机器上得有Python环境。QT C的方案在这方面的优势很明显。2.2 HTTP协议解析自己写还是用现成库QT本身没有提供HTTP服务器端的解析器。QHttpServer是QT 6.4之后才引入的模块QT 5.15没有。所以对于QT 5.15的用户来说HTTP报文的解析必须自己动手。自己写解析器听起来吓人但HTTP/1.1的请求格式其实很规整。一个典型的GET请求长这样GET /api/users?id1 HTTP/1.1\r\n Host: 127.0.0.1:8080\r\n User-Agent: curl/7.68.0\r\n Accept: */*\r\n \r\n请求行 请求头 空行 请求体GET通常没有体。解析的核心就是按\r\n切分第一行提取方法、路径、版本后续行提取头部字段遇到空行说明头部结束后面是body。POST请求的body长度由Content-Length头决定。我选择自己写解析器的原因有两个一是可控性强出问题能直接定位到哪一行代码二是学习价值高写完这一遍你对HTTP协议的理解会比看十篇文章都深。当然如果你的项目对安全性要求极高建议还是用成熟的库因为自己写的解析器在处理畸形报文时容易出漏洞。2.3 路由设计从if-else到RESTful最简单的路由就是一堆if-elseif (path /api/users method GET) { // 返回用户列表 } else if (path /api/users method POST) { // 创建用户 }这种写法在接口少的时候没问题但接口一多就乱套了。我采用的是注册式路由用一个QHashQString, Handler存储路径到处理函数的映射key是“方法路径”value是std::function。注册的时候router.addRoute(GET, /api/users, handleGetUsers); router.addRoute(POST, /api/users, handleCreateUser); router.addRoute(GET, /api/users/:id, handleGetUserById);对于带参数的路径比如/api/users/123我在匹配时做一次模式解析把:id提取出来作为路径参数传给处理函数。这样设计的好处是新增接口只需要注册一行不用改核心逻辑符合RESTful API的规范——用HTTP方法表示操作类型用路径表示资源。2.4 并发模型单线程还是多线程QTcpServer默认是单线程的所有连接的处理都在主线程的事件循环里。对于轻量级场景几十个并发连接、请求处理逻辑简单单线程完全够用而且避免了线程安全问题。但如果某个请求的处理逻辑很耗时比如查数据库、读大文件单线程就会阻塞其他请求。我的做法是核心网络IO保持单线程耗时操作丢到线程池。QT提供了QThreadPool和QtConcurrent用起来很方便。具体来说当解析完请求后如果处理函数标记为“异步”就把它提交到线程池处理完再通过信号槽回到主线程发送响应。这种混合模型兼顾了简单性和性能。对于大多数内部工具级别的HTTP服务器这个方案足够用了。3. 核心细节解析与实操要点3.1 TCP连接的建立与管理QTcpServer的使用非常直接QTcpServer *server new QTcpServer(this); connect(server, QTcpServer::newConnection, this, HttpServer::onNewConnection); if (!server-listen(QHostAddress::Any, 8080)) { qDebug() Listen failed: server-errorString(); }listen的第二个参数是端口号。这里有个坑端口被占用时listen会返回false但错误信息可能不够明确。我在实际调试中遇到过“error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address”这类报错本质就是端口冲突。解决办法是换端口或者用SO_REUSEADDR选项。QTcpServer默认会设置SO_REUSEADDR但在某些系统上仍然可能冲突这时候可以先用netstat -ano | findstr 8080Windows或lsof -i:8080Linux查一下谁占用了。新连接到来时nextPendingConnection()返回一个QTcpSocket*。这个socket的生命周期需要管理好——我把它存到一个QSetQTcpSocket*里在disconnected信号触发时从集合中移除并调用deleteLater()。如果不管理连接多了之后内存会持续增长。void HttpServer::onNewConnection() { QTcpSocket *socket server-nextPendingConnection(); clients.insert(socket); connect(socket, QTcpSocket::readyRead, this, HttpServer::onReadyRead); connect(socket, QTcpSocket::disconnected, this, HttpServer::onDisconnected); }3.2 HTTP请求报文的完整解析readyRead信号触发时数据可能不是一次性到齐的。TCP是流式协议一个HTTP请求可能分多次到达。所以不能假设一次readAll()就能拿到完整报文。我的处理策略是每个socket关联一个缓冲区每次readyRead把数据追加到缓冲区然后尝试解析。解析分两步先找\r\n\r\n如果找到了说明头部完整再根据Content-Length判断body是否完整。如果都完整就处理请求并从缓冲区中移除已消费的数据否则等待下次readyRead。void HttpServer::onReadyRead() { QTcpSocket *socket qobject_castQTcpSocket*(sender()); QByteArray buffer buffers[socket]; buffer.append(socket-readAll()); int headerEnd buffer.indexOf(\r\n\r\n); if (headerEnd -1) return; // 头部不完整 QByteArray header buffer.left(headerEnd); HttpRequest req parseHeader(header); int contentLength req.header(Content-Length).toInt(); int totalLength headerEnd 4 contentLength; if (buffer.size() totalLength) return; // body不完整 req.body buffer.mid(headerEnd 4, contentLength); buffer.remove(0, totalLength); handleRequest(socket, req); }解析请求行时要注意方法可能是GET、POST、PUT、DELETE、OPTIONS等路径可能带查询字符串?keyvalueHTTP版本通常是HTTP/1.1。我用QString::split按空格切分然后对路径部分再按?切分分别得到纯路径和查询参数。查询参数的解析我用QUrlQueryQUrl url(req.path); QUrlQuery query(url.query()); QString id query.queryItemValue(id);头部字段的解析就是按行切分每行按第一个:切分key做trimvalue做trim。注意头部字段名是大小写不敏感的我统一转成小写存储。3.3 响应报文的组装与发送HTTP响应的格式HTTP/1.1 200 OK\r\n Content-Type: application/json\r\n Content-Length: 27\r\n Connection: close\r\n \r\n {message:hello world}状态码和状态描述要对应常见的200 OK、201 Created、400 Bad Request、404 Not Found、405 Method Not Allowed、500 Internal Server Error。Content-Length必须是body的字节数不是字符数。如果返回中文一个汉字在UTF-8下占3个字节用QString::toUtf8().size()来算。Connection头我通常设为close因为实现keep-alive需要处理更多边界情况比如超时、管道化请求。对于轻量级服务器每次请求后关闭连接是最简单的做法。如果性能要求高可以改成keep-alive但要在socket上设置超时定时器。发送响应时用socket-write()然后socket-flush()确保数据发出。最后调用socket-disconnectFromHost()等disconnected信号触发后清理资源。void HttpServer::sendResponse(QTcpSocket *socket, const HttpResponse resp) { QByteArray data; data.append(HTTP/1.1 resp.statusCode resp.statusText \r\n); for (auto it resp.headers.begin(); it ! resp.headers.end(); it) { data.append(it.key() : it.value() \r\n); } data.append(\r\n); data.append(resp.body); socket-write(data); socket-flush(); socket-disconnectFromHost(); }3.4 RESTful路由的注册与匹配路由注册我用一个结构体存储struct Route { QString method; QString pattern; Handler handler; }; QListRoute routes;匹配时遍历routes先比较method再比较路径。对于带参数的路径我把pattern按/切分把请求路径也按/切分逐段比较。如果pattern段以:开头就是参数段提取出来存入pathParams。bool matchRoute(const QString pattern, const QString path, QHashQString,QString params) { QStringList patternParts pattern.split(/, Qt::SkipEmptyParts); QStringList pathParts path.split(/, Qt::SkipEmptyParts); if (patternParts.size() ! pathParts.size()) return false; for (int i 0; i patternParts.size(); i) { if (patternParts[i].startsWith(:)) { params[patternParts[i].mid(1)] pathParts[i]; } else if (patternParts[i] ! pathParts[i]) { return false; } } return true; }处理函数签名统一为std::functionvoid(const HttpRequest, HttpResponse)这样每个handler只需要关注业务逻辑不用管socket的读写。3.5 JSON的序列化与反序列化QT 5.15内置了QJsonDocument、QJsonObject、QJsonArray处理JSON非常方便。返回JSON时QJsonObject obj; obj[code] 0; obj[message] success; QJsonDocument doc(obj); resp.body doc.toJson(QJsonDocument::Compact); resp.headers[Content-Type] application/json; charsetutf-8;解析请求body中的JSONQJsonParseError err; QJsonDocument doc QJsonDocument::fromJson(req.body, err); if (err.error ! QJsonParseError::NoError) { // 返回400 } QJsonObject obj doc.object();注意QJsonDocument::fromJson接受的是QByteArray如果body是UTF-8编码的中文直接传没问题。但如果客户端发来的编码不是UTF-8需要先转换。4. 完整实操过程与核心环节实现4.1 项目结构搭建我用的是qmake构建系统QT 5.15的默认选择项目文件httpserver.proQT core network QT - gui CONFIG c11 console CONFIG - app_bundle TARGET httpserver TEMPLATE app SOURCES main.cpp \ httpserver.cpp \ httprequest.cpp \ httpresponse.cpp \ router.cpp HEADERS httpserver.h \ httprequest.h \ httpresponse.h \ router.h如果你用CMake对应的CMakeLists.txtcmake_minimum_required(VERSION 3.5) project(httpserver LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_AUTOMOC ON) find_package(Qt5 COMPONENTS Core Network REQUIRED) add_executable(httpserver main.cpp httpserver.cpp httprequest.cpp httpresponse.cpp router.cpp ) target_link_libraries(httpserver Qt5::Core Qt5::Network)这里有个细节QT - gui表示这是一个控制台程序不需要GUI模块。如果你在Windows下用MinGW编译不加这一行会链接GUI库程序启动时会弹出一个空窗口。另外CONFIG console确保有控制台输出方便调试。4.2 核心类设计我设计了四个核心类HttpServer继承QObject管理QTcpServer和所有客户端连接负责接收连接、读取数据、分发请求。HttpRequest纯数据类存储方法、路径、查询参数、头部、body。HttpResponse纯数据类存储状态码、头部、body。Router管理路由注册和匹配。HttpServer的头文件class HttpServer : public QObject { Q_OBJECT public: explicit HttpServer(QObject *parent nullptr); bool start(quint16 port); Router router() { return m_router; } private slots: void onNewConnection(); void onReadyRead(); void onDisconnected(); private: void handleRequest(QTcpSocket *socket, const HttpRequest req); void sendResponse(QTcpSocket *socket, const HttpResponse resp); QTcpServer *m_server; Router m_router; QHashQTcpSocket*, QByteArray m_buffers; QSetQTcpSocket* m_clients; };4.3 请求解析的完整实现HttpRequest的解析函数bool HttpRequest::parse(const QByteArray rawData) { int headerEnd rawData.indexOf(\r\n\r\n); if (headerEnd -1) return false; QByteArray headerPart rawData.left(headerEnd); QListQByteArray lines headerPart.split(\n); if (lines.isEmpty()) return false; // 解析请求行 QByteArray requestLine lines[0].trimmed(); QListQByteArray parts requestLine.split( ); if (parts.size() ! 3) return false; method QString::fromUtf8(parts[0]); QString fullPath QString::fromUtf8(parts[1]); version QString::fromUtf8(parts[2]); // 分离路径和查询字符串 int qPos fullPath.indexOf(?); if (qPos ! -1) { path fullPath.left(qPos); queryString fullPath.mid(qPos 1); } else { path fullPath; } // 解析头部 for (int i 1; i lines.size(); i) { QByteArray line lines[i].trimmed(); if (line.isEmpty()) continue; int colonPos line.indexOf(:); if (colonPos -1) continue; QString key QString::fromUtf8(line.left(colonPos)).trimmed().toLower(); QString value QString::fromUtf8(line.mid(colonPos 1)).trimmed(); headers[key] value; } // 解析body int contentLength headers.value(content-length, 0).toInt(); if (contentLength 0) { body rawData.mid(headerEnd 4, contentLength); } return true; }这里有几个容易出错的地方。第一split(\n)之后每行末尾可能还有\r所以要用trimmed()去掉。第二头部key要转小写因为HTTP规范规定头部字段名大小写不敏感。第三Content-Length可能不存在GET请求通常没有默认值设为0。4.4 路由注册与请求分发Router类的实现class Router { public: using Handler std::functionvoid(const HttpRequest, HttpResponse); void addRoute(const QString method, const QString pattern, Handler handler) { m_routes.append({method.toUpper(), pattern, handler}); } bool dispatch(const HttpRequest req, HttpResponse resp) { QHashQString, QString params; for (const auto route : m_routes) { if (route.method ! req.method) continue; if (matchRoute(route.pattern, req.path, params)) { req.pathParams params; route.handler(req, resp); return true; } } return false; } private: struct Route { QString method; QString pattern; Handler handler; }; QListRoute m_routes; };在main.cpp中注册路由int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); HttpServer server; server.router().addRoute(GET, /api/health, [](const HttpRequest req, HttpResponse resp) { resp.statusCode 200; resp.headers[Content-Type] application/json; QJsonObject obj; obj[status] ok; obj[timestamp] QDateTime::currentDateTime().toString(Qt::ISODate); resp.body QJsonDocument(obj).toJson(QJsonDocument::Compact); }); server.router().addRoute(GET, /api/users, [](const HttpRequest req, HttpResponse resp) { QJsonArray users; QJsonObject u1; u1[id] 1; u1[name] Alice; QJsonObject u2; u2[id] 2; u2[name] Bob; users.append(u1); users.append(u2); resp.statusCode 200; resp.headers[Content-Type] application/json; resp.body QJsonDocument(users).toJson(QJsonDocument::Compact); }); server.router().addRoute(GET, /api/users/:id, [](const HttpRequest req, HttpResponse resp) { QString id req.pathParams[id]; QJsonObject obj; obj[id] id; obj[name] User id; resp.statusCode 200; resp.headers[Content-Type] application/json; resp.body QJsonDocument(obj).toJson(QJsonDocument::Compact); }); server.router().addRoute(POST, /api/users, [](const HttpRequest req, HttpResponse resp) { QJsonParseError err; QJsonDocument doc QJsonDocument::fromJson(req.body, err); if (err.error ! QJsonParseError::NoError) { resp.statusCode 400; resp.headers[Content-Type] application/json; QJsonObject obj; obj[error] Invalid JSON; resp.body QJsonDocument(obj).toJson(QJsonDocument::Compact); return; } QJsonObject input doc.object(); QJsonObject output; output[id] 100; output[name] input[name]; resp.statusCode 201; resp.headers[Content-Type] application/json; resp.body QJsonDocument(output).toJson(QJsonDocument::Compact); }); if (!server.start(8080)) { qDebug() Server start failed; return 1; } qDebug() Server listening on port 8080; return app.exec(); }4.5 编译与运行验证在Linux下qmake httpserver.pro make -j4 ./httpserver在Windows下用MinGWqmake httpserver.pro mingw32-make -j4 httpserver.exe启动后用curl测试curl http://127.0.0.1:8080/api/health # {status:ok,timestamp:2024-01-15T10:30:00} curl http://127.0.0.1:8080/api/users # [{id:1,name:Alice},{id:2,name:Bob}] curl http://127.0.0.1:8080/api/users/42 # {id:42,name:User42} curl -X POST http://127.0.0.1:8080/api/users -H Content-Type: application/json -d {name:Charlie} # {id:100,name:Charlie}如果curl返回curl: (35) tcp connection reset by peer说明服务器端在发送响应前就关闭了连接。检查socket-disconnectFromHost()是否在write之前调用了。正确的顺序是write→flush→disconnectFromHost。4.6 性能压测与参数调优我用abApache Bench做了简单压测ab -n 1000 -c 10 http://127.0.0.1:8080/api/health单线程模式下QPS大约在2000-3000左右取决于机器性能。这个数字对于内部工具完全够用。如果要做优化有几个方向启用keep-alive减少TCP握手开销QPS能提升30%左右。多线程处理把请求处理丢到QThreadPool但要注意线程安全。减少内存拷贝QByteArray的mid和left会拷贝数据可以用QByteArrayViewQT 6或直接操作指针。我实测下来对于并发不超过50的场景单线程keep-alive是最佳平衡点。再往上就需要考虑用QThread做多线程accept或者换用更底层的网络库。5. 常见问题与排查技巧实录5.1 端口被占用导致启动失败这是最常见的问题。报错信息通常是error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address。排查步骤Windows下netstat -ano | findstr 8080找到PID后在任务管理器里结束进程。Linux下lsof -i:8080或ss -tlnp | grep 8080。如果找不到占用进程可能是之前的程序没有正常退出socket处于TIME_WAIT状态。等30秒或设置SO_REUSEADDR。QTcpServer默认会设置SO_REUSEADDR但在Windows上这个选项的行为和Linux不同。如果确实需要立即重用端口可以在listen之前手动设置server-setSocketOption(QAbstractSocket::ReuseAddressHint, 1);5.2 请求体读取不完整前面提到过TCP是流式协议一个请求可能分多次到达。我遇到过一个典型场景客户端发送一个较大的JSON body比如100KB服务器只收到了前几KB就触发了readyRead解析时发现body不完整直接返回了400。解决办法就是前面说的缓冲区机制。关键点是不要假设一次readyRead就是一个完整请求。每次收到数据都追加到缓冲区然后尝试解析解析成功才处理否则继续等。还有一个细节如果客户端发送了Expect: 100-continue头服务器需要先返回HTTP/1.1 100 Continue客户端才会发送body。很多HTTP客户端库比如Python的requests在body较大时会自动加这个头。如果不处理客户端会一直等直到超时。if (req.headers.value(expect).toLower() 100-continue) { socket-write(HTTP/1.1 100 Continue\r\n\r\n); socket-flush(); }5.3 中文乱码问题返回中文时Content-Type必须指定charsetutf-8并且body要用toUtf8()转换。如果客户端是浏览器不指定charset的话浏览器可能按ISO-8859-1解码导致乱码。resp.headers[Content-Type] application/json; charsetutf-8; resp.body doc.toJson(QJsonDocument::Compact); // QJsonDocument默认输出UTF-8如果手动拼接字符串记得QString text 你好; resp.body text.toUtf8();5.4 内存泄漏与连接管理每个QTcpSocket对象如果不手动释放会一直占用内存。我的做法是在disconnected信号中清理void HttpServer::onDisconnected() { QTcpSocket *socket qobject_castQTcpSocket*(sender()); if (!socket) return; m_clients.remove(socket); m_buffers.remove(socket); socket-deleteLater(); }用deleteLater()而不是delete是因为disconnected信号可能在socket的事件处理过程中触发直接delete会导致悬空指针。deleteLater()会在当前事件循环结束后安全释放。另外如果客户端连接后一直不发数据比如恶意连接socket会一直占用。我加了一个超时定时器QTimer::singleShot(30000, socket, [socket]() { if (socket-state() QAbstractSocket::ConnectedState) { socket-disconnectFromHost(); } });30秒内没有完成请求就强制断开。5.5 QT版本兼容性问题QT 5.15和QT 6在Network模块上有一些差异。比如QAbstractSocket::error()在QT 5.15中返回QAbstractSocket::SocketError在QT 6中改名为errorOccurred信号。如果你从QT 5迁移到QT 6需要改这些地方。另外报错fatal: cannot mix incompatible qt library (version ex50601) with this library通常是因为编译时用的QT版本和运行时链接的库版本不一致。解决办法是确保qmake和运行时库来自同一个QT安装。在Windows下可以用windeployqt自动拷贝正确的DLL。还有一个常见报错qt.qpa.plugin: could not find the qt platform plugin linuxfb。这是因为程序找不到平台插件。如果是控制台程序加上QT - gui就不会加载平台插件了。如果确实需要GUI确保plugins/platforms目录在可执行文件旁边或者设置QT_QPA_PLATFORM_PLUGIN_PATH环境变量。5.6 常见问题速查表问题现象可能原因解决方法listen失败端口被占用其他进程占用端口netstat/lsof查进程换端口或结束进程请求体不完整TCP分包使用缓冲区累积数据按Content-Length判断中文乱码未指定charsetContent-Type加charsetutf-8body用toUtf8内存持续增长socket未释放disconnected信号中deleteLatercurl报connection reset响应未发完就断开write→flush→disconnectFromHostQT版本冲突编译和运行库不一致用同一QT安装windeployqt打包平台插件找不到缺少plugins目录控制台程序去掉gui模块或设置插件路径100-continue超时未处理Expect头收到后先回100 Continue6. 进阶扩展与个人实操心得6.1 支持文件上传与静态文件服务在现有框架上扩展文件上传并不复杂。核心是解析multipart/form-data格式的body。这个格式用boundary分隔各个字段解析起来比JSON麻烦一些但逻辑是清晰的先从头部的Content-Type中提取boundary然后按boundary切分body每段再解析头部和内容。静态文件服务更简单收到GET请求后把路径映射到本地文件路径检查文件是否存在读取内容根据扩展名设置Content-Type.html→text/html.css→text/css.js→application/javascript.png→image/png。注意要做路径安全检查防止../目录穿越攻击。QString safePath QDir::cleanPath(rootDir req.path); if (!safePath.startsWith(rootDir)) { resp.statusCode 403; return; }6.2 用QThreadPool处理耗时请求如果某个接口需要查数据库或者调用外部服务处理时间可能几百毫秒甚至几秒。在单线程模式下这会阻塞所有其他请求。我的做法是给handler加一个标记表示是否异步server.router().addAsyncRoute(GET, /api/slow, [](const HttpRequest req, HttpResponse resp) { QThread::sleep(2); // 模拟耗时操作 resp.statusCode 200; resp.body {\result\:\done\}; });异步路由的处理逻辑是在主线程中把请求拷贝一份提交到QThreadPool处理完后通过QMetaObject::invokeMethod回到主线程发送响应。这里要注意socket不能跨线程使用所以响应发送必须在主线程做。6.3 日志记录与调试技巧调试HTTP服务器时把原始请求报文打印出来非常有用。我在onReadyRead里加了一行qDebug() Raw request: buffer;但生产环境不能这么干会泄露敏感信息。我的做法是用一个编译开关控制#ifdef HTTP_DEBUG qDebug() Raw request: buffer; #endif在.pro文件中加DEFINES HTTP_DEBUG来开启调试模式。另外用Wireshark抓包可以看到TCP三次握手和HTTP报文的具体内容。如果遇到诡异的连接问题抓包是最直接的排查手段。我遇到过一次客户端发送的请求头里Content-Length和实际body长度不一致导致服务器一直等body最后超时。抓包一看就发现了。6.4 我踩过的几个坑第一个坑是信号槽的连接方式。QTcpSocket::readyRead信号在数据到达时触发但如果我在槽函数里调用了readAll()而数据还没读完下一次readyRead可能不会立即触发。解决办法是循环读取直到bytesAvailable()为0。不过对于HTTP请求通常一次readAll就够了因为请求体不会太大。第二个坑是QByteArray的隐式共享。QByteArray在拷贝时是浅拷贝修改时才会深拷贝。这在多线程环境下可能导致问题。如果要把buffer传给其他线程用QByteArray::fromRawData或者显式调用detach()。第三个坑是HTTP状态码和状态描述的对应。我一开始只写了状态码状态描述随便填结果某些客户端比如Postman显示异常。后来老老实实按RFC规范填200对应OK201对应Created400对应Bad Request404对应Not Found500对应Internal Server Error。第四个坑是IPv6监听。QHostAddress::Any在有些系统上只监听IPv4要同时监听IPv6需要用QHostAddress::AnyIPv6或者分别监听。对于内部工具监听IPv4就够了但如果客户端用localhost访问有些系统会解析到::1IPv6导致连接失败。解决办法是同时监听两个地址或者客户端明确用127.0.0.1。6.5 这个项目还能怎么扩展如果你已经跑通了基础版本可以考虑这几个方向继续深入WebSocket支持在HTTP升级请求Upgrade: websocket时切换到WebSocket协议实现双向通信。QT提供了QWebSocketServer但自己实现升级握手过程能学到更多。HTTPS支持用QSslSocket替换QTcpSocket加载证书和私钥实现TLS加密。QT的SSL支持依赖OpenSSL库需要额外配置。RESTful API的完整实现加上PUT更新、DELETE删除、PATCH部分更新以及分页、过滤、排序等查询参数。集成数据库用QSqlDatabase连接SQLite或MySQL把路由处理函数中的数据操作落到数据库。压力测试与性能优化用wrk或ab做压测找出瓶颈尝试多线程、连接池、零拷贝等优化手段。我个人在实际操作中的体会是写这个HTTP服务器的最大收获不是学会了某个API而是把TCP连接、HTTP协议、事件驱动、路由设计这些概念串成了一条线。以前看HTTP协议总觉得是纸上谈兵自己动手解析一遍报文之后那些头部字段、状态码、Content-Length都变得具体了。如果你也在学C和网络编程强烈建议自己动手写一遍哪怕只实现GET请求收获也比看十篇教程大。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门