這是學習網絡編程後寫的一個練手的小程序,能夠幫助複習I/O模型,epoll使用,線程池,HTTP協議等內容。html
程序代碼是基於《Linux高性能服務器編程》一書編寫的。c++
首先回顧程序中的核心內容和主要問題,最後給出相關代碼。web
0. 功能和I/O模型編程
實現簡易的HTTP服務端,現僅支持GET方法,經過瀏覽器訪問能夠返回相應內容。小程序
I/O模型採用Reactor(I/O複用 + 非阻塞I/O) + 線程池。 使用epoll事件循環用做事件通知,若是listenfd上可讀,則調用accept,把新建的fd加入epoll中;數組
是已鏈接sockfd,將其加入到線程池中由工做線程競爭執行任務。瀏覽器
1. 線程池怎麼實現?服務器
程序採用c++編寫,要本身封裝一個簡易的線程池類。大體思路是建立固定數目的線程(如跟核數相同),而後類內部維護一個生產者—消費者隊列。網絡
提供相應的添加任務(生產者)和執行任務接口(消費者)。按照操做系統書中典型的生產者—消費者模型維護增減隊列任務(使用mutex和semaphore)。併發
mutex用於互斥,保證任意時刻只有一個線程讀寫隊列,semaphore用於同步,保證執行順序(隊列爲空時不要讀,隊列滿了不要寫)。
2. epoll用條件觸發(LT)仍是邊緣觸發(ET)?
考慮這樣的狀況,一個工做線程在讀一個fd,但沒有讀完。若是採用LT,則下一次事件循環到來的時候,又會觸發該fd可讀,此時線程池頗有可能將該fd分配給其餘的線程處理數據。
這顯然不是咱們想要看到的,而ET則不會在下一次epoll_wait的時候返回,除非讀完之後又有新數據才返回。因此這裏應該使用ET。
固然ET用法在《Tinychatserver: 一個簡易的命令行羣聊程序》也有總結過。用法的模式是固定的,把fd設爲nonblocking,若是返回某fd可讀,循環read直到EAGAIN。
3. 繼續上面的問題,若是某個線程在處理fd的同時,又有新的一批數據發來(不是老數據沒讀完,是來新數據了),即便使用了ET模式,由於新數據的到來,仍然會觸發該fd可讀,因此仍然存在將該fd分給其餘線程處理的狀況。
這裏就用到了EPOLLONESHOT事件。對於註冊了EPOLLONESHOT事件的文件描述符,操做系統最大觸發其上註冊的一個可讀、可寫或者異常事件,且只觸發一次。
除非咱們使用epoll_ctl函數重置該文件描述符上註冊的EPOLLONESHOT事件。這樣,當一個線程處理某個socket時,其餘線程是不可能有機會操做該socket的,
便可解決該問題。但同時也要注意,若是註冊了EPOLLONESHOT的socket一旦被某個線程處理完畢,則應該當即重置這個socket上的EPOLLONESHOT事件,
以確保下一次可讀時,其EPOLLIN事件可以觸發。
4. HTTP協議解析怎麼作?數據讀到一半怎麼辦?
首先理解這個問題。HTTP協議並未提供頭部字段的長度,判斷頭部結束依據是遇到一個空行,該空行只包含一對回車換行符(<CR><LF>)。同時,若是一次讀操做沒有讀入整個HTTP請求
的頭部,咱們必須等待用戶繼續寫數據再次讀入(好比讀到 GET /index.html HTT
就結束了,必須維護這個狀態,下一次必須繼續讀‘P’)。
即咱們須要斷定當前解析的這一行是什麼(請求行?請求頭?消息體?),還須要判斷解析一行是否結束?
解決上述問題,能夠採起有限狀態機。
參考【1】中設計方式,設計主從兩個狀態機(主狀態機解決前半部分問題,從狀態機解決後半部分問題)。
先分析從狀態機,從狀態機用於處理一行信息(即parse_line函數)。其中包括三個狀態:LINE_OPEN, LINE_OK,LINE_BAD,轉移過程以下所示:
當從狀態機parse_line讀到完整的一行,就能夠將改行內容遞交給process_read函數中的主狀態機處理。
主狀態機也有三種狀態表示正在分析請求行(CHECK_STATE_REQUESTINE),正在分析頭部字段(CHECK_STATE_HEADER),和正在分析內容(CHECK_CONTENT)。
主狀態機使用checkstate變量來記錄當前的狀態。
若是當前的狀態是CHECK_STATE_REQUESTLINE,則表示parse_line函數解析出的行是請求行,因而主狀態機調用parse_requestline來分析請求行;
若是當前的狀態是CHECK_STATE_HEADER,則表示parse_line函數解析出來的是頭部字段,因而主狀態機調用parse_header來分析頭部字段。
若是當前狀態是CHECK_CONTENT,則表示parse_line函數解析出來的是消息體,咱們調用parse_content來分析消息體(實際上實現時候並無分析,只是判斷是否完整讀入)
checkstate變量的初始值是CHECK_STATE_REQUESTLINE,調用相應的函數(parse_requestline,parse_header)後更新checkstate實現狀態轉移。
與主狀態機有關的核心函數以下:
http_conn::HTTP_CODE http_conn::process_read()//完整的HTTP解析 { 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 ) ){//知足條件:正在進行HTTP解析、讀取一個完整行 text = get_line();//從讀緩衝區(HTTP請求數據)獲取一行數據 m_start_line = m_checked_idx;//行的起始位置等於正在每行解析的第一個字節 printf( "got 1 http line: %s", text ); switch ( m_check_state )//HTTP解析狀態跳轉 { case CHECK_STATE_REQUESTLINE://正在分析請求行 { ret = parse_request_line( text );//分析請求行 if ( ret == BAD_REQUEST ) { return BAD_REQUEST; } break; } case CHECK_STATE_HEADER://正在分析請求頭部 { ret = parse_headers( text );//分析頭部 if ( ret == BAD_REQUEST ) { return BAD_REQUEST; } else if ( ret == GET_REQUEST ) { return do_request();//當得到一個完整的鏈接請求則調用do_request分析處理資源頁文件 } break; } case CHECK_STATE_CONTENT:// 解析消息體 { ret = parse_content( text ); if ( ret == GET_REQUEST ) { return do_request(); } line_status = LINE_OPEN; break; } default: { return INTERNAL_ERROR;//內部錯誤 } } } return NO_REQUEST; }
5. HTTP響應怎麼作?怎麼發送效率高一些?
首先介紹readv和writev函數。其功能能夠簡單歸納爲對數據進行整合傳輸及發送,即所謂分散讀,集中寫。
也就是說,writev函數能夠把分散保存在多個緩衝中的數據一併發送,經過readv函數能夠由多個緩衝分別接收。所以適當採用這兩個函數能夠減小I/O次數。
例如這裏要作的HTTP響應。其包含一個狀態行,多個頭部字段,一個空行和文檔的內容。前三者可能被web服務器放置在一塊內存中,
而文檔的內容則一般被讀入到另一塊單獨的內存中(經過read函數或mmap函數)。這裏能夠採用writev函數將他們一併發出。
相關接口以下:
ssize_t readv(int fd, const struct iovec *iov, int iovcnt); ssize_t writev(int fd, const struct iovec *iov, int iovcnt); 其中第二個參數爲以下結構體的數組 struct iovec { void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ }; 第三個參數爲第二個參數的傳遞的數組的長度。
這裏還能夠再學習一下mmap與munmap函數。可是這裏關於mmap與read效率的比較,應該沒有那麼簡單的答案。mmap能夠減小系統調用和內存拷貝,可是其引起的pagefault也是開銷。效率的比較取決於不一樣系統對於這兩個效率實現的不一樣,因此這裏就簡單談一談用法。
#include <sys/mman.h> /**addr參數容許用戶使用某個特定的地址做爲這段內存的起始地址,設置爲NULL則自動分配地址。 *length參數指定內存段的長度. *prot參數用來設置內*存段的訪問權限,好比PROT_READ可讀, PROT_WRITE可寫。 *flags控制內存段內容被修改後程序的行爲。如MAP_PRIVATE指內存段爲調用進程所私有,對該內存段的修改不會反映到被映射的文件中。 */ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
因此根據不一樣狀況(200,404)填充HTTP的程序以下:
bool http_conn::process_write( HTTP_CODE ret )//填充HTTP應答 { switch ( ret ) { 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; } 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; } case NO_RESOURCE: { add_status_line( 404, error_404_title ); add_headers( strlen( error_404_form ) ); if ( ! add_content( error_404_form ) ) { return false; } break; } case FORBIDDEN_REQUEST: { add_status_line( 403, error_403_title ); add_headers( strlen( error_403_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 );//m_file_stat資源頁文件狀態 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_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 false; } } m_iv[ 0 ].iov_base = m_write_buf; m_iv[ 0 ].iov_len = m_write_idx; m_iv_count = 1; return true; }
bool http_conn::write()//將資源頁文件發送給客戶端 { int temp = 0; int bytes_have_send = 0; int bytes_to_send = m_write_idx; if ( bytes_to_send == 0 ) { modfd( m_epollfd, m_sockfd, EPOLLIN );//EPOLLONESHOT事件每次須要重置事件 init(); return true; } while( 1 )// { temp = writev( m_sockfd, m_iv, m_iv_count );//集中寫,m_sockfd是http鏈接對應的描述符,m_iv是iovec結構體數組表示內存塊地址,m_iv_count是數組的長度即多少個內存塊將一次集中寫到m_sockfd if ( temp <= -1 )//集中寫失敗 { if( errno == EAGAIN ) { modfd( m_epollfd, m_sockfd, EPOLLOUT );//重置EPOLLONESHOT事件,註冊可寫事件表示若m_sockfd沒有寫失敗則關閉鏈接 return true; } unmap();//解除內存映射 return false; } bytes_to_send -= temp;//待發送數據 bytes_have_send += temp;//已發送數據 if ( bytes_to_send <= bytes_have_send ) { unmap();//該資源頁已經發送完畢該解除映射 if( m_linger )//若要保持該http鏈接 { init();//初始化http鏈接 modfd( m_epollfd, m_sockfd, EPOLLIN ); return true; } else { modfd( m_epollfd, m_sockfd, EPOLLIN ); return false; } } } }
6.忽略SIGPIPE
這是一個看似很小,可是若是不注意會直接引起bug的地方。若是往一個讀端關閉的管道或者socket中寫數據,會引起SIGPIPE,程序收到SIGPIPE信號後默認的操做時終止進程。
這也就是說,若是客戶端意外關閉,那麼服務器可能也就跟着直接掛了,這顯然不是咱們想要的。因此網絡程序中服務端通常會忽略SIGPIPE信號。
7. 程序代碼
程序中有比較詳細的註釋,雖然主幹在上面問題中分析過了,可是諸如如何解析一行數據之類的操做,仍是很煩的...能夠直接參考代碼
1 #ifndef THREADPOOL_H 2 #define THREADPOOL_H 3 4 #include <list> 5 #include <cstdio> 6 #include <exception> 7 #include <pthread.h> 8 #include "locker.h" //簡單封裝了互斥量和信號量的接口 9 10 //線程池類模板參數T是任務類型,T中必須有接口process 11 template< typename T > 12 class threadpool 13 { 14 public: 15 threadpool( int thread_number = 8, int max_requests = 10000 );//線程數目和最大鏈接處理數 16 ~threadpool(); 17 bool append( T* request ); 18 19 private: 20 static void* worker( void* arg );//線程工做函數 21 void run(); //啓動線程池 22 23 private: 24 int m_thread_number;//線程數量 25 int m_max_requests;//最大鏈接數目 26 pthread_t* m_threads;//線程id數組 27 std::list< T* > m_workqueue;//工做隊列:各線程競爭該隊列並處理相應的任務邏輯T 28 locker m_queuelocker;//工做隊列互斥量 29 sem m_queuestat;//信號量:用於工做隊列 30 bool m_stop;//終止標誌 31 }; 32 33 template< typename T > 34 threadpool< T >::threadpool( int thread_number, int max_requests ) : 35 m_thread_number( thread_number ), m_max_requests( max_requests ), m_stop( false ), m_threads( NULL ) 36 { 37 if( ( thread_number <= 0 ) || ( max_requests <= 0 ) ) 38 { 39 throw std::exception(); 40 } 41 42 m_threads = new pthread_t[ m_thread_number ];//工做線程數組 43 if( ! m_threads ) 44 { 45 throw std::exception(); 46 } 47 48 for ( int i = 0; i < thread_number; ++i )//建立工做線程 49 { 50 printf( "create the %dth thread\n", i ); 51 if( pthread_create( m_threads + i, NULL, worker, this ) != 0 ) 52 { 53 delete [] m_threads; 54 throw std::exception(); 55 } 56 if( pthread_detach( m_threads[i] ) ) //分離線程使得其它線程回收和殺死該線程 57 { 58 delete [] m_threads; 59 throw std::exception(); 60 } 61 } 62 } 63 64 template< typename T > 65 threadpool< T >::~threadpool() 66 { 67 delete [] m_threads; 68 m_stop = true; 69 } 70 71 template< typename T > 72 bool threadpool< T >::append( T* request )//向工做隊列添加任務T 73 { 74 m_queuelocker.lock();//對工做隊列操做前加鎖 75 if ( m_workqueue.size() > m_max_requests )//任務隊列滿,不能加進去 76 { 77 m_queuelocker.unlock(); 78 return false; 79 } 80 m_workqueue.push_back( request ); 81 m_queuelocker.unlock(); 82 m_queuestat.post();//信號量的V操做,多了一個工做任務T使得信號量+1 83 return true; 84 } 85 86 template< typename T > 87 void* threadpool< T >::worker( void* arg )//工做線程函數 88 { 89 threadpool* pool = ( threadpool* )arg; //獲取線程池對象,以前建立的時候傳的this 90 pool->run(); //調用線程池run函數 91 return pool; 92 } 93 94 template< typename T > 95 void threadpool< T >::run() //工做線程真正工做邏輯:從任務隊列領取任務T並執行任務T,消費者 96 { 97 while ( ! m_stop ) 98 { 99 m_queuestat.wait();//信號量P操做,申請信號量獲取任務T 100 m_queuelocker.lock();//對工做隊列操做前加鎖 101 if ( m_workqueue.empty() ) 102 { 103 m_queuelocker.unlock();//任務隊列空沒法消費 104 continue; 105 } 106 T* request = m_workqueue.front();//獲取任務T 107 m_workqueue.pop_front(); 108 m_queuelocker.unlock(); 109 if ( ! request ) 110 { 111 continue; 112 } 113 request->process();//執行任務T的相應邏輯,任務T中必須有process接口 114 } 115 } 116 117 #endif
#ifndef HTTPCONNECTION_H #define HTTPCONNECTION_H #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/epoll.h> #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <sys/stat.h> #include <string.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <stdarg.h> #include <errno.h> #include "locker.h" class http_conn { public: static const int FILENAME_LEN = 200;//文件名最大長度,文件是HTTP請求的資源頁文件 static const int READ_BUFFER_SIZE = 2048;//讀緩衝區,用於讀取HTTP請求 static const int WRITE_BUFFER_SIZE = 1024;//寫緩衝區,用於HTTP回答 enum METHOD { GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH };//HTTP請求方法,本程序只定義了GET邏輯 enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT };//HTTP請求狀態:正在解析請求行、正在解析頭部、解析中 enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBIDDEN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION };//HTTP請求結果:未完整的請求(客戶端仍須要提交請求)、完整的請求、錯誤請求...只用了前三個 enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN };//HTTP每行解析狀態:改行解析完畢、錯誤的行、正在解析行 public: http_conn(){} ~http_conn(){} public: void init( int sockfd, const sockaddr_in& addr );//初始化新的HTTP鏈接 void close_conn( bool real_close = true ); void process();//處理客戶請求,這是HTTP請求的入口函數,與在線程池中調用!!! bool read();//讀取客戶發送來的數據(HTTP請求) bool write();//將請求結果返回給客戶端 private: void init();//初始化鏈接,用於內部調用 HTTP_CODE process_read();//解析HTTP請求,內部調用parse_系列函數 bool process_write( HTTP_CODE ret );//填充HTTP應答,一般是將客戶請求的資源頁發送給客戶,內部調用add_系列函數 HTTP_CODE parse_request_line( char* text );//解析HTTP請求的請求行 HTTP_CODE parse_headers( char* text );//解析HTTP頭部數據 HTTP_CODE parse_content( char* text );//獲取解析結果 HTTP_CODE do_request();//處理HTTP鏈接:內部調用process_read(),process_write() char* get_line() { return m_read_buf + m_start_line; }//獲取HTTP請求數據中的一行數據 LINE_STATUS parse_line();//解析行內部調用parse_request_line和parse_headers //下面的函數被process_write填充HTTP應答 void unmap();//解除內存映射,這裏內存映射是指將客戶請求的資源頁文件映射經過mmap映射到內存 bool add_response( const char* format, ... ); bool add_content( const char* content ); bool add_status_line( int status, const char* title ); bool add_headers( int content_length ); bool add_content_length( int content_length ); bool add_linger(); bool add_blank_line(); public: static int m_epollfd;//全部socket上的事件都註冊到一個epoll事件表中因此用static static int m_user_count;//用戶數量 private: int m_sockfd;//HTTP鏈接對應的客戶在服務端的描述符m_sockfd和地址m_address sockaddr_in m_address; char m_read_buf[ READ_BUFFER_SIZE ];//讀緩衝區,讀取HTTP請求 int m_read_idx;//已讀入的客戶數據最後一個字節的下一個位置,即未讀數據的第一個位置 int m_checked_idx;//當前已經解析的字節(HTTP請求須要逐個解析) int m_start_line;//當前解析行的起始位置 char m_write_buf[ WRITE_BUFFER_SIZE ];//寫緩衝區 int m_write_idx;//寫緩衝區待發送的數據 CHECK_STATE m_check_state;//HTTP解析的狀態:請求行解析、頭部解析 METHOD m_method;//HTTP請求方法,只實現了GET char m_real_file[ FILENAME_LEN ];//HTTP請求的資源頁對應的文件名稱,和服務端的路徑拼接就造成了資源頁的路徑 char* m_url;//請求的具體資源頁名稱,如:www.baidu.com/index.html char* m_version;//HTTP協議版本號,通常是:HTTP/1.1 char* m_host;//主機名,客戶端要在HTTP請求中的目的主機名 int m_content_length;//HTTP消息體的長度,簡單的GET請求這個爲空 bool m_linger;//HTTP請求是否保持鏈接 char* m_file_address;//資源頁文件內存映射後的地址 struct stat m_file_stat;//資源頁文件的狀態,stat文件結構體 struct iovec m_iv[2];//調用writev集中寫函數須要m_iv_count表示被寫內存塊的數量,iovec結構體存放了一段內存的起始位置和長度, int m_iv_count;//m_iv_count是指iovec結構體數組的長度即多少個內存塊 }; #endif
1 #include "http_conn.h" 2 3 const char* ok_200_title = "OK"; 4 const char* error_400_title = "Bad Request"; 5 const char* error_400_form = "Your request has bad syntax or is inherently impossible to satisfy.\n"; 6 const char* error_403_title = "Forbidden"; 7 const char* error_403_form = "You do not have permission to get file from this server.\n"; 8 const char* error_404_title = "Not Found"; 9 const char* error_404_form = "The requested file was not found on this server.\n"; 10 const char* error_500_title = "Internal Error"; 11 const char* error_500_form = "There was an unusual problem serving the requested file.\n"; 12 const char* doc_root = "/var/www/html";//服務端資源頁的路徑,將其和HTTP請求中解析的m_url拼接造成資源頁的位置 13 14 int setnonblocking( int fd )//將fd設置爲非阻塞 15 { 16 int old_option = fcntl( fd, F_GETFL ); 17 int new_option = old_option | O_NONBLOCK; 18 fcntl( fd, F_SETFL, new_option ); 19 return old_option; 20 } 21 22 void addfd( int epollfd, int fd, bool one_shot )//將fd添加到事件表epollfd 23 { 24 epoll_event event; 25 event.data.fd = fd; 26 event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; 27 if( one_shot ) 28 { 29 event.events |= EPOLLONESHOT; 30 } 31 epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event ); 32 setnonblocking( fd ); 33 } 34 35 void removefd( int epollfd, int fd )//將fd從事件表epollfd中移除 36 { 37 epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 ); 38 close( fd ); 39 } 40 41 void modfd( int epollfd, int fd, int ev )//EPOLLONESHOT須要重置事件後事件才能進行下次偵聽 42 { 43 epoll_event event; 44 event.data.fd = fd; 45 event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP; 46 epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event ); 47 } 48 49 int http_conn::m_user_count = 0;//鏈接數 50 int http_conn::m_epollfd = -1;//事件表,注意是static故全部http_con類對象共享一個事件表 51 52 void http_conn::close_conn( bool real_close )//關閉鏈接,從事件表中移除描述符 53 if( real_close && ( m_sockfd != -1 ) ) 54 { 55 //modfd( m_epollfd, m_sockfd, EPOLLIN ); 56 removefd( m_epollfd, m_sockfd ); 57 m_sockfd = -1; 58 m_user_count--; 59 } 60 } 61 62 void http_conn::init( int sockfd, const sockaddr_in& addr )//初始化鏈接 63 { 64 m_sockfd = sockfd;//sockfd是http鏈接對應的描述符用於接收http請求和http回答 65 m_address = addr;//客戶端地址 66 int error = 0; 67 socklen_t len = sizeof( error ); 68 getsockopt( m_sockfd, SOL_SOCKET, SO_ERROR, &error, &len ); 69 int reuse = 1; 70 setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );//獲取描述符狀態,能夠在調試時用 71 addfd( m_epollfd, sockfd, true ); 72 m_user_count++;//多了一個http用戶 73 74 init();//調用重載函數 75 } 76 77 void http_conn::init()//重載init函數進行些鏈接前的初始化操做 78 { 79 m_check_state = CHECK_STATE_REQUESTLINE; 80 m_linger = false; 81 82 m_method = GET; 83 m_url = 0; 84 m_version = 0; 85 m_content_length = 0; 86 m_host = 0; 87 m_start_line = 0; 88 m_checked_idx = 0; 89 m_read_idx = 0; 90 m_write_idx = 0; 91 memset( m_read_buf, '\0', READ_BUFFER_SIZE ); 92 memset( m_write_buf, '\0', WRITE_BUFFER_SIZE ); 93 memset( m_real_file, '\0', FILENAME_LEN ); 94 } 95 96 /*從狀態機,用於解析出一行內容*/ 97 http_conn::LINE_STATUS http_conn::parse_line() 98 { 99 char temp; 100 //checked_index指向buffer(應用程序讀緩衝區)中當前正在分析的字節,read_index指向buffer中客戶數據尾部的下一個字節; 101 //buffer中的第0 ~ checked_index字節都已分析完畢, 第checked_index ~ (read_index - 1)字節由下面的循環挨個分析 102 for ( ; m_checked_idx < m_read_idx; ++m_checked_idx ) 103 { 104 //得到當前要分析的字節; 105 temp = m_read_buf[ m_checked_idx ]; 106 //若是是「\r」,即回車符號,說明可能讀取到一個完整的行 107 if ( temp == '\r' ) 108 { 109 //若是\r恰巧是目前buffer中最後一個已經被讀入的客戶數據,那麼此次分析沒有讀取到完整的行, 110 //返回LINE_OPEN表示還須要繼續讀取客戶數據來進一步分析 111 if ( ( m_checked_idx + 1 ) == m_read_idx ) 112 { 113 return LINE_OPEN; 114 } 115 //若是下一個字符是\n,說明讀到一個完整的行 116 else if ( m_read_buf[ m_checked_idx + 1 ] == '\n' ) 117 { 118 m_read_buf[ m_checked_idx++ ] = '\0'; 119 m_read_buf[ m_checked_idx++ ] = '\0'; 120 return LINE_OK; 121 } 122 //不然,說明存在語法問題 123 return LINE_BAD; 124 } 125 //若是當前字符是\n,說明也可能讀到一個完整的行 126 //not necessary 127 else if( temp == '\n' ) 128 { 129 if( ( m_checked_idx > 1 ) && ( m_read_buf[ m_checked_idx - 1 ] == '\r' ) ) 130 { 131 m_read_buf[ m_checked_idx-1 ] = '\0'; 132 m_read_buf[ m_checked_idx++ ] = '\0'; 133 return LINE_OK; 134 } 135 return LINE_BAD; 136 } 137 } 138 //若是全部內容分析完畢都沒有遇到\r字符,則返回LINE_OPEN,表示還須要繼續讀取客戶數據才能進一步分析 139 return LINE_OPEN; 140 } 141 142 //循環讀取客戶數據,直到無數據可讀或者對方關閉鏈接 143 bool http_conn::read() 144 { 145 if( m_read_idx >= READ_BUFFER_SIZE ) 146 { 147 return false; 148 } 149 150 int bytes_read = 0; 151 while( true ) 152 { 153 bytes_read = recv( m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0 ); 154 if ( bytes_read == -1 ) 155 { 156 if( errno == EAGAIN || errno == EWOULDBLOCK ) 157 { 158 break; 159 } 160 return false; 161 } 162 else if ( bytes_read == 0 ) 163 { 164 return false; 165 } 166 167 m_read_idx += bytes_read; 168 } 169 return true; 170 } 171 172 173 http_conn::HTTP_CODE http_conn::parse_request_line( char* text ) 174 { 175 //strpbrk()函數檢索兩個字符串中首個相同字符的位置,其原型爲: 176 // char *strpbrk( char *s1, char *s2) 177 m_url = strpbrk( text, " \t" ); 178 179 //請求行中若是沒有\t,則該HTTP請求有問題 180 if ( ! m_url ) 181 { 182 return BAD_REQUEST; 183 } 184 185 *m_url++ = '\0'; 186 187 char* method = text; 188 //定義函數:int strcasecmp (const char *s1, const char *s2); 189 //函數說明:strcasecmp()用來比較參數s1 和s2 字符串,比較時會自動忽略大小寫的差別。 190 //返回值:若參數s1 和s2 字符串相同則返回0。s1 長度大於s2 長度則返回大於0 的值,s1 長度若小於s2 長度則返回小於0 的值。 191 if ( strcasecmp( method, "GET" ) == 0 ) 192 { 193 m_method = GET; 194 } 195 else 196 { 197 return BAD_REQUEST; 198 } 199 //strspn() 函數用來計算字符串 str 中連續有幾個字符都屬於字符串 accept,其原型爲: 200 //size_t strspn(const char *str, const char * accept); 201 m_url += strspn( m_url, " \t" ); 202 m_version = strpbrk( m_url, " \t" ); 203 if ( ! m_version ) 204 { 205 return BAD_REQUEST; 206 } 207 *m_version++ = '\0'; 208 m_version += strspn( m_version, " \t" ); 209 if ( strcasecmp( m_version, "HTTP/1.1" ) != 0 ) 210 { 211 return BAD_REQUEST; 212 } 213 214 if ( strncasecmp( m_url, "http://", 7 ) == 0 ) 215 { 216 m_url += 7; 217 //strchr() 用來查找某字符在字符串中首次出現的位置,其原型爲: 218 //char * strchr (const char *str, int c) 219 m_url = strchr( m_url, '/' ); 220 } 221 222 if ( ! m_url || m_url[ 0 ] != '/' ) 223 { 224 return BAD_REQUEST; 225 } 226 //HTTP請求行處理完畢,狀態轉移到頭部字段的分析 227 m_check_state = CHECK_STATE_HEADER; 228 return NO_REQUEST; 229 } 230 231 /*解析http請求的一個頭部信息*/ 232 http_conn::HTTP_CODE http_conn::parse_headers( char* text ) 233 { //空行,頭部字段解析完畢 234 if( text[ 0 ] == '\0' ) 235 { 236 if ( m_method == HEAD ) 237 { 238 return GET_REQUEST; 239 } 240 //若是HTTP請求有消息體,則還須要讀取m_content_length字節的消息體,從狀態機轉移到CHECK_STATE_CONTENT狀態 241 if ( m_content_length != 0 ) 242 { 243 m_check_state = CHECK_STATE_CONTENT; 244 return NO_REQUEST; 245 } 246 //不然,說明獲得了一個完整的HTTP請求 247 return GET_REQUEST; 248 } 249 //處理connection頭部字段 250 else if ( strncasecmp( text, "Connection:", 11 ) == 0 ) 251 { 252 text += 11; 253 text += strspn( text, " \t" ); 254 if ( strcasecmp( text, "keep-alive" ) == 0 ) 255 { 256 m_linger = true; 257 } 258 } 259 //處理Content_length頭部字段 260 else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) 261 { 262 text += 15; 263 text += strspn( text, " \t" ); 264 m_content_length = atol( text ); 265 } 266 //處理host頭部字段 267 else if ( strncasecmp( text, "Host:", 5 ) == 0 ) 268 { 269 text += 5; 270 text += strspn( text, " \t" ); 271 m_host = text; 272 } 273 else 274 { 275 printf( "oop! unknow header %s\n", text ); 276 } 277 278 return NO_REQUEST; 279 280 } 281 //消息體不解析,判斷一下是否完整讀入 282 http_conn::HTTP_CODE http_conn::parse_content( char* text ) 283 { 284 if ( m_read_idx >= ( m_content_length + m_checked_idx ) ) 285 { 286 text[ m_content_length ] = '\0'; 287 return GET_REQUEST; 288 } 289 290 return NO_REQUEST; 291 } 292 293 http_conn::HTTP_CODE http_conn::process_read() 294 { 295 LINE_STATUS line_status = LINE_OK; 296 HTTP_CODE ret = NO_REQUEST; 297 char* text = 0; 298 299 while ( ( ( m_check_state == CHECK_STATE_CONTENT ) && ( line_status == LINE_OK ) ) 300 || ( ( line_status = parse_line() ) == LINE_OK ) ) 301 { 302 text = get_line(); 303 m_start_line = m_checked_idx; 304 printf( "got 1 http line: %s\n", text ); 305 306 switch ( m_check_state ) 307 { 308 case CHECK_STATE_REQUESTLINE: 309 { 310 ret = parse_request_line( text ); 311 if ( ret == BAD_REQUEST ) 312 { 313 return BAD_REQUEST; 314 } 315 break; 316 } 317 case CHECK_STATE_HEADER: 318 { 319 ret = parse_headers( text ); 320 if ( ret == BAD_REQUEST ) 321 { 322 return BAD_REQUEST; 323 } 324 else if ( ret == GET_REQUEST ) 325 { 326 return do_request(); 327 } 328 break; 329 } 330 case CHECK_STATE_CONTENT: 331 { 332 ret = parse_content( text ); 333 if ( ret == GET_REQUEST ) 334 { 335 return do_request(); 336 } 337 line_status = LINE_OPEN; 338 break; 339 } 340 default: 341 { 342 return INTERNAL_ERROR; 343 } 344 } 345 } 346 347 return NO_REQUEST; 348 } 349 350 http_conn::HTTP_CODE http_conn::do_request()//用於獲取資源頁文件的狀態 351 { 352 strcpy( m_real_file, doc_root ); 353 int len = strlen( doc_root ); 354 strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 ); 355 if ( stat( m_real_file, &m_file_stat ) < 0 ) 356 { 357 return NO_RESOURCE;//若資源頁不存在則HTTP解析結果爲404 358 } 359 360 if ( ! ( m_file_stat.st_mode & S_IROTH ) ) 361 { 362 return FORBIDDEN_REQUEST;//資源沒有權限獲取 363 } 364 365 if ( S_ISDIR( m_file_stat.st_mode ) ) 366 { 367 return BAD_REQUEST;//請求有錯 368 } 369 370 int fd = open( m_real_file, O_RDONLY ); 371 m_file_address = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );//將資源頁文件映射到內存 372 close( fd ); 373 return FILE_REQUEST;//資源頁請求成功 374 } 375 376 void http_conn::unmap()//解除資源頁文件映射的內存 377 { 378 if( m_file_address ) 379 { 380 munmap( m_file_address, m_file_stat.st_size );//解除映射 381 m_file_address = 0; 382 } 383 } 384 385 bool http_conn::write()//將資源頁文件發送給客戶端 386 { 387 int temp = 0; 388 int bytes_have_send = 0; 389 int bytes_to_send = m_write_idx; 390 if ( bytes_to_send == 0 ) 391 { 392 modfd( m_epollfd, m_sockfd, EPOLLIN );//EPOLLONESHOT事件每次須要重置事件 393 init(); 394 return true; 395 } 396 397 while( 1 )// 398 { 399 temp = writev( m_sockfd, m_iv, m_iv_count );//集中寫,m_sockfd是http鏈接對應的描述符,m_iv是iovec結構體數組表示內存塊地址,m_iv_count是數組的長度即多少個內存塊將一次集中寫到m_sockfd 400 if ( temp <= -1 )//集中寫失敗 401 { 402 if( errno == EAGAIN ) 403 { 404 modfd( m_epollfd, m_sockfd, EPOLLOUT );//重置EPOLLONESHOT事件,註冊可寫事件表示若m_sockfd沒有寫失敗則關閉鏈接 405 return true; 406 } 407 unmap();//解除內存映射 408 return false; 409 } 410 411 bytes_to_send -= temp;//待發送數據 412 bytes_have_send += temp;//已發送數據 413 if ( bytes_to_send <= bytes_have_send ) 414 { 415 unmap();//該資源頁已經發送完畢該解除映射 416 if( m_linger )//若要保持該http鏈接 417 { 418 init();//初始化http鏈接 419 modfd( m_epollfd, m_sockfd, EPOLLIN ); 420 return true; 421 } 422 else 423 { 424 modfd( m_epollfd, m_sockfd, EPOLLIN ); 425 return false; 426 } 427 } 428 } 429 } 430 431 bool http_conn::add_response( const char* format, ... )//HTTP應答主要是將應答數據添加到寫緩衝區m_write_buf 432 { 433 if( m_write_idx >= WRITE_BUFFER_SIZE ) 434 { 435 return false; 436 } 437 va_list arg_list; 438 va_start( arg_list, format ); 439 int len = vsnprintf( m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list );//將fromat內容輸出到m_write_buf 440 if( len >= ( WRITE_BUFFER_SIZE - 1 - m_write_idx ) ) 441 { 442 return false; 443 } 444 m_write_idx += len; 445 va_end( arg_list ); 446 return true; 447 } 448 449 bool http_conn::add_status_line( int status, const char* title ) 450 { 451 return add_response( "%s %d %s\r\n", "HTTP/1.1", status, title ); 452 } 453 454 bool http_conn::add_headers( int content_len ) 455 { 456 add_content_length( content_len ); 457 add_linger(); 458 add_blank_line(); 459 } 460 461 bool http_conn::add_content_length( int content_len ) 462 { 463 return add_response( "Content-Length: %d\r\n", content_len ); 464 } 465 466 bool http_conn::add_linger() 467 { 468 return add_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" ); 469 } 470 471 bool http_conn::add_blank_line() 472 { 473 return add_response( "%s", "\r\n" ); 474 } 475 476 bool http_conn::add_content( const char* content ) 477 { 478 return add_response( "%s", content ); 479 } 480 481 bool http_conn::process_write( HTTP_CODE ret ) 482 { 483 switch ( ret ) 484 { 485 case INTERNAL_ERROR: 486 { 487 add_status_line( 500, error_500_title ); 488 add_headers( strlen( error_500_form ) ); 489 if ( ! add_content( error_500_form ) ) 490 { 491 return false; 492 } 493 break; 494 } 495 case BAD_REQUEST: 496 { 497 add_status_line( 400, error_400_title ); 498 add_headers( strlen( error_400_form ) ); 499 if ( ! add_content( error_400_form ) ) 500 { 501 return false; 502 } 503 break; 504 } 505 case NO_RESOURCE: 506 { 507 add_status_line( 404, error_404_title ); 508 add_headers( strlen( error_404_form ) ); 509 if ( ! add_content( error_404_form ) ) 510 { 511 return false; 512 } 513 break; 514 } 515 case FORBIDDEN_REQUEST: 516 { 517 add_status_line( 403, error_403_title ); 518 add_headers( strlen( error_403_form ) ); 519 if ( ! add_content( error_403_form ) ) 520 { 521 return false; 522 } 523 break; 524 } 525 case FILE_REQUEST: 526 { 527 add_status_line( 200, ok_200_title ); 528 if ( m_file_stat.st_size != 0 ) 529 { 530 add_headers( m_file_stat.st_size ); 531 m_iv[ 0 ].iov_base = m_write_buf; 532 m_iv[ 0 ].iov_len = m_write_idx; 533 m_iv[ 1 ].iov_base = m_file_address; 534 m_iv[ 1 ].iov_len = m_file_stat.st_size; 535 m_iv_count = 2; 536 return true; 537 } 538 else 539 { 540 const char* ok_string = "<html><body></body></html>"; 541 add_headers( strlen( ok_string ) ); 542 if ( ! add_content( ok_string ) ) 543 { 544 return false; 545 } 546 } 547 } 548 default: 549 { 550 return false; 551 } 552 } 553 554 m_iv[ 0 ].iov_base = m_write_buf; 555 m_iv[ 0 ].iov_len = m_write_idx; 556 m_iv_count = 1; 557 return true; 558 } 559 560 void http_conn::process() 561 { 562 HTTP_CODE read_ret = process_read(); 563 if ( read_ret == NO_REQUEST ) 564 { 565 modfd( m_epollfd, m_sockfd, EPOLLIN ); 566 return; 567 } 568 569 bool write_ret = process_write( read_ret ); 570 if ( ! write_ret ) 571 { 572 close_conn(); 573 } 574 575 modfd( m_epollfd, m_sockfd, EPOLLOUT ); 576 }
1 #ifndef LOCKER_H 2 #define LOCKER_H 3 4 #include <exception> 5 #include <pthread.h> 6 #include <semaphore.h> 7 8 class sem 9 { 10 public: 11 sem() 12 { 13 if( sem_init( &m_sem, 0, 0 ) != 0 ) 14 { 15 throw std::exception(); 16 } 17 } 18 ~sem() 19 { 20 sem_destroy( &m_sem ); 21 } 22 bool wait() 23 { 24 return sem_wait( &m_sem ) == 0; 25 } 26 bool post() 27 { 28 return sem_post( &m_sem ) == 0; 29 } 30 31 private: 32 sem_t m_sem; 33 }; 34 35 class locker 36 { 37 public: 38 locker() 39 { 40 if( pthread_mutex_init( &m_mutex, NULL ) != 0 ) 41 { 42 throw std::exception(); 43 } 44 } 45 ~locker() 46 { 47 pthread_mutex_destroy( &m_mutex ); 48 } 49 bool lock() 50 { 51 return pthread_mutex_lock( &m_mutex ) == 0; 52 } 53 bool unlock() 54 { 55 return pthread_mutex_unlock( &m_mutex ) == 0; 56 } 57 58 private: 59 pthread_mutex_t m_mutex; 60 }; 61 62 class cond 63 { 64 public: 65 cond() 66 { 67 if( pthread_mutex_init( &m_mutex, NULL ) != 0 ) 68 { 69 throw std::exception(); 70 } 71 if ( pthread_cond_init( &m_cond, NULL ) != 0 ) 72 { 73 pthread_mutex_destroy( &m_mutex ); 74 throw std::exception(); 75 } 76 } 77 ~cond() 78 { 79 pthread_mutex_destroy( &m_mutex ); 80 pthread_cond_destroy( &m_cond ); 81 } 82 bool wait() 83 { 84 int ret = 0; 85 pthread_mutex_lock( &m_mutex ); 86 ret = pthread_cond_wait( &m_cond, &m_mutex ); 87 pthread_mutex_unlock( &m_mutex ); 88 return ret == 0; 89 } 90 bool signal() 91 { 92 return pthread_cond_signal( &m_cond ) == 0; 93 } 94 95 private: 96 pthread_mutex_t m_mutex; 97 pthread_cond_t m_cond; 98 }; 99 100 #endif
1 #include <sys/socket.h> 2 #include <netinet/in.h> 3 #include <arpa/inet.h> 4 #include <stdio.h> 5 #include <unistd.h> 6 #include <errno.h> 7 #include <string.h> 8 #include <fcntl.h> 9 #include <stdlib.h> 10 #include <cassert> 11 #include <sys/epoll.h> 12 13 #include "locker.h" 14 #include "threadpool.h" 15 #include "http_conn.h" 16 17 #define MAX_FD 65536 18 #define MAX_EVENT_NUMBER 10000 19 20 extern int addfd( int epollfd, int fd, bool one_shot ); 21 extern int removefd( int epollfd, int fd ); 22 23 void addsig( int sig, void( handler )(int), bool restart = true ) 24 { 25 struct sigaction sa; 26 memset( &sa, '\0', sizeof( sa ) ); 27 sa.sa_handler = handler; 28 if( restart ) 29 { 30 sa.sa_flags |= SA_RESTART; 31 } 32 sigfillset( &sa.sa_mask ); 33 assert( sigaction( sig, &sa, NULL ) != -1 ); 34 } 35 36 void show_error( int connfd, const char* info ) 37 { 38 printf( "%s", info ); 39 send( connfd, info, strlen( info ), 0 ); 40 close( connfd ); 41 } 42 43 44 int main( int argc, char* argv[] ) 45 { 46 if( argc <= 2 ) 47 { 48 printf( "usage: %s ip_address port_number\n", basename( argv[0] ) ); 49 return 1; 50 } 51 const char* ip = argv[1]; 52 int port = atoi( argv[2] ); 53 54 addsig( SIGPIPE, SIG_IGN ); 55 56 //建立線程池 57 threadpool< http_conn >* pool = NULL; 58 try 59 { 60 pool = new threadpool< http_conn >; 61 } 62 catch( ... ) 63 { 64 return 1; 65 } 66 67 //預先爲每一個客戶鏈接分配一個http_conn對象 68 http_conn* users = new http_conn[ MAX_FD ]; 69 assert( users ); 70 int user_count = 0; 71 72 int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); 73 assert( listenfd >= 0 ); 74 struct linger tmp = { 1, 0 }; 75 setsockopt( listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof( tmp ) ); 76 77 int ret = 0; 78 struct sockaddr_in address; 79 bzero( &address, sizeof( address ) ); 80 address.sin_family = AF_INET; 81 inet_pton( AF_INET, ip, &address.sin_addr ); 82 address.sin_port = htons( port ); 83 84 ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) ); 85 assert( ret >= 0 ); 86 87 ret = listen( listenfd, 5 ); 88 assert( ret >= 0 ); 89 90 epoll_event events[ MAX_EVENT_NUMBER ]; 91 int epollfd = epoll_create( 5 ); 92 assert( epollfd != -1 ); 93 addfd( epollfd, listenfd, false ); 94 http_conn::m_epollfd = epollfd; 95 96 while( true ) 97 { 98 int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 ); 99 if ( ( number < 0 ) && ( errno != EINTR ) ) 100 { 101 printf( "epoll failure\n" ); 102 break; 103 } 104 105 for ( int i = 0; i < number; i++ ) 106 { 107 int sockfd = events[i].data.fd; 108 if( sockfd == listenfd ) 109 { 110 struct sockaddr_in client_address; 111 socklen_t client_addrlength = sizeof( client_address ); 112 int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength ); 113 if ( connfd < 0 ) 114 { 115 printf( "errno is: %d\n", errno ); 116 continue; 117 } 118 if( http_conn::m_user_count >= MAX_FD ) 119 { 120 show_error( connfd, "Internal server busy" ); 121 continue; 122 } 123 //初始化客戶鏈接 124 users[connfd].init( connfd, client_address ); 125 } 126 else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) 127 { 128 users[sockfd].close_conn(); 129 } 130 else if( events[i].events & EPOLLIN ) 131 { 132 //根據讀的結果,以爲將任務加入線程池,仍是關閉鏈接 133 if( users[sockfd].read() ) 134 { 135 pool->append( users + sockfd ); 136 } 137 else 138 { 139 users[sockfd].close_conn(); 140 } 141 } 142 else if( events[i].events & EPOLLOUT ) 143 { 144 //根據寫的結果,以爲是否關閉鏈接 145 if( !users[sockfd].write() ) 146 { 147 users[sockfd].close_conn(); 148 } 149 } 150 else 151 {} 152 } 153 } 154 155 close( epollfd ); 156 close( listenfd ); 157 delete [] users; 158 delete pool; 159 return 0; 160 }
8. 參考資料
[1]. 遊雙. Linux高性能服務器編程[M]. 機械工業出版社, 2013.
[2]. 如何寫一個Web服務器 http://lifeofzjs.com/blog/2015/05/16/how-to-write-a-server/
[3]. 尹聖雨. TCP/IP網絡編程[M]. 人民郵電出版社, 2014.
[4]. W.RICHARDSTEVENS, STEPHENA.RAGO. UNIX環境高級編程[M]. 人民郵電出版社, 2014.