目錄
如何拿到瀏覽器發來的http請求
如何給瀏覽器發送響應
響應基本原理
給瀏覽器發送一個網頁作為響應
給瀏覽器發送一個圖片作為響應
接下來我們要做什么
完善業務邏輯
瀏覽器如何訪問特定文件
訪問根目錄下的文件
訪問子文件夾下的文件
習慣性目錄結構
GET請求帶參
服務器長久運行
認識POST請求
總結
逐步構建高性能http服務器
CGI機制
什么是CGI機制
單進程版本http服務器
? ? ? ? http_conn.hpp
? ? ? ? http_conn.cpp
????????LOG.hpp
? ? ? ? LOG.cpp
? ? ? ? main.cpp
? ? ? ? wwwroot/calculator.hpp
? ? ? ? wwwroot/index.html
? ? ? ? wwwroot/404.html
? ? ? ? wwwroot/500.html
宏觀邏輯如下圖:
代碼分析
signal(SIGPIPE, SIG_IGN)在干什么
cgi機制中為什么要建立父子通訊管道
cgi機制中為什么要使用環境變量
cgi機制中為什么GET請求通過環境變量傳遞參數,POST請求通過管道傳遞參數
cgi機制中為什么要進行重定向到標準輸入輸出
問題分析
發送問題
接收問題
效率問題
多線程版本http服務器
? ? ? ? main.cpp
問題分析
多進程版本的http服務器
代碼分析
問題分析
錯誤檢查
ls -l /proc/pid/fd
? ? ? ? 錯誤一:下圖已更正
????????錯誤二:下圖已更正
線程池版本的http服務器
? ? ? ? 目錄結構
? ? ? ? http_conn.cpp
? ? ? ? http_conn.hpp
? ? ? ? LOG.hpp
? ? ? ? LOG.cpp
? ? ? ? common.hpp
? ? ? ? locker.hpp
????????threadpool.hpp
? ? ? ? main.cpp
進程池版本的cgi服務器
? ? ? ? 服務器目錄結構
? ? ? ? ?processpool.h
? ? ? ?server.cpp
? ? ? ? ?cgi.h
?????????test.cpp
????????進程池代碼分析
? ? ? ? 此處的進程池單例真的是個單例嗎?
? ? ? ? 子進程繼承的文件描述符
? ? ? ? 統一事件源
? ? ? ? SIGCHLD信號
? ? ? ? 父子通信管道
總結
逐步構建高性能聊天室服務器
服務器邏輯:
基于epoll的多線程版本聊天室服務器
? ? ? ? 服務器目錄結構
? ? ? ? 客戶端目錄結構
? ? ? ? chatserver.cpp
? ? ? ? chatclient.cpp
? ? ? ? common.hpp
? 代碼分析
????????ET + EPOLLONESHOT模式下
問題分析
線程池版本的聊天室服務器
????????服務器目錄結構
? ? ? ? chatserver.cpp
? ? ? ? threadpool.hpp
總結
如何拿到瀏覽器發來的http請求
? ? ? ? 我們將設計一個簡單的程序,讓大家先看到如何從服務器拿到瀏覽器發來的http請求。示例代碼如下:
#include <stdlib.h>// atoi
#include <iostream>
#include <sys/types.h> // socket()
#include <sys/socket.h> // socket()
#include <assert.h> // assert()
#include <arpa/inet.h> // struct sockaddr_in
#include <string.h> //bzero#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;return 0;
}
? ? ? ? 在瀏覽器輸入 服務器ip:8080? 后輸出如下
GET / HTTP/1.1
Host: 49.233.89.193:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
? ? ? ? 此時我們已經拿到了http請求的全部內容,如果大家不清楚http請求的格式,應當先去學習一下。
如何給瀏覽器發送響應
響應基本原理
? ? ? ? 瀏覽器給我們發送請求的大部分目的都是為了得到資源,我們接下來將圍繞這個話題來探討如何給瀏覽器發送一個網頁作為響應。
? ? ? ? 資源都在服務器的磁盤里,瀏覽器想要得到那個文件資源呢?
- 答案在請求行的url里,url是一個文件路徑,它告訴瀏覽器,我想要哪個資源
- 當在瀏覽器輸入框輸入 ip + : +?port后(49.233.89.193:8080),瀏覽器自動補全為?ip + : +?port + / (49.233.89.193:8080/)。? 即自動請求根目錄
- 這里的根目錄,指的是服務器運行的目錄
- 我們可以通過在輸入框輸入?ip + : +?port + / +資源名(49.233.89.193:8080/文件夾/index.html)來有目標的獲取特定資源文件
? ? ? ? 服務器發送給瀏覽器目標資源后,需要告訴瀏覽器:這個資源文件的類型和大小
-
這是為了瀏覽器去識別文件,然后正確的解釋文件。比如說服務器給瀏覽器發送了一個圖片,底層數據是二進制字節流,服務器就不能按照識別文本字符的方式去把字節流的一個個字節解釋成字符,而應該將其解釋成圖片。再比如服務器給瀏覽器發送了一個網頁,瀏覽器就應當將其解釋成網頁。
-
服務器通過在請求頭中添加Content-type字段,用于標識資源類型。例如網頁html文件對應“Content-Type: text/html"。對于更多資源類型,大家可以搜索Content-Type對照表查詢即可。
? ? ? ? 服務器發送給瀏覽器目標資源時,需要檢查一下該目標資源是否存在,是否可以被訪問
- ?服務器通過給瀏覽器發送 “狀態碼” 和? “狀態碼描述” 來說明瀏覽器請求的目標資源的情況。例如:“200 OK” 來表示資源正常,響應成功;“404? Not Found”來表示資源未找到,其余情況大家可以查詢http響應狀態碼和狀態碼描述表。
? ? ? ? 協議版本
- 瀏覽器給服務器發送協議版本號,來告訴服務器,你應該采用這種版本的規則,去理解去解析我給你發的這些信息。
- 服務器給瀏覽器發送協議版本號,來告訴瀏覽器,你應該采用這種版本的規則,去理解去解析我給你發的這些信息。
- 兩者意圖是一樣的。
- 在我們目前實際編寫服務器的過程中,協議版本號的存在感很低,我們只要加上這個固定信息即可。
給瀏覽器發送一個網頁作為響應
? ? ? ? 這里我們簡化一下,無論瀏覽器給我們發送什么文件,我們都給其回應一個我們準備的這個簡易index1.html文件。
? ? ? ? 訪問方式
- 在瀏覽器搜索框輸入:49.233.89.193:8080。注意你要輸入自己的服務器地址,或者本地127.0.0.1環回地址
- 每次運行,如果8080端口運行不起來,就換一個端口。
? ? ? ? index.html(放在服務器運行目錄下)
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF - 8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>簡單的HTML網頁</title>
</head><body><h1>歡迎來到我的網頁</h1><p>這是一個簡單的段落,用于展示基本的http響應流程。</p>
</body></html>
? ? ? ? 源文件
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;// 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open("index.html", O_RDONLY); //以只讀方式打開// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;// 構建響應狀態行std::string firsr_line;// 構建響應頭std::string header_line;firsr_line += "HTTP/1.1 200 OK\r\n";header_line += "Content-Length: " + std::to_string(file_size) + "\r\n";header_line += "Content-Type: text/html\r\n";header_line += "\r\n";// 構建響應正文,也就是index.html文件內容// 將文件內容讀取到用戶層發送緩沖區中char write_buffer[BUFFER_SIZE]; //在這里我們的index.html文件很小的,4096字節肯定裝的下了read(fd,write_buffer,file_size);// 將數據發送給瀏覽器send(connfd,firsr_line.c_str(),firsr_line.size(),0);send(connfd,header_line.c_str(),header_line.size(),0);send(connfd,write_buffer,file_size,0);close(fd);close(connfd);close(listenfd);return 0;
}
? ? ? ? 網頁結果
給瀏覽器發送一個圖片作為響應
? ? ? ? 1.jpg(跟程序放在同一個文件夾下)
? ? ? ? 源代碼
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';// cout << read_buffer;// 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open("1.jpg", O_RDONLY); //以只讀方式打開// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << file_size << endl;// 構建響應狀態行std::string firsr_line;// 構建響應頭std::string header_line;firsr_line += "HTTP/1.1 200 OK\r\n";header_line += "Content-Length: " + std::to_string(file_size) + "\r\n";header_line += "Content-Type: image/jpeg\r\n";header_line += "\r\n";// 構建響應正文,也就是1.jpg文件內容cout << header_line << endl;// 將文件內容讀取到用戶層發送緩沖區中// char write_buffer[BUFFER_SIZE]; //在這里我們的index.html文件很小的,4096字節肯定裝的下了char* p_jpg = new char[1024 * 1024];read(fd,p_jpg,file_size);// 將數據發送給瀏覽器send(connfd,firsr_line.c_str(),firsr_line.size(),0);send(connfd,header_line.c_str(),header_line.size(),0);cout << send(connfd,p_jpg,file_size,0) << endl;delete[] p_jpg;close(fd);close(connfd);close(listenfd);return 0;
}
? ? ? ? 不了解sendfile的可以看如下文章:
- 高級IO函數之sendfile_sendfile 函數-CSDN博客
接下來我們要做什么
? ? ? ? 剛才小試牛刀,我們用不到100行代碼,就足以構建一個邏輯閉環的http服務器。那么我們為什么還要去寫更復雜的服務器處理邏輯呢?總體可以分為兩個宏觀原因。
- 完善業務處理,能夠應對更多的業務場景。這部分不是我們關注的重點,但是如果我們不清晰業務邏輯,會讓我們在構建服務器時產生很多困擾。
- 提高服務器的性能。我們將編寫多版本的服務器并分析,逐步構建更高效更理想的服務器。
完善業務邏輯
? ? ? ? 在這部分我們將給大家演示說明瀏覽器與服務器之間如何交互。我們會用盡可能最簡單的代碼進行演示,以便大家能夠更輕松的理解交互邏輯。
瀏覽器如何訪問特定文件
訪問根目錄下的文件
? ? ? ? 文件夾結構如下:
?????????訪問方式如下:
- 在瀏覽器輸入框輸入:49.233.89.193:8080/index.html。便可以獲取根目錄下的index.html網頁文件。
- 在瀏覽器輸入框輸入:49.233.89.193:8080/1.jpg。便可以獲取根目錄下的1.jpg圖片文件。? ?
? ? ? ? 服務器代碼如下:
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE); // 讀緩沖區ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 請求行std::string request_header; // 請求頭std::string request_content; // 請求正文std::string request_method; // 請求方法std::string requset_url; // 請求的urlstd::string request_version; // 請求協議版本std::string suffix; // 目標資源后綴,用于構建響應Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法沒有請求正文,我們在本代碼中不構建請求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url = request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open(requset_url.c_str(), O_RDONLY); //以只讀方式打開cout << "fd: " << fd << endl;// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 構建響應狀態行std::string repose_line;// 構建響應頭std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 將文件內容讀取到用戶層發送緩沖區中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 將數據發送給瀏覽器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);close(listenfd);return 0;
}
訪問子文件夾下的文件
????????文件夾結構如下:
? ? ? ? 只需要創建source文件夾,然后把兩個資源文件移動到里面即可。代碼不需要重新編譯可直接運行。
習慣性目錄結構
? ? ? ? 通常我們不會把資源直接放在服務器運行目錄下,而是創建一個文件夾wwwroot,然后將資源放在這個文件夾下。如下圖所示
- 在瀏覽器輸入框輸入:49.233.89.193:8080/index.html。代表要獲取wwwroot目錄下的index.html網頁文件。
- 在瀏覽器輸入框輸入:49.233.89.193:8080/1.jpg。代表要獲取wwwroot目錄下的1.jpg圖片文件。
? ? ? ? 為此我們需要稍微改進一下源代碼
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE); // 讀緩沖區ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 請求行std::string request_header; // 請求頭std::string request_content; // 請求正文std::string request_method; // 請求方法std::string requset_url; // 請求的urlstd::string request_version; // 請求協議版本std::string suffix; // 目標資源后綴,用于構建響應Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法沒有請求正文,我們在本代碼中不構建請求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open(requset_url.c_str(), O_RDONLY); //以只讀方式打開cout << "fd: " << fd << endl;// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 構建響應狀態行std::string repose_line;// 構建響應頭std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 將文件內容讀取到用戶層發送緩沖區中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 將數據發送給瀏覽器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);close(listenfd);return 0;
}
GET請求帶參
??????????訪問方式如下:
- 在瀏覽器輸入框輸入:49.233.89.193:8080/index.html?x=1&y=2&z=3。便可以獲取根目錄下的index.html網頁文件,并將參數x=1&y=2&z=3傳遞給服務器。
- 在瀏覽器輸入框輸入:49.233.89.193:8080/1.jpg?x=1&y=2&z=3。便可以獲取根目錄下的1.jpg圖片文件,并將參數x=1&y=2&z=3傳遞給服務器。
- 以后我們約定根目錄就是wwwroot
? ? ? ? 參數與url一起形成一個大url字符串
? ? ? ? 我們拿到帶參GET請求,并打印一下看看,源代碼如下。我們用的是文章最開頭的程序:
#include <stdlib.h>// atoi
#include <iostream>
#include <sys/types.h> // socket()
#include <sys/socket.h> // socket()
#include <assert.h> // assert()
#include <arpa/inet.h> // struct sockaddr_in
#include <string.h> //bzero#define BUFFER_SIZE 4096using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );char read_buffer[BUFFER_SIZE];ssize_t bytes_read = recv(connfd,read_buffer,sizeof(read_buffer),0);read_buffer[bytes_read] = '\0';cout << read_buffer;return 0;
}
? ? ? ? 打印結果為:
GET /index.html?x=1&y=2&z=3 HTTP/1.1
Host: 49.233.89.193:8081
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.520.400 QQBrowser/19.2.6473.400
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Scheme: http
? ? ? ? 這些參數有什么用?
- 我們可以對參數做計算,然后把結果返回給瀏覽器,從而相當于創建了一個計算服務
- 我們可以將參數傳遞給cgi程序,讓cgi程序去處理。后文會詳細講解演示cgi機制
服務器長久運行
? ? ? ? 在上面的例子中,我們都是回應一次http請求后,就直接結束了。這顯然不符合服務器的運行邏輯。我們的服務器應當長久運行,持續響應多個http請求。
? ? ? ? 源代碼如下:
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );while(1){cout << "******************************************開始********************************************" << endl;struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE); // 讀緩沖區ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 請求行std::string request_header; // 請求頭std::string request_content; // 請求正文std::string request_method; // 請求方法std::string requset_url; // 請求的urlstd::string request_version; // 請求協議版本std::string suffix; // 目標資源后綴,用于構建響應Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;// GET方法沒有請求正文,我們在本代碼中不構建請求正文n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; // 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open(requset_url.c_str(), O_RDONLY); //以只讀方式打開cout << "fd: " << fd << endl;// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 構建響應狀態行std::string repose_line;// 構建響應頭std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 將文件內容讀取到用戶層發送緩沖區中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 將數據發送給瀏覽器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);cout << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
? ? ? ? 至此大家就可以多次訪問了。
認識POST請求
? ? ? ? 大家應當先簡單的自行了解一下,前端html文件中的form表單都是大概什么意思。這里我們重在講解交互邏輯。
? ? ? ?目錄結構
????????index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF - 8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>簡單的HTML網頁</title>
</head><body><form action="/1.jpg" method="POST">First name:<br><input type="text" name="firstname" value="Mickey"><br>Last name:<br><input type="text" name="lastname" value="Mouse"><br><br><input type="submit" value="Submit"></form> <p>如果您點擊提交,表單數據會被發送到名為 1.jpg 的頁面。</p>
</body></html>
? ? ? ? 說明
- action用于指定這次請求的目標資源
- method用于指定這次請求的方法
- name和value是正文數據
- 點擊submit按鈕瀏覽器會重新發送請求。請求方法由method指定;請求正文由name和value指定
? ? ? ? 操作方法
? ? ? ? 1.在瀏覽器輸入49.233.89.193:8080/index.html后來到這個頁面
? ? ? ? 2.你可以修改輸入框內容
? ? ? ? 3.點擊提交按鈕相當于再次向瀏覽器發送一個請求。action用于指定這次請求的目標資源。請求方法由method指定;請求正文由name和value指定。點擊提交
? ? ? ? 4.服務器會打印出這次請求的內容
? ? ? ? 源代碼如下
#include <stdlib.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <assert.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );struct linger tmp = { 1, 0 };int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret >= 0 );ret = listen( listenfd, 5 );assert( ret >= 0 );while(1){cout << "******************************************開始********************************************" << endl;struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );std::string read_buffer; read_buffer.resize(BUFFER_SIZE); // 讀緩沖區ssize_t bytes_read = recv(connfd,(void*)read_buffer.c_str(),BUFFER_SIZE,0);std::string request_line; // 請求行std::string request_header; // 請求頭std::string request_content; // 請求正文std::string request_method; // 請求方法std::string requset_url; // 請求的urlstd::string request_version; // 請求協議版本std::string suffix; // 目標資源后綴,用于構建響應Content-Typesize_t n1,n2;n1 = read_buffer.find('\r');request_line = read_buffer.substr(0,n1);cout << request_line << endl; n2 = read_buffer.rfind('\r');request_header = read_buffer.substr(n1+2,n2-n1);cout << request_header;n1 = request_line.find(' ');request_method = request_line.substr(0,n1);cout << "request_method: " << request_method << endl;n2 = request_line.find(' ', n1+1);requset_url += ROOT_PATH + request_line.substr(n1+2,n2-n1-2);cout << "requset_url: " << requset_url << endl; request_version = request_line.substr(n2+1);cout << "request_version: " << request_version << endl;suffix = requset_url.substr(requset_url.find('.')+1);cout << "suffix: " << suffix << endl; if(request_method == "POST") // 如果是POST請求則構建請求正文,GET請求沒有請求正文{request_content = read_buffer.substr(read_buffer.rfind('\n') + 1);cout << "request_content: " << request_content << endl; }// 打開這個文件,會創建一個文件描述符fd,并且內核為該fd關聯一個內核級緩沖區,里面放的是這個文件的內容int fd = open(requset_url.c_str(), O_RDONLY); //以只讀方式打開cout << "fd: " << fd << endl;// 獲取一下文件大小struct stat file_stat;fstat(fd, &file_stat);int file_size = file_stat.st_size;cout << "file_size: " << file_size << endl;// 構建響應狀態行std::string repose_line;// 構建響應頭std::string repose_header;repose_line += "HTTP/1.1 200 OK\r\n";repose_header += "Content-Length: " + std::to_string(file_size) + "\r\n";if(suffix == "jpg")repose_header += "Content-Type: image/jpeg\r\n";if(suffix == "html")repose_header += "Content-Type: text/html; charset=UTF-8\r\n";repose_header += "\r\n";cout << "repose_header: " << repose_header << endl;// 將文件內容讀取到用戶層發送緩沖區中char* p_file = new char[1024 * 1024];read(fd,p_file,file_size);// 將數據發送給瀏覽器cout << send(connfd,(void*)repose_line.c_str(),repose_line.size(),0) << endl;;cout << send(connfd,repose_header.c_str(),repose_header.size(),0) << endl;cout << send(connfd,p_file,file_size,0) << endl;delete[] p_file;close(fd);close(connfd);cout << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
總結
? ? ? ? 至此我們已經簡單認識了,網頁請求和響應的基本邏輯。接下來進入文章重點內容,逐步構建高性能的服務器。
逐步構建高性能http服務器
CGI機制
什么是CGI機制
- 瀏覽器與服務器交互大致可以總結為兩種邏輯。第一種邏輯:瀏覽器向服務器請求資源。第二種:瀏覽器給服務器發送數據,服務器fork()一個子進程去處理數據,子進程將處理結果返回給父進程,父進程返回給瀏覽器。第二種就是CGI機制。
- 為什么要有CGI機制:難道父進程不可以自己處理數據嗎,還偏偏非要fork一個子進程去處理數據。這大概有這么幾個原因:1.服務器業務邏輯與數據處理邏輯解耦,方便后臺人員的協同開發。2.服務器會處理很多類型數據,這些數據的處理邏輯不盡相同,那么要想完成數據處理任務,服務器的代碼就會非常龐大,編譯效率、運行效率都會受到影響,造成資源浪費。
- 稍后我們會在代碼中詳細講解CGI機制
單進程版本http服務器
? ? ? ? 經過上面的講解,我們已經明白了瀏覽器和服務器交互的大致邏輯,對于cgi機制我們會在本小節代碼中詳細分析。下面是一個完整的初級http服務器。
? ? ? ? http_conn.hpp
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include "LOG.hpp"
#define WEB_ROOT "wwwroot" // Web根目錄
#define HOME_PAGE "index.html" // 默認資源文件
#define BAD_REQUEST_PAGE "400.html" // 請求錯誤
#define NOT_FOUND_PAGE "404.html" // 資源不存在
#define SERVER_ERROR_PAGE "500.html" // 服務器出錯
#define SPACE " " // 空格
#define END_OF_LINE "\r\n" // 行尾回車換行enum StatusCode
{CLOSE = 0, // 連接關閉OK = 200, // 正常情況BAD_REQUEST = 400, // 請求錯誤NOT_FOUND = 404, // 資源不存在SERVER_ERROR = 500 // 服務器出錯
};static std::unordered_map<int, std::string> statusDescMap ={{200, "OK"},{400, "BAD_REQUEST"},{404, "NOT_FOUND"},{500, "SERVER_ERROR"},
};static std::unordered_map<std::string, std::string> suffixMap ={{"html","text/html; charset=UTF-8"},{"css", "text/css"},{"js", "application/javascript"},{"jpg", "image/jpeg"},{"xml", "application/xml"},{"cgi", "text/html; charset=UTF-8"},};class http_conn
{
private:int _sock; // 用于獲得這個客戶連接的socket
public:std::string _request_line; // 請求行std::vector<std::string> _request_v_header; // 請求頭數組std::unordered_map<std::string, std::string> _request_uomap_header; // 請求頭哈希表std::string _request_content; // 請求正文std::string _request_method; // 請求方法std::string _request_url; // 請求urlstd::string _request_url_arg; // 請求參數std::string _requst_version; // 請求版本std::string _request_suffix; // 資源類型
public:std::string _response_line; // 響應行std::vector<std::string> _response_v_header; // 響應頭數組std::string _response_blankline; // 響應空行std::string _response_content; // 響應正文
public:bool _cgi; // 是否采用cgi機制
public:// 構造函數http_conn(int sock) : _sock(sock), _cgi(false), _request_suffix("html"){}~http_conn(){if(_sock >= 0) close(_sock);}public:
void h_run()
{StatusCode ret = h_read();h_write(ret);
}private:
StatusCode h_read();
void h_write(StatusCode ret);private:// 構建請求行_request_line// 失敗則返回:CLOSE// 成功則返回:OKStatusCode h_build_reqline();// 構建請求方法_request_method// 失敗則返回:BAD_REQUEST// 成功則返回:OKStatusCode h_build_reqmethod();// 判斷請求方法是否正確// 我們只處理"GET""POST"請求// 其余請求返回:BAD_REQUEST// 這兩個請求返回:OKStatusCode h_parse_method();// 構建url,順帶著構建參數_request_url_arg// 失敗則返回:BAD_REQUEST// 成功則返回:OKStatusCode h_build_requrl();// 判斷url是否合法// 非法情況:資源不存在,資源無權讀,資源無權執行// 設置_cgi// 失敗則返回:NOT_FOUND// 成功則返回:OKStatusCode h_parse_url();StatusCode h_build_reqsuffix();// 構建_request_line// 任務不存在出錯情況// 只返回:OKStatusCode h_build_reqversion();// 構建請求頭數組_request_v_header// 讀取錯誤返回:CLOSE// 正確則返回:OKStatusCode h_build_reqvheader();// 構建請求頭哈希表_request_uomap_header// 請求頭格式錯誤則返回:BAD_REQUEST// 正確則返回:OKStatusCode h_build_requomapheader();// 構建請求正文_request_content// 讀取錯誤返回:CLOSE// 正確則返回:OKStatusCode h_build_reqcontent();// 執行cgi程序StatusCode h_cgi();private:void HandlerERROR(const std::string &page, int ret);void HandlerOK();private:// 從tcp接收緩沖區拿走一行數據// 這里我們阻塞式讀取,直到遇到 "\r\n"結束// 所以要么我們取走一行完整數據,要么對方關閉連接;h_recv_line才會返回int h_recv_line(int _sock, std::string &str);// 按照sep切分字符串bool cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep);
};
? ? ? ? http_conn.cpp
#include "http_conn.hpp"
// 從tcp接收緩沖區拿走一行數據
// 這里我們阻塞式讀取,直到遇到 "\r\n"結束
// 所以要么我們取走一行完整數據,要么對方關閉連接,要么讀取出錯;h_recv_line才會返回
int http_conn::h_recv_line(int _sock, std::string &str)
{char ch = 'X';while (ch != '\n'){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){if (ch == '\r'){recv(_sock, &ch, 1, MSG_PEEK);if (ch == '\n'){recv(_sock, &ch, 1, 0);}else{ch = '\n';}}str.push_back(ch);}else if (s == 0){return 0;}else{return -1;}}return str.size();
}// 按照sep切分字符串
bool http_conn::cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep)
{size_t pos = str.find(sep);if (pos != std::string::npos){leftOut = str.substr(0, pos);rightOut = str.substr(pos + sep.size());return true;}return false;
}// 構建請求行
// 失敗則返回:CLOSE
// 成功則返回:OK
StatusCode http_conn::h_build_reqline()
{if (h_recv_line(_sock, _request_line) > 0){_request_line.pop_back();LOG(INFO, "h_build_reqline success--->_request_line: " + _request_line);return OK;}else{LOG(ERROR, "h_build_reqline failed");return CLOSE;}
}// 構建請求方法
// 失敗則返回:BAD_REQUEST
// 成功則返回:OK
StatusCode http_conn::h_build_reqmethod()
{int n = _request_line.find(SPACE);if (n != std::string::npos){_request_method = _request_line.substr(0, n);LOG(INFO, "h_build_reqmethod success--->_request_method: " + _request_method);return OK;}else{LOG(ERROR, "_request_method failed");return BAD_REQUEST;}
}// 判斷請求方法是否正確
// 我們只處理"GET""POST"請求
// 其余請求返回:BAD_REQUEST
// 這兩個請求返回:OK
StatusCode http_conn::h_parse_method()
{if (_request_method == "GET" || _request_method == "POST"){LOG(INFO, "h_parse_method success");return OK;}else{LOG(ERROR, "h_parse_method false");return BAD_REQUEST;}
}// 構建url,順帶著構建參數_request_url_arg
// 失敗則返回:BAD_REQUEST
// 成功則返回:OK
StatusCode http_conn::h_build_requrl()
{int n1 = _request_line.find(SPACE);n1 = _request_line.find(SPACE, n1 + 1);if (n1 != std::string::npos){int n2 = _request_line.find('?');if (n2 != std::string::npos) // 這意味著帶參數{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n2 - _request_line.find(SPACE) - 1);_request_url_arg = _request_line.substr(n2 + 1, n1 - n2 - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: " + _request_url_arg);return OK;}else // 這意味著不帶參數{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n1 - _request_line.find(SPACE) - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: NO");return OK;}}else{LOG(ERROR, "h_build_requrl failed");return BAD_REQUEST;}
}// 判斷url是否合法
// 非法情況:資源不存在,資源無權讀,資源無權執行
// 設置_cgi
// 失敗則返回:NOT_FOUND
// 成功則返回:OK
StatusCode http_conn::h_parse_url()
{struct stat fileStat;if (stat(_request_url.c_str(), &fileStat)) // 文件不存在{LOG(ERROR, "path " + _request_url + " NOT_FOUND, err:" + std::string(strerror(errno)));return NOT_FOUND;}else{if (S_ISDIR(fileStat.st_mode)) // 文件存在但是是一個目錄,應該訪問其中的默認頁面{_request_url += '/';_request_url += HOME_PAGE;stat(_request_url.c_str(), &fileStat);}if (_request_method == "GET" && _request_url_arg.size() == 0) // 這代表想要獲取資源,應當對目標文件具有讀權限{_cgi = false;if (fileStat.st_mode & S_IRUSR || fileStat.st_mode & S_IRGRP || fileStat.st_mode & S_IROTH) // 是否具有讀權限{LOG(INFO, "h_parse_url success");return OK;}else{LOG(ERROR, "h_parse_url false NO read");return NOT_FOUND;}}else // 這代表想要運行cgi程序,應當具有執行權限{_cgi = true;if ((fileStat.st_mode & S_IXUSR) || (fileStat.st_mode & S_IXGRP) || (fileStat.st_mode & S_IXOTH)) // 是否具有執行權限{LOG(INFO, "_request_url: true");return OK;}else{LOG(ERROR, "_request_url: false NO executable");return NOT_FOUND;}}}
}// 構建資源類型
// 只返回OK
StatusCode http_conn::h_build_reqsuffix()
{int n = _request_url.find('.');if (n == std::string::npos){LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}else{_request_suffix = _request_url.substr(n + 1);LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}
}
// 構建_request_line
// 任務不存在出錯情況
// 只返回:OK
StatusCode http_conn::h_build_reqversion()
{_requst_version = _request_line.substr(_request_line.rfind(SPACE) + 1);LOG(INFO, "h_build_reqversion success--->_requst_version: " + _requst_version);return OK;
}// 構建請求頭數組_request_v_header
// 讀取錯誤返回:CLOSE
// 正確則返回:OK
StatusCode http_conn::h_build_reqvheader()
{std::vector<std::string> &header = _request_v_header;std::string tmp;while (true){int length = h_recv_line(_sock, tmp);if (length <= 0){LOG(ERROR, "h_build_reqheader failed");return CLOSE;}if (length == 1){break;}tmp.pop_back();header.push_back(tmp);tmp.clear();}LOG(INFO, "h_build_reqheader success");return OK;
}// 構建請求頭哈希表_request_uomap_header
// 請求頭格式錯誤則返回:BAD_REQUEST
// 正確則返回:OK
StatusCode http_conn::h_build_requomapheader()
{std::string key;std::string value;for (auto &str : _request_v_header) // 多行請求報頭接收到了vector中{// 請求報頭中的KV是以 冒號+空格 分隔的if (cut_string(str, key, value, ": ")){_request_uomap_header.insert({key, value});}else{LOG(ERROR, "h_build_requomapheader failed");return BAD_REQUEST;}}LOG(INFO, "h_build_requomapheader success");return OK;
}// 構建請求正文_request_content
// 讀取錯誤返回:CLOSE
// 正確則返回:OK
StatusCode http_conn::h_build_reqcontent()
{if(_request_method == "POST" && _request_uomap_header["Content-Length"] != "0"){cout << 66666<<endl;int content_len = std::stoi(_request_uomap_header["Content-Length"]);char ch = 0;while (content_len--){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){_request_content.push_back(ch);}else // 讀取正文對端寫關閉或者讀取失敗-------------------------------------{LOG(ERROR, "h_build_reqcontent failed");return CLOSE;}}LOG(INFO, "h_build_reqcontent success--->_request_content: " + _request_content);return OK;}else{LOG(INFO, "h_build_reqcontent sucess--->_request_content: NULL");return OK;}
}// 執行cgi程序
StatusCode http_conn::h_cgi()
{/* cgi整體流程:創建子進程,讓cgi進程替換掉子進程,父進程將request中的參數傳給cgi進程,cgi進程處理完后將結果返回給父進程 */// 走這里request一定是POST或者帶參的GET方法,那么此時request請求中的path就是所需要的可執行程序的路徑,此時就要讓這個可執行程序替換掉這里的子進程// auto &path = _request_url; // 可執行程序的路徑// 想要讓父子進程通信,直接用管道來實現int input[2]; // 站在父進程角度的輸入和輸出,父進程用input[0]讀,用output[1]寫int output[2];if (pipe(input) < 0) // 創建input管道{ // 創建input管道失敗LOG(ERROR, "create input pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}if (pipe(output) < 0) // 創建output管道{ // 創建output管道失敗LOG(ERROR, "create output pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}// 走到這里管道就創建好了,此時創建一個子進程就可以進行父子進程通信了// 創建子進程pid_t pid = fork();// 子進程// 設置環境變量:"QUERY_STRING="、"CONTENT_LENGTH="、"METHOD="// 重定向:將標準輸入文件描述符定向到管道output讀端output[0];將標準輸出文件描述符定向到管道input寫端input[1]// cgi程序替換子進程if (pid == 0){close(input[0]);close(output[1]);/* **************************************************設置環境變量********************************************** */if (_request_method == "GET"){std::string queryString_env = "QUERY_STRING=" + _request_url_arg;if (putenv((char *)queryString_env.c_str()) != 0){LOG(ERROR, "putenv queryString failed");exit(1);}}else if (_request_method == "POST"){std::string contentLength_env = "CONTENT_LENGTH=" + _request_uomap_header["Content-Length"];if (putenv((char *)contentLength_env.c_str()) != 0){LOG(ERROR, "POST method, putenv CONTENT_LENGTH failed");exit(1);}}std::string method_env = "METHOD=" + _request_method;if (putenv((char *)method_env.c_str()) != 0){LOG(ERROR, "putenv method failed");exit(1);}/* **************************************************設置環境變量********************************************** */dup2(output[0], 0);dup2(input[1], 1);close(output[0]);close(input[0]);execl(_request_url.c_str(), _request_url.c_str(), nullptr);LOG(ERROR, "execl failed");exit(1);}// 父進程else if (pid > 0){close(input[1]);close(output[0]);if (_request_method == "POST"){size_t total = 0, size_singleTime = 0;size_t size = _request_content.size();while (total < size){size_singleTime = write(output[1], (void *)(_request_content.c_str() + total), size - total);if (size_singleTime <= 0) // 給子進程發送數據失敗{int k = kill(pid, SIGTERM); // 給子進程發送結束信號if (k == 0){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else if (k == -1 && errno == ESRCH){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else{LOG(FATAL, "kill(pid, SIGTERM) failed");return SERVER_ERROR;}}else{total += size_singleTime;}}}// 發送完數據之后就接收CGI進程返回的結果char ch;while (read(input[0], &ch, 1) > 0){_response_content.push_back(ch);}cout << "-->" << "server get result:\n"<< _response_content << endl;int status = 0;pid_t _pid = waitpid(-1, &status, 0);if (pid == _pid){if (WIFEXITED(status)){if (WEXITSTATUS(status) != 0){cout << "exit code ::" << WEXITSTATUS(status) << endl;return BAD_REQUEST;}else{cout << "exit code ::" << WEXITSTATUS(status) << endl;return OK;}}else{LOG(WARNING, "WIFEXITED(status) is false");return SERVER_ERROR;}}else{LOG(WARNING, "waitpid is false");return SERVER_ERROR;}close(input[0]);close(output[1]);// 子進程替換之后會執行完畢,此時替換進程整個空間都會被回收,所以替換進程中可以不需要手動關input[1]和output[0]}else{LOG(WARNING, "fork is false");return SERVER_ERROR;}return OK;
}/* ****************************************************事務入口************************************************************ *//* 構建請求 */
StatusCode http_conn::h_read()
{StatusCode ret = OK;ret = h_build_reqline();if (ret != OK)return ret;ret = h_build_reqmethod();if (ret != OK)return ret;ret = h_parse_method();if (ret != OK)return ret;ret = h_build_requrl();if (ret != OK)return ret;ret = h_parse_url();if (ret != OK)return ret;ret = h_build_reqsuffix();if (ret != OK)return ret;ret = h_build_reqversion();if (ret != OK)return ret;ret = h_build_reqvheader();if (ret != OK)return ret;ret = h_build_requomapheader();if (ret != OK)return ret;ret = h_build_reqcontent();if (ret != OK)return ret;if (_cgi){ret = h_cgi();if (ret != OK)return ret;}return OK;
}void http_conn::HandlerERROR(const std::string &page, int ret)
{std::string pagePath = WEB_ROOT;pagePath += '/';pagePath += page;int fd = open(pagePath.c_str(), O_RDONLY);if (fd >= 0){/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(ret);_response_line += ' ';_response_line += statusDescMap[ret];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";struct stat st;stat(pagePath.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += "text/html; charset=UTF-8";tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;/* 發送狀態行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 發送響應頭 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 發送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 發送資源 */sendfile(_sock, fd, nullptr, st.st_size); // 發送響應正文}else{LOG(WARNING, "open false");}
}void http_conn::HandlerOK()
{if (!_cgi){int fd = open(_request_url.c_str(), O_RDONLY);if (fd > 0){/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";struct stat st;stat(_request_url.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;/* 發送狀態行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 發送響應頭 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 發送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 發送資源 */sendfile(_sock, fd, nullptr, st.st_size); // 發送響應正文}}else{/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";tmp += std::to_string(_response_content.size());tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;/* 發送狀態行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 發送響應頭 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 發送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 發送響應正文 */send(_sock, _response_content.c_str(), _response_content.size(), 0);}
}void http_conn::h_write(StatusCode ret)
{switch (ret){case OK:HandlerOK();break;case NOT_FOUND:HandlerERROR(NOT_FOUND_PAGE,ret);break;case BAD_REQUEST:HandlerERROR(BAD_REQUEST_PAGE,ret); break;case SERVER_ERROR:HandlerERROR(SERVER_ERROR_PAGE,ret); break;case CLOSE:close(_sock);_sock = -1;break;default:HandlerERROR(NOT_FOUND_PAGE,ret);break;}
}
????????LOG.hpp
#pragma once#include<iostream>
#include <string>using std::cout;
using std::cerr;
using std::endl;enum LogLevel
{INFO,WARNING,ERROR,FATAL
};// 宏替換能保證每次調用的地方都是對應的文件和行
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__) // 打印日志// 只聲明 Log 函數
void Log(const std::string& level, const std::string& message, const std::string& fileName, int line);
? ? ? ? LOG.cpp
#include "LOG.hpp"
#include <ctime>void Log(const std::string& level, const std::string& message, const std::string& fileName, int line)
{// 日志格式:// [level]<file:line>{日志內容}==>timetime_t tm = time(nullptr);cerr << "[" << level << "](" << fileName << ":" << line << ")" << "{" << message << "}==> " << ctime(&tm);
}
? ? ? ? main.cpp
#include <arpa/inet.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cout;
using std::cin;
using std::cerr;
using std::endl;int main( int argc, char* argv[] )
{if( argc < 2 ){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi( argv[1] );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );if(listenfd < 0){LOG(FATAL,"socket() failed");}int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );if(ret < 0){LOG(FATAL,"bind() failed");}ret = listen( listenfd, 5 );if(ret < 0){LOG(FATAL,"listen() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );signal(SIGPIPE, SIG_IGN);while(1){cerr << "******************************************開始********************************************" << endl;int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );if( connfd < 0){LOG(INFO,"accept success");}http_conn* p_http = new http_conn(connfd);p_http->h_run();delete p_http;cerr << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
? ? ? ? wwwroot/calculator.hpp
#pragma once#include<iostream>
using std::cout;
using std::cerr;
using std::endl;#include<unistd.h>const static size_t INFO = 1;
const static size_t ERROR = 2;#define CGILOG(level, str) CgiLog(#level, str)void CgiLog(const std::string& level, const std::string& message)
{cerr << "CGI " << level << "==>" << message << endl;
}// 獲取請求方法
char* GetMethod()
{char* method = getenv("METHOD"); // 獲取請求方法,用來判斷如何獲取參數if(method == nullptr){CGILOG(ERROR, "get method env failed");exit(2);}CGILOG(INFO, "get method:" + std::string(method));return method;
}// 獲取參數
void GetQueryString(const std::string &method, std::string& queryString)
{CGILOG(INFO, "GetQueryString function start");// 分兩種,一種POST的,一種帶參GET的if(method == "GET"){ // 從環境變量中獲得參數CGILOG(INFO, "method is GET");queryString = getenv("QUERY_STRING");}else if(method == "POST"){ // 從管道(0)中獲取參數,可以直接用cin,但是可能會有回車啥的,所以就用readCGILOG(INFO, "method is POST");size_t contentLength = std::stoul(getenv("CONTENT_LENGTH"));char ch;while(contentLength--){read(0, &ch, 1); // ---------------------------這里未處理read返回值queryString.push_back(ch);}}else{CGILOG(ERROR, "unknow method[" + method + ']');exit(3);}CGILOG(INFO, "get query string:" + queryString);
}// 切分函數
bool CutString(const std::string& str, const std::string& sep, std::string& leftStr, std::string& rightStr)
{size_t pos = str.find(sep);if(pos != std::string::npos){leftStr = str.substr(0, pos);rightStr = str.substr(pos + sep.size());return true;}else{return false;}
}
? ? ? ??wwwroot/calculator.cpp
#include "calculator.hpp"int main()
{std::string query_string;GetQueryString(GetMethod(), query_string); //獲取參數//以&為分隔符將兩個操作數分開std::string str1;std::string str2;CutString(query_string, "&", str1, str2);//以=為分隔符分別獲取兩個操作數的值std::string name1;std::string value1;CutString(str1, "=", name1, value1);std::string name2;std::string value2;CutString(str2, "=", name2, value2);//處理數據int x = atoi(value1.c_str());int y = atoi(value2.c_str());std::cout<<"<html>";std::cout<<"<head><meta charset=\"UTF-8\"></head>";std::cout<<"<body>";std::cout<<"<h3>"<<x<<" + "<<y<<" = "<<x+y<<"</h3>";std::cout<<"<h3>"<<x<<" - "<<y<<" = "<<x-y<<"</h3>";std::cout<<"<h3>"<<x<<" * "<<y<<" = "<<x*y<<"</h3>";if(x % y){double dx = x;double dy = y;std::cout<<"<h3>"<<x<<" / "<<y<<" = "<<dx / dy<<"</h3>"; //除0后cgi程序崩潰,屬于異常退出}else {std::cout<<"<h3>"<<x<<" / "<<y<<" = "<<x/y<<"</h3>"; //除0后cgi程序崩潰,屬于異常退出}std::cout<<"</body>";std::cout<<"</html>";return 0;
}
? ? ? ? wwwroot/index.html
<html><head><meta charset="UTF-8"></head><body><h1>404 NOT_FOUND:對不起,你所查找的資源不存在</h1></body>
</html>
? ? ? ? wwwroot/400.html
<html><head><meta charset="UTF-8"></head><body><h1>400 BAD_REQUEST:錯誤的請求</h1></body>
</html>
? ? ? ? wwwroot/404.html
<html><head><meta charset="UTF-8"></head><body><h1>404 NOT_FOUND:對不起,你所查找的資源不存在</h1></body>
</html>
? ? ? ? wwwroot/500.html
<html><head><meta charset="UTF-8"></head><body><h1>500 SERVER_ERROR:服務器內部錯誤</h1></body>
</html>
宏觀邏輯如下圖:
代碼分析
signal(SIGPIPE, SIG_IGN)在干什么
? ? ? ? 忽略了SIGPIPE信號,當服務器向一個已經關閉的文件描述或者讀端關閉的管道中寫入數據時,就會觸發這個信號。服務器收到該信號后的默認行為是終止進程,這顯然對服務器來講是很不友好的。當客戶端與服務器建立連接之后,服務器處理完請求,正在給客戶端寫入響應,此時直接退出客戶端或者突然客戶端斷電等就會使服務器觸發這個信號,此時服務器應當忽略這個信號,進入下一輪處理邏輯。
? ? ? ? 不了解signal的可以看如下文章:
- 進程信號之signal系統調用-CSDN博客
- 進程信號之sigaction系統調用_sigaction函數功能說明-CSDN博客
- 進程信號之進程的信號掩碼,信號集,sigprocmask函數-CSDN博客
? ? ? ? 我們可以通過recv函數來判斷,客戶端是否關閉連接。
cgi機制中為什么要建立父子通訊管道
? ? ? ? 這是為了將服務器從瀏覽器接收到的有效參數傳遞給cgi程序,cgi程序將處理結果返回給服務器。
cgi機制中為什么要使用環境變量
? ? ? ? 這還是為了給cgi程序傳遞信息,比如請求方法,比如GET請求的參數
cgi機制中為什么GET請求通過環境變量傳遞參數,POST請求通過管道傳遞參數
- GET請求的參數一般較少。通過環境變量傳遞即可
- POST請求的參數一般比較大。通過管道傳遞才行
cgi機制中為什么要進行重定向到標準輸入輸出
????????進程替換后,原進程的代碼段等全部銷毀,內核數據結構如文件描述符保留。可是此時cgi程序無法得知與父進程通信的管道文件描述符是幾號。而標準輸入輸出規定就是0和1,將標準輸入輸出定向到父子通訊管道后,cgi程序只需總是從標準輸入中取數據,將處理結果寫入標準輸出即可,實現服務器主邏輯與cgi處理邏輯解耦。
問題分析
發送問題
? ? ? ?數據一次寫入不完,就會造成信息發送丟失
接收問題
? ? ? ? 這里阻塞式讀取,如果客戶端一直保持連接,就是不發送數據,服務器豈不是會一直卡著。當然了按照http協議,根本不會出現這種情況,因為瀏覽器會立即發送數據。但是我們想要編寫更可靠的服務器,所以后續我們會解決這個問題。
效率問題
? ? ? ? 現在的服務器程序每次只能處理一個連接,完全處理完這個連接才能繼續處理下一個連接,效率較低。
多線程版本http服務器
? ? ? ? 我們只需要改一下main.cpp即可
? ? ? ? main.cpp
#include <arpa/inet.h>
#include <pthread.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;void *handler(void *args)
{http_conn *p_http = (http_conn *)args;p_http->h_run();delete p_http;
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);while (1){cerr << "******************************************開始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}http_conn *p_http = new http_conn(connfd);pthread_t tid;pthread_create(&tid, nullptr, handler, (void *)p_http);pthread_detach(tid); //設置線程分離cerr << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
問題分析
? ? ? ? 每到來一個連接就立即創建一個線程,如果短時間內突然到來非常多的連接,服務器內部就會含有大量的線程,服務器壓力太大。
多進程版本的http服務器
#include <arpa/inet.h>
#include <pthread.h>
#include "http_conn.hpp"#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;// void *handler(void *args)
// {
// http_conn *p_http = (http_conn *)args;
// p_http->h_run();
// delete p_http;
// }int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);while (1){cerr << "******************************************開始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}pid_t pid1 = fork();if(pid1 == 0)//子進程{close(listenfd); //子進程關閉監聽套接字pid_t pid2 = fork();if(pid2 == 0) // 孫子進程進行任務處理{http_conn* p_http = new http_conn(connfd);p_http->h_run();delete p_http;exit(0);// 孫子進程被操作系統領養}exit(0);}waitpid(-1,NULL,0);// 子進程創建孫子進程后立即退出close(connfd); // 父進程關閉連接套接字// 父進程cerr << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
代碼分析
- 之所以創建孫子進程,是因為以下原因
- 父進程創建子進程,倘若不創建孫子進程,由子進程處理任務,那么父進程必須等待子進程退出后,方能處理下個連接,這是因為父進程要waitpid子進程,清理子進程的內核數據結構。這樣就又變成了,只有這個連接處理完后才能處理下一個連接,效率很低。
- 創建孫子進程后,子進程直接退出,孫子進程進行耗時的任務處理,孫子進程退出后發現自己的父進程已經退出,由而被操作系統領養,由操作系統進行回收
問題分析
? ? ? ? 如果短時間內涌來大量連接,此時操作系統內部會有大量的進程,服務器壓力很大
錯誤檢查
? ? ? ? 本來準備給大家繼續寫線程池的,然后突然想看看我們的服務器打開的文件描述符。發現文件描述符泄露!而且有兩處!
ls -l /proc/pid/fd
? ? ? ? 用于查看進程打開的文件描述符,我們現在運行服務器程序,多次請求我們的服務器,然后每次運行后都查看該服務器進程打開的文件描述符,發現文件描述符不斷增加。
? ? ? ? 按道理來講,我們無論處理多少次請求服務器進程,都應該在查看時只打開一個文件描述符(以及操作系統自己打開的文件描述符)
? ? ? ? 錯誤一:下圖已更正
? ? ? ? 忘記關閉文件描述符
????????錯誤二:下圖已更正
? ? ? ? cgi請求父進程讀取完cgi程序處理后的數據后,應當直接關閉文件描述符。
? ? ? ? 后續代碼中已更正這個錯誤。
線程池版本的http服務器
? ? ? ? 首先在上述幾個地方我們做了更正,其次為了方便編譯,我們設置了公共頭文件common.hpp,除了頭文件包含部分http_conn.hpp、LOG.cpp、LOG.hpp這些文件無其他變化
? ? ? ? 目錄結構
? ? ? ? http_conn.cpp
#include "http_conn.hpp"
// 從tcp接收緩沖區拿走一行數據
// 這里我們阻塞式讀取,直到遇到 "\r\n"結束
// 所以要么我們取走一行完整數據,要么對方關閉連接,要么讀取出錯;h_recv_line才會返回
int http_conn::h_recv_line(int _sock, std::string &str)
{char ch = 'X';while (ch != '\n'){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){if (ch == '\r'){recv(_sock, &ch, 1, MSG_PEEK);if (ch == '\n'){recv(_sock, &ch, 1, 0);}else{ch = '\n';}}str.push_back(ch);}else if (s == 0){return 0;}else{return -1;}}return str.size();
}// 按照sep切分字符串
bool http_conn::cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep)
{size_t pos = str.find(sep);if (pos != std::string::npos){leftOut = str.substr(0, pos);rightOut = str.substr(pos + sep.size());return true;}return false;
}// 構建請求行
// 失敗則返回:CLOSE
// 成功則返回:OK
StatusCode http_conn::h_build_reqline()
{if (h_recv_line(_sock, _request_line) > 0){_request_line.pop_back();LOG(INFO, "h_build_reqline success--->_request_line: " + _request_line);return OK;}else{LOG(ERROR, "h_build_reqline failed");return CLOSE;}
}// 構建請求方法
// 失敗則返回:BAD_REQUEST
// 成功則返回:OK
StatusCode http_conn::h_build_reqmethod()
{int n = _request_line.find(SPACE);if (n != std::string::npos){_request_method = _request_line.substr(0, n);LOG(INFO, "h_build_reqmethod success--->_request_method: " + _request_method);return OK;}else{LOG(ERROR, "_request_method failed");return BAD_REQUEST;}
}// 判斷請求方法是否正確
// 我們只處理"GET""POST"請求
// 其余請求返回:BAD_REQUEST
// 這兩個請求返回:OK
StatusCode http_conn::h_parse_method()
{if (_request_method == "GET" || _request_method == "POST"){LOG(INFO, "h_parse_method success");return OK;}else{LOG(ERROR, "h_parse_method false");return BAD_REQUEST;}
}// 構建url,順帶著構建參數_request_url_arg
// 失敗則返回:BAD_REQUEST
// 成功則返回:OK
StatusCode http_conn::h_build_requrl()
{int n1 = _request_line.find(SPACE);n1 = _request_line.find(SPACE, n1 + 1);if (n1 != std::string::npos){int n2 = _request_line.find('?');if (n2 != std::string::npos) // 這意味著帶參數{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n2 - _request_line.find(SPACE) - 1);_request_url_arg = _request_line.substr(n2 + 1, n1 - n2 - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: " + _request_url_arg);return OK;}else // 這意味著不帶參數{_request_url = WEB_ROOT + _request_line.substr(_request_line.find(SPACE) + 1, n1 - _request_line.find(SPACE) - 1);LOG(INFO, "h_build_requrl success--->_request_url: " + _request_url);LOG(INFO, "h_build_requrl success--->_request_url_arg: NO");return OK;}}else{LOG(ERROR, "h_build_requrl failed");return BAD_REQUEST;}
}// 判斷url是否合法
// 非法情況:資源不存在,資源無權讀,資源無權執行
// 設置_cgi
// 失敗則返回:NOT_FOUND
// 成功則返回:OK
StatusCode http_conn::h_parse_url()
{struct stat fileStat;if (stat(_request_url.c_str(), &fileStat)) // 文件不存在{LOG(ERROR, "path " + _request_url + " NOT_FOUND, err:" + std::string(strerror(errno)));return NOT_FOUND;}else{if (S_ISDIR(fileStat.st_mode)) // 文件存在但是是一個目錄,應該訪問其中的默認頁面{_request_url += '/';_request_url += HOME_PAGE;stat(_request_url.c_str(), &fileStat);}if (_request_method == "GET" && _request_url_arg.size() == 0) // 這代表想要獲取資源,應當對目標文件具有讀權限{_cgi = false;if (fileStat.st_mode & S_IRUSR || fileStat.st_mode & S_IRGRP || fileStat.st_mode & S_IROTH) // 是否具有讀權限{LOG(INFO, "h_parse_url success");return OK;}else{LOG(ERROR, "h_parse_url false NO read");return NOT_FOUND;}}else // 這代表想要運行cgi程序,應當具有執行權限{_cgi = true;if ((fileStat.st_mode & S_IXUSR) || (fileStat.st_mode & S_IXGRP) || (fileStat.st_mode & S_IXOTH)) // 是否具有執行權限{LOG(INFO, "_request_url: true");return OK;}else{LOG(ERROR, "_request_url: false NO executable");return NOT_FOUND;}}}
}// 構建資源類型
// 只返回OK
StatusCode http_conn::h_build_reqsuffix()
{int n = _request_url.find('.');if (n == std::string::npos){LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}else{_request_suffix = _request_url.substr(n + 1);LOG(INFO, "h_build_reqsuffix success--->_request_suffix: " + _request_suffix);return OK;}
}
// 構建_request_line
// 任務不存在出錯情況
// 只返回:OK
StatusCode http_conn::h_build_reqversion()
{_requst_version = _request_line.substr(_request_line.rfind(SPACE) + 1);LOG(INFO, "h_build_reqversion success--->_requst_version: " + _requst_version);return OK;
}// 構建請求頭數組_request_v_header
// 讀取錯誤返回:CLOSE
// 正確則返回:OK
StatusCode http_conn::h_build_reqvheader()
{std::vector<std::string> &header = _request_v_header;std::string tmp;while (true){int length = h_recv_line(_sock, tmp);if (length <= 0){LOG(ERROR, "h_build_reqheader failed");return CLOSE;}if (length == 1){break;}tmp.pop_back();header.push_back(tmp);tmp.clear();}LOG(INFO, "h_build_reqheader success");return OK;
}// 構建請求頭哈希表_request_uomap_header
// 請求頭格式錯誤則返回:BAD_REQUEST
// 正確則返回:OK
StatusCode http_conn::h_build_requomapheader()
{std::string key;std::string value;for (auto &str : _request_v_header) // 多行請求報頭接收到了vector中{// 請求報頭中的KV是以 冒號+空格 分隔的if (cut_string(str, key, value, ": ")){_request_uomap_header.insert({key, value});}else{LOG(ERROR, "h_build_requomapheader failed");return BAD_REQUEST;}}LOG(INFO, "h_build_requomapheader success");return OK;
}// 構建請求正文_request_content
// 讀取錯誤返回:CLOSE
// 正確則返回:OK
StatusCode http_conn::h_build_reqcontent()
{if (_request_method == "POST" && _request_uomap_header["Content-Length"] != "0"){cout << 66666 << endl;int content_len = std::stoi(_request_uomap_header["Content-Length"]);char ch = 0;while (content_len--){ssize_t s = recv(_sock, &ch, 1, 0);if (s > 0){_request_content.push_back(ch);}else // 讀取正文對端寫關閉或者讀取失敗-------------------------------------{LOG(ERROR, "h_build_reqcontent failed");return CLOSE;}}LOG(INFO, "h_build_reqcontent success--->_request_content: " + _request_content);return OK;}else{LOG(INFO, "h_build_reqcontent sucess--->_request_content: NULL");return OK;}
}// 執行cgi程序
StatusCode http_conn::h_cgi()
{/* cgi整體流程:創建子進程,讓cgi進程替換掉子進程,父進程將request中的參數傳給cgi進程,cgi進程處理完后將結果返回給父進程 */// 走這里request一定是POST或者帶參的GET方法,那么此時request請求中的path就是所需要的可執行程序的路徑,此時就要讓這個可執行程序替換掉這里的子進程// auto &path = _request_url; // 可執行程序的路徑// 想要讓父子進程通信,直接用管道來實現int input[2]; // 站在父進程角度的輸入和輸出,父進程用input[0]讀,用output[1]寫int output[2];if (pipe(input) < 0) // 創建input管道{ // 創建input管道失敗LOG(ERROR, "create input pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}if (pipe(output) < 0) // 創建output管道{ // 創建output管道失敗LOG(ERROR, "create output pipe failed, strerror: " + std::string(strerror(errno)));exit(1);}// 走到這里管道就創建好了,此時創建一個子進程就可以進行父子進程通信了// 創建子進程pid_t pid = fork();// 子進程// 設置環境變量:"QUERY_STRING="、"CONTENT_LENGTH="、"METHOD="// 重定向:將標準輸入文件描述符定向到管道output讀端output[0];將標準輸出文件描述符定向到管道input寫端input[1]// cgi程序替換子進程if (pid == 0){close(input[0]);close(output[1]);/* **************************************************設置環境變量********************************************** */if (_request_method == "GET"){std::string queryString_env = "QUERY_STRING=" + _request_url_arg;if (putenv((char *)queryString_env.c_str()) != 0){LOG(ERROR, "putenv queryString failed");exit(1);}}else if (_request_method == "POST"){std::string contentLength_env = "CONTENT_LENGTH=" + _request_uomap_header["Content-Length"];if (putenv((char *)contentLength_env.c_str()) != 0){LOG(ERROR, "POST method, putenv CONTENT_LENGTH failed");exit(1);}}std::string method_env = "METHOD=" + _request_method;if (putenv((char *)method_env.c_str()) != 0){LOG(ERROR, "putenv method failed");exit(1);}/* **************************************************設置環境變量********************************************** */dup2(output[0], 0);dup2(input[1], 1);close(output[0]);close(input[0]);execl(_request_url.c_str(), _request_url.c_str(), nullptr);LOG(ERROR, "execl failed");exit(1);}// 父進程else if (pid > 0){close(input[1]);close(output[0]);if (_request_method == "POST"){size_t total = 0, size_singleTime = 0;size_t size = _request_content.size();while (total < size){size_singleTime = write(output[1], (void *)(_request_content.c_str() + total), size - total);if (size_singleTime <= 0) // 給子進程發送數據失敗{int k = kill(pid, SIGTERM); // 給子進程發送結束信號if (k == 0){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else if (k == -1 && errno == ESRCH){waitpid(-1, NULL, 0);LOG(WARNING, "write output[1] failed");return SERVER_ERROR;}else{LOG(FATAL, "kill(pid, SIGTERM) failed");return SERVER_ERROR;}}else{total += size_singleTime;}}}// 發送完數據之后就接收CGI進程返回的結果char ch;while (read(input[0], &ch, 1) > 0){_response_content.push_back(ch);}cout << "-->" << "server get result:\n"<< _response_content << endl;close(input[0]);close(output[1]);int status = 0;pid_t _pid = waitpid(-1, &status, 0);if (pid == _pid){if (WIFEXITED(status)){if (WEXITSTATUS(status) != 0){cout << "exit code ::" << WEXITSTATUS(status) << endl;return BAD_REQUEST;}else{cout << "exit code ::" << WEXITSTATUS(status) << endl;return OK;}}else{LOG(WARNING, "WIFEXITED(status) is false");return SERVER_ERROR;}}else{LOG(WARNING, "waitpid is false");return SERVER_ERROR;}// 子進程替換之后會執行完畢,此時替換進程整個空間都會被回收,所以替換進程中可以不需要手動關input[1]和output[0]}else{LOG(WARNING, "fork is false");close(input[0]);close(output[1]);return SERVER_ERROR;}// return OK;
}/* ****************************************************事務入口************************************************************ *//* 構建請求 */
StatusCode http_conn::h_read()
{StatusCode ret = OK;ret = h_build_reqline();if (ret != OK)return ret;ret = h_build_reqmethod();if (ret != OK)return ret;ret = h_parse_method();if (ret != OK)return ret;ret = h_build_requrl();if (ret != OK)return ret;ret = h_parse_url();if (ret != OK)return ret;ret = h_build_reqsuffix();if (ret != OK)return ret;ret = h_build_reqversion();if (ret != OK)return ret;ret = h_build_reqvheader();if (ret != OK)return ret;ret = h_build_requomapheader();if (ret != OK)return ret;ret = h_build_reqcontent();if (ret != OK)return ret;if (_cgi){ret = h_cgi();if (ret != OK)return ret;}return OK;
}void http_conn::HandlerERROR(const std::string &page, int ret)
{std::string pagePath = WEB_ROOT;pagePath += '/';pagePath += page;int fd = open(pagePath.c_str(), O_RDONLY);if (fd >= 0){/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(ret);_response_line += ' ';_response_line += statusDescMap[ret];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";struct stat st;stat(pagePath.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += "text/html; charset=UTF-8";tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;/* 發送狀態行 */send(_sock, _response_line.c_str(), _response_line.size(), 0);/* 發送響應頭 */for (auto &str : _response_v_header){send(_sock, str.c_str(), str.size(), 0);}/* 發送空行 */send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);/* 發送資源 */sendfile(_sock, fd, nullptr, st.st_size); // 發送響應正文close(fd);}else{LOG(WARNING, "open false");}
}void http_conn::HandlerOK()
{if (!_cgi){int fd = open(_request_url.c_str(), O_RDONLY);if (fd > 0){/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";struct stat st;stat(_request_url.c_str(), &st);tmp += std::to_string(st.st_size);tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;// /* 發送狀態行 */// send(_sock, _response_line.c_str(), _response_line.size(), 0);// /* 發送響應頭 */// for (auto &str : _response_v_header)// {// send(_sock, str.c_str(), str.size(), 0);// }// /* 發送空行 */// send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);// /* 發送資源 */// sendfile(_sock, fd, nullptr, st.st_size);// 構建響應首部大字符串 big_strstd::string big_str;big_str += _response_line;for (auto &str : _response_v_header){big_str += str;}big_str += _response_blankline;// 發送響應首部ssize_t first = 0;while (first < big_str.size()){ssize_t bytes = send(_sock, big_str.c_str() + first, big_str.size() - first, 0);if (bytes == -1){LOG(FATAL, "send errno");break;}first += bytes;}// 發送文件first = 0;while (first < st.st_size){ssize_t bytes = sendfile(_sock, fd, &first, st.st_size - first);if (bytes == -1){LOG(FATAL, "sendfile errno");break;}first += bytes;}cerr << "first: " << first << endl;close(fd);}}else{/* 構建狀態行 */_response_line += "HTTP/1.1";_response_line += ' ';_response_line += std::to_string(OK);_response_line += ' ';_response_line += statusDescMap[OK];_response_line += END_OF_LINE;/* 構建響應報頭 */std::string tmp = "Content-Length: ";tmp += std::to_string(_response_content.size());tmp += END_OF_LINE;_response_v_header.push_back(tmp);tmp = "Content-Type: ";tmp += suffixMap[_request_suffix];tmp += END_OF_LINE;_response_v_header.push_back(tmp);/* 構建響應空行 */_response_blankline = END_OF_LINE;// /* 發送狀態行 */// send(_sock, _response_line.c_str(), _response_line.size(), 0);// /* 發送響應頭 */// for (auto &str : _response_v_header)// {// send(_sock, str.c_str(), str.size(), 0);// }// /* 發送空行 */// send(_sock, _response_blankline.c_str(), _response_blankline.size(), 0);// /* 發送響應正文 */// send(_sock, _response_content.c_str(), _response_content.size(), 0);// 構建響應首部大字符串 big_strstd::string big_str;big_str += _response_line;for (auto &str : _response_v_header){big_str += str;}big_str += _response_blankline;// 發送響應首部ssize_t first = 0;while (first <= big_str.size()){ssize_t bytes = send(_sock, big_str.c_str() + first, big_str.size() - first, 0);first += bytes;}// 發送響應正文first = 0;while (first <= _response_content.size()){ssize_t bytes = send(_sock, _response_content.c_str() + first, _response_content.size() - first, 0);first += bytes;}}
}void http_conn::h_write(StatusCode ret)
{switch (ret){case OK:HandlerOK();break;case NOT_FOUND:HandlerERROR(NOT_FOUND_PAGE, ret);break;case BAD_REQUEST:HandlerERROR(BAD_REQUEST_PAGE, ret);break;case SERVER_ERROR:HandlerERROR(SERVER_ERROR_PAGE, ret);break;case CLOSE:close(_sock);_sock = -1;break;default:HandlerERROR(NOT_FOUND_PAGE, ret);break;}
}
? ? ? ? http_conn.hpp
#pragma once
#include "LOG.hpp"
#define WEB_ROOT "wwwroot" // Web根目錄
#define HOME_PAGE "index.html" // 默認資源文件
#define BAD_REQUEST_PAGE "400.html" // 請求錯誤
#define NOT_FOUND_PAGE "404.html" // 資源不存在
#define SERVER_ERROR_PAGE "500.html" // 服務器出錯
#define SPACE " " // 空格
#define END_OF_LINE "\r\n" // 行尾回車換行enum StatusCode
{CLOSE = 0, // 連接關閉OK = 200, // 正常情況BAD_REQUEST = 400, // 請求錯誤NOT_FOUND = 404, // 資源不存在SERVER_ERROR = 500 // 服務器出錯
};static std::unordered_map<int, std::string> statusDescMap ={{200, "OK"},{400, "BAD_REQUEST"},{404, "NOT_FOUND"},{500, "SERVER_ERROR"},
};static std::unordered_map<std::string, std::string> suffixMap ={{"html","text/html; charset=UTF-8"},{"css", "text/css"},{"js", "application/javascript"},{"jpg", "image/jpeg"},{"xml", "application/xml"},{"cgi", "text/html; charset=UTF-8"},};class http_conn
{
private:int _sock; // 用于獲得這個客戶連接的socket
public:std::string _request_line; // 請求行std::vector<std::string> _request_v_header; // 請求頭數組std::unordered_map<std::string, std::string> _request_uomap_header; // 請求頭哈希表std::string _request_content; // 請求正文std::string _request_method; // 請求方法std::string _request_url; // 請求urlstd::string _request_url_arg; // 請求參數std::string _requst_version; // 請求版本std::string _request_suffix; // 資源類型
public:std::string _response_line; // 響應行std::vector<std::string> _response_v_header; // 響應頭數組std::string _response_blankline; // 響應空行std::string _response_content; // 響應正文
public:bool _cgi; // 是否采用cgi機制
public:// 構造函數http_conn(int sock) : _sock(sock), _cgi(false), _request_suffix("html"){}~http_conn(){if(_sock >= 0) close(_sock);}public:
void h_run()
{StatusCode ret = h_read();h_write(ret);
}private:
StatusCode h_read();
void h_write(StatusCode ret);private:// 構建請求行_request_line// 失敗則返回:CLOSE// 成功則返回:OKStatusCode h_build_reqline();// 構建請求方法_request_method// 失敗則返回:BAD_REQUEST// 成功則返回:OKStatusCode h_build_reqmethod();// 判斷請求方法是否正確// 我們只處理"GET""POST"請求// 其余請求返回:BAD_REQUEST// 這兩個請求返回:OKStatusCode h_parse_method();// 構建url,順帶著構建參數_request_url_arg// 失敗則返回:BAD_REQUEST// 成功則返回:OKStatusCode h_build_requrl();// 判斷url是否合法// 非法情況:資源不存在,資源無權讀,資源無權執行// 設置_cgi// 失敗則返回:NOT_FOUND// 成功則返回:OKStatusCode h_parse_url();StatusCode h_build_reqsuffix();// 構建_request_line// 任務不存在出錯情況// 只返回:OKStatusCode h_build_reqversion();// 構建請求頭數組_request_v_header// 讀取錯誤返回:CLOSE// 正確則返回:OKStatusCode h_build_reqvheader();// 構建請求頭哈希表_request_uomap_header// 請求頭格式錯誤則返回:BAD_REQUEST// 正確則返回:OKStatusCode h_build_requomapheader();// 構建請求正文_request_content// 讀取錯誤返回:CLOSE// 正確則返回:OKStatusCode h_build_reqcontent();// 執行cgi程序StatusCode h_cgi();private:void HandlerERROR(const std::string &page, int ret);void HandlerOK();private:// 從tcp接收緩沖區拿走一行數據// 這里我們阻塞式讀取,直到遇到 "\r\n"結束// 所以要么我們取走一行完整數據,要么對方關閉連接;h_recv_line才會返回int h_recv_line(int _sock, std::string &str);// 按照sep切分字符串bool cut_string(const std::string &str, std::string &leftOut, std::string &rightOut, const std::string &sep);
};
? ? ? ? LOG.hpp
#pragma once
#include "common.hpp"using std::cout;
using std::cerr;
using std::endl;enum LogLevel
{INFO,WARNING,ERROR,FATAL
};// 宏替換能保證每次調用的地方都是對應的文件和行
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__) // 打印日志// 只聲明 Log 函數
void Log(const std::string& level, const std::string& message, const std::string& fileName, int line);
? ? ? ? LOG.cpp
#include "LOG.hpp"void Log(const std::string& level, const std::string& message, const std::string& fileName, int line)
{// 日志格式:// [level]<file:line>{日志內容}==>timetime_t tm = time(nullptr);cerr << "[" << level << "](" << fileName << ":" << line << ")" << "{" << message << "}==> " << ctime(&tm);
}
? ? ? ? common.hpp
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>#include <arpa/inet.h>
#include <pthread.h>#include<iostream>#include <exception>
#include <semaphore.h>#include <list>
#include <ctime>
? ? ? ? locker.hpp
#pragma once
#include "common.hpp"// 封裝信號量
class sem
{
public:sem(){if( sem_init( &m_sem, 0, 0 ) != 0 ){throw std::exception();}}~sem(){sem_destroy( &m_sem );}bool wait(){return sem_wait( &m_sem ) == 0;}bool post(){return sem_post( &m_sem ) == 0;}private:sem_t m_sem;
};// 封裝后斥鎖
class locker
{
public:locker(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}}~locker(){pthread_mutex_destroy( &m_mutex );}bool lock(){return pthread_mutex_lock( &m_mutex ) == 0;}bool unlock(){return pthread_mutex_unlock( &m_mutex ) == 0;}private:pthread_mutex_t m_mutex;
};// 封裝條件變量
class cond
{
public:cond(){if( pthread_mutex_init( &m_mutex, NULL ) != 0 ){throw std::exception();}if ( pthread_cond_init( &m_cond, NULL ) != 0 ){pthread_mutex_destroy( &m_mutex );throw std::exception();}}~cond(){pthread_mutex_destroy( &m_mutex );pthread_cond_destroy( &m_cond );}bool wait(){int ret = 0;pthread_mutex_lock( &m_mutex );ret = pthread_cond_wait( &m_cond, &m_mutex );pthread_mutex_unlock( &m_mutex );return ret == 0;}bool signal(){return pthread_cond_signal( &m_cond ) == 0;}private:pthread_mutex_t m_mutex;pthread_cond_t m_cond;
};
????????threadpool.hpp
#pragma once
#include "locker.hpp"
#include "LOG.hpp"template <typename T>
class threadpool
{
public:threadpool(int thread_number = 8, int max_requests = 10000);~threadpool();bool append(T request);private:static void *worker(void *arg);void run();private:int m_thread_number; /* 線程池中線程的數量 */int m_max_requests; /* 任務隊列的大小 */pthread_t *m_threads; /* 描述線程池的數組,其大小為m_thread_number */// 線程池內部維護請求隊列,意味著線程池要提供append接口// 請求隊列或者說任務隊列,本身屬于臨界資源,需要加鎖訪問// 請求隊列中存儲的是指向http_conn在堆區位置的指針std::list<T> m_workqueue; /* 任務隊列 */locker m_queuelocker; /* 保護請求隊列的互斥鎖 */sem m_queuestat; /* 是否有任務需要處理 */bool m_stop; /* 是否結束線程 */
};// 構造函數
template <typename T>
threadpool<T>::threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL)
{if ((thread_number <= 0) || (max_requests <= 0)){LOG(FATAL,"線程池創建時參數有誤");}m_threads = new pthread_t[m_thread_number];if (!m_threads){LOG(ERROR,"線程標識符數組創建失敗");}for (int i = 0; i < thread_number; ++i){if (pthread_create(m_threads + i, NULL, worker, this) != 0){delete[] m_threads;LOG(FATAL,"線程創建失敗");}if (pthread_detach(m_threads[i])){delete[] m_threads;LOG(FATAL,"線程分離失敗");}}std::cerr << "***********************************全部線程創建成功***********************************" << std::endl;
}template <typename T>
threadpool<T>::~threadpool()
{delete[] m_threads;m_stop = true;LOG(INFO,"線程池已析構");
}template <typename T>
bool threadpool<T>::append(T request)
{m_queuelocker.lock();if (m_workqueue.size() > m_max_requests){m_queuelocker.unlock();return false;}m_workqueue.push_back(request);m_queuelocker.unlock();m_queuestat.post();//增加信號量return true;
}template <typename T>
void *threadpool<T>::worker(void *arg)
{threadpool *pool = (threadpool *)arg;pool->run();return pool;
}template <typename T>
void threadpool<T>::run()
{while (!m_stop){// 申請信號量LOG(INFO,"有個線程在申請信號量");m_queuestat.wait();LOG(INFO,"有個線程申請信號量成功");m_queuelocker.lock();// 請求隊列為空if (m_workqueue.empty()){m_queuelocker.unlock();continue;}// 從任務隊列取走任務T request = m_workqueue.front();m_workqueue.pop_front();LOG(INFO,"有個線程取走任務");m_queuelocker.unlock();if (!request){continue;}// 執行任務LOG(INFO,"有個線程開始執行任務");request->h_run();delete request;LOG(INFO,"有個線程執行任務結束");}
}
? ? ? ? main.cpp
#include "http_conn.hpp"
#include "threadpool.hpp"
#define BUFFER_SIZE 4096
#define ROOT_PATH "wwwroot/"using std::cerr;
using std::cin;
using std::cout;
using std::endl;int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);threadpool<http_conn*>* pool = new threadpool<http_conn*>(10,10000);// 此時已經有10個線程在嗷嗷待哺while (1){cerr << "******************************************開始********************************************" << endl;int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);if (connfd < 0){LOG(INFO, "accept success");}http_conn *p_http = new http_conn(connfd);pool->append(p_http);cerr << "******************************************結束********************************************" << endl;}close(listenfd);return 0;
}
????????后續無論再請求多少次,文件描述都只能打開一個(以及操作系統自己打開的和保持網絡連接的),大概長這樣
- 這樣每到來一個連接,我們就把它放在連接隊列里
- 然后線程一個個取走這些連接,并處理即可。
接下來我們寫一個進程池版本的建議cgi服務器,目的是為了分析進程池的各種特點。
進程池版本的cgi服務器
? ? ? ? 服務器目錄結構
? ? ? ? ?processpool.h
#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>// process是一個子進程類
class process
{
public:process() : m_pid( -1 ){}public:pid_t m_pid; // 子進程pidint m_pipefd[2];// 父子管道
};/* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ */
// 模板參數T--->CGI類
template< typename T >
class processpool
{
private:// 將構造函數定義為私有的,因此我們只能通過后面的create靜態函數來創建processpool實例processpool( int listenfd, int process_number = 8 );
public:// 單例模式,以保證程序最多創建一個processpoo1實例,這是程序正確處理信號的必要條件(這句話最為關鍵)// 在這個地方細心的同學可能已經發現了,下面這種獲取單例的模式不是線程安全的,可是這里依然是正確的// 什么跟線程安全聯系在一起,是線程競爭資源,這里不存在競爭這種情況/* 在一個多進程或多線程的程序中,信號處理需要保持全局一致性。信號是異步事件,操作系統可以在任何時刻向進程發送信號,如 SIGTERM(終止信號)、SIGINT(中斷信號)等。如果程序中有多個 processpool 實例,每個實例可能會獨立地設置信號處理邏輯,這就會導致信號處理的混亂。例如,當收到 SIGTERM 信號時,多個 processpool 實例可能會同時嘗試關閉不同的資源或者執行不同的清理操作,這樣會造成資源管理的混亂,甚至可能導致程序崩潰。而單例模式確保整個程序中只有一個 processpool 實例,所有的信號處理邏輯都由這個唯一的實例來管理,保證了信號處理的一致性和正確性 */static processpool< T >* create( int listenfd, int process_number = 8 ){if( !m_instance ){m_instance = new processpool< T >( listenfd, process_number );}return m_instance;}~processpool(){delete [] m_sub_process;}// 啟動進程池,自此程序完全交給進程池void run();private:void setup_sig_pipe();void run_parent();// 父進程在創建子進程之后的運行邏輯void run_child();// 子進程在被父進程創建后的運行邏輯private:static const int MAX_PROCESS_NUMBER = 16; //進程池允許的最大子進程數量static const int USER_PER_PROCESS = 65536; //子進程最多能處理的客戶數量static const int MAX_EVENT_NUMBER = 10000; //epoll最多能處理的事件數,該常量交給epoll_waitint m_process_number; //進程池中的進程總數int m_idx; //子進程在進程池中的序號(大家對此或許會存在疑問,不著急往下看)int m_epollfd; //每個進程(注意不是每個子進程)的epoll事件表int m_listenfd; //監聽socketint m_stop; //子進程通過m_stop決定是否停止運行process* m_sub_process; //保存所有子進程的描述信息static processpool< T >* m_instance; //進程池靜態實例
};
template< typename T >
processpool< T >* processpool< T >::m_instance = NULL;
/* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ */// 信號管道--->同一事件源 每個進程都會有這個信號管道
static int sig_pipefd[2];/**********************************************************************************************/
// 靜態方法:設置文件描述符為非阻塞
static int setnonblocking( int fd )
{int old_option = fcntl( fd, F_GETFL );int new_option = old_option | O_NONBLOCK;fcntl( fd, F_SETFL, new_option );return old_option;
}// 靜態方法:向m_epollfd中注冊該文件fd讀事件,et模式
static void addfd( int epollfd, int fd )
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );setnonblocking( fd );
}// 靜態方法:從m_epollfd中移除該文件fd,并且關閉該文件描述符
static void removefd( int epollfd, int fd )
{epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );close( fd );
}// 靜態方法:信號捕捉函數,處理邏輯為--->將sig信號寫進sig_pipefd[1]
static void sig_handler( int sig )
{int save_errno = errno;int msg = sig;send( sig_pipefd[1], ( char* )&msg, 1, 0 );errno = save_errno;
}// 靜態方法:為信號sig自定義信號捕捉,執行信號捕捉函數期間屏蔽所有信號
static void addsig( int sig, void( handler )(int), bool restart = true )
{struct sigaction sa;memset( &sa, '\0', sizeof( sa ) );sa.sa_handler = handler;if( restart ){sa.sa_flags |= SA_RESTART;}sigfillset( &sa.sa_mask );assert( sigaction( sig, &sa, NULL ) != -1 );
}
/**********************************************************************************************//*¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥*/
// 進程池構造函數
// listenfd用于初始化m_listenfd
// process_number用于初始設置進程池中的進程總數m_process_number
// m_idx是:子進程在進程池中的序號,全部初始化為-1// 父進程是main函數進程
// 父進程先獲得進程池靜態實例,隨后父進程創建堆區m_sub_process,父進程創建全雙工父子信道
// 子進程繼承了父進程靜態進程池實例,繼承了父進程堆區m_sub_process,繼承了全雙工父子信道
//此后父進程具有文件描述符:listenfd(物理位置在棧區),process_number個m_sub_process[i].m_pipefd[0](物理位置在堆區)
//此后子進程i具有文件描述符:listenfd(物理位置在棧區),m_sub_process[i].m_pipefd[1](物理位置在堆區),還有嗎?有!
// 父進程不會修改自己實例中的m_idx(-1),每個子進程都會設置自己的m_idx,子進程序號從0開始。
// 父進程堆區m_sub_process[i].m_pid存放了子進程pid,而子進程堆區m_sub_process[i].m_pid == -1;
template< typename T >
processpool< T >::processpool( int listenfd, int process_number ) : m_listenfd( listenfd ), m_process_number( process_number ), m_idx( -1 ), m_stop( false )
{assert( ( process_number > 0 ) && ( process_number <= MAX_PROCESS_NUMBER ) );m_sub_process = new process[ process_number ];//堆區保存所有子進程的描述信息,初始值為8assert( m_sub_process );for( int i = 0; i < process_number; ++i ){/* int m_pipefd[2];// 全雙工父子管道 */int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd );assert( ret == 0 );m_sub_process[i].m_pid = fork();assert( m_sub_process[i].m_pid >= 0 );if( m_sub_process[i].m_pid > 0 )// 父進程關閉m_sub_process[i].m_pipefd[1]{close( m_sub_process[i].m_pipefd[1] );continue;}// 子進程關閉m_sub_process[i].m_pipefd[0]--->此處是否會造成文件描述的泄露呢?// 父進程要與多個子進程通信,自然就有多個 m_sub_process[i].m_pipefd[0]// 父進程在創建第I個子進程時,已經有 m_sub_process[0].m_pipefd[0]、 m_sub_process[1].m_pipefd[0]...... m_sub_process[i-1].m_pipefd[0] // 那么第i個子進程只關閉 m_sub_process[i].m_pipefd[0]后,還有之前的很多個無用的文件描述符沒有被關閉呀 else{close( m_sub_process[i].m_pipefd[0] );m_idx = i;break;}}
}// 統一事件源
// 創建m_epollfd,創建并注冊全雙工信號管道sig_pipefd,設置非阻塞fd,統一SIGCHLD、SIGTERM、SIGINT信號處理,忽略SIGPIPE
template< typename T >
void processpool< T >::setup_sig_pipe()
{m_epollfd = epoll_create( 5 );assert( m_epollfd != -1 );int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, sig_pipefd );assert( ret != -1 );setnonblocking( sig_pipefd[1] );addfd( m_epollfd, sig_pipefd[0] );addsig( SIGCHLD, sig_handler );addsig( SIGTERM, sig_handler );addsig( SIGINT, sig_handler );addsig( SIGPIPE, SIG_IGN );
}// 事件按m_idx分發給父進程和子進程
template< typename T >
void processpool< T >::run()
{if( m_idx != -1 ){run_child();return;}run_parent();
}
/*¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥*/// 子進程處理邏輯
// 子進程i具有文件描述符:listenfd(物理位置在棧區),m_sub_process[i].m_pipefd[1](物理位置在堆區),還有嗎?有!
template< typename T >
void processpool< T >::run_child()
{// 子進程i具有文件描述符:listenfd,m_sub_process[i].m_pipefd[1],sig_pipefd,還有嗎?有!// 子進程關心的文件描述符:sig_pipefd[0],m_sub_process[m_idx].m_pipefd[ 1 ]setup_sig_pipe();int pipefd = m_sub_process[m_idx].m_pipefd[ 1 ];// 此處僅僅只是值的簡單復制而已addfd( m_epollfd, pipefd );epoll_event events[ MAX_EVENT_NUMBER ];// T--->cgi_conn// users是一個數組,數組元素是cgi_connT* users = new T [ USER_PER_PROCESS ];assert( users );int number = 0;int ret = -1;while( ! m_stop ){number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );//阻塞式/* 在等待過程中,函數被信號中斷。應用程序通常可以再次調用 epoll_wait 繼續等待。例如,當程序注冊了某個信號處理函數,在 epoll_wait 等待期間該信號被觸發,epoll_wait 就可能因被信號中斷而返回 -1 且 errno 為 EINTR。 */if ( ( number < 0 ) && ( errno != EINTR ) ){printf( "epoll failure\n" );break;}// 父進程向子進程發來了數據for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;// if( ( sockfd == pipefd ) && ( events[i].events & EPOLLIN ) ){// 注意此處只讀取一個數據,僅僅起到個提醒作用:有新連接了,你來取吧。int client = 0;ret = recv( sockfd, ( char* )&client, sizeof( client ), 0 );// 讀取失敗或者對端關閉,進行下一輪(健壯性)if( ( ( ret < 0 ) && ( errno != EAGAIN ) ) || ret == 0 ) {continue;}// 讀取成功,開始接管一個新連接else{struct sockaddr_in client_address;socklen_t client_addrlength = sizeof( client_address );int connfd = accept( m_listenfd, ( struct sockaddr* )&client_address, &client_addrlength );if ( connfd < 0 ){printf( "errno is: %d\n", errno );continue;}// 子進程關心的文件描述符:sig_pipefd[0],m_sub_process[m_idx].m_pipefd[ 1 ],connfdaddfd( m_epollfd, connfd );users[connfd].init( m_epollfd, connfd, client_address );}}else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) ){int sig;char signals[1024];ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );if( ret <= 0 ){continue;}else{for( int i = 0; i < ret; ++i ){switch( signals[i] ){case SIGCHLD:{pid_t pid;int stat;while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 ){continue;}break;}case SIGTERM:case SIGINT:{m_stop = true;break;}default:{break;}}}}}// 客戶數據到來else if( events[i].events & EPOLLIN ){users[sockfd].process();//此處會阻塞}else{continue;}}}delete [] users;users = NULL;close( pipefd );//close( m_listenfd );close( m_epollfd );
}// 父進程處理邏輯
// 開始前父進程打開的文件描述符:listenfd(物理位置在棧區),process_number個m_sub_process[i].m_pipefd[0](物理位置在堆區)
template< typename T >
void processpool< T >::run_parent()
{// 父進程打開的文件描述符:listenfd,process_number個m_sub_process[i].m_pipefd[0],sig_pipefd// 父進程關心的文件描述符:listened,sig_pipefd[0]setup_sig_pipe();addfd( m_epollfd, m_listenfd );epoll_event events[ MAX_EVENT_NUMBER ];int sub_process_counter = 0;int new_conn = 1;int number = 0;int ret = -1;while( ! m_stop ){number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );//阻塞式/* 在等待過程中,函數被信號中斷。應用程序通常可以再次調用 epoll_wait 繼續等待。例如,當程序注冊了某個信號處理函數,在 epoll_wait 等待期間該信號被觸發,epoll_wait 就可能因被信號中斷而返回 -1 且 errno 為 EINTR。 */if ( ( number < 0 ) && ( errno != EINTR ) ){printf( "epoll failure\n" );break;}for ( int i = 0; i < number; i++ ){int sockfd = events[i].data.fd;// 收到一個新連接:if( sockfd == m_listenfd ){int i = sub_process_counter;do{if( m_sub_process[i].m_pid != -1 ){break;}i = (i+1)%m_process_number;}while( i != sub_process_counter );if( m_sub_process[i].m_pid == -1 ){m_stop = true;break;}sub_process_counter = (i+1)%m_process_number;send( m_sub_process[i].m_pipefd[0], ( char* )&new_conn, sizeof( new_conn ), 0 );printf( "send request to child %d\n", i );}else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) ){int sig;char signals[1024];ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );if( ret <= 0 ){continue;}else{for( int i = 0; i < ret; ++i ){switch( signals[i] ){case SIGCHLD:{pid_t pid;int stat;while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 ){for( int i = 0; i < m_process_number; ++i ){if( m_sub_process[i].m_pid == pid ){printf( "child %d join\n", i );close( m_sub_process[i].m_pipefd[0] );m_sub_process[i].m_pid = -1;}}}m_stop = true;for( int i = 0; i < m_process_number; ++i ){if( m_sub_process[i].m_pid != -1 ){m_stop = false;}}break;}case SIGTERM:case SIGINT:{printf( "kill all the clild now\n" );for( int i = 0; i < m_process_number; ++i ){int pid = m_sub_process[i].m_pid;if( pid != -1 ){kill( pid, SIGTERM );}}break;}default:{break;}}}}}else{continue;}}}//close( m_listenfd );close( m_epollfd );
}#endif
? ? ? ?server.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include "processpool.h"
#include "cgi.h"int main( int argc, char* argv[] )
{if( argc <= 1 ){printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );return 1;}// const char* ip = argv[1];int port = atoi( argv[1] );int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;// inet_pton( AF_INET, ip, &address.sin_addr );address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons( port );int listenfd = socket( PF_INET, SOCK_STREAM, 0 );assert( listenfd >= 0 );ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );assert( ret != -1 );ret = listen( listenfd, 5 );assert( ret != -1 );// 此時主進程打開的文件描述符只有 0 1 2 和 listenfdprocesspool<cgi_conn >* pool = processpool<cgi_conn >::create(listenfd);if(pool){pool->run();delete pool;}close(listenfd);return 0;
}
? ? ? ? ?cgi.h
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include "processpool.h"
#include <iostream>
class cgi_conn
{
private:static const int BUFFER_SIZE = 1024; // 讀緩沖區大小static int m_epollfd; // 全局epollfd句柄int m_sockfd; // 客戶連接sockaddr_in m_address; // 客戶信息char m_buf[BUFFER_SIZE]; // 讀取緩沖區int m_read_idx; // 標記讀緩沖中已經讀入的客戶數據的最后一個字節的下一個位置
public:cgi_conn() {}~cgi_conn() {}/*初始化客戶連接,清空讀緩沖區*/void init(int epollfd, int sockfd, const sockaddr_in &client_addr){m_epollfd = epollfd;m_sockfd = sockfd;m_address = client_addr;memset(m_buf, '\0', BUFFER_SIZE);m_read_idx = 0;}void process(){int idx = 0;int ret = -1;/*循環讀取和分析客戶數據*/while (true){idx = m_read_idx;ret = recv(m_sockfd, m_buf + idx, BUFFER_SIZE - 1 - idx, 0);if (ret < 0){if(errno != EAGAIN){removefd(m_epollfd,m_sockfd);}break;}else if( ret == 0){removefd(m_epollfd,m_sockfd);break;}else{m_read_idx += ret;for(;idx<m_read_idx;++idx){if((idx >= 1) && (m_buf[idx-1] =='\r') &&(m_buf[idx] == '\n')){break;}}if(idx == m_read_idx){continue;}m_buf[idx-1] == '\0';ret = fork();if(ret == -1){removefd(m_epollfd,m_sockfd);std::cout << 1 << std::endl;break;}else if(ret > 0){removefd(m_epollfd,m_sockfd);std::cout << 2 << std::endl;break;}else{close(STDOUT_FILENO);dup(m_sockfd);std::cout << 3 << std::endl;execl("test","test",0);exit(0);}}}}
};
int cgi_conn::m_epollfd = -1;
?????????test.cpp
#include <iostream>
int main()
{std::cout << "我是cgi程序,我已經處理還數據" << std::endl;return 0;
}
? ? ? ? 對于該服務器我們重點關注進程池本身
????????進程池代碼分析
? ? ? ? 此處的進程池單例真的是個單例嗎?
? ? ? ? 當父進程fork()子進程后,父子進程會修改進程池部分成員變量的值,采用的是寫時復制的原則。對于進程池內父子進程執行邏輯的代碼不會改變,這也就決定父子進程依舊遵循我們程序設計之初為其設計的藍圖。也就是說這部分代碼依舊唯一存在,發揮單例作用。
? ? ? ? 子進程繼承的文件描述符
- 子進程關閉m_sub_process[i].m_pipefd[0]--->此處是否會造成文件描述的泄露呢
- 父進程要與多個子進程通信,自然就有多個 m_sub_process[i].m_pipefd[0
- 父進程在創建第I個子進程時,已經有 m_sub_process[0].m_pipefd[0]、 m_sub_process[1].m_pipefd[0]...... m_sub_process[i-1].m_pipefd[0] ? ?
- 那么第i個子進程只關閉 m_sub_process[i].m_pipefd[0]后,還有之前的很多個無用的文件描述符沒有被關閉,這確實會占用系統資源,是需要優化的地方
- 但是這不屬于文件描述符泄露,只要進程池啟動完畢,無論后續提供多少次服務,系統內部的文件描述符總量不變。而文件描述符泄露的特點是,隨服務次數的增多,系統內部文件描述符越來越多。
? ? ? ? 統一事件源
? ? ? ? 該部分代碼用于統一在主循環中進行信號處理,做到統一事件源
? ? ? ? SIGCHLD信號
??
? ? ? ? 子進程進行進程替換之后,原先的代碼段已經全部銷毀,通過SIGCHLD信號進行子進程回收,是較為高效的一種做法
? ? ? ? 父子通信管道
? ? ? ? 父子進程通過管道來進行交流通信,是較為經典的一種做法。在這里父進程主要用于提醒子進程有連接到來。
? ? ? ? 其余方面不再贅述,在代碼中我寫了不少的注釋,用于輔助大家進行理解。這里只是簡易演示進程池機制,大家體會一下即可。
總結
- 此時我們構建的http服務器相對來講已經比較完善
- 根據http協議,http請求是基于短連接的,所以說瀏覽器與服務器建立連接后,將立即發送數據,而且會將數據一次性發送完。所以可以認為根本不存在我們上述說的:線程由于遲遲接受不到數據而一直卡住的情況。這是http協議的特性讓我們被動規避了接收問題。
- 接下來我們將編寫一個聊天室程序,脫離http協議,只關注服務器本身。這樣可以讓我們更加全面的去分析問題,以及擺脫http各種請求頭、響應頭的相關協議規定讓我們更加專注于服務器本身的邏輯。
逐步構建高性能聊天室服務器
服務器邏輯:
????????服務器邏輯很簡單:
- 會有多個客戶端chatclient與服務器chatserver相連接
- 每個客戶上線,服務器就給其他客戶發送該客戶上線了
- 每個客戶下線,服務器就給其他客戶發送該客戶下線了
- 每個客戶發送的消息,服務器就轉發給其他客戶
基于epoll的多線程版本聊天室服務器
? ? ? ? 服務器目錄結構
? ? ? ? 客戶端目錄結構
? ? ? ? chatserver.cpp
#include "LOG.hpp"
#include <sys/epoll.h>
#define MAX_EVENT_NUMBER 10000
#define BUFFER_SIZE 1024using std::cerr;
using std::cin;
using std::cout;
using std::endl;std::unordered_map<int, std::string> users; // 存儲在線客戶// 設置文件描述符為非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 將文件描述符上的讀事件注冊進內核事件表,ET + EPOLLONESHOT模式;并設置文件描述符為非阻塞
// ET + EPOLLONESHOT模式:ET模式下,只有數據被全部讀完后,下次到來數據才會提醒;而該模式下,設置ET + EPOLLONESHOT后,用于只提醒一次,除非重新設置
// 原因:這樣在同一時刻,一個連接永遠只有一個線程在為其服務
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;if (one_shot){event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}
// 重新添加文件描述符上的讀事件
void reset_oneshot(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 將文件描述符從內核事件表中移除
void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}class user_info
{
public:int user_sock;std::string user_name;int user_epollfd;public:user_info(int sock, std::string name, int epollfd = 0) : user_sock(sock), user_name(name), user_epollfd(epollfd) {}
};// 由于用戶p_user_info上下線或者發送數據,而轉發位置在p_str的num字節數據給其他用戶
void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}// 該函數基于:線程有獨立的棧結構
void *handler_online(void *args)
{user_info *p_user_info = (user_info *)args;std::string str;str += p_user_info->user_name + ": online";send_turn(p_user_info, str.c_str(), str.size());LOG(INFO, "handler_online success " + str);delete p_user_info;
}void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收數據if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 對方關閉連接if (bytes == 0){// 該用戶已離開std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除該文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users該成員users.erase(p_user_info->user_sock);// 關閉該連接close(p_user_info->user_sock);break;}if (bytes < 0){// 已經轉發完所有數據且對方沒有關閉連接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加該文件描述符上的讀事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);if (epollfd == -1){LOG(FATAL, "epoll_create failed");}// 監聽套接字不能設置為只提醒一次addfd(epollfd, listenfd, false);while (1){int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if ((number < 0) && (errno != EINTR)){LOG(FATAL, "epoll_wait failed");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 新連接到來if (sockfd == listenfd){int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);// 注冊客戶連接文件描述符,設置為只提醒一次addfd(epollfd, connfd, true);std::string client_ip = inet_ntoa(client_address.sin_addr);int client_port = ntohs(client_address.sin_port);std::string user = client_ip + std::to_string(client_port);if (users[connfd] == "") // 這代表客戶首次到來,我們需要告訴其他人,這個客戶上線了{// 先將該用戶存儲在在線用戶表中users[connfd] = user;// 新啟一個線程去發送上線通知user_info *p_user_info = new user_info(connfd, user);pthread_t tid;pthread_create(&tid, NULL, handler_online, (void *)p_user_info);// 分離該線程pthread_detach(tid);}}// 新數據到來或者對端關閉連接else if (events[i].events & EPOLLIN){user_info *p_user_info = new user_info(sockfd, users[sockfd], epollfd);pthread_t tid;pthread_create(&tid, NULL, handler_turn, (void *)p_user_info);// 分離該線程pthread_detach(tid);}else{}}}close(listenfd);return 0;
}
? ? ? ? chatclient.cpp
#include "LOG.hpp"
#include <poll.h>
#define BUFFER_SIZE 64int main( int argc, char* argv[] )
{if( argc <= 2 ){printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );return 1;}const char* ip = argv[1];int port = atoi( argv[2] );struct sockaddr_in server_address;bzero( &server_address, sizeof( server_address ) );server_address.sin_family = AF_INET;inet_pton( AF_INET, ip, &server_address.sin_addr );server_address.sin_port = htons( port );int sockfd = socket( PF_INET, SOCK_STREAM, 0 );if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 ){printf( "connection failed\n" );close( sockfd );return 1;}pollfd fds[2];fds[0].fd = 0;fds[0].events = POLLIN;fds[0].revents = 0;fds[1].fd = sockfd;fds[1].events = POLLIN | POLLRDHUP;fds[1].revents = 0;char read_buf[BUFFER_SIZE];int pipefd[2];int ret = pipe( pipefd );while( 1 ){ret = poll( fds, 2, -1 );if( ret < 0 ){printf( "poll failure\n" );break;}if( fds[1].revents & POLLRDHUP ){printf( "server close the connection\n" );break;}else if( fds[1].revents & POLLIN ){memset( read_buf, '\0', BUFFER_SIZE );recv( fds[1].fd, read_buf, BUFFER_SIZE-1, 0 );printf( "%s\n", read_buf );}if( fds[0].revents & POLLIN ){ret = splice( 0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );ret = splice( pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );}}close( sockfd );return 0;
}
? ? ? ? common.hpp
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <signal.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/sendfile.h>#include <arpa/inet.h>
#include <pthread.h>#include<iostream>#include <exception>
#include <semaphore.h>#include <list>
#include <ctime>#include <iomanip>
#include <sstream>
? 代碼分析
????????ET + EPOLLONESHOT模式下
void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收數據if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 對方關閉連接if (bytes == 0){// 該用戶已離開std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除該文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users該成員users.erase(p_user_info->user_sock);// 關閉該連接close(p_user_info->user_sock);break;}if (bytes < 0){// 已經轉發完所有數據且對方沒有關閉連接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加該文件描述符上的讀事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}
? ? ? ? 這段代碼很值得大家去分析。在ET + EPOLLONESHOT模式下,進程先讀取數據,由于每次通信都是聊天式的,收發數據量都不大,所以說大概率一次就把數據取完,每次取完數據直接發送數據。IO是耗時操作,如果在發送數據時,該客戶又到來新一批數據,則在下一輪循環中,該線程可以繼續轉發該用戶數據。大家可以去體會一下,如果我把客戶數據全部拿到后一起轉發行不行呢?大家可以去修改一下上述代碼,
if (bytes < 0){// 已經轉發完所有數據且對方沒有關閉連接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加該文件描述符上的讀事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}
? ? ? ? 如果大家把轉發放在if(errno == ...)這個函數體中,發送結束后線程退出,就會引發新問題。發送數據是個耗時操作,如果該線程在這個過程中關閉連接,則無法在下一輪循環中得到這個信息,因為線程已經退出。
問題分析
? ? ? ? 在該代碼中資源競爭問題比較大
void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}
? ? ? ? 如果此時users中被其他線程插入了新用戶,則完全有可能新用戶,無法收到本該在他上線后應該收到的消息。假使我們對users容器進行加鎖保護,這會使效率大幅度下降,因為在同一時刻,永遠只有一個線程能訪問user容器,無法實現多線程轉發的高效率。我們自然可以通過一些手段改進一下這個問題,但是我們轉過頭來想一想。一個新用戶上線后的1秒中之內,沒有收到技術上來講應該收到的消息,這從宏觀上講不算什么問題。
? ? ? ? 如果多線程send時往同一個文件描述符中寫入數據,就有可能造成數據亂序問題。如果我們對send上鎖,這也屬于因噎廢食。假使有1000個用戶同時在線,那么多線程正好往一個文件描述符中寫入數據的概率也不是很大。
? ? ? ? 作為練習來講,我們我不再花大功夫去解決諸如上述這些問題。但是在實際項目中,我們絕對要力求將其寫到最佳。
? ? ? ? 可是有個問題我們應當解決。如果短時間內突然需要大量轉發操作,那么操作系統內部就會有大量線程,服務器壓力太大,我們采用線程池去解決這個問題。
線程池版本的聊天室服務器
????????服務器目錄結構
? ? ? ? chatserver.cpp
#include "LOG.hpp"
#include <sys/epoll.h>
#include "threadpool.hpp"
#define MAX_EVENT_NUMBER 10000
#define BUFFER_SIZE 1024using std::cerr;
using std::cin;
using std::cout;
using std::endl;std::unordered_map<int, std::string> users; // 存儲在線客戶// 設置文件描述符為非阻塞
int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}// 將文件描述符上的讀事件注冊進內核事件表,ET + EPOLLONESHOT模式;并設置文件描述符為非阻塞
// ET + EPOLLONESHOT模式:ET模式下,只有數據被全部讀完后,下次到來數據才會提醒;而該模式下,設置ET + EPOLLONESHOT后,用于只提醒一次,除非重新設置
// 原因:這樣在同一時刻,一個連接永遠只有一個線程在為其服務
void addfd(int epollfd, int fd, bool one_shot)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;if (one_shot){event.events |= EPOLLONESHOT;}epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}
// 重新添加文件描述符上的讀事件
void reset_oneshot(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}// 將文件描述符從內核事件表中移除
void removefd(int epollfd, int fd)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);close(fd);
}class user_info
{
public:int user_sock;std::string user_name;int user_epollfd;int flag; // 0表示上線關聯handler_online 1表示在線發送數據關聯handler_turnpublic:user_info(int sock, std::string name, int epollfd = 0,int f =0) : user_sock(sock), user_name(name), user_epollfd(epollfd),flag(f) {}};// 由于用戶p_user_info上下線或者發送數據,而轉發位置在p_str的num字節數據給其他用戶
void send_turn(user_info *p_user_info, const char *p_str, size_t num)
{for (auto pair : users){if (pair.first != p_user_info->user_sock && pair.second != ""){std::string pre_str;pre_str = '[' + p_user_info->user_name + ']' + ' ';send(pair.first, pre_str.c_str(), pre_str.size(), 0);int count = 0;while (count < num){ssize_t bytes = send(pair.first, p_str + count, num - count, 0);if (bytes == -1 && errno != EAGAIN && errno != EWOULDBLOCK){LOG(FATAL, "send_turn failed");break;}count += bytes;}}}
}// 該函數基于:線程有獨立的棧結構
void *handler_online(void *args)
{user_info *p_user_info = (user_info *)args;std::string str;str += p_user_info->user_name + ": online";send_turn(p_user_info, str.c_str(), str.size());LOG(INFO, "handler_online success " + str);delete p_user_info;
}void *handler_turn(void *args)
{user_info *p_user_info = (user_info *)args;char *p_read = new char[BUFFER_SIZE];int count = 0;while (1){ssize_t bytes = recv(p_user_info->user_sock, p_read + count, BUFFER_SIZE - count, 0);// 正常接收數據if (bytes > 0){int tmp_count = count;count += bytes;send_turn(p_user_info, p_read + tmp_count, bytes);}// 對方關閉連接if (bytes == 0){// 該用戶已離開std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());// 移除該文件描述符removefd(p_user_info->user_epollfd, p_user_info->user_sock);// 移除users該成員users.erase(p_user_info->user_sock);// 關閉該連接close(p_user_info->user_sock);break;}if (bytes < 0){// 已經轉發完所有數據且對方沒有關閉連接if (errno == EAGAIN || errno == EWOULDBLOCK){// 重新添加該文件描述符上的讀事件reset_oneshot(p_user_info->user_epollfd, p_user_info->user_sock);break;}else{LOG(FATAL, "handler_turn recv: failed");std::string str;str += p_user_info->user_name + ": exit";send_turn(p_user_info, str.c_str(), str.size());removefd(p_user_info->user_epollfd, p_user_info->user_sock);users.erase(p_user_info->user_sock);close(p_user_info->user_sock);break;}}}delete p_read;delete p_user_info;
}void* handler (void* args)
{user_info *p_user_info = (user_info *)args;if(p_user_info->flag == 0){handler_online(p_user_info);}else{handler_turn(p_user_info);}
}int main(int argc, char *argv[])
{if (argc < 2){cout << "usage: myserver port_number" << endl;return 1;}int port = atoi(argv[1]);int listenfd = socket(PF_INET, SOCK_STREAM, 0);if (listenfd < 0){LOG(FATAL, "socket() failed");}int opt = 1;// 設置地址復用,防止服務器崩掉后進入TIME_WAIT,短時間連不上當前端口setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(port);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));if (ret < 0){LOG(FATAL, "bind() failed");}ret = listen(listenfd, 5);if (ret < 0){LOG(FATAL, "listen() failed");}struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);signal(SIGPIPE, SIG_IGN);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);if (epollfd == -1){LOG(FATAL, "epoll_create failed");}// 監聽套接字不能設置為只提醒一次addfd(epollfd, listenfd, false);threadpool<user_info*>* pool = new threadpool<user_info*>(10,10000);// 此時已經有10個線程在嗷嗷待哺while (1){int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if ((number < 0) && (errno != EINTR)){LOG(FATAL, "epoll_wait failed");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;// 新連接到來if (sockfd == listenfd){int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);// 注冊客戶連接文件描述符,設置為只提醒一次addfd(epollfd, connfd, true);std::string client_ip = inet_ntoa(client_address.sin_addr);int client_port = ntohs(client_address.sin_port);std::string user = client_ip + std::to_string(client_port);if (users[connfd] == "") // 這代表客戶首次到來,我們需要告訴其他人,這個客戶上線了{// 先將該用戶存儲在在線用戶表中users[connfd] = user;// 新啟一個線程去發送上線通知user_info *p_user_info = new user_info(connfd, user);pthread_t tid;pthread_create(&tid, NULL, handler_online, (void *)p_user_info);// 分離該線程pthread_detach(tid);}}// 新數據到來或者對端關閉連接else if (events[i].events & EPOLLIN){user_info *p_user_info = new user_info(sockfd, users[sockfd], epollfd,1);pthread_t tid;pthread_create(&tid, NULL, handler, (void *)p_user_info);// 分離該線程pthread_detach(tid);}else{}}}close(listenfd);return 0;
}
? ? ? ? threadpool.hpp
#pragma once
#include "locker.hpp"
#include "LOG.hpp"template <typename T>
class threadpool
{
public:threadpool(int thread_number = 8, int max_requests = 10000);~threadpool();bool append(T request);private:static void *worker(void *arg);void run();private:int m_thread_number; /* 線程池中線程的數量 */int m_max_requests; /* 任務隊列的大小 */pthread_t *m_threads; /* 描述線程池的數組,其大小為m_thread_number */// 線程池內部維護請求隊列,意味著線程池要提供append接口// 請求隊列或者說任務隊列,本身屬于臨界資源,需要加鎖訪問// 請求隊列中存儲的是指向user_info在堆區位置的指針std::list<T> m_workqueue; /* 任務隊列 */locker m_queuelocker; /* 保護請求隊列的互斥鎖 */sem m_queuestat; /* 是否有任務需要處理 */bool m_stop; /* 是否結束線程 */
};// 構造函數
template <typename T>
threadpool<T>::threadpool(int thread_number, int max_requests) : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL)
{if ((thread_number <= 0) || (max_requests <= 0)){LOG(FATAL,"線程池創建時參數有誤");}m_threads = new pthread_t[m_thread_number];if (!m_threads){LOG(ERROR,"線程標識符數組創建失敗");}for (int i = 0; i < thread_number; ++i){if (pthread_create(m_threads + i, NULL, worker, this) != 0){delete[] m_threads;LOG(FATAL,"線程創建失敗");}if (pthread_detach(m_threads[i])){delete[] m_threads;LOG(FATAL,"線程分離失敗");}}std::cerr << "***********************************全部線程創建成功***********************************" << std::endl;
}template <typename T>
threadpool<T>::~threadpool()
{delete[] m_threads;m_stop = true;LOG(INFO,"線程池已析構");
}template <typename T>
bool threadpool<T>::append(T request)
{m_queuelocker.lock();if (m_workqueue.size() > m_max_requests){m_queuelocker.unlock();return false;}m_workqueue.push_back(request);m_queuelocker.unlock();m_queuestat.post();//增加信號量return true;
}template <typename T>
void *threadpool<T>::worker(void *arg)
{threadpool *pool = (threadpool *)arg;pool->run();return pool;
}template <typename T>
void threadpool<T>::run()
{while (!m_stop){// 申請信號量LOG(INFO,"有個線程在申請信號量");m_queuestat.wait();LOG(INFO,"有個線程申請信號量成功");m_queuelocker.lock();// 請求隊列為空if (m_workqueue.empty()){m_queuelocker.unlock();continue;}// 從任務隊列取走任務T request = m_workqueue.front();m_workqueue.pop_front();LOG(INFO,"有個線程取走任務");m_queuelocker.unlock();if (!request){continue;}// 執行任務LOG(INFO,"有個線程開始執行任務");handler(request);LOG(INFO,"有個線程執行任務結束");}
}
? ? ? ? 我沒發出來的代碼都是跟之前發生時沒有區別,大家直接復制上文代碼即可。
總結
- 對于聊天室服務器重在關注epoll機制,ET + EPOLLONESHOT模式以及如何配合線程池
- 大家要想把上面寫的代碼全都搞明白,最好先測試運行,然后自己逐行分析,然后自己重寫一遍。
- 對于聊天室服務器我沒有參考其他人的寫法,所以說基于我沒有相關從業經驗,肯定寫的不全面。正如上面說的大家關注重點部分,當做學習使用即可。
- 對于http服務器,我參考了linux高性能服務器編程本書以及網上的某些項目,但是我發現了這些項目存在的很多小問題或者錯誤或者就是邏輯相對混亂或者就是效率較低。我從零重寫了上述http各種版本的服務器,在我們寫的代碼中,主邏輯幾乎是線性的也更加清晰。但是大家想要看懂依舊并不容易,如果大家逐行分析,并且再次從零構建,大家會遇到很多值得思考的問題。