├── .gitignore ├── Condition.h ├── Epoll.cpp ├── Epoll.h ├── HttpHandler.cpp ├── HttpHandler.h ├── Log.cpp ├── Log.h ├── MutexLock.h ├── README.md ├── ThreadPool.cpp ├── ThreadPool.h ├── Timer.cpp ├── Timer.h ├── Utils.cpp ├── Utils.h ├── docs ├── WebServer-1.md └── img │ ├── image-20210512133857068.png │ └── image-20211026112349.png ├── html ├── CGI │ ├── base64 │ └── base64script ├── favicon.ico ├── img.jpg └── index.html ├── main.cpp └── makefile /.gitignore: -------------------------------------------------------------------------------- 1 | # 生成的可执行文件 2 | WebServer 3 | # 生成的中间文件 4 | *.o 5 | # vscode 配置文件 6 | .vscode/ 7 | # gdb 调试相关 { 是的直接手搓gdb调试 :) } 8 | .gdb_history 9 | # 其他需要ignore的文件 10 | ignore_* 11 | .VSCodeCounter/ 12 | -------------------------------------------------------------------------------- /Condition.h: -------------------------------------------------------------------------------- 1 | #ifndef CONDITION_H 2 | #define CONDITION_H 3 | 4 | #include 5 | #include 6 | 7 | #include "MutexLock.h" 8 | 9 | /** 10 | * @brief 条件变量,主要用于多线程中的锁 11 | * 与 MutexLock 一致,无需记住繁杂的函数名称 12 | * 条件变量主要是与mutex进行搭配,常用于资源分配相关的场景, 13 | * 例如当某个线程获取到锁以后,发现没有资源,则此时可以释放资源并等待条件变量 14 | * @note 注意: 使用条件变量时,必须上锁,防止出现多个线程共同使用条件变量 15 | */ 16 | class Condition 17 | { 18 | private: 19 | MutexLock& lock_; // 目标 Mutex 互斥锁 20 | pthread_cond_t cond_; // 条件变量 21 | public: 22 | Condition(MutexLock& mutex) : lock_(mutex) { pthread_cond_init(&cond_, nullptr); } 23 | ~Condition() { pthread_cond_destroy(&cond_); } 24 | void notify() { pthread_cond_signal(&cond_); } 25 | void notifyAll() { pthread_cond_broadcast(&cond_); } 26 | void wait() { pthread_cond_wait(&cond_, lock_.getMutex()); } 27 | /** 28 | * @brief 等待当前的条件变量一段时间 29 | * @param sec 等待的时间(单位:秒) 30 | * @return 成功在时间内等待到则返回 true, 超时则返回 false 31 | */ 32 | bool waitForSeconds(size_t sec) 33 | { 34 | timespec abstime; 35 | // 获取当前系统真实时间 36 | clock_gettime(CLOCK_REALTIME, &abstime); 37 | abstime.tv_sec += (time_t)sec; 38 | return ETIMEDOUT != pthread_cond_timedwait(&cond_, lock_.getMutex(), &abstime); 39 | } 40 | }; 41 | 42 | #endif -------------------------------------------------------------------------------- /Epoll.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "Epoll.h" 6 | #include "Log.h" 7 | 8 | Epoll::Epoll(int flag) : epoll_fd_(-1) 9 | { 10 | create(flag); 11 | } 12 | 13 | Epoll::~Epoll() 14 | { 15 | destroy(); 16 | } 17 | 18 | bool Epoll::isEpollValid() 19 | { 20 | return epoll_fd_ >= 0; 21 | } 22 | 23 | bool Epoll::create(int flag) 24 | { 25 | // 这里添加 epoll_fd_ < 0 的判断条件,防止重复 create. 26 | if(!isEpollValid() 27 | && ((epoll_fd_ = epoll_create1(flag)) == -1)) 28 | { 29 | ERROR("Create Epoll fail! (%s)", strerror(errno)); 30 | return false; 31 | } 32 | return true; 33 | } 34 | 35 | bool Epoll::add(int fd, void* data, int event) 36 | { 37 | if(isEpollValid()) 38 | { 39 | epoll_event ep_event; 40 | ep_event.events = event; 41 | ep_event.data.ptr = data; 42 | 43 | return (epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ep_event) != -1); 44 | } 45 | return false; 46 | } 47 | 48 | bool Epoll::modify(int fd, void* data, int event) 49 | { 50 | if(isEpollValid()) 51 | { 52 | epoll_event ep_event; 53 | ep_event.events = event; 54 | ep_event.data.ptr = data; 55 | 56 | return (epoll_ctl(epoll_fd_, EPOLL_CTL_MOD, fd, &ep_event) != -1); 57 | } 58 | 59 | return false; 60 | } 61 | 62 | bool Epoll::del(int fd) 63 | { 64 | if(isEpollValid()) 65 | return (epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr) != -1); 66 | return false; 67 | } 68 | 69 | int Epoll::wait(int timeout) 70 | { 71 | if(isEpollValid()) 72 | return epoll_wait(epoll_fd_, events_, MAX_EVENTS, timeout); 73 | // -2 表示非 epoll 错误 74 | return -2; 75 | } 76 | 77 | void Epoll::destroy() 78 | { 79 | // 如果文件描述符正常,则进行销毁 80 | if(isEpollValid()) 81 | close(epoll_fd_); 82 | // 重置 epoll_fd_ 为无效描述符 83 | epoll_fd_ = -1; 84 | } 85 | 86 | epoll_event Epoll::getEvent(size_t index) 87 | { 88 | assert(index < MAX_EVENTS); 89 | // 返回一个 const 指针 90 | return events_[index]; 91 | } 92 | -------------------------------------------------------------------------------- /Epoll.h: -------------------------------------------------------------------------------- 1 | #ifndef EPOLL_H 2 | #define EPOLL_H 3 | #include 4 | 5 | #include "Utils.h" 6 | 7 | using namespace std; 8 | 9 | /** 10 | * @brief 供epoll使用的结构体 11 | */ 12 | struct EpollEvent 13 | { 14 | int fd; // 被唤醒的 fd 15 | void* ptr; // 顺便携带的数据 16 | }; 17 | 18 | class Epoll 19 | { 20 | public: 21 | /** 22 | * @brief 默认声明该 Epoll 类实例是自动分配 epoll实例 23 | */ 24 | Epoll(int flag = 0); 25 | ~Epoll(); 26 | 27 | /** 28 | * @brief 确定当前epoll文件描述符是否有效 29 | * @return 有效则返回 true, 无效则返回 false 30 | */ 31 | bool isEpollValid(); 32 | 33 | /** 34 | * @brief 创建一个 epoll 实例 35 | * @param flag 传递给 epoll_create1 的标志.可以设置 0 或 EPOLL_CLOEXEC. 36 | * @return 创建成功返回true,创建失败返回false 37 | * @note 该函数调用的内部函数在错误时会设置 errno 38 | * @note 必须在使用该类成员中的其他函数之前, 确保该函数成功调用 39 | */ 40 | bool create(int flag = 0); 41 | 42 | /** 43 | * @brief 向工作列表中添加条目 44 | * @param fd 目标文件描述符 45 | * @param data 添加到事件的数据指针 46 | * @param event 触发何种事件类型时进入就绪(epoll_event.events) 47 | * @return 成功则返回true, 失败则返回 false 48 | * @note 该函数调用的内部函数在错误时会设置 errno 49 | */ 50 | bool add(int fd, void* data, int event); 51 | 52 | /** 53 | * @brief 修改工作列表中的条目 54 | * @param fd 目标文件描述符 55 | * @param data 添加到事件的数据指针 56 | * @param event 触发何种事件类型时进入就绪(epoll_event.events) 57 | * @return 成功则返回true, 失败则返回 false 58 | * @note 该函数调用的内部函数在错误时会设置 errno 59 | */ 60 | bool modify(int fd, void* data, int event); 61 | 62 | /** 63 | * @brief 删除工作列表中的特定条目 64 | * @param fd 目标文件描述符 65 | * @return 成功则返回true, 失败则返回 false 66 | * @note 该函数调用的内部函数在错误时会设置 errno 67 | */ 68 | bool del(int fd); 69 | 70 | /** 71 | * @brief 等待事件到来 72 | * @param timeout 最大超时时间,单位毫秒. -1 则设置永久等待 73 | * @return 返回处于就绪状态的文件描述符个数,出错时返回 -1 74 | * @note 该函数调用的内部函数在错误时会设置 errno 75 | */ 76 | int wait(int timeout); 77 | 78 | /** 79 | * @brief 释放当前 epoll 实例 80 | */ 81 | void destroy(); 82 | 83 | /** 84 | * @brief 获取事件数组中的对应事件 85 | * @param index 事件数组中的索引 86 | * @note 调用者必须确保 index 不能越界. 87 | */ 88 | epoll_event getEvent(size_t index); 89 | 90 | private: 91 | static const size_t MAX_EVENTS = 1024; 92 | 93 | int epoll_fd_; 94 | epoll_event events_[MAX_EVENTS]; 95 | }; 96 | 97 | #endif -------------------------------------------------------------------------------- /HttpHandler.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "HttpHandler.h" 17 | #include "Log.h" 18 | #include "Utils.h" 19 | 20 | // 声明一下该静态成员变量 21 | // 如果先前没有设置 www 路径,则设置路径为当前的工作路径 22 | string HttpHandler::www_path = "."; 23 | 24 | HttpHandler::HttpHandler(Epoll* epoll, int client_fd, Timer* timer) 25 | // 初始化 client 的 fd 和 epoll event 26 | : client_fd_(client_fd), client_event_{client_fd_, this}, 27 | // 初始化 timer 的 fd 和 epoll event 28 | timer_(timer), epoll_(epoll), curr_parse_pos_(0) 29 | { 30 | // HTTP1.1下,默认是持续连接 31 | // 除非 client http headers 中带有 Connection: close 32 | isKeepAlive_ = true; 33 | // 初始化一些变量 34 | reset(); 35 | // 设置 timer epoll event 36 | if(timer) 37 | timer_event_ = {timer->getFd(), this}; 38 | } 39 | 40 | HttpHandler::~HttpHandler() 41 | { 42 | // 从 epoll 中删除该套接字相关的事件 43 | /// NOTE: 注意先删除 epoll 中的条目,再来关闭 fd 44 | bool ret1 = epoll_->del(client_fd_); 45 | bool ret2 = true; 46 | // 如果不是空定时器,则释放 47 | if(timer_) 48 | { 49 | ret2 = epoll_->del(timer_->getFd()); 50 | // 删除定时器 51 | delete timer_; 52 | } 53 | assert(ret1 && ret2); 54 | // 关闭客户套接字 55 | INFO("------------------------ " 56 | "Connection Closed (socket: %d)" 57 | "------------------------", 58 | client_fd_); 59 | close(client_fd_); 60 | } 61 | 62 | void HttpHandler::reset() 63 | { 64 | // 清除已经处理过的数据 65 | assert(request_.length() >= curr_parse_pos_); 66 | 67 | request_.clear(); 68 | curr_parse_pos_ = 0; 69 | // 重设状态 70 | state_ = STATE_PARSE_URI; 71 | // 重置重试次数 72 | againTimes_ = maxAgainTimes; 73 | // 重置 headers_ 74 | headers_.clear(); 75 | // 重置 body 76 | http_body_.clear(); 77 | // 重置超时时间 78 | if(timer_) 79 | timer_->setTime(timeoutPerRequest, 0); 80 | } 81 | 82 | HttpHandler::ERROR_TYPE HttpHandler::readRequest() 83 | { 84 | INFO("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 85 | "- Request Packet -" 86 | ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> "); 87 | 88 | char buffer[MAXBUF]; 89 | 90 | while(true) 91 | { 92 | // 非阻塞,使用 recv 读取 93 | ssize_t len = recv(client_fd_, buffer, MAXBUF, MSG_DONTWAIT); 94 | if(len < 0) { 95 | // 读取时没有出错 96 | if(errno == EAGAIN) 97 | return ERR_SUCCESS; 98 | else if(errno == EINTR) 99 | continue; 100 | return ERR_READ_REQUEST_FAIL; 101 | } 102 | else if(len == 0) 103 | { 104 | // 如果读取到的字节数为0,则说明 EOF, 远程连接已经被关闭 105 | return ERR_CONNECTION_CLOSED; 106 | } 107 | 108 | // 将读取到的数据组装起来 109 | string request(buffer, buffer + len); 110 | INFO("{%s}", escapeStr(request, MAXBUF).c_str()); 111 | 112 | request_ += request; 113 | } 114 | return ERR_SUCCESS; 115 | } 116 | 117 | HttpHandler::ERROR_TYPE HttpHandler::parseURI() 118 | { 119 | size_t pos1, pos2; 120 | 121 | pos1 = request_.find("\r\n"); 122 | if(pos1 == string::npos) return ERR_AGAIN; 123 | string&& first_line = request_.substr(0, pos1); 124 | // a. 查找get 125 | pos1 = first_line.find(' '); 126 | if(pos1 == string::npos) return ERR_BAD_REQUEST; 127 | string methodStr = first_line.substr(0, pos1); 128 | 129 | string output_method = "Method: "; 130 | if(methodStr == "GET") 131 | method_ = METHOD_GET; 132 | else if(methodStr == "POST") 133 | method_ = METHOD_POST; 134 | else if(methodStr == "HEAD") 135 | method_ = METHOD_HEAD; 136 | else 137 | return ERR_NOT_IMPLEMENTED; 138 | INFO("Method: %s", methodStr.c_str()); 139 | 140 | // b. 查找目标路径 141 | pos1++; 142 | pos2 = first_line.find(' ', pos1); 143 | if(pos2 == string::npos) return ERR_BAD_REQUEST; 144 | 145 | // 获取path时,注意加上 www path 146 | path_ = www_path + "/" + first_line.substr(pos1, pos2 - pos1); 147 | // 检测目录穿越 148 | if(!is_path_parent(www_path, path_)) 149 | return ERR_NOT_FOUND; 150 | 151 | INFO("Path: %s", path_.c_str()); 152 | 153 | // c. 查看HTTP版本 154 | pos2++; 155 | string http_version_str = first_line.substr(pos2, first_line.length() - pos2); 156 | INFO("HTTP Version: %s", http_version_str.c_str()); 157 | 158 | // 检测是否支持客户端 http 版本 159 | if(http_version_str == "HTTP/1.0") 160 | http_version_ = HTTP_1_0; 161 | else if (http_version_str == "HTTP/1.1") 162 | http_version_ = HTTP_1_1; 163 | else 164 | return ERR_HTTP_VERSION_NOT_SUPPORTED; 165 | 166 | // 更新curr_parse_pos_ 167 | curr_parse_pos_ += first_line.length() + 2; 168 | return ERR_SUCCESS; 169 | } 170 | 171 | HttpHandler::ERROR_TYPE HttpHandler::parseHttpHeader() 172 | { 173 | INFO("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 174 | "- Request Info -" 175 | ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); 176 | 177 | size_t pos1, pos2; 178 | for(pos1 = curr_parse_pos_; 179 | (pos2 = request_.find("\r\n", pos1)) != string::npos; 180 | pos1 = pos2 + 2) 181 | { 182 | string&& header = request_.substr(pos1, pos2 - pos1); 183 | // 如果遍历到了空头,则表示http header部分结束 184 | if(header.size() == 0) 185 | { 186 | curr_parse_pos_ = pos1 + 2; 187 | return ERR_SUCCESS; 188 | } 189 | pos1 = header.find(' '); 190 | 191 | if(pos1 == string::npos) return ERR_BAD_REQUEST; 192 | 193 | // key 的格式: `XXX:` 194 | string&& key = header.substr(0, pos1); 195 | 196 | // 消除key里的最后一个冒号字符 197 | if(key.size() < 2 || key.back() != ':') return ERR_BAD_REQUEST; 198 | key.pop_back(); 199 | 200 | // key 转小写 201 | transform(key.begin(), key.end(), key.begin(), ::tolower); 202 | // 获取 value 203 | string&& value = header.substr(pos1 + 1); 204 | 205 | INFO("HTTP Header: [%s : %s]", key.c_str(), value.c_str()); 206 | 207 | headers_[key] = value; 208 | } 209 | 210 | // 执行到这里说明: 没有遍历到空头,即还有数据没有读完 211 | return ERR_AGAIN; 212 | } 213 | 214 | HttpHandler::ERROR_TYPE HttpHandler::parseBody() 215 | { 216 | assert(method_ == METHOD_POST); 217 | 218 | auto content_len_iter = headers_.find("content-length"); 219 | if(content_len_iter == headers_.end()) 220 | return ERR_LENGTH_REQUIRED; 221 | 222 | string len_str = content_len_iter->second; 223 | if(!isNumericStr(len_str)) 224 | return ERR_BAD_REQUEST; 225 | 226 | int len = atoi(len_str.c_str()); 227 | 228 | if(request_.length() < curr_parse_pos_ + len) 229 | return ERR_AGAIN; 230 | http_body_ = request_.substr(curr_parse_pos_, len); 231 | 232 | // 输出剩余的 HTTP body 233 | INFO("HTTP Body: {%s}", escapeStr(http_body_, MAXBUF).c_str()); 234 | 235 | return ERR_SUCCESS; 236 | } 237 | 238 | HttpHandler::ERROR_TYPE HttpHandler::handleRequest() 239 | { 240 | // 设置只在 HTTP/1.1时 默认允许 持续连接 241 | if(http_version_ == HTTP_1_0) 242 | isKeepAlive_ = false; 243 | 244 | // 获取header完成后,处理一下 Connection 头 245 | auto conHeaderIter = headers_.find("connection"); 246 | if(conHeaderIter != headers_.end()) 247 | { 248 | string value = conHeaderIter->second; 249 | transform(value.begin(), value.end(), value.begin(), ::tolower); 250 | if(value == "keep-alive") 251 | isKeepAlive_ = true; 252 | } 253 | 254 | // 获取目标文件的信息 255 | struct stat st; 256 | if(stat(path_.c_str(), &st) == -1) 257 | { 258 | WARN("Can not get file [%s] state ! (%s)", path_.c_str(), strerror(errno)); 259 | if(errno == ENOENT) 260 | return ERR_NOT_FOUND; 261 | else 262 | return ERR_INTERNAL_SERVER_ERR; 263 | } 264 | // 如果试图打开一个文件夹,则添加 index.html 265 | if (S_ISDIR(st.st_mode)) { 266 | path_ += "/index.html"; 267 | if(stat(path_.c_str(), &st) == -1) 268 | { 269 | WARN("Can not get file [%s] state ! (%s)", path_.c_str(), strerror(errno)); 270 | if(errno == ENOENT) 271 | return ERR_NOT_FOUND; 272 | else 273 | return ERR_INTERNAL_SERVER_ERR; 274 | } 275 | } 276 | 277 | // 开始处理请求 278 | // 对于普通的 GET / HEAD 请求,读取文件并发送 279 | if(method_ == METHOD_GET || method_ == METHOD_HEAD) 280 | { 281 | // 试图打开一个文件 282 | int file_fd; 283 | if((file_fd = open(path_.c_str(), O_RDONLY, 0)) == -1) 284 | { 285 | WARN("File [%s] open failed ! (%s)", path_.c_str(), strerror(errno)); 286 | if(errno == ENOENT) 287 | // 如果打开失败,则返回404 288 | return ERR_NOT_FOUND; 289 | else 290 | // 如果是因为其他问题出错,则返回500 291 | return ERR_INTERNAL_SERVER_ERR; 292 | } 293 | // 读取文件, 使用 mmap 来高速读取文件 294 | void* addr = mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, file_fd, 0); 295 | // 记得关闭文件描述符 296 | close(file_fd); 297 | // 异常处理 298 | if(addr == MAP_FAILED) 299 | { 300 | WARN("Can not map file [%s] -> mem! (%s)", path_.c_str(), strerror(errno)); 301 | return ERR_INTERNAL_SERVER_ERR; 302 | } 303 | // 将数据从内存页存入至 responseBody 304 | char* file_data_ptr = static_cast(addr); 305 | string responseBody(file_data_ptr, file_data_ptr + st.st_size); 306 | // 记得删除内存 307 | int res = munmap(addr, st.st_size); 308 | if(res == -1) 309 | WARN("Can not unmap file [%s] -> mem! (%s)", path_.c_str(), strerror(errno)); 310 | // 获取 Content-type 311 | string suffix = path_; 312 | // 通过循环找到最后一个 dot 313 | size_t dot_pos; 314 | while((dot_pos = suffix.find('.')) != string::npos) 315 | suffix = suffix.substr(dot_pos + 1); 316 | 317 | // 发送数据, 在该函数内部, METHOD_HEAD 不发送 http body 318 | return sendResponse("200", "OK", MimeType::getMineType(suffix), responseBody); 319 | } 320 | // 而对于POST来说,将 http body 传入目标可执行文件并将结果返回给客户端 321 | /** 322 | * @brief 多进程调试 323 | * gdb: set follow-fork-mode parent 324 | * set detach-on-fork off 325 | * shell: 326 | * 查看某个进程的pid: ps ax | grep "WebServer" 327 | * 查看某个pid的文件描述符列表: lsof -p 328 | * 查看WebServer的所有子进程: pstree -p -g 329 | */ 330 | else if(method_ == METHOD_POST) 331 | { 332 | // 创建两个管道 333 | int cgi_output[2]; 334 | int cgi_input[2]; 335 | /** 336 | * NOTE: 创建管道时,一定要指定 O_CLOEXEC 337 | * 因为当当前线程 thread1 执行 fork 产生子进程 subproc1 后, 338 | * subproc1 会同步继承这些其他线程 thread2 用于其他进程 subproc2 通信的管道 339 | * 这样当 thread2 关闭了向 subproc2 写入数据的管道 pipe2w 后, 340 | * 由于 subproc1 保存了 pipe2w,因此实际上该管道不会被销毁 341 | * 所以 subproc2 将无法从 pipe2w 中读取数据,因为管道没有关闭,不存在EOF 342 | * 343 | * NOTE: 即便创建管道时指定了 O_CLOEXEC 344 | * 但实际上,在子进程中执行 dup2 操作时,新复制出的文件描述符将不会继承 O_CLOEXEC, 345 | * 这样我们就可以达到:关闭所有的进程间通信管道,只保留当前子进程的输入输出管道,这样的一个目的 346 | */ 347 | if (pipe2(cgi_output, O_CLOEXEC) == -1) { 348 | WARN("cgi_output create error. (%s)", strerror(errno)); 349 | return ERR_INTERNAL_SERVER_ERR; 350 | } 351 | if (pipe2(cgi_input, O_CLOEXEC) == -1) { 352 | WARN("cgi_input create error. (%s)", strerror(errno)); 353 | // 记得关闭之前的管道 354 | close(cgi_output[0]); 355 | close(cgi_output[1]); 356 | return ERR_INTERNAL_SERVER_ERR; 357 | } 358 | // 尝试执行该CGI程序 359 | pid_t pid; 360 | /** 361 | * @note 需要注意的是 fork 在多进程中要慎重使用 362 | * @ref 谨慎使用多线程中的fork https://www.cnblogs.com/liyuan989/p/4279210.html 363 | * @ref 程序员的自我修养(三):fork() 安全 https://liam.page/2017/01/17/fork-safe/ 364 | */ 365 | if((pid = fork()) < 0) 366 | { 367 | WARN("Fork error. (%s)", strerror(errno)); 368 | close(cgi_input[0]); 369 | close(cgi_input[1]); 370 | close(cgi_output[0]); 371 | close(cgi_output[1]); 372 | return ERR_INTERNAL_SERVER_ERR; 373 | } 374 | // 对于子进程来说 375 | if(pid == 0) 376 | { 377 | /** 378 | * 将当前进程的进程号设置为所在组的进程组的组号 379 | * 这有助于WebServer 杀死子进程 380 | * 381 | * kill -pid 时会杀死 PGID为 `-pid` 的所有子进程 382 | * 因此可以利用 setpgid 来达到区分进程的目的 383 | * 384 | * 正常来说,如果没有设置 setpgid,则WebServer所有的子进程,以及子进程的子进程 385 | * 其PGID都为WebServer的PID,这为杀死 pid为某个特定值的子进程以及该子进程的子进程巨大障碍 386 | * 因此在子进程处需要重新设置 pgid 387 | */ 388 | // 正常来说, setpgid 不可能会失败.如果失败了就直接abort 389 | // 因为设置失败将会导致该子进程无法受到父进程的超时限制 390 | if(setpgid(0, 0) == -1) 391 | FATAL("setpgid fail in child process! (%s)", strerror(errno)); 392 | // 设置当父进程死亡时,子进程同步死亡 393 | if(prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) 394 | FATAL("prctl fail in child process! (%s)", strerror(errno)); 395 | // 首先重新设置标准输入输出流 396 | // 注意 dup2 会自动关闭当前打开的 fd0、fd1 和 fd2 397 | if(dup2(cgi_input[0], 0) == -1 398 | || dup2(cgi_output[1], 1) == -1 399 | || dup2(1, 2) == -1) 400 | FATAL("dup2 fail! (%s)", strerror(errno)); 401 | close(cgi_input[0]); 402 | close(cgi_input[1]); 403 | close(cgi_output[0]); 404 | close(cgi_output[1]); 405 | 406 | // 准备参数 407 | char path[path_.size() + 1]; 408 | strcpy(path, path_.c_str()); 409 | char* const args[] = { path, NULL }; 410 | 411 | // 此时已经完成了所有的准备,现在准备执行目标程序 412 | 413 | // 执行 414 | execve(path, args, environ); 415 | // 如果执行到这里,则说明出现了问题 416 | FATAL("execve fail in child process! (%s)", strerror(errno)); 417 | } 418 | // 对于父进程WebServer来说 419 | else 420 | { 421 | close(cgi_input[0]); 422 | close(cgi_output[1]); 423 | 424 | // 将 HTTP body 写入 CGI 程序的标准输入中 425 | ssize_t len = writen(cgi_input[1], http_body_.c_str(), http_body_.length(), true); 426 | // 如果写入失败 427 | if(len <= 0) 428 | WARN("Write %ld bytes to CGI input fail! (%s)", http_body_.length(), strerror(errno)); 429 | 430 | close(cgi_input[1]); 431 | 432 | // 设置超时时间 maxCGIRuntime(ms) 433 | int timeouts = maxCGIRuntime; 434 | /** 435 | * @brief 进入一个死循环,只有当子进程退出后才会break 436 | * @note 该循环将会有2条执行流程 437 | * 1. 执行子进程 -> waitpid -> 子进程退出 -> 结束循环; 438 | * 2. 执行子进程 -> waitpid -> 子进程没有退出 439 | * -> 超时 -> kill -> waitpid -> 子进程退出 -> 结束循环; 440 | */ 441 | while(true) 442 | { 443 | // 单次休息 cgiStepTime(ms) 444 | if(!usleep(cgiStepTime * 1000)) 445 | timeouts -= cgiStepTime; 446 | int wstats = -1; 447 | int waitpid_ret = waitpid(pid, &wstats, WNOHANG); 448 | // 如果waitpid 出错 449 | if(waitpid_ret < 0) 450 | { 451 | WARN("waitpid error. (%s)", strerror(errno)); 452 | // ret 前,一定一定一定要关闭这个读取端口 453 | close(cgi_output[0]); 454 | return ERR_INTERNAL_SERVER_ERR; 455 | } 456 | // 如果子进程状态被修改, 当子进程状态改变后,waitpid 才会设置 status, 否则 status 不变 457 | else if(waitpid_ret > 0) 458 | { 459 | // 只有在子进程自然退出,或者子进程被 kill 时,才会处理,退出该循环 460 | // 至于其他情况,例如子进程遇到了 SIGINT,则忽视 461 | bool ifExited = WIFEXITED(wstats); 462 | // 注意 SIGKILL 会 terminate 子进程,因此使用 WTERMSIG 来获取TERSIG 463 | // 这里不指定是 SIGKILL信号,因为可能有其他信号会kill子进程 464 | bool ifKilled = WIFSIGNALED(wstats) && (WTERMSIG(wstats) != 0); 465 | if(ifExited || ifKilled) 466 | break; 467 | } 468 | // 如果什么也没有发生, 即子进程仍然在跑.如果顺便超时了,则kill 469 | else if(timeouts <= 0) 470 | { 471 | /** 472 | * @brief 把 kill 放到循环内部是为了 waitpid 回收子进程 473 | * NOTE: -pid 指的是杀死当前子进程以及该子进程自身的子进程,例如shell脚本 474 | * NOTE: 再kill一次 pid 是为了防止子进程太久没有轮到执行,仍然处于fork与execl之间的状态 475 | * 此时,之前的 kill -pid 将会不起作用.因此为了确保子进程一定被kill,需要再kill一次pid 476 | */ 477 | int res_kill_sub = kill(pid, SIGKILL); 478 | int res_kill_pgid = 0; 479 | // 只有在子进程的pgid变化后才kill -pid,防止误伤其他线程中的子进程 480 | if(getpgid(pid) == pid) 481 | res_kill_pgid = kill(-pid, SIGKILL); 482 | assert(!res_kill_sub && !res_kill_pgid); 483 | WARN("Sub process timeout."); 484 | } 485 | } 486 | 487 | // 走到这里则说明程序已经执行结束了 488 | string responseBody; 489 | char buf[MAXBUF]; 490 | // 非阻塞读取 491 | if(!setFdNoBlock(cgi_output[0])) 492 | { 493 | WARN("set fd(%d) no block fail! (%s)", cgi_output[0], strerror(errno)); 494 | close(cgi_output[0]); 495 | return ERR_INTERNAL_SERVER_ERR; 496 | } 497 | while((len = readn(cgi_output[0], buf, MAXBUF)) > 0) 498 | responseBody += string(buf, buf + len); 499 | close(cgi_output[0]); 500 | 501 | if(responseBody.empty()) 502 | return ERR_INTERNAL_SERVER_ERR; 503 | // 发送数据 504 | return sendResponse("200", "OK", MimeType::getMineType("txt"), responseBody); 505 | } 506 | } 507 | else 508 | return ERR_INTERNAL_SERVER_ERR; 509 | UNREACHABLE(); 510 | return ERR_SUCCESS; 511 | } 512 | 513 | bool HttpHandler::handleErrorType(HttpHandler::ERROR_TYPE err) 514 | { 515 | // 除了 ERR_SUCESS 和 ERR_AGAIN 没有设置 state 以外, 其他 case 都设置了 state_ 516 | bool isSuccess = false; 517 | switch(err) 518 | { 519 | case ERR_SUCCESS: 520 | isSuccess = true; 521 | /* 注意这里没有设置 STATE */ 522 | break; 523 | case ERR_READ_REQUEST_FAIL: 524 | ERROR("HTTP Read request failed ! (%s)", strerror(errno)); 525 | state_ = STATE_FATAL_ERROR; 526 | break; 527 | case ERR_AGAIN: 528 | --againTimes_; 529 | INFO("HTTP waiting for more messages..."); 530 | /* 注意这里没有设置 STATE , 与 ERR_SUCESS一样 */ 531 | if(againTimes_ <= 0) 532 | { 533 | state_ = STATE_FATAL_ERROR; 534 | WARN("Reach max read times"); 535 | } 536 | break; 537 | case ERR_CONNECTION_CLOSED: 538 | INFO("HTTP Socket(%d) was closed.", client_fd_); 539 | state_ = STATE_FATAL_ERROR; 540 | break; 541 | case ERR_SEND_RESPONSE_FAIL: 542 | ERROR("Send Response failed !"); 543 | state_ = STATE_FATAL_ERROR; 544 | break; 545 | case ERR_BAD_REQUEST: 546 | WARN("HTTP Bad Request."); 547 | sendErrorResponse("400", "Bad Request"); 548 | state_ = STATE_ERROR; 549 | break; 550 | case ERR_NOT_FOUND: 551 | WARN("HTTP Not Found."); 552 | sendErrorResponse("404", "Not Found"); 553 | state_ = STATE_ERROR; 554 | break; 555 | case ERR_LENGTH_REQUIRED: 556 | WARN("HTTP Length Required."); 557 | sendErrorResponse("411", "Length Required"); 558 | state_ = STATE_ERROR; 559 | break; 560 | case ERR_NOT_IMPLEMENTED: 561 | WARN("HTTP Request method is not implemented."); 562 | sendErrorResponse("501", "Not Implemented"); 563 | state_ = STATE_ERROR; 564 | break; 565 | case ERR_INTERNAL_SERVER_ERR: 566 | WARN("HTTP Internal Server Error."); 567 | sendErrorResponse("500", "Internal Server Error"); 568 | state_ = STATE_ERROR; 569 | break; 570 | case ERR_HTTP_VERSION_NOT_SUPPORTED: 571 | WARN("HTTP Request HTTP Version Not Supported."); 572 | sendErrorResponse("505", "HTTP Version Not Supported"); 573 | state_ = STATE_ERROR; 574 | break; 575 | default: 576 | UNREACHABLE(); 577 | } 578 | return isSuccess; 579 | } 580 | 581 | HttpHandler::ERROR_TYPE HttpHandler::sendResponse(const string& responseCode, const string& responseMsg, 582 | const string& responseBodyType, const string& responseBody) 583 | { 584 | stringstream sstream; 585 | sstream << "HTTP/1.1" << " " << responseCode << " " << responseMsg << "\r\n"; 586 | sstream << "Connection: " << (isKeepAlive_ ? "Keep-Alive" : "Close") << "\r\n"; 587 | if(isKeepAlive_) 588 | // Keep-Alive 头中, timeout 表示超时时间(单位s), max表示最多接收请求次数,超过则断开. 589 | sstream << "Keep-Alive: timeout=" << timeoutPerRequest << ", max=" << againTimes_ << "\r\n"; 590 | sstream << "Server: WebServer/1.1" << "\r\n"; 591 | sstream << "Content-length: " << responseBody.size() << "\r\n"; 592 | sstream << "Content-type: " << responseBodyType << "\r\n"; 593 | sstream << "\r\n"; 594 | // 如果是 HEAD 请求,则不发送 http body 595 | if(method_ != METHOD_HEAD) 596 | sstream << responseBody; 597 | 598 | string&& response = sstream.str(); 599 | 600 | ssize_t len = writen(client_fd_, (void*)response.c_str(), response.size()); 601 | 602 | // 输出返回的数据 603 | INFO("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<- Response Packet ->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> "); 604 | INFO("{%s}", escapeStr(response, MAXBUF).c_str()); 605 | 606 | if(len < 0 || static_cast(len) != response.size()) 607 | return ERR_SEND_RESPONSE_FAIL; 608 | return ERR_SUCCESS; 609 | } 610 | 611 | HttpHandler::ERROR_TYPE HttpHandler::sendErrorResponse(const string& errCode, const string& errMsg) 612 | { 613 | string errStr = errCode + " " + errMsg; 614 | string responseBody = 615 | "" 616 | "" + errStr + "" 617 | "" + errStr + 618 | "
Kiprey's Web Server" 619 | "" 620 | ""; 621 | return sendResponse(errCode, errMsg, "text/html", responseBody); 622 | } 623 | 624 | bool HttpHandler::RunEventLoop() 625 | { 626 | // 从socket读取请求数据, 如果读取失败,或者断开连接 627 | if(!handleErrorType(readRequest())) 628 | // 直接断开连接 629 | return false; 630 | 631 | // 解析信息 ------------------------------------------ 632 | // 1. 先解析第一行 633 | if(state_ == STATE_PARSE_URI && handleErrorType(parseURI())) 634 | state_ = STATE_PARSE_HEADER; 635 | // 2. 解析每一条http header 636 | if(state_ == STATE_PARSE_HEADER && handleErrorType(parseHttpHeader())) 637 | state_ = STATE_PARSE_BODY; 638 | // 3. 对于 post 解析 http body 639 | if(state_ == STATE_PARSE_BODY) 640 | { 641 | if(method_ != METHOD_POST || handleErrorType(parseBody())) 642 | state_ = STATE_ANALYSI_REQUEST; 643 | } 644 | // 4. 开始处理数据 645 | if(state_ == STATE_ANALYSI_REQUEST && handleErrorType(handleRequest())) 646 | state_ = STATE_FINISHED; 647 | 648 | // 开始处理当前状态 649 | // 如果这个过程中有任何非致命错误, 或者当前过程圆满结束 650 | if(state_ == STATE_ERROR || state_ == STATE_FINISHED) 651 | { 652 | // 如果 keep Alive, 则重置状态, 并跳出 if 到最后的return 处重新放入 epoll 中 653 | if(isKeepAlive_) 654 | reset(); 655 | else 656 | // 否则,既然已经发生了错误 / 完成了请求,则直接销毁当前实例 657 | return false; 658 | } 659 | // 如果是致命错误,则直接返回 false 660 | else if(state_ == STATE_FATAL_ERROR) 661 | return false; 662 | 663 | // 执行到这里则表示需要更多数据,因此重新放入 epoll 中 664 | bool ret1 = true; 665 | if(timer_) 666 | ret1 = epoll_->modify(timer_->getFd(), getTimerEpollEvent(), getTimerTriggerCond()); 667 | bool ret2 = epoll_->modify(client_fd_, getClientEpollEvent(), getClientTriggerCond()); 668 | assert(ret1 && ret2); 669 | 670 | return true; 671 | } -------------------------------------------------------------------------------- /HttpHandler.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTPHANDLER_H 2 | #define HTTPHANDLER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "Epoll.h" 8 | #include "Timer.h" 9 | 10 | using namespace std; 11 | 12 | /** 13 | * @brief HttpHandler 类处理每一个客户端连接,并根据读入的http报文,动态返回对应的response 14 | * 其支持的 HTTP 版本为 HTTP/1.1 15 | */ 16 | class HttpHandler 17 | { 18 | public: 19 | 20 | /** 21 | * @brief 显式指定 client fd 22 | * @param epoll_fd epoll 实例相关的描述符 23 | * @param client_fd 连接的 client_fd 24 | * @param timer 给当前连接限制时间的timer 25 | */ 26 | explicit HttpHandler(Epoll* epoll, int client_fd, Timer* timer); 27 | 28 | /** 29 | * @brief 释放所有 HttpHandler 所使用的资源 30 | * @note 注意,不会主动关闭 client_fd 31 | */ 32 | ~HttpHandler(); 33 | 34 | /** 35 | * @brief 为当前连接启动事件循环 36 | * @return 若当前文件描述符的数据没有处理完成, 则返回 true; 37 | * 如果完成了所有的事件,需要被释放时则返回 false 38 | * @note 在执行事件循环开始之前,一定要设置 client fd 39 | */ 40 | bool RunEventLoop(); 41 | 42 | // 只有getFd,没有setFd,因为Fd必须在创造该实例时被设置 43 | int getClientFd() { return client_fd_; } 44 | Epoll* getEpoll() { return epoll_; } 45 | Timer* getTimer() { return timer_; } 46 | // 获取 client_fd 和 timer_fd 所需要设置的 epoll 触发条件 47 | int getClientTriggerCond() { return EPOLLET | EPOLLIN | EPOLLONESHOT | EPOLLRDHUP | EPOLLHUP; } 48 | int getTimerTriggerCond() { return EPOLLET | EPOLLIN | EPOLLONESHOT; } 49 | // 获取 client 和 timer 的 epoll event 50 | void* getClientEpollEvent() { return &client_event_; } 51 | void* getTimerEpollEvent() { return &timer_event_; } 52 | 53 | // 设置HTTP处理时, www文件夹的路径 54 | static void setWWWPath(string path) { www_path = path; }; 55 | static string getWWWPath() { return www_path; } 56 | 57 | // HttpHandler 内部状态 58 | enum STATE_TYPE { 59 | STATE_PARSE_URI, // 解析 HTTP 报文中的第一行, [METHOD URI HTTP_VERSION] 60 | STATE_PARSE_HEADER, // 解析 HTTP header 61 | STATE_PARSE_BODY, // 解析 HTTP body (只针对 POST 请求解析. 注: GET 请求不会解析多余的body) 62 | STATE_ANALYSI_REQUEST, // 解析获取到的整体报文,处理并发送对应的响应报文 63 | STATE_FINISHED, // 当前报文已经解析完毕 64 | STATE_ERROR, // 遇到了可恢复的错误 65 | STATE_FATAL_ERROR // 遇到了无法恢复的错误,即将断开连接并销毁当前实例 66 | }; 67 | // 获取状态 68 | STATE_TYPE getState() { return state_; } 69 | 70 | private: 71 | 72 | // HttpHandler内部错误 73 | enum ERROR_TYPE { 74 | ERR_SUCCESS = 0, // 无错误 75 | 76 | ERR_READ_REQUEST_FAIL, // 读取请求数据失败 77 | ERR_AGAIN, // 读取的数据不够,需要等待下一次读取到的数据再来解析 78 | ERR_CONNECTION_CLOSED, // 远程连接已关闭 79 | 80 | ERR_SEND_RESPONSE_FAIL, // 响应包发送失败 81 | 82 | ERR_BAD_REQUEST, // 用户的请求包中存在错误,无法解析 400 Bad Request 83 | ERR_NOT_FOUND, // 目标文件不存在 404 Not Found 84 | ERR_LENGTH_REQUIRED, // POST请求中没有 Content-Length 请求头 411 Length Required 85 | 86 | ERR_NOT_IMPLEMENTED, // 不支持一些特定的请求操作 501 Not Implemented 87 | ERR_INTERNAL_SERVER_ERR, // 程序内部错误 500 Internal Server Error 88 | ERR_HTTP_VERSION_NOT_SUPPORTED // 不支持当前客户端的http版本 505 HTTP Version Not Supported 89 | }; 90 | 91 | // 请求的 HTTP 版本号 92 | enum HTTP_VERSION{ 93 | HTTP_1_0, // HTTP/1.0 94 | HTTP_1_1, // HTTP/1.1 95 | }; 96 | 97 | // 支持的请求方式 98 | enum METHOD_TYPE { 99 | METHOD_GET, // GET 请求 100 | METHOD_POST, // POST 请求 101 | METHOD_HEAD // HEAD 请求,与 GET 处理方式相同,但不返回 body 102 | }; 103 | 104 | // 当前 HTTP handler 的 www 工作目录, 默认情况下为当前工作目录 105 | static string www_path; 106 | 107 | // 一些常量 108 | const size_t MAXBUF = 1024; // 缓冲区大小 109 | const int maxAgainTimes = 10; // 最多重试次数 110 | const int maxCGIRuntime = 1000; // CGI程序最长等待时间(ms) 111 | const int cgiStepTime = 1; // 单次轮询CGI程序是否退出的等待时间(ms, <= 1000) 112 | const int timeoutPerRequest = 10; // 单个请求的超时时间(s) 113 | 114 | // 相关描述符 115 | int client_fd_; 116 | EpollEvent client_event_; 117 | 118 | Timer* timer_; 119 | EpollEvent timer_event_; 120 | 121 | Epoll* epoll_; 122 | 123 | // http 请求包的所有数据 124 | string request_; 125 | // http 头部 126 | map headers_; 127 | // 请求方式 128 | METHOD_TYPE method_; 129 | // 请求路径 130 | string path_; 131 | // http版本号 132 | HTTP_VERSION http_version_; 133 | // 当前handler 状态 134 | STATE_TYPE state_; 135 | // 重试次数 136 | int againTimes_; 137 | // http body 数据 138 | string http_body_; 139 | 140 | // 是否是 `持续连接` 141 | bool isKeepAlive_; 142 | 143 | /** 144 | * @brief 当前解析读入数据的位置 145 | * @note 该成员变量只在 146 | * readRequest -> parseURI -> parseHttpHeader -> RunEventLoop 147 | * 内部中使用 148 | */ 149 | size_t curr_parse_pos_; 150 | 151 | /** 152 | * @brief 初始化,清空所有数据 153 | */ 154 | void reset(); 155 | 156 | /** 157 | * @brief 从client_fd_中读取数据至 request_中 158 | * @return ERR_SUCCESS 表示读取成功; 159 | * ERR_AGAIN 表示读取过程中缺失数据,需要等到下次再读 160 | * 其他则表示读取过程存在错误 161 | * @note 内部函数recvn在错误时会产生 errno 162 | */ 163 | ERROR_TYPE readRequest(); 164 | 165 | /** 166 | * @brief 从0位置处解析 请求方式\URI\HTTP版本等 167 | * @return ERR_SUCCESS 表示读取成功; 168 | * ERR_AGAIN 表示读取过程中缺失数据,需要等到下次再读 169 | * 其他则表示读取过程存在错误 170 | */ 171 | ERROR_TYPE parseURI(); 172 | 173 | /** 174 | * @brief 从request_中的pos位置开始解析 http header 175 | * @return ERR_SUCCESS 表示读取成功; 176 | * ERR_AGAIN 表示读取过程中缺失数据,需要等到下次再读 177 | * 其他则表示读取过程存在错误 178 | */ 179 | ERROR_TYPE parseHttpHeader(); 180 | 181 | /** 182 | * @brief 解析 http body 183 | * @return ERR_SUCCESS 表示读取成功; 184 | * ERR_AGAIN 表示读取过程中缺失数据,需要等到下次再读 185 | * 不存在其他错误情况 186 | */ 187 | ERROR_TYPE parseBody(); 188 | 189 | /** 190 | * @brief 处理获取到的完整请求 191 | * @return ERR_SUCCESS 表示读取成功; 192 | * 其他则表示读取过程存在错误 193 | */ 194 | ERROR_TYPE handleRequest(); 195 | 196 | /** 197 | * @brief 处理传入的错误类型 198 | * @param err 错误类型 199 | * @return 如果传入 ERR_SUCCESS 则返回 true,否则返回 false 200 | */ 201 | bool handleErrorType(ERROR_TYPE err); 202 | 203 | /** 204 | * @brief 发送响应报文给客户端 205 | * @param responseCode http 状态码, http报文第二个字段 206 | * @param responseMsg http 报文第三个字段 207 | * @param responseBodyType 返回的body类型,即 Content-type 208 | * @param responseBody 返回的body内容 209 | * @return ERR_SUCCESS 表示成功发送, 其他则表示发送过程存在错误 210 | */ 211 | ERROR_TYPE sendResponse(const string& responseCode, const string& responseMsg, 212 | const string& responseBodyType, const string& responseBody); 213 | 214 | /** 215 | * @brief 发送错误信息至客户端 216 | * @param errCode 错误http状态码 217 | * @param errMsg 错误信息, http报文第三个字段 218 | * @return ERR_SUCCESS 表示成功发送, 其他则表示发送过程存在错误 219 | */ 220 | ERROR_TYPE sendErrorResponse(const string& errCode, const string& errMsg); 221 | }; 222 | 223 | class MimeType 224 | { 225 | private: 226 | // (suffix -> type) 227 | map mime_map_; 228 | 229 | string getMineType_(string suffix) 230 | { 231 | if(mime_map_.find(suffix) != mime_map_.end()) 232 | return mime_map_[suffix]; 233 | else 234 | return mime_map_["default"]; 235 | } 236 | public: 237 | MimeType() 238 | { 239 | mime_map_["doc"] = "application/msword"; 240 | mime_map_["gz"] = "application/x-gzip"; 241 | mime_map_["ico"] = "application/x-ico"; 242 | 243 | mime_map_["gif"] = "image/gif"; 244 | mime_map_["jpg"] = "image/jpeg"; 245 | mime_map_["png"] = "image/png"; 246 | mime_map_["bmp"] = "image/bmp"; 247 | 248 | mime_map_["mp3"] = "audio/mp3"; 249 | mime_map_["avi"] = "video/x-msvideo"; 250 | 251 | mime_map_["html"] = "text/html"; 252 | mime_map_["htm"] = "text/html"; 253 | mime_map_["css"] = "text/html"; 254 | mime_map_["js"] = "text/html"; 255 | 256 | mime_map_["c"] = "text/plain"; 257 | mime_map_["txt"] = "text/plain"; 258 | mime_map_["default"] = "text/plain"; 259 | } 260 | 261 | static string getMineType(string suffix) 262 | { 263 | static MimeType _mimeTy; 264 | return _mimeTy.getMineType_(suffix); 265 | } 266 | }; 267 | 268 | #endif -------------------------------------------------------------------------------- /Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | 3 | // 全局日志输出锁,确保单个线程可以完整的输出一条语句 4 | MutexLock global_log_lock; -------------------------------------------------------------------------------- /Log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "MutexLock.h" 9 | 10 | #define cRST "\x1b[0m" // 终端红色字体代码 11 | #define cLRD "\x1b[1;91m" // 终端重置字体颜色代码 12 | #define cYEL "\x1b[1;93m" // 终端黄色字体代码 13 | #define cBRI "\x1b[1;97m" // 终端加粗白色字体代码 14 | #define cLBL "\x1b[1;94m" // 终端蓝色字体代码 15 | 16 | // 全局日志输出锁 17 | extern MutexLock global_log_lock; 18 | 19 | #if 1 20 | // 开启所有输出 21 | #define INFO(x...) do { \ 22 | MutexLockGuard log_guard(global_log_lock); \ 23 | fprintf(stdout, "(Thread %lx): ", syscall(SYS_gettid)); \ 24 | fprintf(stdout, cLBL "[*] " cRST x); \ 25 | fprintf(stdout, cRST "\n"); \ 26 | fflush(stdout); \ 27 | } while (0) 28 | 29 | #define WARN(x...) do { \ 30 | MutexLockGuard log_guard(global_log_lock); \ 31 | fprintf(stderr, "(Thread %lx): ", syscall(SYS_gettid)); \ 32 | fprintf(stderr, cYEL "[!] " cBRI "WARNING: " cRST x); \ 33 | fprintf(stderr, cRST "\n"); \ 34 | fflush(stderr); \ 35 | } while (0) 36 | 37 | #define ERROR(x...) do { \ 38 | MutexLockGuard log_guard(global_log_lock); \ 39 | fprintf(stderr, "(Thread %lx): ", syscall(SYS_gettid)); \ 40 | fprintf(stderr, cLRD "[-] " cRST x); \ 41 | fprintf(stderr, cRST "\n"); \ 42 | fflush(stderr); \ 43 | } while (0) 44 | 45 | #define FATAL(x...) do { \ 46 | MutexLockGuard log_guard(global_log_lock); \ 47 | fprintf(stderr, "(Thread %lx): ", syscall(SYS_gettid)); \ 48 | fprintf(stderr, cRST cLRD "[-] PROGRAM ABORT : " cBRI x); \ 49 | fprintf(stderr, cLRD "\n Location : " cRST "%s(), %s:%u\n\n", \ 50 | __FUNCTION__, __FILE__, __LINE__); \ 51 | fflush(stderr); \ 52 | abort(); \ 53 | } while (0) 54 | 55 | #else 56 | 57 | // 关闭所有输出 58 | #define INFO(x...) 59 | #define WARN(x...) 60 | #define ERROR(x...) 61 | #define FATAL(x...) 62 | 63 | #endif 64 | 65 | /** 66 | * @brief 调试用,表示某个代码区域不应该到达 67 | * @param x 可输出的信息 68 | */ 69 | #define UNREACHABLE(x) FATAL("UNREACHABLE CODE"); 70 | 71 | #endif -------------------------------------------------------------------------------- /MutexLock.h: -------------------------------------------------------------------------------- 1 | #ifndef MUTEXLOCK_H 2 | #define MUTEXLOCK_H 3 | 4 | #include 5 | 6 | /** 7 | * @brief MutexLock 将 pthread_mutex 封装成一个类, 8 | * 这样做的好处是不用记住那些繁杂的 pthread 开头的函数使用方式 9 | */ 10 | class MutexLock 11 | { 12 | private: 13 | pthread_mutex_t mutex_; 14 | public: 15 | MutexLock() { pthread_mutex_init(&mutex_, nullptr); } 16 | ~MutexLock() { pthread_mutex_destroy(&mutex_); } 17 | void lock() { pthread_mutex_lock(&mutex_); } 18 | void unlock() { pthread_mutex_unlock(&mutex_); } 19 | pthread_mutex_t* getMutex() { return &mutex_; }; 20 | }; 21 | 22 | /** 23 | * @brief MutexLockGuard 主要是为了自动获取锁/释放锁, 防止意外情况下忘记释放锁 24 | * 而且块状的锁定区域更容易让人理解代码 25 | */ 26 | class MutexLockGuard 27 | { 28 | private: 29 | MutexLock& lock_; 30 | public: 31 | /** 32 | * @brief 声明 MutexLockGuard 时自动上锁 33 | * @param lock 待锁定的资源 34 | */ 35 | MutexLockGuard(MutexLock& mutex) : lock_(mutex) { lock_.lock(); } 36 | /** 37 | * @brief 当前作用域结束时自动释放锁, 防止遗忘 38 | */ 39 | ~MutexLockGuard() { lock_.unlock(); } 40 | }; 41 | 42 | #endif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebServer-1.1 2 | 3 | ## 一、概述 4 | 5 | WebServer-1.0 简单实现了一个基础的 **多并发网络服务程序** 。在该版本中,主要实现了以下重要内容: 6 | 7 | - 线程互斥锁 & 条件变量的封装 8 | 9 | - 线程池的设计,以支持并发 10 | 11 | - 基础网络连接的实现 12 | 13 | - http 协议的简略支持 14 | 15 | - 支持部分常用 HTTP 报文 16 | - 200 OK 17 | - 400 Bad Request 18 | - 500 Internal Server Error 19 | - 501 Not Implemented 20 | - 505 HTTP Version Not Supported 21 | - 支持 HTTP GET 请求 22 | - 支持 HTTP/1.1 **持续连接** 特性 23 | 24 | WebServer-1.0运行时截图: 25 | 26 | ![image-20210512133857068](docs/img/image-20210512133857068.png) 27 | 28 | > 1.0版本的项目代码位于 [Kiprey/WebServer CommitID: 6473f5d - github](https://github.com/Kiprey/WebServer/tree/6473f5d512097f235ab209b13b53e28d7946a0f6) 29 | > 30 | > 可以使用`git checkout v1.0`命令来切换版本。 31 | 32 | WebServer-1.1 在原先 1.0 版本的基础上大量重构了代码,相对于旧版本来说,新版本主要更新了以下内容: 33 | 34 | - 替换并发方式,从**多线程并发** 更换为 **epoll 并发** 35 | - HTTP报文处理添加 POST 和 HEAD 方式的处理 36 | - 支持自定义 WebServer 的 www 目录路径 37 | - 使用 timerfd API,对每个 HTTP/1.1 Keep-Alive 的 TCP 链接设置了超时时间,超时后若还没有请求,则强制关闭该连接。 38 | - 支持 Post 请求使用 CGI 程序。其中CGI程序是可执行文件(例如shell脚本,附带 `#!` 的 python 脚本, ELF可执行文件等等)。 39 | - 支持自定义 www 目录路径,不再限制为当前工作目录。 40 | - 支持互斥 pretty LOG 输出 41 | - 支持更多的 Http 错误报文 42 | - 404 Not Found 43 | - 411 Length Required 44 | - 支持 Address Sanitizer 检测当前程序的潜在漏洞 45 | - 更多的功能等待发现...... 46 | 47 | WebServer-1.1 运行时截图: 48 | 49 | ![image-20211026112349](docs/img/image-20211026112349.png) 50 | 51 | > 可以使用`git checkout v1.1`命令来切换版本 52 | 53 | > 注意:该程序的实现大量参考了 [linyacool/WebServer - github](https://github.com/linyacool/WebServer) 的代码。 54 | 55 | ## 二、编译、运行与调试 56 | 57 | - 使用以下指令编译: 58 | 59 | ```bash 60 | make 61 | ``` 62 | 63 | - WebServer-1.0使用以下指令运行 64 | 65 | ```bash 66 | ./WebServer 67 | ``` 68 | 69 | > 注意一些**特殊端口**的绑定需要使用 root 权限,例如 80 端口。 70 | 71 | WebServer-1.1 使用以下指令执行 72 | 73 | ```bash 74 | ./WebServer [] 75 | ``` 76 | 77 | - 使用 GDB 进行调试。 78 | 79 | ## 三、技术文档 80 | 81 | 请点击 [此处 WebServer-1](docs/WebServer-1.md) 跳转至更加详细的WebServer1.0技术文档。 82 | 83 | WebServer1.1技术文档因为时间原因暂时没有完成,但主要的技术细节已经以大量注释的形式写入了源代码中,可以直接阅读源代码来理解。 84 | 85 | ## 四、测试方式 86 | 87 | - 单个测试 88 | 89 | ```bash 90 | # GET 请求 91 | curl http://localhost:8012/html/index.html 92 | curl -d http://localhost:8012/html/CGI/base64script 93 | # POST 请求 94 | 95 | ``` 96 | 97 | - 使用 apache 测试工具 `ab` 来进行大批量测试 98 | 99 | ```bash 100 | # -c 并发数 101 | # -n 总请求数 102 | # -s 单个请求的超时时间 103 | 104 | # GET 测试 105 | ab -c 500 -n 10000 -s 300 http://127.0.0.1:8012/html/index.html 106 | # POST 测试 107 | ab -c 500 -n 10000 -s 300 -p ignore_post.txt http://127.0.0.1:8012/html/CGI/base64script 108 | ``` -------------------------------------------------------------------------------- /ThreadPool.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include "ThreadPool.h" 3 | #include "Utils.h" 4 | 5 | ThreadPool::ThreadPool(size_t threadNum, ShutdownMode shutdown_mode, size_t maxQueueSize) 6 | : threadNum_(threadNum), 7 | maxQueueSize_(maxQueueSize), 8 | // 使用 类成员变量 threadpool_mutex_ 来初始化 threadpool_cond_ 9 | threadpool_cond_(threadpool_mutex_), 10 | shutdown_mode_(shutdown_mode) 11 | { 12 | // 开始循环创建线程 13 | while(threads_.size() < threadNum_) 14 | { 15 | pthread_t thread; 16 | // 如果线程创建成功,则将其压入栈内存中 17 | if(!pthread_create(&thread, nullptr, TaskForWorkerThreads_, this)) 18 | { 19 | threads_.push_back(thread); 20 | // // 注意这里只修改已启动的线程数量 21 | // startedThreadNum_++; 22 | } 23 | } 24 | } 25 | 26 | ThreadPool::~ThreadPool() 27 | { 28 | // 向任务队列中添加退出线程事件,注意上锁 29 | // 注意在 cond 使用之前一定要上 mutex 30 | { 31 | // 操作 task_queue_ 时一定要上锁 32 | MutexLockGuard guard(threadpool_mutex_); 33 | // 如果需要立即关闭当前的线程池,则 34 | if(shutdown_mode_ == IMMEDIATE_SHUTDOWN) 35 | // 先将当前队列清空 36 | while(!task_queue_.empty()) 37 | task_queue_.pop(); 38 | 39 | // 往任务队列中添加退出线程任务 40 | for(size_t i = 0; i < threadNum_; i++) 41 | { 42 | auto pthreadExit = [](void*) { pthread_exit(0); }; 43 | ThreadpoolTask task = { pthreadExit, nullptr }; 44 | task_queue_.push(task); 45 | } 46 | // 唤醒所有线程以执行退出操作 47 | threadpool_cond_.notifyAll(); 48 | } 49 | for(size_t i = 0; i < threadNum_; i++) 50 | { 51 | // 回收线程资源 52 | pthread_join(threads_[i], nullptr); 53 | } 54 | } 55 | 56 | bool ThreadPool::appendTask(void (*function)(void*), void* arguments) 57 | { 58 | // 由于会操作事件队列,因此需要上锁 59 | MutexLockGuard guard(threadpool_mutex_); 60 | // 如果队列长度过长,则将当前task丢弃 61 | if(task_queue_.size() > maxQueueSize_) 62 | return false; 63 | else 64 | { 65 | // 添加task至列表中 66 | ThreadpoolTask task = { function, arguments }; 67 | task_queue_.push(task); 68 | // 每当有新事件进入之时,只唤醒一个等待线程 69 | threadpool_cond_.notify(); 70 | return true; 71 | } 72 | } 73 | 74 | void* ThreadPool::TaskForWorkerThreads_(void* arg) 75 | { 76 | ThreadPool* pool = (ThreadPool*)arg; 77 | // 启动当前线程 78 | ThreadpoolTask task; 79 | // 对于子线程来说,事件循环开始 80 | for(;;) 81 | { 82 | // 首先获取事件 83 | { 84 | // 获取事件时需要上个锁 85 | MutexLockGuard guard(pool->threadpool_mutex_); 86 | 87 | /** 88 | * 如果好不容易获得到锁了,但是没有事件可以执行 89 | * 则陷入沉睡,释放锁,并等待唤醒 90 | * NOTE: 注意, pthread_cond_signal 会唤醒至少一个线程 91 | * 也就是说,可能存在被唤醒的线程仍然没有事件处理的情况 92 | * 这时只需循环wait即可. 93 | */ 94 | while(pool->task_queue_.size() == 0) 95 | pool->threadpool_cond_.wait(); 96 | // 唤醒后一定有事件 97 | assert(pool->task_queue_.size() != 0); 98 | task = pool->task_queue_.front(); 99 | pool->task_queue_.pop(); 100 | } 101 | // 执行事件 102 | (task.function)(task.arguments); 103 | } 104 | // 注意: UNREACHABLE, 控制流不可能会到达此处 105 | // 因为线程的退出不会走这条控制流,而是执行退出事件 106 | UNREACHABLE(); 107 | return nullptr; 108 | } -------------------------------------------------------------------------------- /ThreadPool.h: -------------------------------------------------------------------------------- 1 | #ifndef THREADPOOL_H 2 | #define THREADPOOL_H 3 | 4 | #include 5 | #include 6 | 7 | #include "Condition.h" 8 | #include "MutexLock.h" 9 | 10 | using namespace std; 11 | 12 | class ThreadPool 13 | { 14 | public: 15 | // 线程池摧毁时,当前正在工作的线程是等待工作完成后退出(graceful) 还是直接退出(immediate) 16 | enum ShutdownMode { GRACEFUL_QUIT, IMMEDIATE_SHUTDOWN } ; 17 | /*** 18 | * @brief 创建线程池 19 | * @param threadNum 线程池线程个数 20 | * @param shutdown_mode 当前线程池的摧毁方案 21 | * @param maxQueueSize 线程池事件队列最大大小, 默认不设限制(-1) 22 | */ 23 | ThreadPool( size_t threadNum, 24 | ShutdownMode shutdown_mode = GRACEFUL_QUIT, 25 | size_t maxQueueSize = -1 26 | ); 27 | 28 | /*** 29 | * @brief 销毁线程池 30 | */ 31 | ~ThreadPool(); 32 | 33 | /*** 34 | * @brief 将当前task加入至线程池中 35 | * @param task 待处理的 task 36 | * @return 返回添加结果, true 表示添加成功, false 表示队列已满, 添加失败 37 | * @note 这里的 arguments 指针指向的对象,将 **不会** 在子线程内部事件执行完成后自动释放 38 | * 也就是说,外部调用者需要自己考虑到内存释放 39 | */ 40 | bool appendTask(void (*function)(void*), void* arguments); 41 | 42 | // /** 43 | // * @brief 声明一些获取线程池属性的方法.不管有没有用到,实现一下接口总是没错的. 44 | // */ 45 | // size_t getThreadNum() { return threadNum_; } 46 | // size_t getWorkingThreadNum() { return workingThreadNum_; } 47 | // size_t getIdleThreadNum() { return idleThreadNum_; } 48 | // size_t getStartedThreadNum() { return startedThreadNum_; } 49 | 50 | private: 51 | /** 52 | * @brief 每个子线程所要执行的函数, 在该函数中轮询事件队列 53 | * @param pool 当前线程所属的线程池 54 | */ 55 | static void* TaskForWorkerThreads_(void* arg); 56 | 57 | /*** 58 | * 每个线程的基本事件单元 59 | */ 60 | struct ThreadpoolTask 61 | { 62 | void (*function)(void*); 63 | void* arguments; 64 | }; 65 | 66 | size_t threadNum_; // 线程个数 67 | 68 | // size_t workingThreadNum_; // 正在工作的线程个数 69 | // size_t idleThreadNum_; // 空闲线程个数 70 | // size_t startedThreadNum_; // 已经启动的线程个数,注意已经启动的线程分为 正在工作 和 空闲 两类 71 | 72 | size_t maxQueueSize_; // 事件队列最大长度,超出则停止添加新事件 73 | queue task_queue_; // 事件队列 74 | 75 | vector threads_; // 线程的标识符 76 | 77 | MutexLock threadpool_mutex_; // 线程池的锁,保证每次最多只能有一个线程正在操作该线程池 78 | Condition threadpool_cond_; // 线程池的条件变量,对于来新task时,唤醒空闲线程 79 | 80 | ShutdownMode shutdown_mode_; // 线程池析构时,剩余工作线程的处理方式 81 | 82 | }; 83 | 84 | #endif -------------------------------------------------------------------------------- /Timer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Log.h" 4 | #include "Timer.h" 5 | #include "Utils.h" 6 | 7 | Timer::Timer(int flag, time_t sec, long nsec) : timer_fd_(-1) 8 | { 9 | if(create(flag)) 10 | setTime(sec, nsec); 11 | } 12 | 13 | Timer::~Timer() 14 | { 15 | cancel(); 16 | destroy(); 17 | } 18 | 19 | int Timer::getFd() 20 | { 21 | return timer_fd_; 22 | } 23 | 24 | void Timer::setFd(int fd) 25 | { 26 | timer_fd_ = fd; 27 | } 28 | 29 | bool Timer::isValid() 30 | { 31 | return timer_fd_ >= 0; 32 | } 33 | 34 | bool Timer::create(int flag) 35 | { 36 | // 这里使用 CLOCK_BOOTTIME **相对时间**, 排除了系统时间与系统休眠时间的干扰 37 | if(!isValid() && ((timer_fd_ = timerfd_create(CLOCK_BOOTTIME, flag)) == -1)) 38 | return false; 39 | return true; 40 | } 41 | 42 | bool Timer::setTime(time_t sec, long nsec) 43 | { 44 | struct itimerspec timerspec; 45 | // 初始化为0 46 | memset(&timerspec, 0, sizeof(timerspec)); 47 | // 设置超时事件,注意该定时器只是一次性的, 因为itimerspec.interval两个字段全为0 48 | timerspec.it_value.tv_nsec = nsec; 49 | timerspec.it_value.tv_sec = sec; 50 | if(!isValid() || (timerfd_settime(timer_fd_, 0, &timerspec, nullptr) == -1)) 51 | return false; 52 | return true; 53 | } 54 | 55 | bool Timer::cancel() 56 | { 57 | // 设置itimerspec的value两个字段全为0时则表示取消 58 | return setTime(0, 0); 59 | } 60 | 61 | void Timer::destroy() 62 | { 63 | if(isValid()) 64 | close(timer_fd_); 65 | timer_fd_ = -1; 66 | } 67 | 68 | timespec Timer::getNextTimeout() 69 | { 70 | itimerspec nextTime; 71 | if(!isValid() || (timerfd_gettime(timer_fd_, &nextTime) == -1)) 72 | { 73 | ERROR("Timer getNextTimeout fail! (%s)", strerror(errno)); 74 | timespec ret; 75 | ret.tv_nsec = ret.tv_sec = -1; 76 | return ret; 77 | } 78 | return nextTime.it_interval; 79 | } -------------------------------------------------------------------------------- /Timer.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMER_H 2 | #define TIMER_H 3 | 4 | #include 5 | 6 | class Timer 7 | { 8 | private: 9 | int timer_fd_; 10 | public: 11 | /** 12 | * @brief 初始时自动创建 timer fd,释放时自动释放timer fd 13 | * @param flag 用以设置 timer fd 的属性, TFD_NONBLOCK / TFD_NONBLOCK 14 | * @param sec 超时时间,单位秒 15 | * @param nsec 超时时间,单位纳秒 16 | */ 17 | Timer(int flag = 0, time_t sec = 0, long nsec = 0); 18 | ~Timer(); 19 | 20 | /** 21 | * @brief 获取与设置当前Timer的文件描述符 22 | */ 23 | int getFd(); 24 | void setFd(int fd); 25 | 26 | /** 27 | * @brief 判断当前timer fd是否可用 28 | * @return true表示可用,false表示不可用 29 | */ 30 | bool isValid(); 31 | 32 | /** 33 | * @brief 主动创建一个 timer fd,如果原先fd有效则不会重复创建 34 | * @param flag 用以设置 timer fd 的属性, TFD_NONBLOCK / TFD_NONBLOCK 35 | * @note 该函数调用的内部函数在错误时会设置 errno 36 | * @note 必须在使用该类成员中的其他函数之前, 确保该函数成功调用 37 | */ 38 | bool create(int flag = 0); 39 | 40 | /** 41 | * @brief 设置一次性定时器 42 | * @param sec 表示定时器的秒级时间 43 | * @param nsec 表示定时器的纳秒级时间 44 | * @return true 表示设置正常,false表示设置失败 45 | * @note 若sec和nsec同时为0 ,则表示关闭定时器. sec & nsec 共同表示定时器的超时时间 46 | * @note 该函数调用的内部函数在错误时会设置 errno 47 | */ 48 | bool setTime(time_t sec, long nsec); 49 | 50 | /** 51 | * @brief 取消定时器, 即setTime(0 ,0) 52 | * @return true 表示设置正常,false表示设置失败 53 | * @note 该函数调用的内部函数在错误时会设置 errno 54 | */ 55 | bool cancel(); 56 | 57 | /** 58 | * @brief 销毁timer fd 59 | */ 60 | void destroy(); 61 | 62 | /** 63 | * @brief 获取当前定时器距离下一次超时的时间 64 | * @return 返回timespec结构的时间 65 | * @note 若该函数执行失败,则返回的timespec结构体中,两个字段均为负数 66 | * @note 该函数调用的内部函数在错误时会设置 errno 67 | */ 68 | timespec getNextTimeout(); 69 | }; 70 | 71 | #endif -------------------------------------------------------------------------------- /Utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Log.h" 13 | #include "MutexLock.h" 14 | #include "Utils.h" 15 | 16 | int socket_bind_and_listen(int port) 17 | { 18 | int listen_fd = 0; 19 | // 开始创建 socket, 注意这是阻塞模式的socket 20 | // AF_INET : IPv4 Internet protocols 21 | // SOCK_STREAM : TCP socket 22 | if((listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)) == -1) 23 | return -1; 24 | 25 | // 绑定端口 26 | sockaddr_in server_addr; 27 | // 初始化一下 28 | memset(&server_addr, '\0', sizeof(server_addr)); 29 | // 设置一下基本操作 30 | server_addr.sin_family = AF_INET; 31 | server_addr.sin_port = htons((unsigned short)port); 32 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 33 | // 端口复用 34 | int opt = 1; 35 | if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) 36 | return -1; 37 | // 试着bind 38 | if(bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) 39 | return -1; 40 | // 试着listen, 设置最大队列长度为 1024 41 | if(listen(listen_fd, 1024) == -1) 42 | return -1; 43 | 44 | return listen_fd; 45 | } 46 | 47 | bool setFdNoBlock(int fd) 48 | { 49 | // 获取fd对应的flag 50 | int flag = fcntl(fd, F_GETFD); 51 | if(flag == -1) 52 | return -1; 53 | flag |= O_NONBLOCK; 54 | if(fcntl(fd, F_SETFL, flag) == -1) 55 | return false; 56 | return true; 57 | } 58 | 59 | bool setSocketNoDelay(int fd) 60 | { 61 | int enable = 1; 62 | if(setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void *)&enable, sizeof(enable)) == -1) 63 | return false; 64 | return true; 65 | } 66 | 67 | ssize_t readn(int fd, void* buf, size_t len) 68 | { 69 | // 这里将 void* 转换成 char* 是为了在下面进行自增操作 70 | char *pos = (char*)buf; 71 | size_t leftNum = len; 72 | ssize_t readNum = 0; 73 | while(leftNum > 0) 74 | { 75 | // 尝试循环读取,如果报错,则进行判断 76 | // 注意, read 的返回值为0则表示读取到 EOF,是正常现象 77 | ssize_t tmpRead = read(fd, pos, leftNum); 78 | 79 | if(tmpRead < 0) 80 | { 81 | if(errno == EINTR) 82 | tmpRead = 0; 83 | // 如果始终读取不到数据,则提前返回,因为这个取决于远程 fd,无法预测要等多久 84 | else if (errno == EAGAIN) 85 | return readNum; 86 | else 87 | return -1; 88 | } 89 | // 读取的0,则说明远程连接已被关闭 90 | if(tmpRead == 0) 91 | break; 92 | readNum += tmpRead; 93 | pos += tmpRead; 94 | 95 | leftNum -= tmpRead; 96 | } 97 | return readNum; 98 | } 99 | 100 | ssize_t writen(int fd, const void* buf, size_t len, bool isWrite) 101 | { 102 | // 这里将 void* 转换成 char* 是为了在下面进行自增操作 103 | char *pos = (char*)buf; 104 | size_t leftNum = len; 105 | ssize_t writtenNum = 0; 106 | while(leftNum > 0) 107 | { 108 | ssize_t tmpWrite = 0; 109 | 110 | if(isWrite) 111 | tmpWrite = write(fd, pos, leftNum); 112 | else 113 | tmpWrite = send(fd, pos, leftNum, 0); 114 | 115 | // 尝试循环写入,如果报错,则进行判断 116 | // 注意,write返回0属于异常现象,因此判断时需要包含 117 | if(tmpWrite < 0) 118 | { 119 | // 与read不同的是,如果 EAGAIN,则继续重复写入,因为写入操作是有Server这边决定的 120 | if(errno == EINTR || errno == EAGAIN) 121 | tmpWrite = 0; 122 | else 123 | return -1; 124 | } 125 | if(tmpWrite == 0) 126 | break; 127 | writtenNum += tmpWrite; 128 | pos += tmpWrite; 129 | leftNum -= tmpWrite; 130 | } 131 | return writtenNum; 132 | } 133 | 134 | void handleSigpipe() 135 | { 136 | struct sigaction sa; 137 | memset(&sa, '\0', sizeof(sa)); 138 | sa.sa_handler = SIG_IGN; 139 | sa.sa_flags = 0; 140 | if(sigaction(SIGPIPE, &sa, NULL) == -1) 141 | ERROR("Ignore SIGPIPE failed! (%s)", strerror(errno)); 142 | } 143 | 144 | void printConnectionStatus(int client_fd_, string prefix) 145 | { 146 | // 输出连接信息 [Server]IP:PORT <---> [Client]IP:PORT 147 | sockaddr_in serverAddr, peerAddr; 148 | socklen_t serverAddrLen = sizeof(serverAddr); 149 | socklen_t peerAddrLen = sizeof(peerAddr); 150 | 151 | if((getsockname(client_fd_, (struct sockaddr *)&serverAddr, &serverAddrLen) != -1) 152 | && (getpeername(client_fd_, (struct sockaddr *)&peerAddr, &peerAddrLen) != -1)) 153 | INFO("%s: (socket %d) [Server] %s:%d <---> [Client] %s:%d", 154 | prefix.c_str(), client_fd_, 155 | inet_ntoa(serverAddr.sin_addr), ntohs(serverAddr.sin_port), 156 | inet_ntoa(peerAddr.sin_addr), ntohs(peerAddr.sin_port)); 157 | else 158 | ERROR("printConnectionStatus failed ! (%s)", strerror(errno)); 159 | } 160 | 161 | string escapeStr(const string& str, size_t MAXBUF) 162 | { 163 | string msg = str; 164 | // 遍历所有字符 165 | for(size_t i = 0; i < msg.length(); i++) 166 | { 167 | char ch = msg[i]; 168 | // 如果当前字符无法打印,则转义 169 | if(!isprint(ch)) 170 | { 171 | // 这里只对\r\n做特殊处理 172 | string substr; 173 | if(ch == '\r') 174 | substr = "\\r"; 175 | else if(ch == '\n') 176 | substr = "\\n"; 177 | else 178 | { 179 | char hex[10]; 180 | // 注意这里要设置成 unsigned,即零扩展 181 | snprintf(hex, 10, "\\x%02x", static_cast(ch)); 182 | substr = hex; 183 | } 184 | msg.replace(i, 1, substr); 185 | } 186 | } 187 | // 将读取到的数据输出 188 | if(msg.length() > MAXBUF) 189 | return msg.substr(0, MAXBUF) + " ... ... "; 190 | else 191 | return msg; 192 | } 193 | 194 | bool isNumericStr(string str) 195 | { 196 | for(size_t i = 0; i < str.length(); i++) 197 | if(!isdigit(str[i])) 198 | return false; 199 | return true; 200 | } 201 | 202 | size_t closeRemainingConnect(int listen_fd, int* idle_fd) { 203 | close(*idle_fd); 204 | 205 | size_t count = 0; 206 | for(;;) { 207 | int client_fd = accept4(listen_fd, nullptr, nullptr, 208 | SOCK_NONBLOCK | SOCK_CLOEXEC); 209 | if(client_fd == -1 && errno == EAGAIN) 210 | break; 211 | close(client_fd); 212 | ++count; 213 | } 214 | // 重新恢复空闲描述符 215 | *idle_fd = open("/dev/null", O_RDONLY | O_CLOEXEC); 216 | return count; 217 | } 218 | 219 | bool is_path_parent(const string& parent_path, const string& child_path) { 220 | bool result = false; 221 | char* parent_p = nullptr, *child_p = nullptr; 222 | char separator; 223 | 224 | parent_p = canonicalize_file_name(parent_path.c_str()); 225 | if(!parent_p) { 226 | ERROR("is_path_parent failed, cannot get parent path [%s] (%s)", 227 | parent_path.c_str(), 228 | strerror(errno)); 229 | goto clean_parent; 230 | } 231 | 232 | child_p = canonicalize_file_name(child_path.c_str()); 233 | if(!child_p) { 234 | ERROR("is_path_parent failed, cannot get child path [%s] (%s)", 235 | child_path.c_str(), 236 | strerror(errno)); 237 | goto clean_child; 238 | } 239 | 240 | // INFO("resolved parent path: %s", parent_p); 241 | INFO("resolved path: %s", child_p); 242 | 243 | /* 判断是否存在目录穿越漏洞,判断条件: 244 | 1. parent_path 是否在 child_path 的起始位置, 245 | 例如 parent: /usr/class/html 246 | 与 child: /usr/class/html/index.html 247 | 2. 判断 parent_path 末尾是否分割符 248 | 例如 parent: /usr/class/html 249 | 与 child: /usr/class/htmlflag/../../../../flag 250 | ----------------------------A-------------------- 251 | 这里没有在 html 后面加 /,说明两个路径不对应 252 | */ 253 | if(child_p == strstr(child_p, parent_p)) { 254 | // parent 在 child 中,因此 child[parent.len] 不会越界 255 | separator = child_p[strlen(parent_p)]; 256 | if (separator == '\0' || separator == '/') 257 | return true; 258 | } 259 | 260 | free(child_p); 261 | clean_child: 262 | free(parent_p); 263 | clean_parent: 264 | return result; 265 | } 266 | -------------------------------------------------------------------------------- /Utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using std::cout; 9 | using std::cerr; 10 | using std::endl; 11 | using std::string; 12 | using std::ostream; 13 | 14 | /** 15 | * @brief 绑定一个端口号并返回一个 fd 16 | * @param port 目标端口号 17 | * @return 运行正常则返回 fd, 否则返回 -1 18 | * @note 该函数在错误时会生成 errno 19 | */ 20 | int socket_bind_and_listen(int port); 21 | 22 | /** 23 | * @brief 设置传入的文件描述符为非阻塞模式 24 | * @param fd 传入的目标套接字 25 | * @return true表示设置成功, false表示设置失败 26 | * @note fcntl函数在错误时会生成 errno 27 | */ 28 | bool setFdNoBlock(int fd); 29 | 30 | /** 31 | * @brief 设置socket禁用 nagle算法 32 | * @param fd 目标套接字 33 | * @return true 表示设置成功, false 表示设置失败 34 | * @note setsockopt函数在错误时会生成 errno 35 | */ 36 | bool setSocketNoDelay(int fd); 37 | 38 | /** 39 | * @brief 非阻塞模式 read 的wrapper 40 | * @param fd 源文件描述符 41 | * @param buf 缓冲区地址 42 | * @param len 目标读取的字节个数 43 | * @return 成功读取的长度 44 | * @note 内部函数在错误时会生成 errno 45 | * @note 非阻塞模式下,无论有没有读取到数据,都会马上返回 46 | * @note readn 不能用于替代 recv 47 | * 因为当 readn 返回0时,调用者无法知道是连接关闭,还是当前暂时无数据可读 48 | */ 49 | ssize_t readn(int fd, void* buf, size_t len); 50 | 51 | /** 52 | * @brief write/send的wrapper 53 | * @param fd 源文件描述符 54 | * @param buf 缓冲区地址 55 | * @param len 目标读取的字节个数 56 | * @param isWrite 启用 write 函数 57 | * @return 成功读取的长度 58 | * @note 内部函数在错误时会生成 errno 59 | * @note 该函数将 **阻塞** 写入数据, 除非有其他错误发生 60 | * @note writen 可以用于替代 send 进行阻塞写入操作 61 | */ 62 | ssize_t writen(int fd, const void* buf, size_t len, bool isWrite = false); 63 | 64 | /** 65 | * @brief 忽略 SIGPIPE信号 66 | * @note 当远程主机强迫关闭socket时,Server端会产生 SIGPIPE 信号 67 | * 但SIGPIPE信号默认关闭当前进程,因此在Server端处需要忽略该信号 68 | */ 69 | void handleSigpipe(); 70 | 71 | /** 72 | * @brief 将当前client_fd_对应的连接信息,以 LOG(INFO) 的形式输出 73 | * @param client_fd_ 待输出信息的 fd 74 | * @param prefix 输出信息的前缀,例如 ": " 75 | */ 76 | void printConnectionStatus(int client_fd_, string prefix); 77 | 78 | /** 79 | * @brief 将传入的字符串转义成终端可以直接显示的输出 80 | * @param str 待输出的字符串 81 | * @param MAXBUF 最长能输出的字符串长度 82 | * @return 转义后的字符串 83 | * @note 是将 '\r' 等无法在终端上显示的字符,转义成 "\r"字符串 输出 84 | */ 85 | string escapeStr(const string& str, size_t MAXBUF); 86 | 87 | /** 88 | * @brief 判断字符串是否全为数字 89 | * @return true 表示字符串全为数字, 否则返回false 90 | */ 91 | bool isNumericStr(string str); 92 | 93 | /** 94 | * @brief 清空当前剩余尚未 accept 的连接 95 | * @param listen_fd 当前所监听的文件描述符 96 | * @param idle_fd 空闲的文件描述符 97 | * @return 返回清空的连接数量 98 | */ 99 | size_t closeRemainingConnect(int listen_fd, int* idle_fd); 100 | 101 | /** 102 | * @brief 检测两个 path 是否包含从属关系,以防止目录穿越漏洞 103 | * @param root_dir 最外层的路径 104 | * @param child_dir 内层路径 105 | * @return 返回从属关系 106 | */ 107 | bool is_path_parent(const string& parent_path, const string& child_path); 108 | 109 | #endif -------------------------------------------------------------------------------- /docs/WebServer-1.md: -------------------------------------------------------------------------------- 1 | # WebServer v1.0 文档 2 | 3 | 4 | 5 | - [WebServer v1.0 文档](#webserver-v10-文档) 6 | - [概述](#概述) 7 | - [一、互斥锁 & 条件变量](#一互斥锁--条件变量) 8 | - [1. 互斥锁](#1-互斥锁) 9 | - [2. 条件变量](#2-条件变量) 10 | - [二、线程池](#二线程池) 11 | - [1. 概述](#1-概述) 12 | - [2. 实现前的准备工作](#2-实现前的准备工作) 13 | - [3. 子线程的目标函数](#3-子线程的目标函数) 14 | - [4. 创建线程池](#4-创建线程池) 15 | - [5. 添加事件](#5-添加事件) 16 | - [6. 销毁线程池](#6-销毁线程池) 17 | - [7. 参考链接](#7-参考链接) 18 | - [三、网络连接](#三网络连接) 19 | - [1. 概述](#1-概述-1) 20 | - [2. socket](#2-socket) 21 | - [3. bind](#3-bind) 22 | - [4. listen](#4-listen) 23 | - [5. accept](#5-accept) 24 | - [6. read/recv & write/send](#6-readrecv--writesend) 25 | - [a. 错误码](#a-错误码) 26 | - [b. 阻塞/非阻塞 读取](#b-阻塞非阻塞-读取) 27 | - [c. 返回值](#c-返回值) 28 | - [d. 最终实现的代码](#d-最终实现的代码) 29 | - [7. 建立连接](#7-建立连接) 30 | - [8. 忽略 SIGPIPE 信号](#8-忽略-sigpipe-信号) 31 | - [四、日志输出](#四日志输出) 32 | - [五、http 请求处理](#五http-请求处理) 33 | - [1. 概述](#1-概述-2) 34 | - [2. 连接](#2-连接) 35 | - [3. 错误类型](#3-错误类型) 36 | - [4. 读取请求数据](#4-读取请求数据) 37 | - [5. 解析URI](#5-解析uri) 38 | - [6. 解析 HTTP header](#6-解析-http-header) 39 | - [7. 发送响应报文](#7-发送响应报文) 40 | - [8. 错误处理](#8-错误处理) 41 | - [9. 事件循环(重要)](#9-事件循环重要) 42 | - [六、编译 & 调试](#六编译--调试) 43 | 44 | 45 | 46 | ## 概述 47 | 48 | WebServer 1.0 简单实现了一个基础的 **多并发网络服务程序** 。在该版本中,主要实现了以下重要内容: 49 | 50 | - 线程互斥锁 & 条件变量的封装 51 | - 线程池的设计,以支持并发 52 | - 基础网络连接的实现 53 | - http 协议的简略支持 54 | - 支持部分常用 HTTP 报文 55 | - 200 OK 56 | - 400 Bad Request 57 | - 500 Internal Server Error 58 | - 501 Not Implemented 59 | - 505 HTTP Version Not Supported 60 | - 支持 HTTP GET 请求 61 | - 支持 HTTP/1.1 **持续连接** 特性 62 | 63 | 1.0 版本的项目代码位于 [Kiprey/WebServer CommitID: 4095cc - github](https://github.com/Kiprey/WebServer/tree/4095ccc6fd3facd3988ea71178cacad7b4e0dd13) 64 | 65 | 最新版本的项目代码位于 [Kiprey/WebServer - github](https://github.com/Kiprey/WebServer) 66 | 67 | 68 | 69 | 运行示例: 70 | 71 | ![image-20210512133857068](https://kiprey.github.io/2021/05/WebServer-1/image-20210512133857068.png) 72 | 73 | > 注意:该程序的实现大量参考了 [linyacool/WebServer - github](https://github.com/linyacool/WebServer) 的代码。 74 | 75 | ## 一、互斥锁 & 条件变量 76 | 77 | ### 1. 互斥锁 78 | 79 | 对于当前的多线程程序来说,可能会出现多个线程同时读写同一个数据结构的情况,那么此时势必会造成脏读这种错误情况。而互斥锁的使用,是为了保证数据共享操作的完整性,确保任一时刻,只能有一个线程访问目标对象。 80 | 81 | 在linux中,互斥锁主要使用以下函数来实现: 82 | 83 | ```cpp 84 | // 初始化 mutex 对象 85 | pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 86 | int pthread_mutex_init(pthread_mutex_t *restrict mutex, 87 | const pthread_mutexattr_t *restrict attr); 88 | // mutex 加锁,在获得锁之前将会阻塞 89 | int pthread_mutex_lock(pthread_mutex_t *mutex); 90 | // mutex 加锁,如果能马上获取锁则返回0,无法获取锁则马上返回errno 91 | int pthread_mutex_trylock(pthread_mutex_t *mutex); 92 | // mutex 释放锁 93 | int pthread_mutex_unlock(pthread_mutex_t *mutex); 94 | // 销毁 mutex 对象 95 | int pthread_mutex_destroy(pthread_mutex_t *mutex); 96 | ``` 97 | 98 | 由于 pthread 族的库函数名称较长,并且调用方式也互不相同,因此在这些库函数上做了一个简单的封装: 99 | 100 | ```cpp 101 | /** 102 | * @brief MutexLock 将 pthread_mutex 封装成一个类, 103 | * 这样做的好处是不用记住那些繁杂的 pthread 开头的函数使用方式 104 | */ 105 | class MutexLock 106 | { 107 | private: 108 | pthread_mutex_t mutex_; 109 | public: 110 | MutexLock() { pthread_mutex_init(&mutex_, nullptr); } 111 | ~MutexLock() { pthread_mutex_destroy(&mutex_); } 112 | void lock() { pthread_mutex_lock(&mutex_); } 113 | void unlock() { pthread_mutex_unlock(&mutex_); } 114 | pthread_mutex_t* getMutex() { return &mutex_; }; 115 | }; 116 | ``` 117 | 118 | 正常来说,我们使用锁时,需要经过以下过程:**获取锁->进入临界区->释放锁**。但在实际使用锁时,容易忘记释放锁,而这是一个非常严重的错误。因此我们可以实现一个 `MutexLockGuard`类,借助类的构造函数和析构函数,来帮助我们自动获取锁和释放锁,只需一个简单的声明即可**获取锁&释放锁**。 119 | 120 | ```cpp 121 | /** 122 | * @brief MutexLockGuard 主要是为了自动获取锁/释放锁, 防止意外情况下忘记释放锁 123 | * 而且块状的锁定区域更容易让人理解代码 124 | */ 125 | class MutexLockGuard 126 | { 127 | private: 128 | MutexLock& lock_; 129 | public: 130 | /** 131 | * @brief 声明 MutexLockGuard 时自动上锁 132 | * @param lock 待锁定的资源 133 | */ 134 | MutexLockGuard(MutexLock& mutex) : lock_(mutex) { lock_.lock(); } 135 | /** 136 | * @brief 当前作用域结束时自动释放锁, 防止遗忘 137 | */ 138 | ~MutexLockGuard() { lock_.unlock(); } 139 | }; 140 | ``` 141 | 142 | ### 2. 条件变量 143 | 144 | 一说起条件变量,就不得不说先说起**管程**。管程保证了**同一时刻只有一个线程在管程内活动**,但**不能保证**线程在进入管程后,能继续一次性执行下去直到管程结束。 145 | 146 | > 例如某个线程好不容易进入了管程,但执行了一段时间,突然发现某个条件没有满足,使得当前线程必须阻塞,无法继续执行。但要是该线程原地阻塞,一直占用这个管程,那其他的线程自然就无法进入管程,造成死锁。 147 | 148 | 那该怎么办呢?这就轮到条件变量出场了。 149 | 150 | 继续以上面的这个例子为例,由于该线程进入管程后可能会阻塞,因此非常肯定的是,必须在该进程进入阻塞状态前释放管程,否则会造成死锁。但是该线程已经进入管程,且没办法继续执行下去,因此只能**原地释放管程**,并等待**条件**满足后,**重新获取管程锁**,并将该线程唤醒,使其继续执行。 151 | 152 | 条件变量起到的作用,就相当于控制线程在管程中挂起和唤醒的作用。上面的语句可能有点难以理解,请思考一下这个例子: 153 | 154 | > 线程池中,当子线程需要读取事件队列来获取事件之前,需要先获取队列的锁。当子线程获取到锁以后,如果队列为空,则条件不满足(注意这里的条件是:**队列非空**),因此子线程就无法从中获取事件,没法继续执行。此时可以使用条件变量让子线程在管程中挂起,等到条件满足时再通过条件变量来唤醒,回到管程继续执行。 155 | > 156 | > 注意:使用条件变量时,一定要确保**在已经获取到管程锁的前提下**使用,否则条件变量容易被多个子线程修改/使用。 157 | 158 | 条件变量相关的函数如下: 159 | 160 | ```cpp 161 | // 初始化条件变量 162 | pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 163 | int pthread_cond_init(pthread_cond_t *restrict cond, 164 | const pthread_condattr_t *restrict attr); 165 | // 唤醒 **至少一个** 被目标条件变量阻塞的线程 166 | int pthread_cond_signal(pthread_cond_t *cond); 167 | // 唤醒 **所有** 被目标条件变量阻塞的线程 168 | int pthread_cond_broadcast(pthread_cond_t *cond); 169 | // 让目标条件变量阻塞当前线程 170 | int pthread_cond_wait(pthread_cond_t *restrict cond, 171 | pthread_mutex_t *restrict mutex); 172 | // 让目标条件变量阻塞当前线程,并设置最大阻塞时间 173 | int pthread_cond_timedwait(pthread_cond_t *restrict cond, 174 | pthread_mutex_t *restrict mutex, 175 | const struct timespec *restrict abstime); 176 | // 销毁条件变量 177 | int pthread_cond_destroy(pthread_cond_t *cond); 178 | ``` 179 | 180 | 与上面的互斥锁一样,这里也实现了一个 `Condition`类来简化条件变量的使用: 181 | 182 | ```cpp 183 | /** 184 | * @brief 条件变量,主要用于多线程中的锁 185 | * 与 MutexLock 一致,无需记住繁杂的函数名称 186 | * 条件变量主要是与mutex进行搭配,常用于资源分配相关的场景, 187 | * 例如当某个线程获取到锁以后,发现没有资源,则此时可以释放资源并等待条件变量 188 | * @note 注意: 使用条件变量时,必须上锁,防止出现多个线程共同使用条件变量 189 | */ 190 | class Condition 191 | { 192 | private: 193 | MutexLock& lock_; // 目标 Mutex 互斥锁 194 | pthread_cond_t cond_; // 条件变量 195 | public: 196 | Condition(MutexLock& mutex) : lock_(mutex) { pthread_cond_init(&cond_, nullptr); } 197 | ~Condition() { pthread_cond_destroy(&cond_); } 198 | void notify() { pthread_cond_signal(&cond_); } 199 | void notifyAll() { pthread_cond_broadcast(&cond_); } 200 | void wait() { pthread_cond_wait(&cond_, lock_.getMutex()); } 201 | /** 202 | * @brief 等待当前的条件变量一段时间 203 | * @param sec 等待的时间(单位:秒) 204 | * @return 成功在时间内等待到则返回 true, 超时则返回 false 205 | */ 206 | bool waitForSeconds(size_t sec) 207 | { 208 | timespec abstime; 209 | // 获取当前系统真实时间 210 | clock_gettime(CLOCK_REALTIME, &abstime); 211 | abstime.tv_sec += (time_t)sec; 212 | return ETIMEDOUT != pthread_cond_timedwait(&cond_, lock_.getMutex(), &abstime); 213 | } 214 | }; 215 | ``` 216 | 217 | ## 二、线程池 218 | 219 | ### 1. 概述 220 | 221 | - 线程池是一种多线程的处理方式,常常用在高并发服务器上。线程池可以有效的利用高并发服务器上的线程资源。 222 | 223 | - 线程用于处理各个请求,其流程大致为:**创建线程 => 传递信息至子线程 => 线程分离 => 线程运行 => 线程销毁**。对于较小规模的通信来说,上述的这个流程可以满足基本需求。但是对于高并发服务器来说,重复的创建线程与销毁线程,其开销不可忽视。因此可以使用线程池来让线程复用。 224 | 225 | ### 2. 实现前的准备工作 226 | 227 | - 对于一个线程所要执行的任务,我们需要明确以下几点: 228 | 229 | - 当前线程所要执行的函数,最好是与主线程没有较大关联的,即尽量降低耦合性。 230 | 231 | - 所要执行的事件,可以传入一个参数,但需要明确**不能有返回值**。 232 | 233 | > 要想有应该也可以做,不过这就是后面的事情了。 234 | 235 | 因此,我们便可以设计出以下的 task 结构体 236 | 237 | ```cpp 238 | // 每个线程的基本事件单元 239 | struct ThreadpoolTask 240 | { 241 | void (*function)(void*); 242 | void* arguments; 243 | }; 244 | ``` 245 | 246 | - 线程池除了一些特定的变量(线程个数、事件队列等等)以外,还需要**互斥锁**以及**条件变量**。 247 | 248 | - 对于每个线程来说,这些线程是有可能同时访问线程池,因此需要在每个线程访问之前**加以上锁**。 249 | - 上锁之后,对于每个线程来说,有可能出现这种**获取到锁,但没有事件可以执行**的情况。对于这类情况,子线程必须先释放锁等待事件的到来,等到事件到来之后再重新上锁,获取事件,而这就是**条件变量**的用处。 250 | 251 | ### 3. 子线程的目标函数 252 | 253 | 由于子线程只会在**线程池创建之时创建**,在**线程池销毁之时销毁**,因此,在子线程中必然要执行一个事件循环,其中重复执行 **获取事件、执行事件** 的动作。 254 | 255 | 但这里需要注意两件事情, 256 | 257 | 1. 获取事件时,需要给线程池上锁,因为要访问消息队列;必要时刻还需要设置条件变量来暂时释放锁。 258 | 2. 当线程池被销毁时,子线程该如何终止? 259 | 260 | 针对问题2,有两种方式: 261 | 262 | 1. 一种是在线程池设置一个标志,子线程定期轮询该标志以确认是否退出。 263 | 2. 再一种就是当前所实现的:添加一个**退出事件**至事件队列中,子线程执行到该事件时自动退出。 264 | 265 | 因此具体实现的代码如下所示: 266 | 267 | > 注意,`pthread_cond_signal` 会唤醒**至少**一个线程,注意是**至少**。因此可能会出现唤醒多个线程但只有一个事件等待处理的情况。针对于这种情况,只需设置子线程在被唤醒后,循环检测是否有剩余事件等待处理即可。 268 | 269 | ```cpp 270 | void* ThreadPool::TaskForWorkerThreads_(void* arg) 271 | { 272 | ThreadPool* pool = (ThreadPool*)arg; 273 | // 启动当前线程 274 | ThreadpoolTask task; 275 | // 对于子线程来说,事件循环开始 276 | for(;;) 277 | { 278 | // 首先获取事件 279 | { 280 | // 获取事件时需要上个锁 281 | MutexLockGuard guard(pool->threadpool_mutex_); 282 | 283 | /** 284 | * 如果好不容易获得到锁了,但是没有事件可以执行 285 | * 则陷入沉睡,释放锁,并等待唤醒 286 | * NOTE: 注意, pthread_cond_signal 会唤醒至少一个线程 287 | * 也就是说,可能存在被唤醒的线程仍然没有事件处理的情况 288 | * 这时只需循环wait即可. 289 | */ 290 | while(pool->task_queue_.size() == 0) 291 | pool->threadpool_cond_.wait(); 292 | // 唤醒后一定有事件 293 | assert(pool->task_queue_.size() != 0); 294 | task = pool->task_queue_.front(); 295 | pool->task_queue_.pop(); 296 | } 297 | // 执行事件 298 | (task.function)(task.arguments); 299 | } 300 | // 注意: UNREACHABLE, 控制流不可能会到达此处 301 | // 因为线程的退出不会走这条控制流,而是执行退出事件 302 | assert(0 && "TaskForWorkerThreads_ UNREACHABLE!"); 303 | return nullptr; 304 | } 305 | ``` 306 | 307 | ### 4. 创建线程池 308 | 309 | 创建线程池较为简单,直接循环创建线程即可。 310 | 311 | 需要注意的是,这里设置了销毁线程池时的处理方式。具体信息将在下面**销毁线程池**的那部分中详细讲解。 312 | 313 | ```cpp 314 | ThreadPool::ThreadPool(size_t threadNum, ShutdownMode shutdown_mode, size_t maxQueueSize) 315 | : threadNum_(threadNum), 316 | maxQueueSize_(maxQueueSize), 317 | // 使用 类成员变量 threadpool_mutex_ 来初始化 threadpool_cond_ 318 | threadpool_cond_(threadpool_mutex_), 319 | shutdown_mode_(shutdown_mode) 320 | { 321 | // 开始循环创建线程 322 | while(threads_.size() < threadNum_) 323 | { 324 | pthread_t thread; 325 | // 如果线程创建成功,则将其压入栈内存中 326 | if(!pthread_create(&thread, nullptr, TaskForWorkerThreads_, this)) 327 | { 328 | threads_.push_back(thread); 329 | // // 注意这里只修改已启动的线程数量 330 | // startedThreadNum_++; 331 | } 332 | } 333 | } 334 | ``` 335 | 336 | ### 5. 添加事件 337 | 338 | 添加新事件时,需要设置一下锁,防止脏读。在新事件添加完成后,使用条件变量来唤醒其中某一个空闲线程以执行新事件。 339 | 340 | ```cpp 341 | bool ThreadPool::appendTask(void (*function)(void*), void* arguments) 342 | { 343 | // 由于会操作事件队列,因此需要上锁 344 | MutexLockGuard guard(threadpool_mutex_); 345 | // 如果队列长度过长,则将当前task丢弃 346 | if(task_queue_.size() > maxQueueSize_) 347 | return false; 348 | else 349 | { 350 | // 添加task至列表中 351 | ThreadpoolTask task = { function, arguments }; 352 | task_queue_.push(task); 353 | // 每当有新事件进入之时,只唤醒一个等待线程 354 | threadpool_cond_.notify(); 355 | return true; 356 | } 357 | } 358 | ``` 359 | 360 | ### 6. 销毁线程池 361 | 362 | 销毁线程池时,需要判断销毁的方式。 363 | 364 | 这里设置了两种销毁方式,分别是 365 | 366 | 1. IMMEDIATE_SHUTDOWN 367 | 368 | 2. GRACEFUL_QUIT 369 | 370 | 对于第一种销毁方式,线程池将马上清空事件队列中的全部事件,并添加与线程个数相对应量的**退出事件**。这将会使每个子线程在**执行完当前事件后,马上执行退出事件**以退出。 371 | 372 | > **退出事件**如下:每个线程简单执行 pthread_exit 以退出。 373 | 374 | ```cpp 375 | auto pthreadExit = [](void*) { pthread_exit(0); }; 376 | ``` 377 | 378 | 而对于第二种销毁方式来说,只是简单的添加退出事件,没有额外的清空之前的事件。这样线程池只会在**所有事件全部结束**后才真正的被销毁。 379 | 380 | 以下是具体的实现代码: 381 | 382 | ```cpp 383 | ThreadPool::~ThreadPool() 384 | { 385 | // 向任务队列中添加退出线程事件,注意上锁 386 | // 注意在 cond 使用之前一定要上 mutex 387 | { 388 | // 操作 task_queue_ 时一定要上锁 389 | MutexLockGuard guard(threadpool_mutex_); 390 | // 如果需要立即关闭当前的线程池,则 391 | if(shutdown_mode_ == IMMEDIATE_SHUTDOWN) 392 | // 先将当前队列清空 393 | while(!task_queue_.empty()) 394 | task_queue_.pop(); 395 | 396 | // 往任务队列中添加退出线程任务 397 | for(size_t i = 0; i < threadNum_; i++) 398 | { 399 | auto pthreadExit = [](void*) { pthread_exit(0); }; 400 | ThreadpoolTask task = { pthreadExit, nullptr }; 401 | task_queue_.push(task); 402 | } 403 | // 唤醒所有线程以执行退出操作 404 | threadpool_cond_.notifyAll(); 405 | } 406 | for(size_t i = 0; i < threadNum_; i++) 407 | { 408 | // 回收线程资源 409 | pthread_join(threads_[i], nullptr); 410 | } 411 | } 412 | ``` 413 | 414 | ### 7. 参考链接 415 | 416 | - [linyacool/WebServer - github](https://github.com/linyacool/WebServer) 417 | 418 | - [线程池原理及C语言实现线程池](https://blog.csdn.net/qq_36359022/article/details/78796784) 419 | 420 | ## 三、网络连接 421 | 422 | ### 1. 概述 423 | 424 | 执行一次完整的网络连接通常需要执行数个 **socket 类** 函数。 425 | 426 | 为了弄懂这些函数的使用,本人将在下面随着代码的编写,尽量讲解所使用到的函数内容。 427 | 428 | 注:以下部分主要参考自 **Linux/Posix manual page**(`man`指令真是一个好东西 XD)。 429 | 430 | ### 2. socket 431 | 432 | - socket 函数主要用于创建网络交互(communication)中的一个终端(endpoint),即创建一个 socket fd 文件描述符。 433 | 434 | - socket 函数的类型声明如下: 435 | 436 | ```cpp 437 | #include /* See NOTES */ 438 | #include 439 | 440 | // 执行成功则返回一个新文件描述符fd,失败则返回-1并设置errno 441 | int socket(int domain, int type, int protocol); 442 | ``` 443 | 444 | 其中,对于参数 domain,我们主要用到以下两种类型: 445 | 446 | - AF_UNIX / AF_LOCAL:本地通信,通常用于**进程间通信**。其通信不经过网卡,速度远远大于 AF_INET。 447 | - AF_INET: IPv4 协议通信,数据需要经过网卡。 448 | 449 | > IPv6 和 bluetooth 等类型暂且不表。比较诧异的是,AF_VSOCK用于虚拟机程序与宿主机进行通信。 450 | 451 | 对于参数 type,常用的主要有以下几种: 452 | 453 | - SOCK_STREAM: TCP 通信 454 | 455 | - SOCK_DGRAM: UDP 通信 456 | 457 | - SOCK_NONBLOCK: 设置非阻塞 socket。使用 or 运算符来附加属性 458 | 459 | > 与设置 O_NONBLOCK 至对应文件描述符操作**等同**。 460 | 461 | - SOCK_CLOEXEC: 设置若当前程序执行 exec 时,对应文件描述符将在子进程中给关闭。使用 or 运算符来附加属性 462 | 463 | > 与设置 FD_CLOEXEC 至对应文件描述符等同。 464 | 465 | 参数 protocol 通常用于指定某一个特定的套接字协议。如果给定协议系列只有一个协议可以支持特定的套接字类型,则 protocol 可以指定为0。但是若给定协议系列中可能存在多个可以支持套接字的类型,这时候就必须设置 protocol 以指定具体类型。 466 | 467 | - 简单举例:创建一个 IPv4 的 TCP 套接字(最常用) 468 | 469 | ```cpp 470 | int listen_fd = socket(AF_INET, SOCK_STREAM, 0); 471 | ``` 472 | 473 | ### 3. bind 474 | 475 | - 对于一个**新创建**的 socket(注意是**新创建**的),还没有任何的地址用于赋给该 socket。而 bind 函数就是用于赋以一个地址给该 socket。 476 | 477 | 需要注意的是:如果当前 socket 在执行 bind 前已经被使用,则**操作系统将会自动分配地址以及端口号**等等,这也是为什么一些网络程序向外通信时使用的端口号是随机的,因为操作系统会在后面调控。 478 | 479 | - bind 函数的声明如下: 480 | 481 | ```cpp 482 | #include /* See NOTES */ 483 | #include 484 | 485 | // 执行成功则返回0,失败则返回-1并设置errno 486 | int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 487 | ``` 488 | 489 | - 其中,**第一个参数sockfd** 用以传入目标 socket 文件描述符 490 | 491 | - 至于**第二个参数addr**,其中使用的规则与结构体,在地址族之间有所不同。 492 | - **第三个参数addrlen**用于表示**第二个参数addr**所指向结构体的size。 493 | 494 | 对于 AF_INET:所使用到的 address format 如下 495 | 496 | ```cpp 497 | #include // ! 注意头文件!! 498 | struct sockaddr_in { 499 | sa_family_t sin_family; /* address family: AF_INET */ 500 | in_port_t sin_port; /* port in network byte order */ 501 | struct in_addr sin_addr; /* internet address */ 502 | }; 503 | 504 | /* Internet address. */ 505 | struct in_addr { 506 | uint32_t s_addr; /* address in network byte order */ 507 | }; 508 | ``` 509 | 510 | 其中,sin_family 始终为 AF_INET;sin_port 设置为目标端口;sin_addr用以保存**监听目标的地址**。 511 | 512 | 这里多提一句,由于现代计算机可能有多张网卡,因此指定 sin_addr 可以使得只监听特定网卡的连接。如果想监听**全部网卡的连接**,则可以使用宏定义 **INADDR_ANY**(实际上就是 0.0.0.0)。 513 | 514 | ```cpp 515 | /* Address to accept any incoming messages. */ 516 | #define INADDR_ANY ((in_addr_t) 0x00000000) 517 | ``` 518 | 519 | 而如果绑定 127.0.0.1 回环地址,则**只能监听到主动发送至回环地址的请求**,其他发送到当前该机器但目标IP非回环地址的请求则不会被处理。 520 | 521 | > 注意:sin_port、sin_addr 变量都必须以**网络端序**来保存数据(即大端序)。 522 | > 523 | > socket提供了端序转换的一些函数,便于转换(其中,h表示host,n表示network)。 524 | > 525 | > ```cpp 526 | > /* Functions to convert between host and network byte order. 527 | > 528 | > Please note that these functions normally take `unsigned long int' or 529 | > `unsigned short int' values as arguments and also return them. But 530 | > this was a short-sighted decision since on different systems the types 531 | > may have different representations but the values are always the same. */ 532 | > 533 | > extern uint32_t ntohl (uint32_t __netlong) __THROW __attribute__ ((__const__)); 534 | > extern uint16_t ntohs (uint16_t __netshort) 535 | > __THROW __attribute__ ((__const__)); 536 | > extern uint32_t htonl (uint32_t __hostlong) 537 | > __THROW __attribute__ ((__const__)); 538 | > extern uint16_t htons (uint16_t __hostshort) 539 | > __THROW __attribute__ ((__const__)); 540 | > ``` 541 | > 542 | > 如果需要**网络端序IP地址<--->字符串**类型转变,则请参照以下函数: 543 | > 544 | > ```cpp 545 | > #include 546 | > #include 547 | > #include 548 | > 549 | > int inet_aton(const char *cp, struct in_addr *inp); 550 | > 551 | > in_addr_t inet_addr(const char *cp); 552 | > 553 | > in_addr_t inet_network(const char *cp); 554 | > 555 | > char *inet_ntoa(struct in_addr in); 556 | > 557 | > struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host); 558 | > 559 | > in_addr_t inet_lnaof(struct in_addr in); 560 | > 561 | > in_addr_t inet_netof(struct in_addr in); 562 | > ``` 563 | 564 | 对于一个 AF_NET 地址族来说,执行 bind 的一个简单例子如下: 565 | 566 | ```cpp 567 | // 绑定端口 568 | sockaddr_in server_addr; 569 | // 初始化一下 570 | memset(&server_addr, '\0', sizeof(server_addr)); 571 | // 设置一下基本操作 572 | server_addr.sin_family = AF_INET; 573 | server_addr.sin_port = htonl((unsigned short)port); 574 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 575 | 576 | // 试着bind 577 | if(bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) 578 | return -1; 579 | ``` 580 | 581 | ### 4. listen 582 | 583 | listen 函数将会使得传入的 socket fd 变为**等待连接状态**。该函数原型如下: 584 | 585 | ```cpp 586 | #include /* See NOTES */ 587 | #include 588 | 589 | // 成功则返回0,失败则返回-1并设置 errno 590 | int listen(int sockfd, int backlog); 591 | ``` 592 | 593 | 该函数主要有两个参数:参数 sockfd 传入目标 fd;backlog 指定最大**挂起连接**的等待队列长度,如果队列满了,则新连接将会被拒绝(ECONNREFUSED)。而对于某些特殊协议,将会在一段时间后重新发起连接。 594 | 595 | ### 5. accept 596 | 597 | - accept 函数将会取出 **listen_fd的挂起连接等待队列** 中的第一个连接,创建一个新的 socket fd(client fd),并将其返回。后续与该连接的交互都是通过该client fd 完成。 598 | - 需要注意的是, accept 会从 listen_fd 中取出挂起的连接,并尝试连接。一旦完成连接后,将会创建一个新的 client_fd。**原先的 listen_fd 不会有任何改变**。 599 | - 如果当前 listen_fd 为**阻塞式**的,则如果当前挂起连接等待队列中不存在任何连接,那么**执行 accept 时将阻塞**,直到有新连接的到来。 600 | 601 | - 该函数的原型如下: 602 | 603 | ```cpp 604 | #include /* See NOTES */ 605 | #include 606 | 607 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 608 | ``` 609 | 610 | - 第一个参数为传入的监听socket listen_fd 611 | - 第二个参数为指向 sockaddr 结构体的 一个指针,accept 函数将会把 **远程 socket 的address**写入目标结构体中 612 | - 第三个参数是一个**存放 sockaddr 结构体大小**的指针。 613 | 614 | ### 6. read/recv & write/send 615 | 616 | 由于 read/recv & write/send 操作涉及到**阻塞与非阻塞式读写**,因此我们需要额外对其做一些异常处理。 617 | 618 | > 需要注意的是,socket读写中,除了使用read/write以外,还可以使用专用于套接字通信的send/recv函数族等等。 619 | 620 | #### a. 错误码 621 | 622 | 该类函数中**最常返回的错误**为 **EINTR** 以及 **EAGAIN**,其他错误暂时忽略。其中: 623 | 624 | - **EINTR**:该错误常见于**阻塞式**的操作,提示当前操作被**中断**。 625 | 626 | 如果一个进程在一个慢系统调用中阻塞时,捕获到信号并执行完信号处理例程返回时,这个系统调用将**不再被阻塞,而是被中断**,返回 EINTR。 627 | 628 | 对于读写函数来说,当返回这类错误时,最常用的做法就是**重新执行**目标函数。 629 | 630 | - **EAGAIN**:该错误常见于**非阻塞式**的操作,提示用户稍后再**重新执行**。 631 | 632 | 例如: 633 | 634 | - 当以**非阻塞**方式大量发送数据时,如果缓冲区爆满,则产生 Resource temporarily unavailable的错误(资源暂时不可用),并返回 EAGAIN。 635 | - 当以**非阻塞**模式下读取数据,如果多次读取数据但没有数据可读,则此时不会阻塞等待数据,而是直接返回 EAGAIN 636 | 637 | 对于 read 函数来说,由于数据取决于**远程**,因此当接收到 EAGAIN 时终止读取,直接返回; 638 | 639 | 但对于 write 函数来说,由于数据取决于**当前服务器**,因此可以继续循环写入,直至数据完全写入。 640 | 641 | 对于 recv/send 函数来说,与 read/write 相比,将会额外多出部分专用于 socket 的错误码,例如 ECONNREFUSED、EPIPE 以及 ECONNRESET 等等。出于调试目的,在实现 读写函数的 wrapper时,将这两类读写函数全部集成在 wrapper中,并用一个bool参数来控制启用 read/write 还是 recv/send 函数。 642 | 643 | #### b. 阻塞/非阻塞 读取 644 | 645 | 对于读取操作来说,阻塞读取和非阻塞读取又有所不同: 646 | 647 | - 当有数据到来时,阻塞和非阻塞的实现相同,都是读取数据并**立即返回**。 648 | - 但是当没有数据到来时,由于非阻塞读取时会返回 EAGAIN 错误,因此可以**立即返回**;而阻塞读取此时就必须阻塞,直到数据到来才返回。 649 | 650 | 在具体实现 读取操作的wrapper函数时,同样使用一个bool参数来控制是否是阻塞/非阻塞读取。 651 | 652 | #### c. 返回值 653 | 654 | read/recv & write/send 函数的返回值 655 | 656 | - 若为负数则说明存在错误 657 | - 若为0则说明**连接中断** 658 | - 若为正数则该数为成功读取/发送的字节数 659 | 660 | #### d. 最终实现的代码 661 | 662 | 综上所述,read/recv 函数重新实现的 wrapper 如下: 663 | 664 | ```cpp 665 | ssize_t readn(int fd, void*buf, size_t len, bool isBlock, bool isRead) 666 | { 667 | // 这里将 void* 转换成 char* 是为了在下面进行自增操作 668 | char *pos = (char*)buf; 669 | size_t leftNum = len; 670 | ssize_t readNum = 0; 671 | while(leftNum > 0) 672 | { 673 | ssize_t tmpRead = 0; 674 | // 尝试循环读取,如果报错,则进行判断 675 | // 注意, read 的返回值为0则表示读取到 EOF,是正常现象 676 | if(isRead) 677 | tmpRead = read(fd, pos, leftNum); 678 | else 679 | tmpRead = recv(fd, pos, leftNum, (isBlock ? 0 : MSG_DONTWAIT)); 680 | 681 | if(tmpRead < 0) 682 | { 683 | if(errno == EINTR) 684 | tmpRead = 0; 685 | // 如果始终读取不到数据,则提前返回,因为这个取决于远程 fd,无法预测要等多久 686 | else if (errno == EAGAIN) 687 | return readNum; 688 | else 689 | return -1; 690 | } 691 | // 读取的0,则说明远程连接已被关闭 692 | if(tmpRead == 0) 693 | break; 694 | readNum += tmpRead; 695 | pos += tmpRead; 696 | 697 | // 如果是阻塞模式下,并且读取到的数据较小,则说明数据已经全部读取完成,直接返回 698 | if(isBlock && static_cast(tmpRead) < leftNum) 699 | break; 700 | 701 | leftNum -= tmpRead; 702 | } 703 | return readNum; 704 | } 705 | ``` 706 | 707 | write/send 函数的 wrapper 同理: 708 | 709 | ```cpp 710 | ssize_t writen(int fd, void*buf, size_t len, bool isWrite) 711 | { 712 | // 这里将 void* 转换成 char* 是为了在下面进行自增操作 713 | char *pos = (char*)buf; 714 | size_t leftNum = len; 715 | ssize_t writtenNum = 0; 716 | while(leftNum > 0) 717 | { 718 | ssize_t tmpWrite = 0; 719 | 720 | if(isWrite) 721 | tmpWrite = write(fd, pos, leftNum); 722 | else 723 | tmpWrite = send(fd, pos, leftNum, 0); 724 | 725 | // 尝试循环写入,如果报错,则进行判断 726 | if(tmpWrite < 0) 727 | { 728 | // 与read不同的是,如果 EAGAIN,则继续重复写入,因为写入操作是有Server这边决定的 729 | if(errno == EINTR || errno == EAGAIN) 730 | tmpWrite = 0; 731 | else 732 | return -1; 733 | } 734 | if(tmpWrite == 0) 735 | break; 736 | writtenNum += tmpWrite; 737 | pos += tmpWrite; 738 | leftNum -= tmpWrite; 739 | } 740 | return writtenNum; 741 | } 742 | ``` 743 | 744 | ### 7. 建立连接 745 | 746 | 综上各类函数的分析,现在我们可以为服务器端开启一个**监听套接字**,并等待客户端连接: 747 | 748 | ```cpp 749 | int socket_bind_and_listen(int port) 750 | { 751 | int listen_fd = 0; 752 | // 开始创建 socket, 注意这是阻塞模式的socket 753 | // AF_INET : IPv4 Internet protocols 754 | // SOCK_STREAM : TCP socket 755 | if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 756 | return -1; 757 | 758 | // 绑定端口 759 | sockaddr_in server_addr; 760 | // 初始化一下 761 | memset(&server_addr, '\0', sizeof(server_addr)); 762 | // 设置一下基本操作 763 | server_addr.sin_family = AF_INET; 764 | server_addr.sin_port = htons((unsigned short)port); 765 | server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 766 | // 端口复用 767 | int opt = 1; 768 | if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) 769 | return -1; 770 | // 试着bind 771 | if(bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) 772 | return -1; 773 | // 试着listen, 设置最大队列长度为 1024 774 | if(listen(listen_fd, 1024) == -1) 775 | return -1; 776 | 777 | return listen_fd; 778 | } 779 | ``` 780 | 781 | 注意到这部分代码,设置**端口复用**属性: 782 | 783 | ```cpp 784 | // 端口复用 785 | int opt = 1; 786 | if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) 787 | return -1; 788 | ``` 789 | 790 | 正常来说,对于某个网络程序,一个端口只能绑定一个套接字,别的套接字无法使用这个端口。而如果需要让同一程序的不同套接字绑定统一端口,则需要设置**端口复用**属性。 791 | 792 | > 不过在当前WebServer-1.0版本中,设置端口复用貌似是不必要的,就算删除也无伤大雅。 793 | 794 | ### 8. 忽略 SIGPIPE 信号 795 | 796 | **SIGPIPE 信号**将在远程连接被中断时发出。默认的处理例程是**终止程序**,而这很明显不是我们所期望的处理方式。因此我们必须设置 WebServer 忽视 SIGPIPE 信号,以免被意外终止。 797 | 798 | ```cpp 799 | void handleSigpipe() 800 | { 801 | struct sigaction sa; 802 | memset(&sa, '\0', sizeof(sa)); 803 | sa.sa_handler = SIG_IGN; 804 | sa.sa_flags = 0; 805 | if(sigaction(SIGPIPE, &sa, NULL) == -1) 806 | LOG(ERROR) << "Ignore SIGPIPE failed! " << strerror(errno) << std::endl; 807 | } 808 | ``` 809 | 810 | ## 四、日志输出 811 | 812 | WebServer-1.0版本中实现的输出功能较为简单,只将信息输出到终端的stdout、stderr,没有建立日志文件。 813 | 814 | 具体实现如下: 815 | 816 | ```cpp 817 | /** 818 | * @brief 输出信息相关宏定义与函数 819 | * 使用 `LOG(INFO) << "msg";` 形式以执行信息输出. 820 | * @note 注意: 该宏功能尚未完备,多线程下使用LOG宏将会导致输出数据混杂 821 | */ 822 | #define INFO 1 /* 普通输出 */ 823 | #define ERROR 2 /* 错误输出 */ 824 | #define LOG(x) logmsg(x) /* 调用输出函数 */ 825 | 826 | std::ostream& logmsg(int flag) 827 | { 828 | // 输出信息时,设置线程互斥 829 | // 获取线程 TID 830 | long tid = syscall(SYS_gettid); 831 | if(flag == ERROR) 832 | { 833 | std::cerr << tid << ": [ERROR]\t"; 834 | return std::cerr; 835 | } 836 | else if(flag == INFO) 837 | { 838 | std::cout << tid << ": [INFO]\t"; 839 | return std::cout; 840 | } 841 | else 842 | { 843 | logmsg(ERROR) << "错误的 LOG 选择" << std::endl; 844 | abort(); 845 | } 846 | } 847 | ``` 848 | 849 | 如果有信息需要输出到终端,则按照以下调用方式使用即可,简单方便: 850 | 851 | ```cpp 852 | LOG(INFO) << "my msg" << std::endl; 853 | ``` 854 | 855 | 输出信息时,会自动将当前子线程的 LWP 号以及信息类型(INFO/ERROR)输出,例如: 856 | 857 | ![image-20210512133247864](https://kiprey.github.io/2021/05/WebServer-1/image-20210512133247864.png) 858 | 859 | 需要注意的是,该输出功能**没有**设置多线程互斥,因此可能会造成输出格式异常,即多个线程同时使用LOG功能,输出的数据在终端上揉成一团,显示的不太雅观。 860 | 861 | ## 五、http 请求处理 862 | 863 | ### 1. 概述 864 | 865 | 当 Server 成功与 Client 建立连接后,Client 将会发送数据至 Server,此时 Server 就需要解析数据并进一步将目标数据传送回 Client。其中,http报文的解析和http header的处理便是重点。 866 | 867 | ### 2. 连接 868 | 869 | 当建立起一个新的客户端套接字(**client_fd**)后,目标事件将被放进事件队列中,并等待空闲线程处理。 870 | 871 | 而这里的事件便是以下函数: 872 | 873 | ```cpp 874 | void handlerConnect(void* arg) 875 | { 876 | int* fd_ptr = (int*)arg; 877 | int client_fd = *fd_ptr; 878 | delete fd_ptr; 879 | 880 | if(client_fd < 0) 881 | { 882 | LOG(ERROR) << "client_fd error in handlerConnect" << endl; 883 | return; 884 | } 885 | HttpHandler handler(client_fd); 886 | handler.RunEventLoop(); 887 | close(client_fd); 888 | } 889 | ``` 890 | 891 | 我们可以很容易的看到,该函数只做了几件事情: 892 | 893 | 1. 取出 client_fd 894 | 2. 初始化 HttpHandler 类实例 895 | 3. 调用 `HttpHandler::RunEventLoop` 函数 896 | 4. 最终释放 client_fd 897 | 898 | 这里的 **HttpHandler** 类,就是下文中的重点。 899 | 900 | HttpHandler 支持部分 HTTP/1.1 版本的特性——**持续连接**。默认情况下,执行其 RunEventLoop 成员函数时,将循环读取来自客户端的请求,处理并返回对应的响应报文。 901 | 902 | HttpHandler 的整体代码结构如下所示,主要是由多个成员函数以及少数几个成员变量组成。RunEventLoop 函数是启动整个处理请求循环的一个开关函数: 903 | 904 | ```cpp 905 | /** 906 | * @brief HttpHandler 类处理每一个客户端连接,并根据读入的http报文,动态返回对应的response 907 | * 其支持的 HTTP 版本为 HTTP/1.1 908 | * @note 该类只实现了部分异常处理,没有涵盖大部分的异常(不过暂时也够了) 909 | */ 910 | class HttpHandler 911 | { 912 | public: 913 | /** 914 | * @brief HttpHandler内部状态 915 | */ 916 | enum ERROR_TYPE { 917 | ERR_SUCCESS = 0, // 无错误 918 | ERR_READ_REQUEST_FAIL, // 读取请求数据失败 919 | ERR_NOT_IMPLEMENTED, // 不支持一些特定的请求操作,例如 Post 920 | ERR_HTTP_VERSION_NOT_SUPPORTED, // 不支持当前客户端的http版本 921 | ERR_INTERNAL_SERVER_ERR, // 程序内部错误 922 | ERR_CONNECTION_CLOSED, // 远程连接已关闭 923 | ERR_BAD_REQUEST, // 用户的请求包中存在错误,无法解析 924 | ERR_SEND_RESPONSE_FAIL // 响应包发送失败 925 | }; 926 | 927 | /** 928 | * @brief 显式指定 client fd 929 | * @param fd 连接的 fd, 初始值为 -1 930 | */ 931 | explicit HttpHandler(int fd = -1); 932 | 933 | /** 934 | * @brief 释放所有 HttpHandler 所使用的资源 935 | * @note 注意,不会主动关闭 client_fd 936 | */ 937 | ~HttpHandler(); 938 | 939 | /** 940 | * @brief 为当前连接启动事件循环 941 | * @note 1. 在执行事件循环开始之前,一定要设置 client fd 942 | * 2. 异常处理不完备 943 | */ 944 | void RunEventLoop(); 945 | 946 | // 只有getFd,没有setFd,因为Fd必须在创造该实例时被设置 947 | int getClientFd() { return client_fd_; } 948 | 949 | private: 950 | const size_t MAXBUF = 1024; 951 | 952 | int client_fd_; 953 | // http 请求包的所有数据 954 | string request_; 955 | // http 头部 956 | unordered_map headers_; 957 | 958 | // 请求方式 959 | string method_; 960 | // 请求路径 961 | string path_; 962 | // http版本号 963 | string http_version_; 964 | // 是否是 `持续连接` 965 | // NOTE: 为了防止bug的产生,对于每一个类中的isKeepAlive_来说, 966 | // 值只能从 true -> false,而不能再次从 false -> true 967 | bool isKeepAlive_; 968 | 969 | // 当前解析读入数据的位置 970 | /** 971 | * NOTE: 该成员变量只在 972 | * readRequest -> parseURI -> parseHttpHeader -> RunEventLoop 973 | * 内部中使用 974 | */ 975 | size_t pos_; 976 | 977 | /** 978 | * @brief 将当前client_fd_对应的连接信息,以 LOG(INFO) 的形式输出 979 | */ 980 | void printConnectionStatus(); 981 | 982 | /** 983 | * @brief 从client_fd_中读取数据至 request_中 984 | * @return 0 表示读取成功, 其他则表示读取过程存在错误 985 | * @note 内部函数recvn在错误时会产生 errno 986 | */ 987 | ERROR_TYPE readRequest(); 988 | 989 | /** 990 | * @brief 从0位置处解析 请求方式\URI\HTTP版本等 991 | * @return 0 表示成功解析, 其他则表示解析过程存在错误 992 | */ 993 | ERROR_TYPE parseURI(); 994 | 995 | /** 996 | * @brief 从request_中的pos位置开始解析 http header 997 | * @return 0 表示成功解析, 其他则表示解析过程存在错误 998 | */ 999 | ERROR_TYPE parseHttpHeader(); 1000 | 1001 | /** 1002 | * @brief 发送响应报文给客户端 1003 | * @param responseCode http 状态码, http报文第二个字段 1004 | * @param responseMsg http 报文第三个字段 1005 | * @param responseBodyType 返回的body类型,即 Content-type 1006 | * @param responseBody 返回的body内容 1007 | * @return 0 表示成功发送, 其他则表示发送过程存在错误 1008 | */ 1009 | ERROR_TYPE sendResponse(const string& responseCode, const string& responseMsg, 1010 | const string& responseBodyType, const string& responseBody); 1011 | 1012 | /** 1013 | * @brief 发送错误信息至客户端 1014 | * @param errCode 错误http状态码 1015 | * @param errMsg 错误信息, http报文第三个字段 1016 | * @return 0 表示成功发送, 其他则表示发送过程存在错误 1017 | */ 1018 | ERROR_TYPE handleError(const string& errCode, const string& errMsg); 1019 | 1020 | /** 1021 | * @brief 将传入的字符串转义成终端可以直接显示的输出 1022 | * @param str 待输出的字符串 1023 | * @return 转义后的字符串 1024 | * @note 是将 '\r' 等无法在终端上显示的字符,转义成 "\r"字符串 输出 1025 | */ 1026 | string escapeStr (const string& str); 1027 | }; 1028 | ``` 1029 | 1030 | ### 3. 错误类型 1031 | 1032 | HttpHandler 中实现了以下错误类型: 1033 | 1034 | ```cpp 1035 | /** 1036 | * @brief HttpHandler内部状态 1037 | */ 1038 | enum ERROR_TYPE { 1039 | ERR_SUCCESS = 0, // 无错误 1040 | ERR_READ_REQUEST_FAIL, // 读取请求数据失败 1041 | ERR_NOT_IMPLEMENTED, // 不支持一些特定的请求操作,例如 Post 1042 | ERR_HTTP_VERSION_NOT_SUPPORTED, // 不支持当前客户端的http版本 1043 | ERR_INTERNAL_SERVER_ERR, // 程序内部错误 1044 | ERR_CONNECTION_CLOSED, // 远程连接已关闭 1045 | ERR_BAD_REQUEST, // 用户的请求包中存在错误,无法解析 1046 | ERR_SEND_RESPONSE_FAIL // 响应包发送失败 1047 | }; 1048 | ``` 1049 | 1050 | 除了第一种 `ERR_SUCCESS` 表示**无错误**以外,其余的错误类型都有对应的错误处理方式,例如**终止连接**或者**向客户端发送一个特定的响应报文**,我们将在下面的内容中提到这些错误处理方式。 1051 | 1052 | ### 4. 读取请求数据 1053 | 1054 | 当远程客户端发送数据至服务器端时,无论传来的是什么数据,首先要做的就是将数据从缓存中读取并保存至自己的缓冲区内。读取时需要明确一点:使用**阻塞方式**读取。因为每个客户端连接都是由单独的线程进行处理的,倘若服务器端没有将所有的请求数据全部读完,那么自然就无法继续执行下去。 1055 | 1056 | 同时还需要明确一点的是,调用 readn 函数读取数据时,有可能客户端传来的数据较多,使得读取到的字节数刚好等于传入 readn 的最大缓冲区大小,那么此时就必须保存并继续循环读取,因为这里可能还有一部分数据没有读取完成,仍然需要继续读取。只有当 readn 函数返回的值小于传入的最大缓冲区大小,才能说明来自客户端的数据已经全部读取完成。此时就可以退出*读取请求函数*。 1057 | 1058 | 最后,readn 函数可能会因为出错、远程连接中断等意外情况返回负数,因此这里需要额外写一点错误处理,返回对应原因的错误枚举 ERR_READ_REQUEST_FAIL 或者 ERR_CONNECTION_CLOSED 等等。 1059 | 1060 | 综上所述,最终实现的代码如下所示: 1061 | 1062 | ```cpp 1063 | HttpHandler::ERROR_TYPE HttpHandler::readRequest() 1064 | { 1065 | // 清除之前的数据 1066 | request_.clear(); 1067 | pos_ = 0; 1068 | 1069 | char buffer[MAXBUF]; 1070 | 1071 | // 循环阻塞读取 ------------------------------------------ 1072 | for(;;) 1073 | { 1074 | ssize_t len = readn(client_fd_, buffer, MAXBUF, true, true); 1075 | if(len < 0) 1076 | return ERR_READ_REQUEST_FAIL; 1077 | /** 1078 | * 如果此时没读取到信息并且之前已经读取过信息了,则直接返回. 1079 | * 这里需要注意,有些连接可能会提前连接过来,但是不会马上发送数据.因此需要阻塞等待 1080 | * 这里有个坑点: chromium在每次刷新过后,会额外开一个连接,用来缩短下次发送请求的时间 1081 | * 也就是说这里大概率会出现空连接,即连接到了,但是不会马上发送数据,而是等下一次的请求. 1082 | * 1083 | * 如果读取到的字节数为0,则说明远程连接已经被关闭. 1084 | */ 1085 | else if(len == 0) 1086 | { 1087 | // 对于已经读取完所有数据的这种情况 1088 | if(request_.length() > 0) 1089 | // 直接停止读取 1090 | break; 1091 | // 如果此时既没读取到数据,之前的 request_也为空,则表示远程连接已经被关闭 1092 | else 1093 | return ERR_CONNECTION_CLOSED; 1094 | } 1095 | // 将读取到的数据组装起来 1096 | string request(buffer, buffer + len); 1097 | request_ += request; 1098 | 1099 | // 由于当前的读取方式为阻塞读取,因此如果读取到的数据已经全部读取完成,则直接返回 1100 | if(static_cast(len) < MAXBUF) 1101 | break; 1102 | } 1103 | return ERR_SUCCESS; 1104 | } 1105 | ``` 1106 | 1107 | ### 5. 解析URI 1108 | 1109 | 接下来是 HTTP 请求报文的解析,我们先简单看看 请求报文的格式: 1110 | 1111 | ![image-20210507204906032](https://kiprey.github.io/2021/05/WebServer-1/image-20210507204906032.png) 1112 | 1113 | 首先,我们需要从报文中获取第一个以 `\r\n`结尾的行,并从这行中解析出**请求方法**、**目标URL**以及**HTTP版本**。任何一种因为错误报文格式所导致的解析失败,都是 ERR_BAD_REQUEST 错误。 1114 | 1115 | 其次,目前 WebServer-1.0 版本只支持 GET 的请求方法,倘若识别到其他请求方法都会返回 ERR_NOT_IMPLEMENTED 错误。 1116 | 1117 | 由于请求 URL 可能是一个文件夹地址,而不是文件。因此如果URL指向的是一个文件夹,那么我们就必须在这个URL地址后添加`/index.html`字符串,使得请求的目标地址一定是一个文件(即便该文件可能不存在)。 1118 | 1119 | 最后,目前的 WebServer-1.0版本只支持 HTTP/1.0 和 HTTP/1.1 版本,因此如果识别到了其他的 HTTP版本,则马上返回 ERR_HTTP_VERSION_NOT_SUPPORTED 错误。 1120 | 1121 | 综上所述,最后实现的代码如下所示: 1122 | 1123 | ```cpp 1124 | HttpHandler::ERROR_TYPE HttpHandler::parseURI() 1125 | { 1126 | if(request_.empty()) return ERR_BAD_REQUEST; 1127 | 1128 | size_t pos1, pos2; 1129 | 1130 | pos1 = request_.find("\r\n"); 1131 | if(pos1 == string::npos) return ERR_BAD_REQUEST; 1132 | string&& first_line = request_.substr(0, pos1); 1133 | // a. 查找get 1134 | pos1 = first_line.find(' '); 1135 | if(pos1 == string::npos) return ERR_BAD_REQUEST; 1136 | method_ = first_line.substr(0, pos1); 1137 | 1138 | string output_method = "Method: "; 1139 | if(method_ == "GET") 1140 | output_method += "GET"; 1141 | else 1142 | return ERR_NOT_IMPLEMENTED; 1143 | LOG(INFO) << output_method << endl; 1144 | 1145 | // b. 查找目标路径 1146 | pos1++; 1147 | pos2 = first_line.find(' ', pos1); 1148 | if(pos2 == string::npos) return ERR_BAD_REQUEST; 1149 | 1150 | // 获取path时,注意去除 path 中的第一个斜杠 1151 | pos1++; 1152 | path_ = first_line.substr(pos1, pos2 - pos1); 1153 | // 如果 path 为空,则添加一个 . 表示当前文件夹 1154 | if(path_.length() == 0) 1155 | path_ += "."; 1156 | 1157 | // 判断目标路径是否是文件夹 1158 | struct stat st; 1159 | if(stat(path_.c_str(), &st) == 0) 1160 | { 1161 | // 如果试图打开一个文件夹,则添加 index.html 1162 | if (S_ISDIR(st.st_mode)) 1163 | path_ += "/index.html"; 1164 | } 1165 | 1166 | LOG(INFO) << "Path: " << path_ << endl; 1167 | 1168 | // c. 查看HTTP版本 1169 | // NOTE 这里只支持 HTTP/1.0 和 HTTP/1.1 1170 | pos2++; 1171 | http_version_ = first_line.substr(pos2, first_line.length() - pos2); 1172 | LOG(INFO) << "HTTP Version: " << http_version_ << endl; 1173 | 1174 | // 检测是否支持客户端 http 版本 1175 | if(http_version_ != "HTTP/1.0" && http_version_ != "HTTP/1.1") 1176 | return ERR_HTTP_VERSION_NOT_SUPPORTED; 1177 | // 设置只在 HTTP/1.1时 允许 持续连接 1178 | if(http_version_ != "HTTP/1.1") 1179 | isKeepAlive_ = false; 1180 | 1181 | // 更新pos_ 1182 | pos_ = first_line.length() + 2; 1183 | return ERR_SUCCESS; 1184 | } 1185 | ``` 1186 | 1187 | ### 6. 解析 HTTP header 1188 | 1189 | 从HTTP报文第二行开始,每个以 `\r\n`为结尾的一行数据中,都有一个 `key: value`的键值对(header最后一行除外)。因此我们需要继续遍历请求报文的数据,将每个 HTTP header 存入数据结构中。如果解析报文的时候出现错误,则返回 ERR_BAD_REQUEST 错误。 1190 | 1191 | 这里有个点需要注意:HTTP/1.1默认支持**持续连接**,因此 HttpHandler 的成员变量 isKeepAlive\_ 默认为 true。但如果客户端中存在这样的 http header `Connection: close`,则说明当前连接并非**持续性**的,因此处理完当前 http 请求后必须马上断开连接。所以当我们接收到了`Connection: close`这样的http header时,必须设置 isKeepAlive\_ 变量为 false。 1192 | 1193 | 综上所述,最终实现的代码如下: 1194 | 1195 | ```cpp 1196 | HttpHandler::ERROR_TYPE HttpHandler::parseHttpHeader() 1197 | { 1198 | // 清除之前的 http header 1199 | headers_.clear(); 1200 | 1201 | size_t pos1, pos2; 1202 | for(pos1 = pos_; 1203 | (pos2 = request_.find("\r\n", pos1)) != string::npos; 1204 | pos1 = pos2 + 2) 1205 | { 1206 | string&& header = request_.substr(pos1, pos2 - pos1); 1207 | // 如果遍历到了空头,则表示http header部分结束 1208 | if(header.size() == 0) 1209 | break; 1210 | pos1 = header.find(' '); 1211 | if(pos1 == string::npos) return ERR_BAD_REQUEST; 1212 | // key处减去1是为了消除key里的最后一个冒号字符 1213 | string&& key = header.substr(0, pos1 - 1); 1214 | // key 转小写 1215 | transform(key.begin(), key.end(), key.begin(), ::tolower); 1216 | // 获取 value 1217 | string&& value = header.substr(pos1 + 1); 1218 | 1219 | LOG(INFO) << "HTTP Header: [" << key << " : " << value << "]" << endl; 1220 | 1221 | headers_[key] = value; 1222 | } 1223 | // 获取header完成后,处理一下 Connection 头 1224 | auto conHeaderIter = headers_.find("connection"); 1225 | if(conHeaderIter != headers_.end()) 1226 | { 1227 | string value = conHeaderIter->second; 1228 | transform(value.begin(), value.end(), value.begin(), ::tolower); 1229 | if(value != "keep-alive") 1230 | isKeepAlive_ = false; 1231 | } 1232 | // 判断处理空 header 条目的 \r\n 1233 | if((request_.size() < pos1 + 2) || (request_.substr(pos1, 2) != "\r\n")) 1234 | return ERR_BAD_REQUEST; 1235 | 1236 | pos_ = pos1 + 2; 1237 | return ERR_SUCCESS; 1238 | } 1239 | ``` 1240 | 1241 | ### 7. 发送响应报文 1242 | 1243 | http响应报文格式如下所示: 1244 | 1245 | ![image-20210507205312687](https://kiprey.github.io/2021/05/WebServer-1/image-20210507205312687.png) 1246 | 1247 | 照着这个报文格式,照葫芦画瓢构建一个报文并将其发送至客户端即可。 1248 | 1249 | 这里要注意一点,当前连接是否继续保持**取决于 isKeepAlive_ 变量**。具体实现如下所示: 1250 | 1251 | ```cpp 1252 | HttpHandler::ERROR_TYPE HttpHandler::sendResponse(const string& responseCode, const string& responseMsg, 1253 | const string& responseBodyType, const string& responseBody) 1254 | { 1255 | stringstream sstream; 1256 | sstream << "HTTP/1.1" << " " << responseCode << " " << responseMsg << "\r\n"; 1257 | sstream << "Connection: " << (isKeepAlive_ ? "Keep-Alive" : "Close") << "\r\n"; 1258 | sstream << "Server: WebServer/1.0" << "\r\n"; 1259 | sstream << "Content-length: " << responseBody.size() << "\r\n"; 1260 | sstream << "Content-type: " << responseBodyType << "\r\n"; 1261 | sstream << "\r\n"; 1262 | sstream << responseBody; 1263 | 1264 | string&& response = sstream.str(); 1265 | ssize_t len = writen(client_fd_, (void*)response.c_str(), response.size()); 1266 | 1267 | // 输出返回的数据 1268 | LOG(INFO) << "<<<<- Response Packet ->>>> " << endl; 1269 | LOG(INFO) << "{" << escapeStr(response) << "}" << endl; 1270 | 1271 | if(len < 0 || static_cast(len) != response.size()) 1272 | return ERR_SEND_RESPONSE_FAIL; 1273 | return ERR_SUCCESS; 1274 | } 1275 | ``` 1276 | 1277 | ### 8. 错误处理 1278 | 1279 | 当 handlerError 错误处理函数被调用时,在该函数内部将简单构建一个 html 错误提示页面,并将该页面发送至远程客户端。具体实现如下所示: 1280 | 1281 | ```cpp 1282 | HttpHandler::ERROR_TYPE HttpHandler::handleError(const string& errCode, const string& errMsg) 1283 | { 1284 | string errStr = errCode + " " + errMsg; 1285 | string responseBody = 1286 | "" 1287 | "" + errStr + "" 1288 | "" + errStr + 1289 | "
Kiprey's Web Server" 1290 | "" 1291 | ""; 1292 | return sendResponse(errCode, errMsg, "text/html", responseBody); 1293 | } 1294 | ``` 1295 | 1296 | ### 9. 事件循环(重要) 1297 | 1298 | HttpHandler 中的 RunEventLoop 函数维护了整个连接的事件循环。具体操作如下: 1299 | 1300 | - 首先,由于 HTTP/1.1 协议支持 持续连接,因此控制流将会进入一个 while 循环,循环进行**读取请求并发送响应**这样的过程。 1301 | - while 循环内部中,首先读取来自客户端的数据,之后进行 URI 与 http header 的解析。如果上面中任何一步存在错误,则发送对应的错误页面给客户端,或者退出循环断开连接。 1302 | - 如果上述步骤没有错误,则打开目标文件,将文件数据通过 mmap 函数映射到内存,读取并发送至远程客户端。而如果目标文件不存在,则返回 404 错误;文件映射失败则返回 500 错误。 1303 | - 最后返回 while 循环头部,继续等待新的请求报文。 1304 | 1305 | ```cpp 1306 | void HttpHandler::RunEventLoop() 1307 | { 1308 | ERROR_TYPE err_ty; 1309 | LOG(INFO) << "------------------- New Connection -------------------" << endl; 1310 | 1311 | // 输出连接 1312 | printConnectionStatus(); 1313 | 1314 | // 持续连接 1315 | while(isKeepAlive_) 1316 | { 1317 | LOG(INFO) << "<<<<- Request Packet ->>>> " << endl; 1318 | // 从socket读取请求数据, 如果读取失败,或者断开连接 1319 | // NOTE 这里的 readRequest 必须完整读取整个 http 报文 1320 | if((err_ty = readRequest()) != ERR_SUCCESS) 1321 | { 1322 | if(err_ty == ERR_READ_REQUEST_FAIL) 1323 | LOG(ERROR) << "Read request failed ! " << strerror(errno) << endl; 1324 | else if(err_ty == ERR_CONNECTION_CLOSED) 1325 | LOG(INFO) << "Socket(" << client_fd_ << ") was closed." << endl; 1326 | else 1327 | assert(0 && "UNREACHABLE"); 1328 | // 断开连接 1329 | break; 1330 | } 1331 | LOG(INFO) << "{" << escapeStr(request_) << "}" << endl; 1332 | 1333 | // 解析信息 ------------------------------------------ 1334 | LOG(INFO) << "<<<<- Request Info ->>>> " << endl; 1335 | 1336 | // 1. 先解析第一行 1337 | if((err_ty = parseURI()) != ERR_SUCCESS) 1338 | { 1339 | if(err_ty == ERR_NOT_IMPLEMENTED) 1340 | { 1341 | LOG(ERROR) << "Request method is not implemented." << endl; 1342 | handleError("501", "Not Implemented"); 1343 | } 1344 | else if(err_ty == ERR_HTTP_VERSION_NOT_SUPPORTED) 1345 | { 1346 | LOG(ERROR) << "Request HTTP Version Not Supported." << endl; 1347 | handleError("505", "HTTP Version Not Supported"); 1348 | } 1349 | else if(err_ty == ERR_BAD_REQUEST) 1350 | { 1351 | LOG(ERROR) << "Bad Request." << endl; 1352 | handleError("400", "Bad Request"); 1353 | } 1354 | else 1355 | assert(0 && "UNREACHABLE"); 1356 | continue; 1357 | } 1358 | // 2. 解析每一条http header 1359 | if((err_ty = parseHttpHeader()) != ERR_SUCCESS) 1360 | { 1361 | if(err_ty == ERR_BAD_REQUEST) 1362 | { 1363 | LOG(ERROR) << "Bad Request." << endl; 1364 | handleError("400", "Bad Request"); 1365 | } 1366 | else 1367 | assert(0 && "UNREACHABLE"); 1368 | continue; 1369 | } 1370 | // 3. 输出剩余的 HTTP body 1371 | LOG(INFO) << "HTTP Body: {" 1372 | << escapeStr(request_.substr(pos_, request_.length() - pos_)) 1373 | << "}" << endl; 1374 | 1375 | // 发送目标数据 ------------------------------------------ 1376 | 1377 | // 试图打开一个文件 1378 | int file_fd; 1379 | if((file_fd = open(path_.c_str(), O_RDONLY, 0)) == -1) 1380 | { 1381 | // 如果打开失败,则返回404 1382 | LOG(ERROR) << "File [" << path_ << "] open failed ! " << strerror(errno) << endl; 1383 | handleError("404", "Not Found"); 1384 | continue; 1385 | } 1386 | else 1387 | { 1388 | // 获取目标文件的大小 1389 | struct stat st; 1390 | if(stat(path_.c_str(), &st) == -1) 1391 | { 1392 | LOG(ERROR) << "Can not get file [" << path_ << "] state ! " << endl; 1393 | handleError("500", "Internal Server Error"); 1394 | continue; 1395 | } 1396 | // 读取文件, 使用 mmap 来高速读取文件 1397 | void* addr = mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, file_fd, 0); 1398 | // 记得关闭文件描述符 1399 | close(file_fd); 1400 | // 异常处理 1401 | if(addr == MAP_FAILED) 1402 | { 1403 | LOG(ERROR) << "Can not map file [" << path_ << "] -> mem ! " << endl; 1404 | handleError("500", "Internal Server Error"); 1405 | continue; 1406 | } 1407 | // 将数据从内存页存入至 responseBody 1408 | char* file_data_ptr = static_cast(addr); 1409 | string responseBody(file_data_ptr, file_data_ptr + st.st_size); 1410 | // 记得删除内存 1411 | int res = munmap(addr, st.st_size); 1412 | if(res == -1) 1413 | LOG(ERROR) << "Can not unmap file [" << path_ << "] <-> mem ! " << endl; 1414 | // 获取 Content-type 1415 | string suffix = path_; 1416 | // 通过循环找到最后一个 dot 1417 | size_t dot_pos; 1418 | while((dot_pos = suffix.find('.')) != string::npos) 1419 | suffix = suffix.substr(dot_pos + 1); 1420 | 1421 | // 发送数据 1422 | if(sendResponse("200", "OK", MimeType::getMineType(suffix), responseBody) != ERR_SUCCESS) 1423 | LOG(ERROR) << "Send Response failed !" << endl; 1424 | } 1425 | } 1426 | LOG(INFO) << "------------------ Connection Closed ------------------" << endl; 1427 | } 1428 | ``` 1429 | 1430 | ## 六、编译 & 调试 1431 | 1432 | 最后简单说说编译和调试。使用`make`命令即可构建带有调试信息的 WebServer 二进制文件。这里的makefile是直接抄自 [linyacool/WebServer 中的 makefile](https://github.com/linyacool/WebServer/blob/master/old_version/old_version_0.1/Makefile),并在其基础之上,修改了编译优化选项为 `-O0`,以及设置编译时附带额外的调试信息`-g3 -ggdb3`。 1433 | 1434 | `-g`所携带的调试信息可以被多个调试器所共用,而`-ggdb`所携带的调试信息是专供 gdb 使用,两者不完全等同。`-ggdb3`的调试等级甚至可以调试**宏**。 1435 | 1436 | 这里的调试主要是使用 **gdb + pwndbg** 来完成(gdb 永远的神)。因为多线程程序在gdb下调试非常的方便,它可以很快的切换线程上下文(使用`info `)以及栈帧上下文(使用`f `);而且临时查看 `errno` 以及临时调用 `strerror(errno)`查看错误信息等等都非常地方便。 1437 | -------------------------------------------------------------------------------- /docs/img/image-20210512133857068.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiprey/WebServer/6c391c4ce0422664a0bb4c3e2bac08eaf3217a98/docs/img/image-20210512133857068.png -------------------------------------------------------------------------------- /docs/img/image-20211026112349.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiprey/WebServer/6c391c4ce0422664a0bb4c3e2bac08eaf3217a98/docs/img/image-20211026112349.png -------------------------------------------------------------------------------- /html/CGI/base64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiprey/WebServer/6c391c4ce0422664a0bb4c3e2bac08eaf3217a98/html/CGI/base64 -------------------------------------------------------------------------------- /html/CGI/base64script: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | /usr/bin/base64 3 | -------------------------------------------------------------------------------- /html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiprey/WebServer/6c391c4ce0422664a0bb4c3e2bac08eaf3217a98/html/favicon.ico -------------------------------------------------------------------------------- /html/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiprey/WebServer/6c391c4ce0422664a0bb4c3e2bac08eaf3217a98/html/img.jpg -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | Hello, Kiprey's Concurrent WebServer 1.0 ! 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Epoll.h" 10 | #include "HttpHandler.h" 11 | #include "Log.h" 12 | #include "ThreadPool.h" 13 | #include "Utils.h" 14 | 15 | using namespace std; 16 | 17 | /** 18 | * @brief 处理新的连接 19 | * @param epoll 存放新连接的Epoll类实例 20 | * @param listen_fd 新连接所对应的 listen 描述符 21 | */ 22 | void handleNewConnections(Epoll* epoll, int listen_fd, int* idle_fd) 23 | { 24 | // 注意:可能会有很多个 connect 动作,但只会有一个 event 25 | sockaddr_in client_addr; 26 | socklen_t client_addr_len = 0; 27 | 28 | /** 29 | * 如果 30 | * 1. accept 没有发生错误 31 | * 2. accppt 发生了 EINTR 错误 32 | * 3. accept 发生了 ECONNABORTED 错误(该错误是远程连接被中断) 33 | * 则重新循环. 其中第三点, 若发生了 aborted 错误,则继续循环接受下一个socket 的请求 34 | */ 35 | for(;;) { 36 | int client_fd = accept4(listen_fd, (sockaddr*)&client_addr, &client_addr_len, 37 | SOCK_NONBLOCK | SOCK_CLOEXEC); 38 | // accept 的错误处理 39 | if(client_fd == -1) { 40 | // 如果是因为一些无关的错误所阻断,则继续 accept 41 | if(errno == EINTR || errno == ECONNABORTED) 42 | continue; 43 | // 正常情况下,如果处理了所有的 accept后, errno == EAGAIN,则直接退出 44 | else if (errno == EAGAIN) 45 | break; 46 | // 如果由于文件描述符不够用了,则会返回 EMFILE,此时清空全部的尚未 accept 连接 47 | else if(errno == EMFILE) { 48 | int closed_conn_num = closeRemainingConnect(listen_fd, idle_fd); 49 | WARN("No reliable pipes in new connection, close %d conns", closed_conn_num); 50 | break; 51 | } 52 | // 如果是其他的错误,则输出信息 53 | else 54 | ERROR("Accept Error! (%s)", strerror(errno)); 55 | } 56 | // 如果 accept 正常 57 | else { 58 | /** 构建一个新的 HttpHandler,并放入 epoll 实例中 59 | * 注意这里使用了 ONESHOT, 每个套接字只会在 边缘触发,可读时处于就绪状态 60 | * 且每个套接字只会被一个线程处理 61 | * NOTE: 每个 client_fd 只会在 HttpHandler 中被 close + 下面的 timer 异常处理中被关闭 62 | * 每个 client_handler 也只会在 setConnectionClosed 之后, 执行完 RunEventLoop 函数结束时被释放 63 | * 每个 Timer 在此处创建, 在 HttpHandler 中被释放 64 | * 可以看出,现在指针已经满天飞了 2333 65 | */ 66 | Timer* timer = new Timer(TFD_NONBLOCK | TFD_CLOEXEC); 67 | // 如果timer创建失败,则清空当前所有尚未 accept 的连接,因为文件描述符满 68 | if(!timer->isValid()) 69 | { 70 | delete timer; 71 | // 直接关闭,告诉远程这里放不下了 72 | close(client_fd); 73 | 74 | int closed_conn_num = closeRemainingConnect(listen_fd, idle_fd); 75 | WARN("No reliable pipes in new connection, close %d conns", closed_conn_num); 76 | break; 77 | } 78 | HttpHandler* client_handler = new HttpHandler(epoll, client_fd, timer); 79 | /** 80 | * @brief EPOLLRDHUP EPOLLHUP 不同点,前者是半关闭连接时出发,后者是完全关闭后触发 81 | * @ref tcp 源码 https://elixir.bootlin.com/linux/v4.19/source/net/ipv4/tcp.c#L524 82 | * @ref TCP: When is EPOLLHUP generated? https://stackoverflow.com/questions/52976152/tcp-when-is-epollhup-generated 83 | */ 84 | bool ret1 = epoll->add(client_fd, client_handler->getClientEpollEvent(), client_handler->getClientTriggerCond()); 85 | // 设置定时器以边缘-单次触发方式 86 | bool ret2 = epoll->add(timer->getFd(), client_handler->getTimerEpollEvent(), client_handler->getTimerTriggerCond()); 87 | assert(ret1 && ret2); 88 | // 输出相关信息 89 | printConnectionStatus(client_fd, "-------->>>>> New Connection"); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * @brief 处理旧的连接 96 | * @param epoll 被唤醒的 epoll 97 | * @param fd 被唤醒的文件描述符 98 | * @param thread_pool 目标线程池 99 | * @param event 待处理的事件 100 | */ 101 | void handleOldConnection(Epoll* epoll, int fd, ThreadPool* thread_pool, epoll_event* event) 102 | { 103 | EpollEvent* curr_epoll_event = static_cast(event->data.ptr); 104 | HttpHandler* handler = static_cast(curr_epoll_event->ptr); 105 | // 处理一些错误事件 106 | int events_ = event->events; 107 | // 如果远程关闭了当前连接 108 | if ((events_ & EPOLLHUP) || (events_ & EPOLLRDHUP)) { 109 | INFO("Socket(%d) was closed by peer.", handler->getClientFd()); 110 | // 当某个 handler 无法使用时,一定要销毁内存 111 | delete handler; 112 | // 之后重新开始遍历新的事件. 113 | return; 114 | } 115 | // 如果当前 socket / events_ 存在错误 116 | else if ((events_ & EPOLLERR) || !(events_ & EPOLLIN)) { 117 | ERROR("Socket(%d) error.", handler->getClientFd()); 118 | // 当某个 handler 无法使用时,一定要销毁内存 119 | delete handler; 120 | // 之后重新开始遍历新的事件. 121 | return; 122 | } 123 | // 如果没有错误发生 124 | // 1. 如果是因为超时 125 | if(fd == handler->getTimer()->getFd()) 126 | { 127 | INFO("-------->>>>> " 128 | "New Message: socket(%d) - timerfd(%d) timeout." 129 | " <<<<<--------", 130 | handler->getClientFd(), handler->getTimer()->getFd()); 131 | /* 这里不像下面需要从epoll中关闭 timer fd 132 | 因为 timer将会在HttpHandler的析构函数中从epoll内部删除 */ 133 | // 删除 handler 实例 134 | delete handler; 135 | } 136 | // 2. 如果不是因为超时 137 | else 138 | { 139 | // 则从epoll中关闭 timer, 防止条件竞争 140 | epoll->modify(handler->getTimer()->getFd(), nullptr, 0); 141 | // 并将其放入线程池中并行执行 142 | thread_pool->appendTask( 143 | // lambda 函数 144 | [](void* arg) 145 | { 146 | HttpHandler* handler = static_cast(arg); 147 | 148 | printConnectionStatus(handler->getClientFd(), "-------->>>>> New Message"); 149 | 150 | // 如果出现无法恢复的错误,则直接释放该实例以及对应的 client_fd 151 | if(!(handler->RunEventLoop())) 152 | delete handler; 153 | }, 154 | handler); 155 | } 156 | } 157 | 158 | int main(int argc, char* argv[]) 159 | { 160 | // 获取传入的参数 161 | if (argc < 2 || !isNumericStr(argv[1])) 162 | { 163 | ERROR("usage: %s []", argv[0]); 164 | exit(EXIT_FAILURE); 165 | } 166 | int port = atoi(argv[1]); 167 | if(argc > 2) 168 | HttpHandler::setWWWPath(argv[2]); 169 | // 输出当前进程的 PID,便于调试 170 | INFO("PID: %d", getpid()); 171 | // 忽略 SIGPIPE 信号 172 | handleSigpipe(); 173 | // 创建线程池 174 | ThreadPool thread_pool(8); 175 | 176 | // 空闲 fd,用于关闭溢出的文件描述符 177 | int idle_fd = open("/dev/null", O_RDONLY | O_CLOEXEC); 178 | int listen_fd = -1; 179 | if((listen_fd = socket_bind_and_listen(port)) == -1) 180 | { 181 | ERROR("Bind %d port failed ! (%s)", port, strerror(errno)); 182 | exit(EXIT_FAILURE); 183 | } 184 | 185 | // 声明一个 epoll 实例,该实例将在整个main函数结束时被释放 186 | Epoll epoll(EPOLL_CLOEXEC); 187 | assert(epoll.isEpollValid()); 188 | // 将 listen_fd 添加进 epoll 实例 189 | EpollEvent* listen_epollevent = new EpollEvent{listen_fd, nullptr}; 190 | epoll.add(listen_fd, listen_epollevent, EPOLLET | EPOLLIN); 191 | 192 | // 开始事件循环 193 | for(;;) 194 | { 195 | // 阻塞等待新的事件 196 | int event_num = epoll.wait(-1); 197 | // 如果报错 198 | if(event_num < 0) 199 | { 200 | // 表示该错误一定不是因为无效的 epoll 导致的 201 | assert(event_num != -2); 202 | // 如果只是中断,则直接重新循环 203 | if(errno == EINTR) 204 | continue; 205 | // 如果是其他异常,则输出信息并终止. 206 | else 207 | FATAL("epoll_wait fail! (%s)", strerror(errno)); 208 | } 209 | // 如果什么也没读到,则可能是因为 signal 导致的.例如 SIGINT XD 210 | else if(event_num == 0) 211 | continue; 212 | 213 | // 遍历获取到的事件 214 | for(int i = 0; i < event_num; i++) 215 | { 216 | // 获取事件相关的信息 217 | epoll_event&& event = epoll.getEvent(static_cast(i)); 218 | EpollEvent* curr_epoll_event = static_cast(event.data.ptr); 219 | 220 | int fd = curr_epoll_event->fd; 221 | 222 | // 如果当前文件描述符是 listen_fd, 则建立连接 223 | if(fd == listen_fd) 224 | handleNewConnections(&epoll, listen_fd, &idle_fd); 225 | else 226 | handleOldConnection(&epoll, fd, &thread_pool, &event); 227 | } 228 | } 229 | delete listen_epollevent; 230 | 231 | return 0; 232 | } -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | SOURCE := $(wildcard *.cpp) 2 | INCLUDE := 3 | OBJS := $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCE))) 4 | 5 | TARGET := WebServer 6 | CC := g++ 7 | LIBS := -lpthread 8 | CFLAGS := -std=c++11 -g3 -ggdb3 -Wall -O0 -fsanitize=address $(INCLUDE) 9 | CXXFLAGS:= $(CFLAGS) 10 | 11 | .PHONY : objs clean veryclean rebuild all 12 | all : $(TARGET) 13 | objs : $(OBJS) 14 | rebuild: veryclean all 15 | clean : 16 | rm -rf *.o 17 | veryclean : clean 18 | rm -rf $(TARGET) 19 | 20 | $(TARGET) : $(OBJS) 21 | $(CC) $(CXXFLAGS) -o $@ $(OBJS) $(LDFLAGS) $(LIBS) 22 | --------------------------------------------------------------------------------