半同步/半反應堆線程池實現簡單web服務器,解析http請求
此篇爲《linux高性能服務器編程》第15章線程池實例的學習筆記。
半同步/半反應堆線程池模型與進程池模型類似,不過需要考慮使用請求隊列,互斥鎖來同步線程之間的工作。
locker.h
首先,locker.h文件實現了NPTL線程的三種同步機制的封裝。將其封裝成對象,便於管理。
#pragma once
/*
此文件是對三種線程同步機制的封裝
*/
#ifndef LOCKER_H
#define LOCKER_H
#include <exception>
#include <pthread.h>
#include <semaphore.h>
//封裝信號量的類
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_cond_t m_cond;
pthread_mutex_t m_mutex;
};
#endif // !LOCKER_H
threadpool.h
threadpool.h聲明瞭線程池的成員,定義了各個成員函數的邏輯。
#pragma once
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <list>
#include <vector>
#include <exception>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <string.h>
#include "locker.h"
//線程池類
template<typename T>
class threadpool {
public:
threadpool(int thread_number = 8, int max_requests = 10000);
~threadpool();
//向請求隊列中添加任務
bool append(T* request);
static void* worker(void *arg);
void run();
private:
int m_thread_number;
int m_max_requests;//請求隊列中允許的最大請求數
pthread_t* m_threaeds;//線程池數組
std::list<T*>m_workqueue;//請求隊列
locker m_queuelocker;//保護請求隊列的互斥鎖
sem m_queuestat;//是否有任務需要處理
bool m_stop; //是否結束線程
};
template<typename T>
threadpool<T>::threadpool(int thread_number = 8, int max_requests = 10000) :m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL) {
if ((thread_number <= 0 || (max_requests <= 0))) {
throw std::exception();
}
m_threads = new pthread_t[m_thread_number];
if (!m_threads) {
throw std::exception();
}
for (int i = 0; i < m_thread_number; i++) {
printf("create the %dth thread\n", i);
/*
worker是類的靜態成員函數,爲了調用動態成員,有兩種方法
1.通過類的靜態對象調用
2.將類的對象作爲參數傳遞給該靜態函數
所以這裏使用第二種方法,將this指針傳遞給worker.
*/
if (pthread_create(m_threads + i, NULL, worker, this) != 0) {
delete[]m_threaeds;
throw std::exception();
}
/*
pthread_detach:pthread_detach將指定的線程指明爲分離態,在線程執行完畢之後立即退出並釋放所有資源,是非阻塞的
*/
if (pthread_detach(m_threads[i])) {
delete[]m_threads;
throw std::exception();
}
}
}
template<typename T>
threadpool<T>::~threadpool() {
delete[]m_threaeds;
m_stop = true;
}
template<typename T>
bool threadpool<T>::append(T* requests) {
//操作請求隊列,一定需要加鎖,請求隊列屬於臨界資源
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) {
m_queuestat.wait();
m_queuelocker.lock();
if(m_workqueue.empty(){
m_queuelocker.unlock();
continue;
}
T* request=m_workqueue.front();
m_workqueue.pop_front();
m_queuelocker.unlock();
if(!request){
continue;
}
//取出請求之後,由工作線程完成process()處理請求的邏輯計算
request->process();
}
}
#endif // !THREADPOOL_H
代碼中比較重要的就是pthread_create傳入類靜態成員函數的解決方法,因爲靜態成員函數無法調用動態成員,所以有兩種方法解決這個問題:
1.通過類的靜態對象來調用,比如單例模式中,維護了一個全局唯一的單例指針實例,可以通過這個實例來調用。
2.像代碼中的一樣,將類的對象指針this作爲參數傳給該靜態函數,即可通過此指針調用動態成員。
在代碼中,worker函數使用了傳入的this指針,獲取了類的實例,從而調用動態成員。
除此之外,run()函數則是工作線程的主要邏輯,爲模板參數類的process()提供計算力。
http_conn.h:模板參數類,實現處理http的邏輯
http_conn類作爲傳入線程池的模板參數T,需要實現process()函數,以完成對http的解析。
#pragma once
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <cstdio>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <errno.h>
#include <stdarg.h>
#include <assert.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include "locker.h"
class http_conn {
public:
//文件名最大長度
static const int FILENAME_LEN = 200;
//讀緩衝區大小
static const int READ_BUFFER_SIZE = 2048;
//寫緩衝區大小
static const int WRITE_BUFFER_SIZE = 1024;
//HTTP的請求方法
enum METHOD{
GET = 0, POST = 1, HEAD = 2, PUT = 3, DELETE = 4, TRACE = 5, OPTIONS = 6, CONNECT = 7, PATCH = 8
};
enum CHECK_STATE {
CHECK_STATE_REQUESTLINE = 0,
CHECK_STATE_HEADER = 1,
CHECK_STATE_CONTENT = 2
};
enum HTTP_CODE {
NO_REQUEST, GET_REQUEST, BAD_REQUEST,
NO_RESOURCE,FORBIDDEN_REQUEST,FILE_REQUEST,
INTERNAL_ERROR,CLOSED_CONNECTION
};
enum LINE_STATUS {
LINE_OK = 0, LINE_BAD = 1, LINE_OPEN = 2
};
public:
http_conn();
~http_conn();
public:
//初始化新接受的連接
void init(int sockfd, const sockaddr_in& addr);
//關閉連接
void close_conn(bool read_close = true);
//處理客戶請求
void process();
//非阻塞讀入操作
bool read();
//非阻塞寫操作
bool write();
private:
//初始化連接
void init();
HTTP_CODE process_read();
//填充http應答
bool process_write(HTTP_CODE ret);
//下面的一組函數由process_write函數調用,分析http請求
//解析HTTP請求行,獲得請求方法,目標URL,HTTP版本號
HTTP_CODE parse_request_line(char *text);
//解析http請求的一個頭部信息
HTTP_CODE parse_headers(char *text);
//我們沒有真正解析HTTP請求的請求體,而是判斷他是否被完整讀入
HTTP_CODE parse_content(char *text);
//do_request進行內存映射
HTTP_CODE do_request();
char *get_line() {
return m_read_buf + m_start_line;
}
//從狀態機,從buffer中解析一行http
//如果獲得完整的行,返回LINE_OK,並從m_read_idx到m_check_idx即爲請求行的內容
LINE_STATUS parse_line();
//下面這一組函數被process_write函數用以填充http應答
void unmap();
//c++省略號實現可變參數函數,可通過使用va_list,va_arg等宏定義獲取參數,定義在<cstdarg>頭文件中
bool add_response(const char *format, ...);
bool add_content(const char* content);
bool add_status_line(int status, const char* title);
bool add_content_length(int content_length);
bool add_headers(int content_length);
bool add_linger();
bool add_blank_line();
public:
static int m_epollfd;
static int m_user_count;//統計用戶數量
private:
//該http連接的socket
int m_sockfd;
//對法的socket地址
sockaddr_in m_address;
//讀緩衝區
char m_read_buf[READ_BUFFER_SIZE];
//讀下標地址
int m_read_idx;
//當前正在分析的行的起始位置
int m_check_idx;
//當前正在解析的行的起始地址
int m_start_line;
//寫緩衝區
char m_write_buf[WRITE_BUFFER_SIZE];
//寫緩衝區下標
int m_write_idx;
//主狀態機當前處於的狀態,採用有限狀態機的思想
CHECK_STATE m_check_state;
//請求方法
METHOD m_method;
//客戶請求的目標文件的完整路徑,其內容等於doc_root+m_url,doc_root爲網站的根目錄
char m_real_file[FILENAME_LEN];
//客戶請求的目標文件名
char *m_url;
//http協議版本號,支持HTTP/1.1
char *m_version;
//主機名
char *m_host;
//HTTP請求消息體長度
int m_content_length;
//http請求是否要求保持連接
bool m_linger;
//客戶請求的目標文件被mmap到內存中的起始位置
char *m_file_address;
//目標文件的狀態。判斷目標文件是否存在,是否可讀可寫,並獲取目標文件的大小信息。
struct stat m_file_stat;
//我們採用writev函數執行寫操作,所以定義下面兩個成員,其中m_iv_count表示被寫內存塊的數量
struct iovec m_iv[2];//將http分爲兩個連續存儲區,一個存儲請求頭和請求行,一個存儲請求體
int m_iv_count;
};
#endif
函數衆多,但其實通過分類與梳理,就會發現,private的函數都是爲public函數服務的,而public函數就只有5個
-
void init(int sockfd, const sockaddr_in& addr);
init()函數初始化了http_conn的連接socket,以及客戶端socket地址addr,再通過調用private的init()函數完成其他參數的初始化,如清空緩衝區,初始化m_epollfd等。
-
void close_conn(bool read_close = true);
關閉連接,close掉連接socket並釋放資源
-
void process();
process()函數最爲重要,看一下函數主體
//http_conn的工作函數,由線程池中的工作線程調用,這是處理http請求的入口函數 void http_conn::process() { HTTP_CODE read_ret = process_read(); if (read_ret == NO_REQUEST) { //NO_REQUEST表示沒有數據,提醒讀入事件 modfd(m_epollfd, m_sockfd, EPOLLIN); return; } //根據process_read的請求結果,返回特定的信息,如404,403 bool write_ret = process_write(read_ret); if (!write_ret) { close_conn(); } //成功則說明緩衝區有數據可寫,提醒寫事件 modfd(m_epollfd, m_sockfd, EPOLLOUT); }
我們發現,process()函數調用process_read()函數與process_write()函數,process_read()函數負責讀取和解析服務器讀取客戶發送過來的請求,他調用了一系列解析http的狀態機函數,如下:
//解析HTTP請求行,獲得請求方法,目標URL,HTTP版本號 HTTP_CODE parse_request_line(char *text); //解析http請求的一個頭部信息 HTTP_CODE parse_headers(char *text); //我們沒有真正解析HTTP請求的請求體,而是判斷他是否被完整讀入 HTTP_CODE parse_content(char *text); //do_request進行內存映射 HTTP_CODE do_request(); char *get_line() { return m_read_buf + m_start_line; } //從狀態機,從buffer中解析一行http //如果獲得完整的行,返回LINE_OK,並從m_read_idx到m_check_idx即爲請求行的內容 LINE_STATUS parse_line();
這些函數使用有限狀態機的思想,讀取並解析http請求行,請求頭以及請求體,並返回請求結果,我們只需根據請求結果進行process_write(),向客戶端反饋即可。
process_write()函數負責將結果反饋給客戶端,有以下函數的調用
//c++省略號實現可變參數函數,可通過使用va_list,va_arg等宏定義獲取參數,定義在<cstdarg>頭文件中 bool add_response(const char *format, ...); bool add_content(const char* content); bool add_status_line(int status, const char* title); bool add_content_length(int content_length); bool add_headers(int content_length); bool add_linger(); bool add_blank_line();
函數功能即爲名字描述,向結果緩衝區中加入一系列字符串即可。
-
bool read();
read()函數即時從當前有事件的epollfd上循環讀取數據,由process_read函數調用
//循環讀取客戶數據,直到無數據可讀或者對方關閉連接 bool http_conn::read() { if (m_read_idx >= READ_BUFFER_SIZE) { return false; } int bytes_read = 0; while (true) { bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0); if (bytes_read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { continue; } return false; } if (bytes_read == 0) { //有可能關閉連接,也有可能緩衝區衝爆了 return false; } m_read_idx += bytes_read; if (m_read_idx >= READ_BUFFER_SIZE)break; } return true; }
-
bool write()
write()函數則是向客戶端socket寫入反饋信息,由process_write函數調用
//寫HTTP響應 bool http_conn::write() { int temp = 0; int bytes_have_send = 0; int bytes_to_send = m_write_idx; if (bytes_to_send == 0) { //沒東西可寫,需要讓m_sockfd去讀 modfd(m_epollfd, m_sockfd, EPOLLIN); init(); return true; } while (1) { //writev以順序iov[0]、iov[1]至iov[iovcnt-1]從各緩衝區中聚集輸出數據到fd,減少了read和write的系統調用 //readv則相反,將fd的內容一個一個填滿iov[0],iov[1]...iov[count-1] temp = writev(m_sockfd, m_iv, m_iv_count); if (temp <= -1) { /* 如果TCP寫緩衝區沒有空間,則等待下一輪EPOLLOUT事件。雖然在此期間,服務器無法立即接受到同一個客戶的下一個請求, 但這樣可以保證連接的完整性 */ if (errno == EAGAIN || errno == EWOULDBLOCK) { modfd(m_epollfd, m_sockfd, EPOLLOUT); return true; } unmap(); return false; } bytes_to_send -= temp; bytes_have_send += temp; if (bytes_to_send <= bytes_have_send) { //發送HTTP響應成功,根據HTTP請求中的Connection字段決定是否關閉連接 unmap(); if (m_linger) { init(); modfd(m_epollfd, m_sockfd, EPOLLIN); return true; } else { modfd(m_epollfd, m_sockfd, EPOLLIN); return false; } } } }
http_conn.cpp,實現了http_conn聲明的各種函數
/*
此文件爲對http_conn.h頭文件函數的實現
*/
#include "http_conn.h"
//定義HTTP響應的一些狀態信息
const char* ok_200_title = "OK";
const char* error_400_title = "Bad Request";
const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n";
const char* error_403_title = "Forbidden";
const char* error_403_form = "You don't have permission to get file from this sever.\n";
const char* error_404_title = "Not Found";
const char* error_404_form = "The request file was not found on this server.\n";
const char* error_500_title = "Internal Error";
const char* error_500_form = "There was an unusual problem serving the requested file.\n";
//網站根目錄
const char* doc_root = "/var/www/html";//根據自己情況進行修改
//設置文件描述符爲非阻塞
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;
}
//one_shot爲EPOLLONESHOT事件
void add_read_fd(int epollfd, int fd,bool one_shot) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLHUP;
if (one_shot) {
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
void add_write_fd(int epollfd, int fd, bool one_shot) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLOUT | EPOLLET | EPOLLHUP;
if (one_shot) {
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
void removefd(int epollfd, int fd) {
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);
}
//修改事件
void modfd(int epollfd, int fd, int ev) {
epoll_event event;
event.data.fd = fd;
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLHUP;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;
void http_conn::close_conn(bool real_close) {
if (real_close && (m_sockfd != -1)) {
removefd(m_epollfd, m_sockfd);
m_sockfd = -1;
m_user_count--;//總連接數-1
}
}
//初始化,addr爲client_address,即連接客戶端地址,sockfd爲連接fd
void http_conn::init(int sockfd, const sockaddr_in& addr) {
m_sockfd = sockfd;
m_address = addr;
//如下兩行使用SO_REUSEADDR避免TIME_WAIT狀態,僅作爲調試時使用,實際使用應當刪除
int reuse = 1;
setsockopt(m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
add_read_fd(m_epollfd, m_sockfd, true);
m_user_count++;
init();
}
void http_conn::init() {
m_check_state = CHECK_STATE_REQUESTLINE;
//是否保持連接
m_linger = false;
//方法,只支持GET
m_method = GET;
//初始化url
m_url = 0;
//初始化版本
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_check_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
memset(m_read_buf, '\0', sizeof(m_read_buf));
memset(m_write_buf, '\0', sizeof(m_write_buf));
memset(m_real_file, '\0', sizeof(m_real_file));
}
//從狀態機,從buffer中解析一行http
//如果獲得完整的行,返回LINE_OK,並從m_read_idx到m_check_idx即爲請求行的內容
//否則返回LINE_OPEN:表示一行未讀取完;LINE_BAD:請求行錯誤
http_conn::LINE_STATUS http_conn::parse_line() {
char temp;
for (; m_check_idx < m_read_idx; m_check_idx++) {
temp = m_read_buf[m_check_idx];
//http請求行結束的標誌是一個回車換行符\r\n
if (temp == '\r') {
if (m_check_idx + 1 == m_read_idx) {
//一行請求沒有讀完,LINE_OPEN表示請求繼續讀
return LINE_OPEN;
}
else if (m_read_buf[m_check_idx + 1] == '\n') {
//說明讀到了一個完整的行
m_read_buf[m_check_idx++] = '\0';
m_read_buf[m_check_idx++] = '\0';
return LINE_OK;
}
else return LINE_BAD;
}
else if (temp == '\n') {
if (m_check_idx > 1 && m_read_buf[m_check_idx - 1] == '\r') {
m_read_buf[m_check_idx - 1] = '\0';
m_read_buf[m_check_idx++] = '\0';
return LINE_OK;
}
else return LINE_BAD;
}
}
return LINE_OPEN;
}
//循環讀取客戶數據,直到無數據可讀或者對方關閉連接
bool http_conn::read() {
if (m_read_idx >= READ_BUFFER_SIZE) {
return false;
}
int bytes_read = 0;
while (true) {
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue;
}
return false;
}
if (bytes_read == 0) {
//有可能關閉連接,也有可能緩衝區衝爆了
return false;
}
m_read_idx += bytes_read;
if (m_read_idx >= READ_BUFFER_SIZE)break;
}
return true;
}
//解析HTTP請求行,獲得請求方法,目標URL,HTTP版本號
http_conn::HTTP_CODE http_conn::parse_request_line(char* text) {
m_url = strpbrk(text, "\t");
if (!m_url) {
return BAD_REQUEST;
}
*m_url++ = '\0';
char* method = text;
if (strcasecmp(method, "GET") == 0) {
m_method = GET;
}
else {
return BAD_REQUEST;
}
m_url += strspn(m_url, "\t");
m_version = strpbrk(m_url, "\t");
if (!m_version) {
return BAD_REQUEST;
}
*m_version++ = '\0';
m_version += strspn(m_version, "\t");
if (strcasecmp(m_version, "HTTP/1.1") != 0) {
return BAD_REQUEST;
}
if (strncasecmp(m_url, "http://", 7) == 0) {
m_url += 7;
m_url = strchr(m_url, '/');
}
if (!m_url || m_url[0] != '/') {
return BAD_REQUEST;
}
m_check_state = CHECK_STATE_HEADER;
return NO_REQUEST;
}
//解析http請求的一個頭部信息
//請求頭
http_conn::HTTP_CODE http_conn::parse_headers(char* text) {
//遇到空行,表示頭部信息處理完畢,接下來是正文
if (text[0] == '\0') {
//如果http請求有消息體,則還需要讀取m_content_length字節的消息體
if (m_content_length) {
m_check_state = CHECK_STATE_CONTENT;
return NO_REQUEST;
}
//否則表示沒有請求體,http請求處理完成
return GET_REQUEST;
}
//處理Connection頭部字段
else if (strncasecmp(text, "Connention:", 11) == 0) {
text += 11;
text += strspn(text, "\t");
if (strcasecmp(text, "keep-alive") == 0) {
m_linger = true;
}
}
//處理Content-Length頭部字段
else if (strncasecmp(text, "Content-Length:", strlen("Content-Length:") == 0)) {
text += strlen("Content-Length:");
text += strspn(text, "\t");
m_content_length = atol(text);
}
//處理Host頭部字段
else if (strncasecmp(text, "Host:", strlen("Host:")) == 0) {
text += strlen("Host:");
text += strspn(text, "\t");
m_host = text;
}
else {
printf("oops!The server don't know the header!\n");
}
return NO_REQUEST;
}
//我們沒有真正解析HTTP請求的請求體,而是判斷他是否被完整讀入
http_conn::HTTP_CODE http_conn::parse_content(char *text) {
if (m_read_idx >= (m_content_length + m_check_idx)) {
text[m_content_length] = '\0';
return GET_REQUEST;
}
return NO_REQUEST;
}
//主狀態機,根據m_check_stat的狀態進行轉移
/*
CHECK_STATE_CONTENT:表示請求行解析成功,準備解析請求體
CHECK_STATE_REQUESTLINE:表示準備解析請求行,LINE_OK狀態只表示行形式正確
CHECK_STATE_HEADER:表示準備解析請求頭,必須是請求行解析完成之後
*/
http_conn::HTTP_CODE http_conn::process_read() {
LINE_STATUS line_status = LINE_OK;
HTTP_CODE ret = NO_REQUEST;
char* text = 0;
while (((m_check_state == CHECK_STATE_CONTENT) && (line_status == LINE_OK)) || ((line_status == parse_line()) == LINE_OK)) {
text = get_line();
m_start_line = m_check_idx;
printf("got 1 http line: %s\n", text);
//根據m_check_state進行轉移
switch (m_check_state) {
//如果準備解析請求行,調用parse_request_line函數
case CHECK_STATE_REQUESTLINE:
{
ret = parse_request_line(text);
if (ret == BAD_REQUEST) {
return BAD_REQUEST;
}
break;
}
//如果準備解析請求頭,調用parse_headers函數
case CHECK_STATE_HEADER: {
ret = parse_headers(text);
if (ret == BAD_REQUEST) {
return BAD_REQUEST;
}
else if (ret == GET_REQUEST) {
//解析完請求頭,如果沒有請求體,則會返回GET_REQUEST,此時表示http請求已經解析完成,do_request執行
return do_request();
}
break;
}
//如果準備解析請求體,調用parse_content函數
case CHECK_STATE_CONTENT: {
ret = parse_content(text);
if (ret == GET_REQUEST) {
//解析完請求體,返回GET_REQUEST,do_request執行
return do_request();
}
//恢復line_status狀態
line_status = LINE_OPEN;
break;
}
default: {
return INTERNAL_ERROR;
}
}
}
return NO_REQUEST;
}
/*
do_request:當得到一個完整的,正確的http請求時,我們需要分析目標文件的屬性。如果目標文件存在,對所有用戶可讀,且不是目錄,則使用
mmap將其映射到內存地址m_file_address處,並告訴調用者獲取文件成功。
*/
http_conn::HTTP_CODE http_conn::do_request() {
strcpy(m_real_file, doc_root);
int len = strlen(doc_root);
strncpy(m_real_file + len, m_url, FILENAME_LEN - len + 1);
if (stat(m_real_file, &m_file_stat) < 0) {
return NO_REQUEST;
}
if (!(m_file_stat.st_mode & S_IROTH)) {
return FORBIDDEN_REQUEST;
}
if (S_ISDIR(m_file_stat.st_mode)) {
return BAD_REQUEST;
}
int fd=open(m_real_file,O_RDONLY);
m_file_address = (char*)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);
return FILE_REQUEST;
}
//收回共享內存
void http_conn::unmap() {
if (m_file_address) {
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
//寫HTTP響應
bool http_conn::write() {
int temp = 0;
int bytes_have_send = 0;
int bytes_to_send = m_write_idx;
if (bytes_to_send == 0) {
//沒東西可寫,需要讓m_sockfd去讀
modfd(m_epollfd, m_sockfd, EPOLLIN);
init();
return true;
}
while (1) {
//writev以順序iov[0]、iov[1]至iov[iovcnt-1]從各緩衝區中聚集輸出數據到fd,減少了read和write的系統調用
//readv則相反,將fd的內容一個一個填滿iov[0],iov[1]...iov[count-1]
temp = writev(m_sockfd, m_iv, m_iv_count);
if (temp <= -1) {
/*
如果TCP寫緩衝區沒有空間,則等待下一輪EPOLLOUT事件。雖然在此期間,服務器無法立即接受到同一個客戶的下一個請求,
但這樣可以保證連接的完整性
*/
if (errno == EAGAIN || errno == EWOULDBLOCK) {
modfd(m_epollfd, m_sockfd, EPOLLOUT);
return true;
}
unmap();
return false;
}
bytes_to_send -= temp;
bytes_have_send += temp;
if (bytes_to_send <= bytes_have_send) {
//發送HTTP響應成功,根據HTTP請求中的Connection字段決定是否關閉連接
unmap();
if (m_linger) {
init();
modfd(m_epollfd, m_sockfd, EPOLLIN);
return true;
}
else {
modfd(m_epollfd, m_sockfd, EPOLLIN);
return false;
}
}
}
}
//向寫緩衝區中寫入待發送的數據,由add_stauts_line,add_content_length等函數調用
//將需要寫的請求行,請求頭等信息寫入緩衝區
bool http_conn::add_response(const char* format, ...) {
if (m_write_idx >= WRITE_BUFFER_SIZE) {
return false;
}
va_list arg_list;//獲取省略號的可變參數的內容
va_start(arg_list, format);
//vsnprintf:將格式化數據從可變參數列表寫入大小緩衝區
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) {
return false;
}
m_write_idx += len;
va_end(arg_list);
return true;
}
bool http_conn::add_status_line(int status, const char* title) {
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
bool http_conn::add_headers(int content_len) {
add_content_length(content_len);
add_linger();
add_blank_line();
}
bool http_conn::add_content_length(int content_len) {
return add_response("Content-Length: %d\r\n", content_len);
}
bool http_conn::add_linger() {
return add_response("Connecetion %s\r\n", (m_linger) ? "keep-alive" : "close" );
}
bool http_conn::add_blank_line() {
return add_response("%s", "\r\n");
}
bool http_conn::add_content(const char * content) {
return add_response("%s", content);
}
//根據服務器處理http請求的結果,決定返回客戶端的內容
bool http_conn::process_write(HTTP_CODE ret) {
switch (ret) {
//500服務器內部錯誤
case INTERNAL_ERROR: {
add_status_line(500, error_500_title);
add_headers(strlen(error_500_form));
if (!add_content(error_500_form)) {
return false;
}
break;
}
//400請求錯誤
case BAD_REQUEST: {
add_status_line(400, error_400_title);
add_headers(strlen(error_400_form));
if (!add_content(error_400_form)) {
return false;
}
break;
}
//404資源確實,沒有響應
case NO_REQUEST: {
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form)) {
return false;
}
break;
}
//403禁止訪問
case FORBIDDEN_REQUEST: {
add_status_line(403, error_403_title);
add_headers(strlen(error_400_form));
if (!add_content(error_403_form)) {
return false;
}
break;
}
//請求文件成功
case FILE_REQUEST: {
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0) {
add_headers(m_file_stat.st_size);
//HTTP響應信息位於m_iv[0],HTTP請求的文件內容放入m_iv[1]
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
return true;
}
else {
const char* ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string)) {
return false;
}
}
}
default: {
return true;
}
}
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
return true;
}
//http_conn的工作函數,由線程池中的工作線程調用,這是處理http請求的入口函數
void http_conn::process() {
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST) {
//NO_REQUEST表示沒有數據,提醒讀入事件
modfd(m_epollfd, m_sockfd, EPOLLIN);
return;
}
//根據process_read的請求結果,返回特定的信息,如404,403
bool write_ret = process_write(read_ret);
if (!write_ret) {
close_conn();
}
//成功則說明緩衝區有數據可寫,提醒寫事件
modfd(m_epollfd, m_sockfd, EPOLLOUT);
}
main.cpp,爲主線程的邏輯
在main.cpp中,主線程使用epoll進行io複用操作,將一個個連接任務分發給線程池中的工作線程處理(半同步/半反應堆模型),工作模式和半同步/半異步進程池大致相同,不在贅述。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "locker.h"
#include "http_conn.h"
#include "threadpool.h"
#define MAX_FD 65536
#define MAX_EVENT_NUMBER 10000
extern int add_read_fd(int epollfd, int fd, bool one_shot);
extern int add_write_fd(int epollfd, int fd, bool one_shot);
//處理信號
void addsig(int sig, void(handler)(int), bool restart = true) {
//用sigaction 捕捉信號
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);
}
//向connfd打印錯誤信息
void show_error(int connfd, const char* info) {
printf("%s", info);
send(connfd, info, strlen(info), 0);
close(connfd);
}
signed 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]);
//忽略SIGPIPE信號
addsig(SIGPIPE, SIG_IGN);
//創建線程池
threadpool<http_conn>* pool = NULL;
try {
pool = new threadpool<http_conn>;
}
catch (...) {
return 1;
}
//預先爲每個可能的客戶分配一個http_conn對象
http_conn* users = new http_conn[MAX_FD];
assert(users);
int user_count = 0;
//綁定socket
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
/*
socket中的SO_LINGER可以決定close(socketfd)之後的行爲:
1.默認情況下close會立即關閉tcp連接,可能會導致四次揮手的close_wait1狀態丟失,TIME_WAIT狀態也會丟失
2.linger的兩個參數,如果都爲非0的情況下,則可以實現真正的四次揮手,linger結構體的第二個參數指定了延時關閉的時間。
所以如果實際應用記得將temp的第二個參數改過來,這裏只是demo測試
*/
struct linger temp = { 1,0 };
setsockopt(listenfd, SOL_SOCKET, SO_LINGER, &temp, sizeof(temp));
//socket地址初始化
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_port = htons(port);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret >= 0);
ret = listen(listenfd, 5);
assert(ret >= 0);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd >= 0);
add_read_fd(epollfd, listenfd, false);
http_conn::m_epollfd = epollfd;
while (true) {
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && errno != EINTR) {
printf("epoll_wait failure!\n");
break;
}
for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;
//說明有一個新的連接
if (sockfd == listenfd) {
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
if (connfd < 0) {
printf("accept failure! errno is %s\n.", errno);
continue;
}
//如果有MAX_FD個連接了,返回500錯誤表示服務器繁忙
if (http_conn::m_user_count >= MAX_FD) {
show_error(connfd, "Internal server busy");
continue;
}
//用文件描述符唯一索引
users[connfd].init(connfd, client_address);
}
//如果有異常,直接關閉客戶連接
else if (events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) {
users[sockfd].close_conn();
}
//讀事件
else if (events[i].events & (EPOLLIN)) {
//根據讀的結果,決定是將任務添加到線程池,還是關閉連接
if (users[sockfd].read()) {
pool->append(users + sockfd);
}
else {
users[sockfd].close_conn();
}
}
//寫事件
else if (events[i].events & (EPOLLOUT)) {
if (users[sockfd].write()) {
//可以在今後添加寫事件完成後的邏輯,現在暫時不需要
}
else {
users[sockfd].close_conn();
}
}
}
}
close(epollfd);
close(listenfd);
delete[] users;
delete pool;
return 0;
}
總結
最後整理一下,整個半同步/半反應堆線程池邏輯流程如下:
- 主線程創建線程池,完成初始化監聽fd,綁定socket地址,創建epollfd等一系列操作
- 主線程通過epoll循環獲取事件,根據事件不同做不同處理
- 如果是新連接(listenfd上的事件),則初始化新的http_conn對象,獲取客戶端地址
- 如果是讀事件(EPOLLIN),則調用對應sockfd的http_conn對象,進行數據讀取(read()函數),read函數則會解析http並將結果寫入寫緩衝區中
- 如果是寫事件(EPOLLOUT),則調用對應sockfd的http_conn對象,進行數據寫(write()函數),write函數則會將寫緩衝區的數據反饋客戶端。