尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

高并发服务器day20

高并发服务器day20 服务器实现长链接测试超时测试HTTP 异常请求处理与连接关闭优化在 HTTP 服务端中除了正常请求还必须考虑一种非常重要的异常情况客户端声明的正文长度与实际发送的数据长度不一致。例如下面的测试代码中客户端声明Content-Length: 100但实际正文只有bitejiuyeke也就是说服务端认为正文还没有接收完整因此 ]]会继续等待后续数据。代码注释中写的是“1024 字节”但实际测试报文使用的是Content-Length: 100。1. 异常请求测试#include ../source/server.hpp int main() { Socket cli_sock; cli_sock.CreateClient(8085, 127.0.0.1); std::string req GET /hello HTTP/1.1\r\n Connection: keep-alive\r\n Content-Length: 100\r\n \r\n bitejiuyeke; while(1) { assert(cli_sock.Send(req.c_str(), req.size()) ! -1); assert(cli_sock.Send(req.c_str(), req.size()) ! -1); assert(cli_sock.Send(req.c_str(), req.size()) ! -1); char buf[1024] {0}; assert(cli_sock.Recv(buf, 1023)); DBG_LOG([%s], buf); sleep(3); } cli_sock.Close(); return 0; }测试的核心就是告诉服务器正文有 100 字节但实际上没有发送够 100 字节。2. 只发送一次时会发生什么服务端解析到Content-Length: 100于是RecvHttpBody()会认为还需要继续接收正文。其核心逻辑是size_t real_len content_length - _request._body.size(); if (buf-ReadAbleSize() real_len) { // 正文完整 ... _recv_statu RECV_HTTP_OVER; } else { // 正文还不完整 _request._body.append( buf-ReadPosition(), buf-ReadAbleSize() ); buf-MoveReadOffset(buf-ReadAbleSize()); }因此声明正文 100 字节 ↓ 实际只收到少量正文 ↓ 正文不足 ↓ 保存已经收到的正文 ↓ 等待下一批网络数据此时请求状态并没有进入RECV_HTTP_OVER所以OnMessage()会直接返回等待下一次数据到来if (context-RecvStatu() ! RECV_HTTP_OVER) { return; }也就是说服务器不会进行业务处理也不会立即给 ]]客户端响应。如果客户端后续一直不补全数据最终可能因为非活跃连接超时而被关闭。3. 连续发送多个错误请求会发生什么问题就出现在这里。TCP 本身只是字节流并不知道这是第一个 HTTP 请求 这是第二个 HTTP 请求 这是第三个 HTTP 请求服务器只能依靠 HTTP 协议中的数据进行划分。假设第一个请求声明Content-Length: 100但正文没有 100 字节。此时第二个请求来了GET /hello HTTP/1.1 Connection: keep-alive ...服务器不会认为这是“第二个请求”。因为在当前解析状态中_recv_statu RECV_HTTP_BODY它仍然认为第一个请求的正文还没接收完。于是第二个 HTTP 请求的一部分就会被当成第一个请求的正文。形成请求1首行 请求1头部 请求1少量正文 请求2报文 ← 被当成请求1正文 请求3报文 ...这就是典型的HTTP 报文边界错乱。4. 为什么最后会解析出错随着后续数据不断到来服务器最终可能凑够第一个请求要求的Content-Length。此时_recv_statu RECV_HTTP_OVER;第一个请求被认为“接收完成”。但是缓冲区中剩余的数据已经不是一个完整的新 HTTP 请求了因为前面的一部分已经被错误地当成正文吃掉了。之后服务器再次按照请求行 请求头 请求正文进行解析时就可能拿到类似残缺 HTTP 数据然后请求行正则匹配失败_recv_statu RECV_HTTP_ERROR; _resp_statu 400;最终返回400 Bad Request所以这里的问题不是简单的“某一行错了”而是一旦 Content-Length 与实际正文不匹配后续 TCP 字节流中的 HTTP 请求边界就可能整体错位。5. 原来的错误处理存在什么问题OnMessage()本身使用循环不断处理缓冲区while (buffer-ReadAbleSize() 0) { ... }正常情况下这种设计是为了支持一次 recv ↓ 收到多个 HTTP 请求 ↓ 请求1 → 处理 请求2 → 处理 请求3 → 处理也就是处理 HTTP 长连接中的连续请求。但是解析错误之后就不能继续这么干了。因为此时缓冲区中的数据边界已经不可信已经无法确定哪里是正文 哪里是下一个请求 哪里是新的请求行如果错误之后还不断尝试解析剩余数据就可能出现解析失败 ↓ 返回 400 ↓ 继续解析残留数据 ↓ 再次失败 ↓ 再次返回 400 ↓ ……造成大量无意义的错误解析和错误响应。6. 为什么仅仅调用Shutdown()还不够这里还有一个非常关键的问题。底层Connection::Shutdown()并不是直接关闭 socketvoid Shutdown() { _loop-RunInLoop( std::bind(Connection::ShutdownInLoop, this) ); }真正进入的是void ShutdownInLoop() { _statu DISCONNECTING; if (_in_buffer.ReadAbleSize() 0) { if (_message_callback) _message_callback( shared_from_this(), _in_buffer ); } ... }也就是说执行Shutdown()时如果接收缓冲区还有数据它还会再次调用_message_callback()处理这些数据。这就产生了问题HTTP 已经解析错误 ↓ 准备关闭连接 ↓ _in_buffer 还有错误残留数据 ↓ ShutdownInLoop() ↓ 再次调用 message_callback ↓ OnMessage() 又解析这些错误数据 ↓ 再次 400因此关闭之前必须把已经确定无效的输入数据清掉。7. 正确的错误处理方式最终在OnMessage()中对解析错误统一执行if (context-RespStatu() 400) { ErrorHandler(req, rsp); WriteReponse(conn, req, rsp); context-ReSet(); buffer-MoveReadOffset( buffer-ReadAbleSize() ); conn-Shutdown(); return; }对应的处理流程为HTTP解析错误 ↓ 生成错误响应 ]]↓ 发送错误响应 ↓ 重置 HttpContext ↓ 清空接收缓冲区 ↓ Shutdown() ↓ return代码中 ]]正是采用了“错误响应 → 重置上下文 → 清 ]]空剩余输入 → Shutdown → return”的处理顺序。8. 为什么错误后要清空整个接收缓冲区核心原因只有一句话HTTP 解析一旦发生严重错误剩余字节已经无法可靠判断报文边界因此没有继续解析的意义。直接buffer-MoveReadOffset( buffer-Read ]]AbleSize() );把所有剩余数据标记为已读。相当于reader_idx → writer_idx此时ReadAbleSize() 0]]于是 ]]后面的ShutdownInLoop()就不会再次调用_message_callback()处理这些错误数据。9. 为什么发送完错误响应后不能直接Release()这里还涉及服务器发送模型。调用conn-Send(...);并不代表数据已经真正通过 socket 发出去了。SendInLoop()实际上只是_out_buffer.WriteBufferAndPush(buf); if (_channel.WriteAble() false) { _channel.EnableWrite(); }即响应数据 ↓ 写入发送缓冲区 ↓ 开启 EPOLLOUT ↓ 等待 socket 可写 ↓ HandleWrite() ↓ 真正发送所以如果错误响应刚放进发送缓冲区就立即Release()可能导致连接直接被关闭客户端根本收不到这个400响应。因此这里必须使用conn-Shutdown();而不是直接conn-Release();10.Shutdown()的真正作用Shutdown()是一种优雅关闭。首先进入_statu DISCONNECTING;然后判断发送缓冲区。如果还有数据if (_out_buffer.ReadAbleSize() 0) { _channel.EnableWrite(); }继续等待发送。等HandleWrite()把数据全部发送完if (_out_buffer.ReadAbleSize() 0) { _channel.DisableWrite(); if (_statu DISCONNECTING) { return Release(); } }最终才真正释放连接。因此整个过程是发现 HTTP 错误 ↓ 组织 400 响应 ↓ Send() ↓ 响应进入 _out_buffer ↓ 清空 _in_buffer ↓ Shutdown() ↓ 状态变为 DISCONNECTING ↓ 等待 _out_buffer 发送完 ↓ Release() ↓ 真正关闭连接11. 最终核心结论这部分最重要的是理解三个问题① TCP 没有 HTTP 报文边界当]]Content-Length写错时后面的 HTTP 请求可能直接被当成前一个请求的正文。② HTTP 解析严重出错后不应该继续尝试解析剩余缓冲区因为剩余数据的协议边界已经不可信应该直接清空。③Shutdown()≠ 立即关闭Shutdown()会让连接进入DISCONNECTING状态等待已经放入发送缓冲区的错误响应发送完成后再真正Release()。因此错误处理的关键逻辑可以总结为解析失败 → 返回一次错误响应 → 清理解析状态 → 丢弃剩余错误数据 → 优雅关闭连接这既避免了同一批错误数据被反复解析、连续产生 400 响应又保证了已经 ]]生成的错误响应能够真正发送给客户端。
返回列表