半同步/半反應堆線程池實現簡單web服務器

半同步/半反應堆線程池實現簡單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個

  1. void init(int sockfd, const sockaddr_in& addr);
    

    init()函數初始化了http_conn的連接socket,以及客戶端socket地址addr,再通過調用private的init()函數完成其他參數的初始化,如清空緩衝區,初始化m_epollfd等。

  2. void close_conn(bool read_close = true);
    

    關閉連接,close掉連接socket並釋放資源

  3. 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();
    

    函數功能即爲名字描述,向結果緩衝區中加入一系列字符串即可。

  4. 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;
    }
    
  5. 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;
}

總結

最後整理一下,整個半同步/半反應堆線程池邏輯流程如下:

  1. 主線程創建線程池,完成初始化監聽fd,綁定socket地址,創建epollfd等一系列操作
  2. 主線程通過epoll循環獲取事件,根據事件不同做不同處理
    1. 如果是新連接(listenfd上的事件),則初始化新的http_conn對象,獲取客戶端地址
    2. 如果是讀事件(EPOLLIN),則調用對應sockfd的http_conn對象,進行數據讀取(read()函數),read函數則會解析http並將結果寫入寫緩衝區中
    3. 如果是寫事件(EPOLLOUT),則調用對應sockfd的http_conn對象,進行數據寫(write()函數),write函數則會將寫緩衝區的數據反饋客戶端。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章