事先声明:鄙人才疏学浅,可能会有错误的地方,如果有错误敬请指出,理性讨论,请多多包涵!
鄙人花了几天时间学习了下本仓库的代码,总体感觉比原作者的代码优雅且易读很多,代码使用了C++11特性并且风格统一,个人感觉学到了不少东西。
不过我对项目里HTTP请求解析的部分有疑惑,在《Linux高性能服务器编程》中,作者采用的是有限状态自动机边读取边解析的方式来应对TCP协议面向字节流的特点(也就是应用层读取一次获得的并不一定就是一个完整的HTTP请求),通过判断HTTP请求中的\r\n
来确定当前的状态,在没有获取完整的HTTP请求之前,是不会开始返回响应的。
我仔细学习了一下本仓库的代码,发现在HTTP请求解析的部分,貌似假定了应用层一次(这里的一次是指检测到了一次EPOLLIN
事件)就能读取到一个完整的HTTP请求,读取一次之后就立即开始解析紧接着返回响应,这样的问题是,倘若客户端发送的是一个有效的请求,但是因为某种原因,该请求并不在一次事件中被应用层一次接收到,这时候由于服务端假定了一次就能接收到完整的HTTP请求,所以服务端就会把收到的请求的部分内容当成完整的请求进行解析,从而返回一个错误的响应。
带着这个疑惑,我决定尝试调试一下看看实际结果和我的想法是否一致。
因此我尝试使用cgdb
(对gdb
的包装,TUI
界面更好)对服务端的代码进行调试,首先要确定HTTP请求解析的位置。
第一个是WebServer.cc
中的Start()
函数:
1. void WebServer::Start() {
2. int timeMS = -1; /* epoll wait timeout == -1 无事件将阻塞 */
3. if(!isClose_) { LOG_INFO("========== Server start =========="); }
4. while(!isClose_) {
5. if(timeoutMS_ > 0) {
6. timeMS = timer_->GetNextTick();
7. }
8. int eventCnt = epoller_->Wait(timeMS);
9. for(int i = 0; i < eventCnt; i++) {
10. /* 处理事件 */
11. int fd = epoller_->GetEventFd(i);
12. uint32_t events = epoller_->GetEvents(i);
13. if(fd == listenFd_) {
14. DealListen_();
15. }
16. else if(events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
17. assert(users_.count(fd) > 0);
18. CloseConn_(&users_[fd]);
19. }
20. else if(events & EPOLLIN) {
21. assert(users_.count(fd) > 0);
22. DealRead_(&users_[fd]); <--------------------------------------
23. }
24. else if(events & EPOLLOUT) {
25. assert(users_.count(fd) > 0);
26. DealWrite_(&users_[fd]);
27. } else {
28. LOG_ERROR("Unexpected event");
29. }
30. }
31. }
32. }
首先客户端请求连接客户端,发起一个三次握手,服务端接受请求并将其加入到epoll
的监听中,监听其EPOLLIN
读事件。
紧接着客户端发送HTTP请求,服务端监听到EPOLLIN
事件,也就是:
20. else if(events & EPOLLIN) {
21. assert(users_.count(fd) > 0);
22. DealRead_(&users_[fd]);
23. }
下一步就是调用DealRead_(&users_[fd]);
,该函数同样在WebServer.cc
中,这里主要完成的任务就是将WebServer
类中的OnRead()
包装成一个可调用对象,并传递this
和client
,将这个对象放入线程池的队列中,由线程竞争获得并消费。
1. void WebServer::DealRead_(HttpConn* client) {
2. assert(client);
3. ExtentTime_(client);
4. threadpool_->AddTask(std::bind(&WebServer::OnRead_, this, client)); <--------------------------------------
5. }
各线程获得任务的关键代码在threadpool.h
中:
1. explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared<Pool>()) {
2. assert(threadCount > 0);
3. for(size_t i = 0; i < threadCount; i++) {
4. std::thread([pool = pool_] {
5. std::unique_lock<std::mutex> locker(pool->mtx);
6. while(true) {
7. if(!pool->tasks.empty()) {
8. auto task = std::move(pool->tasks.front());
9. pool->tasks.pop();
10. locker.unlock();
11. task(); <--------------------------------------
12. locker.lock();
13. }
14. else if(pool->isClosed) break;
15. else pool->cond.wait(locker);
16. }
17. }).detach();
18. }
19. }
可以看到第11行获得了对象并直接执行task()
,这就是在执行刚才绑定的那个WebServer::OnRead()
。
1. void WebServer::OnRead_(HttpConn* client) {
2. assert(client);
3. int ret = -1;
4. int readErrno = 0;
5. ret = client->read(&readErrno);
6. if(ret <= 0 && readErrno != EAGAIN) {
7. CloseConn_(client);
8. return;
9. }
10. OnProcess(client); <--------------------------------------
11. }
在WebServer::OnRead_(HttpConn* client)
中,又调用了client->read(&readErrno)
,这里就是读取的关键,这一行读取了请求之后,如果没有发生错误(即readErrno == EAGAIN
)或者客户端关闭,那么就会进一步执行第10行的OnProcess(client)
。
再来看它的代码:
1. void WebServer::OnProcess(HttpConn* client) {
2. if(client->process()) { <--------------------------------------
3. epoller_->ModFd(client->GetFd(), connEvent_ | EPOLLOUT);
4. } else {
5. epoller_->ModFd(client->GetFd(), connEvent_ | EPOLLIN);
6. }
在if()
中调用了client->process()
,这里进行了请求的解析,这个函数在httpconn.cpp
中:
1. bool HttpConn::process() {
2. request_.Init();
3. if(readBuff_.ReadableBytes() <= 0) {
4. return false;
5. }
6. else if(request_.parse(readBuff_)) { <--------------------------------------
7. LOG_DEBUG("%s", request_.path().c_str());
8. response_.Init(srcDir, request_.path(), request_.IsKeepAlive(), 200);
9. } else {
10. response_.Init(srcDir, request_.path(), false, 400);
11. }
12.
13. response_.MakeResponse(writeBuff_);
14. /* 响应头 */
15. iov_[0].iov_base = const_cast<char*>(writeBuff_.Peek());
16. iov_[0].iov_len = writeBuff_.ReadableBytes();
17. iovCnt_ = 1;
18.
19. /* 文件 */
20. if(response_.FileLen() > 0 && response_.File()) {
21. iov_[1].iov_base = response_.File();
22. iov_[1].iov_len = response_.FileLen();
23. iovCnt_ = 2;
24. }
25. LOG_DEBUG("filesize:%d, %d to %d", response_.FileLen() , iovCnt_, ToWriteBytes());
26. return true;
27. }
重点在:
6. else if(request_.parse(readBuff_)) {
7. LOG_DEBUG("%s", request_.path().c_str());
8. response_.Init(srcDir, request_.path(), request_.IsKeepAlive(), 200);
9. } else {
10. response_.Init(srcDir, request_.path(), false, 400);
11. }
可以看到,如果请求解析成功,就会返回一个正确的响应,如果请求解析失败,那么就会返回400
响应。
至此整个请求的解析逻辑梳理完毕,总结一下就是:
WebServer::Start()
-> WebServer::DealRead_(&users_[fd])
--> ThreadPool::threadpool_->AddTask(std::bind(&WebServer::OnRead_, this, client))
---> WebServer::OnRead_(HttpConn* client)
----> WebServer::OnProcess(HttpConn* client)
-----> HttpConn::process()
------> request_.parse(readBuff_)
由此可以看出,服务端只会读取一次(这里的一次是指检测到了一次EPOLLIN
事件),无论这一次是否读取到了一个完整的HTTP请求,都会把它当成一个完整的HTTP请求进行解析。
接下来进行调试验证,开启一个终端,执行nc -v 192.168.1.101 1316
,用netcat
来模拟一个客户端程序:
dylan@dylan-VirtualBox:~/Workspaces/repo/WebServer$ nc -v 192.168.1.101 1316
Connection to 192.168.1.101 1316 port [tcp/*] succeeded!
假设我们有一个请求为:
GET /index.html HTTP/1.1\r\n\r\n
在netcat
输入这一行,然后按Ctrl+D(即EOF,如果直接按回车会在后面加一个\n
),发现服务器成功返回了页面:
dylan@dylan-VirtualBox:~/Workspaces/repo/WebServer$ nc -v 192.168.1.101 1316
Connection to 192.168.1.101 1316 port [tcp/*] succeeded!
GET /index.html HTTP/1.1\r\n\r\nHTTP/1.1 200 OK
Connection: close
Content-type: text/html
Content-length: 3148
<!--
* @Author : mark
* @Date : 2020-06-30
* @copyleft GPL 2.0
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MARK-首页</title>
<link rel="icon" href="images/favicon.ico">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/animate.css">
<link rel="stylesheet" href="css/magnific-popup.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
<!-- Main css -->
<link rel="stylesheet" href="css/style.css">
</head>
<body data-spy="scroll" data-target=".navbar-collapse" data-offset="50">
...省略...
</body>
</html>
接下来我们试一下,如果先只发送一部分会发生什么:
dylan@dylan-VirtualBox:~/Workspaces/repo/WebServer$ nc -v 192.168.1.101 1316
Connection to 192.168.1.101 1316 port [tcp/*] succeeded!
GET /index.htmlHTTP/1.1 404 Not Found
Connection: close
Content-type: text/html
Content-length: 3149
...省略...
返回了一个404,不过这里发送的请求是不包含请求首部的情况,我们假设一下,如果一个HTTP请求包含了很多首部字段,以至于分次才能收到一个完整的HTTP请求,这样的话服务端岂不是会出错?
再用cgdb
调试一下,使用cgdb ./server
载入文件信息,然后在webserver.cpp
中给DealRead()
打一个断点:
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./server...done.
(gdb) break webserver.cpp: 98
Breakpoint 1 at 0x2399c: file ../code/server/webserver.cpp, line 98.
输入run
开始执行程序,端口1316:
(gdb) run
Starting program: /home/dylan/Workspaces/repo/WebServer/bin/server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff5d52700 (LWP 12577)]
[New Thread 0x7ffff5551700 (LWP 12578)]
[New Thread 0x7ffff4d50700 (LWP 12579)]
[New Thread 0x7ffff454f700 (LWP 12580)]
[New Thread 0x7ffff3d4e700 (LWP 12581)]
[New Thread 0x7ffff354d700 (LWP 12582)]
[New Thread 0x7ffff2b3a700 (LWP 12583)]
程序执行成功,线程也初始化了,然后另一个终端启动netcat
,发送一个不完整的请求:
dylan@dylan-VirtualBox:~/Workspaces/repo/WebServer$ nc -v 192.168.1.101 1316
Connection to 192.168.1.101 1316 port [tcp/*] succeeded!
GET /index.html
然后跟踪断点
可以看到,执行流直接走到了返回400的语句,根本没有等待客户端发送另一部分HTTP请求。
不知道这算不算一个bug,分析就到这里。