這里將會敘述的并不會很完整,因為不同目的的網(wǎng)絡(luò)程序,需要關(guān)注的信息也大不相同
特別是這個程序關(guān)注的是如何使用C語言編寫一個服務(wù)器
read到的信息。進行過系統(tǒng)編程的都應(yīng)該會知道這個函數(shù),與之對應(yīng)的是write。與 C標(biāo)準(zhǔn)庫 為我們提供的標(biāo)準(zhǔn)格式化輸入輸出不同的地方在于其操作的對象。read/write操作的是一個在叫做 文件描述符(file description) 的int類型的東西,而標(biāo)準(zhǔn)庫的函數(shù)(printf/scanf)操作的則是一個FILE*特殊的結(jié)構(gòu)體指針,這兩者之間可以互相轉(zhuǎn)換,通過fdopen(fd-->FILE*)/fileno(FILE*-->fd)具體相關(guān)知識,查閱相關(guān)信息,如著名的APUE。
*nix下的文件描述符(file description)在Windows下近似相當(dāng)于 文件句柄(file handler),只不過前者是有規(guī)律的遞增,而后者則不是。
如何存儲?
typedef unsigned char boolean;
struct connection {
int file_dsp;
#define CONN_BUF_SIZE 512
int r_buf_offset;
int w_buf_offset;
string_t r_buf;
string_t w_buf;
struct {
/* Is it Keep-alive in Application Layer */
boolean conn_linger : 1;
boolean set_ep_out : 1;
boolean is_read_done : 1; /* Read from Peer Done? */
boolean request_http_v : 2; /* HTTP/1.1 1.0 0.9 2.0 */
boolean request_method : 2; /* GET HEAD POST */
int content_type : 4; /* 2 ^ 4 -> 16 Types */
int content_length; /* For POST */
string_t requ_res_path; /* / */
}conn_res;
};
typedef struct connection conn_client;
其中有一個陌生的事物,string_t,這個是用來進行字符串操作的一個自己寫的結(jié)構(gòu),用于簡化操作,可以把它看成一個可以自動增長的字符串類型。
再者就是,內(nèi)嵌結(jié)構(gòu)體中使用到了 位域 這個方式,主要是因為C中沒有原生的bool類型,使用int來表示又太過奢侈
這個位域的寫法在某些人看來似乎不太感冒,實際上還有替代的方法可以用,也就是使用掩碼的思想,在一個int型中的不同位包含不同的信息,實際上和我這個的原理是相同的,只不過我將它拆開了,這樣就可以不寫各種處理宏
/* 另一種寫法 */
...
struct {
int status_set;
int content_length;
string_t request_length;
}conn_res
...
enum {
SET_CONN_LINGGER = 1,
SET_EPOLLOUT = 1 << 1,
...
}
/* 幾乎對于每一個位置的操作都有三個,設(shè)置,復(fù)位,檢測 */
#define SET_CONN_LINGER(MASK_SET) (MASK_SET &= SET_CONN_LINGER)
#define CLR_CONN_LINGER(MASK_SET) (MASK_SET &= (~SET_CONN_LINGER)&0xFFFF)
#define IS_CONN_LINGER(MASK_SET) (MASK_SET & SET_CONN_LINGER)
依此類推。
實際上,對于這個
string_t的設(shè)計是一個想當(dāng)然的失敗,當(dāng)時是想嘗試使用面向?qū)ο蟮南敕?,但是沒有考慮到其使用時候的冗余,后面會看到這個小麻煩,但是總體上還是可以得。這次總結(jié)出來的就是,在C里面使用面向?qū)ο蟮乃季S實在有點勉強,具體等后方說到這個
string_t時會再提到。2016-08-28 將其修改為正常的C風(fēng)格。
所以實際上來看一看,我存儲了哪些狀態(tài)信息
file_dsp: 這個肯定是必要的,不然你怎么對這個新連接進行操作。r_buf和r_buf_offset) :
w_buf_offset)
conn_res
conn_linger : 是否保持連接(keep-alive)set_ep_out : 是否設(shè)置監(jiān)聽寫事件(EPOLLOUT)is_read_done : 是否已經(jīng)讀取信息完畢request_http_v : HTTP協(xié)議版本request_method : HTTP請求方法content_type : 響應(yīng)報文 中的 屬性content_length : 同上requ_res_path : 對端想請求的資源handle_loop里面,并且將總體流程已經(jīng)過了一遍handle_loop 就是一個事件循環(huán),我們整個程序的編程模型就是一個 事件驅(qū)動 的編程體系,什么是事件驅(qū)動,可以查閱相關(guān)資料,如 UNP 等書籍。在這個事件循環(huán)中,我們使用兩個事件驅(qū)動我們的流程 : 讀事件, 寫事件listen_thread 和 workers_thread,常理來說前者一個就夠了,后者可以酌情處理。listen_thread回到handle_loop的代碼中可以看到有一個獨立的代碼塊{},這個代碼塊的作用就是將我們之前創(chuàng)建的服務(wù)器套接字,添加到一個epoll實例中,準(zhǔn)備傳給listen_thread。在該epoll實例中,我們監(jiān)聽了它的讀事件,以及錯誤事件 EPOLLERR
{ /* Register listen fd to the listen_epfd */
struct epoll_event event;
event.data.fd = file_dsption;
event.events = EPOLLET | EPOLLERR | EPOLLIN;
epoll_ctl(listen_epfd, EPOLL_CTL_ADD, file_dsption, &event);
}
先說前兩個
listen_thread
/* Listener's Thread
* @param arg will be a epoll instance
* */
static void * listen_thread(void * arg) {
int listen_epfd = (int)arg;
struct epoll_event new_client = {0};
/* Adding new Client Sock to the Workers' thread */
int balance_index = 0;
while (terminal_server != CLOSE_SERVE) {
這是一個永不停止的循環(huán),除非在外部傳入了一個信號CTRL+C,其實沒什么意義,不過還是寫了
//這是監(jiān)聽的阻塞地點,在此處會返回有多少個事件發(fā)生了,當(dāng)然這里只有一個
int is_work = epoll_wait(listen_epfd, &new_client, 1, 2000);
int sock = 0;
// 如果不是因為超時才到了這里
while (is_work > 0) { /* New Connect */
//接受并創(chuàng)建新連接
sock = accept(new_client.data.fd, NULL, NULL);
if (sock > 0) {
// 如果沒有意外的話
set_nonblock(sock);
clear_clients(&clients[sock]);
clients[sock].file_dsp = sock;
// 分配新連接給各個workers_thread
add_event(epfd_group[balance_index], sock, EPOLLIN);
balance_index = (balance_index+1) % workers;
} else /* sock == -1 means nothing to accept */
break;
} /* new Connect */
}/* main while */
close(listen_epfd);
pthread_exit(0);
}
其實在上面的
accept和set_nonblock可以用一個系統(tǒng)調(diào)用來解決,accept4,而不需要使用兩個不同的系統(tǒng)調(diào)用來完成這個功能,具體可以查詢文檔。
listen_thread 的職責(zé)非常簡單,就只是單純的接受創(chuàng)建新連接,設(shè)置一些屬性,并且分配給workers_thread,所以真正復(fù)雜的工作還是在后者身上整個的代碼有點冗長,但是邏輯十分清晰,大體可以分成讀寫兩部分
static void * workers_thread(void * arg) {
int deal_epfd = (int)arg;
struct epoll_event new_apply = {0};
while(terminal_server != CLOSE_SERVE) {
int is_apply = epoll_wait(deal_epfd, &new_apply, 1, 2000);
if(is_apply > 0) { /* New Apply */
int sock = new_apply.data.fd;
conn_client * new_client = &clients[sock];
到此處為止,前面的邏輯和listen_thread 十分相似,需要額外說的就是 epoll_wait 接口中的第二,三個參數(shù) , 代表著有事件改變狀態(tài)的新連接(new_apply[i]),和有多少個這樣的新連接(i)。代碼中寫的是(,&new_apply,1,)代表著我每次只想得到一個,說明及替代方案在后面會提到,跳過也無所謂。
/* 讀事件 */
if (new_apply.events & EPOLLIN) { /* Reading Work */
/* handle_read 是接收并解析HTTP請求報文的地方 */
int err_code = handle_read(new_client);
/* 此處省略一個很重要的分片錯誤處理 */
else if (err_code != HANDLE_READ_SUCCESS) {
/* Read Bad Things */
close(sock);
continue;
}
} // Read Event
以上便是簡化的讀事件的處理,拋開來看,一切的核心就是handle_read這個函數(shù),后放會詳細(xì)講解。
/* 寫事件 */
else if (new_apply.events & EPOLLOUT) { /* Writing Work */
int err_code = handle_write(new_client);
/* TCP's Write buffer is Busy */
if (HANDLE_WRITE_AGAIN == err_code)
mod_event(deal_epfd, sock, EPOLLONESHOT | EPOLLOUT);
else if (HANDLE_WRITE_FAILURE == err_code) { /* Peer Close */
close(sock);
continue;
}
/* if Keep-alive */
if(1 == new_client->conn_res.conn_linger)
mod_event(deal_epfd, sock, EPOLLIN);
else{
close(sock);
continue;
}
} /* EPOLLOUT */
所謂clear_clients其實就是清除一些現(xiàn)有狀態(tài),不然下次有別的連接占用的時候就會錯亂了。
else { /* EPOLLRDHUG EPOLLERR EPOLLHUG */
close(sock);
}
} /* New Apply */
} /* main while */
return (void*)0;
}
if - else 分支組成,分別處理 讀事件,寫事件,錯誤事件 前面提到一個名詞,叫做 HTTP狀態(tài)機,指的就是狀態(tài)的轉(zhuǎn)換,在C語言中,可以使用enum來實現(xiàn)
typedef enum {
HANDLE_READ_SUCCESS = -(1 << 1),
HANDLE_READ_FAILURE = -(1 << 2),
...
}HANDLE_STATUS;
代表了,handle_read 是成功還是失敗,有一個額外的 MESSAGE_IMCOMPLETE 狀態(tài)也輸一這個范疇內(nèi),但是設(shè)計的時候出現(xiàn)了差錯,可以選擇將其放在里面。
MESSAGE_IMCOMPLETE 是為了應(yīng)對TCP分片問題,所以在顯示網(wǎng)絡(luò)中很常見,但是本地測試的時候可能不容易發(fā)現(xiàn),可以使用工具 tc 來模擬弱環(huán)境。
HANDLE_STATUS handle_read(conn_client * client)
HANDLE_STATUS handle_read(conn_client * client) {
int err_code = 0;
/* Reading From Socket */
err_code = read_n(client);
if (err_code != READ_SUCCESS) { /* If read Fail then End this connect */
return HANDLE_READ_FAILURE;
}
到這里為止是讀取所有可以讀到的數(shù)據(jù)
/* Parsing the Reading Data */
err_code = parse_reading(client);
if (err_code == MESSAGE_INCOMPLETE)
return MESSAGE_INCOMPLETE;
if (err_code != PARSE_SUCCESS) { /* If Parse Fail then End this connect */
return HANDLE_READ_FAILURE;
}
到這里為止是處理所有已經(jīng)讀到的數(shù)據(jù)
return HANDLE_READ_SUCCESS;
}
到了這里,就證明讀和處理都已經(jīng)正確完成了。
巧用
gdb能讓你輕松理解整個狀態(tài)機的邏輯
conn_client類型的指針,回想一下,這就是我們存儲每個新連接的各種信息的地方,返回值就是這個動作的狀態(tài)了。從功能上看,這個函數(shù)主要的工作就是將handle_read拆分成兩大部分:
read_n)
socket中)GET 而言就是是否讀取到了一個空行\r\nPOST 來說就是是否一句Content-length屬性的值將 body 讀取完整了parse_reading)
Connectionread_n)static int read_n(conn_client * client)
實現(xiàn)一個read函數(shù)的加強版
__thread char read_buf2[CONN_BUF_SIZE] = {0};
static int read_n(conn_client * client) {
int read_offset2 = 0;
int fd = client->file_dsp;
char * buf = &read_buf2[0];
int buf_index = read_offset2;
int read_number = 0;
int less_capacity = 0;
從前往后依次是讀緩沖區(qū)位移, 處理的連接套接字, buf純粹多此一舉還可能阻礙編譯器優(yōu)化,但我還是寫了,強迫癥吧, buf_index同理,read_number是本次讀的字符個數(shù),less_capacity是緩沖區(qū)的容量余量
while (1) {
/* 因為是非阻塞,所以要不停地讀,直到`read`返回-1,且errno為EAGAIN */
less_capacity = CONN_BUF_SIZE - buf_index;
if (less_capacity <= 1) {/* Overflow Protection */
/* 萬一這本地的緩沖區(qū)容量不夠了,就刷新進 conn_client 中 */
buf[buf_index] = '\0'; /* Flush the buf to the r_buf String */
/* 對于 STRING 宏,可以看看我的源碼中的 wsx_string.h */
cappend_string(client->r_buf, STRING(buf));
client->r_buf_offset += read_offset2;//- client->read_offset;
read_offset2 = 0;
buf_index = 0;
less_capacity = CONN_BUF_SIZE - buf_index;
/* 清空緩沖區(qū)成功 */
}
上面的代碼中,有一個APPEND宏,是用來簡化代碼的,功能是
#define APPEND(str) str,(strlen(str)+1)
read_number = (int)read(fd, buf+buf_index, less_capacity);
/* 0代表對端關(guān)閉了連接或者說是已經(jīng)讀完了 EOF(對端調(diào)用close()/shutdown()) */
if (0 == read_number) { /* We must close connection */
return READ_FAIL;
}
/* -1 代表現(xiàn)在沒東西可以讀了 */
else if (-1 == read_number) { /* Nothing to read */
if (EAGAIN == errno || EWOULDBLOCK == errno) {
/* 這個時候,我們該做的就是將緩沖區(qū)的東西,存儲起來 */
buf[buf_index] = '\0';
append_string(client->r_buf, STRING(buf));
client->r_buf_offset += read_offset2;//client->read_offset;
return READ_SUCCESS;
}
return READ_FAIL;
}
else { /* Continue to Read */
/* 能讀取到信息,就繼續(xù)讀 */
buf_index += read_number;
read_offset2 = buf_index;
}
} /* while(1) */
}
__thread關(guān)鍵字是多線程編程里一個挺有用的一個關(guān)鍵字,具體可以查詢資料,簡單來說,就是讓每個線程擁有一個自己的全局變量。
read_n之后,我們就(可能)獲取到了完整的數(shù)據(jù)了,接下來就是解析它們,引入一個狀態(tài)PARSE_STATUS
typedef enum {
/* Parse the Reading Success, set the event to Write Event */
PARSE_SUCCESS = 1 << 1,
/* Parse the Reading Fail, for the Wrong Syntax */
PARSE_BAD_SYNTAX = 1 << 2,
/* Parse the Reading Success, but Not Implement OR No Such Resources*/
PARSE_BAD_REQUT = 1 << 3,
}PARSE_STATUS;
解釋的很清楚了,不再贅述。
PARSE_STATUS parse_reading(conn_client * client)
PARSE_STATUS parse_reading(conn_client * client) {
int err_code = 0;
requ_line line_status = {0};
client->r_buf_offset = 0; /* Set the real Storage offset to 0, the end of buf is '\0' */
requ_line是一個結(jié)構(gòu)體,用來存儲狀態(tài)行所含有的三個信息: 請求方法, 請求資源, HTTP版本號
/* Get Request line */
err_code = deal_requ(client, &line_status);
/* 回想一下這個狀態(tài),TCP分片的情況 */
if (MESSAGE_INCOMPLETE == err_code) /* Incompletely reading */
return MESSAGE_INCOMPLETE;
if (DEAL_LINE_REQU_FAIL == err_code) /* Bad Request */
return PARSE_BAD_REQUT;
到這里為止是處理狀態(tài)行的代碼
/* Get Request Head Attribute until /r/n */
err_code = deal_head(client); /* The second line to the Empty line */
if (DEAL_HEAD_FAIL == err_code)
return PARSE_BAD_SYNTAX;
到這里為止是處理完了所有的頭屬性
/* Response Page maker */
err_code = make_response_page(client);
if (MAKE_PAGE_FAIL == err_code)
return PARSE_BAD_REQUT;
return PARSE_SUCCESS;
}
對于deal_requ,deal_head來說,只是一個很簡單的從大字符串中識別出小字符串,并存儲起來的問題,不想過多的敘述。在這個處理過程中,自己實現(xiàn)了一個get_line按行讀取的函數(shù),同樣會被后面的deal_head使用
deal_head中,可以按行進行循環(huán)讀取(get_line),知道你發(fā)現(xiàn)空行,那么你就處理完成了,如果是POST方法,你還需要繼續(xù)讀取,直到讀取完它的body?,F(xiàn)在想想,conn_client這個結(jié)構(gòu)體中的那些屬性是干什么的,就是從這里解析出來的。額外的補充
這個經(jīng)驗分享系列馬上就要到頭了,下一步的我也許就該畢業(yè)了