這里將會(huì)敘述的并不會(huì)很完整,因?yàn)椴煌康牡木W(wǎng)絡(luò)程序,需要關(guān)注的信息也大不相同
特別是這個(gè)程序關(guān)注的是如何使用C語言編寫一個(gè)服務(wù)器
read到的信息。進(jìn)行過系統(tǒng)編程的都應(yīng)該會(huì)知道這個(gè)函數(shù),與之對(duì)應(yīng)的是write。與 C標(biāo)準(zhǔn)庫 為我們提供的標(biāo)準(zhǔn)格式化輸入輸出不同的地方在于其操作的對(duì)象。read/write操作的是一個(gè)在叫做 文件描述符(file description) 的int類型的東西,而標(biāo)準(zhǔn)庫的函數(shù)(printf/scanf)操作的則是一個(gè)FILE*特殊的結(jié)構(gòu)體指針,這兩者之間可以互相轉(zhuǎn)換,通過fdopen(fd-->FILE*)/fileno(FILE*-->fd)具體相關(guān)知識(shí),查閱相關(guān)信息,如著名的APUE。
*nix下的文件描述符(file description)在Windows下近似相當(dāng)于 文件句柄(file handler),只不過前者是有規(guī)律的遞增,而后者則不是。
如何存儲(chǔ)?
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;
其中有一個(gè)陌生的事物,string_t,這個(gè)是用來進(jìn)行字符串操作的一個(gè)自己寫的結(jié)構(gòu),用于簡(jiǎn)化操作,可以把它看成一個(gè)可以自動(dòng)增長的字符串類型。
再者就是,內(nèi)嵌結(jié)構(gòu)體中使用到了 位域 這個(gè)方式,主要是因?yàn)镃中沒有原生的bool類型,使用int來表示又太過奢侈
這個(gè)位域的寫法在某些人看來似乎不太感冒,實(shí)際上還有替代的方法可以用,也就是使用掩碼的思想,在一個(gè)int型中的不同位包含不同的信息,實(shí)際上和我這個(gè)的原理是相同的,只不過我將它拆開了,這樣就可以不寫各種處理宏
/* 另一種寫法 */
...
struct {
int status_set;
int content_length;
string_t request_length;
}conn_res
...
enum {
SET_CONN_LINGGER = 1,
SET_EPOLLOUT = 1 << 1,
...
}
/* 幾乎對(duì)于每一個(gè)位置的操作都有三個(gè),設(shè)置,復(fù)位,檢測(cè) */
#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)
依此類推。
實(shí)際上,對(duì)于這個(gè)
string_t的設(shè)計(jì)是一個(gè)想當(dāng)然的失敗,當(dāng)時(shí)是想嘗試使用面向?qū)ο蟮南敕ǎ菦]有考慮到其使用時(shí)候的冗余,后面會(huì)看到這個(gè)小麻煩,但是總體上還是可以得。這次總結(jié)出來的就是,在C里面使用面向?qū)ο蟮乃季S實(shí)在有點(diǎn)勉強(qiáng),具體等后方說到這個(gè)
string_t時(shí)會(huì)再提到。2016-08-28 將其修改為正常的C風(fēng)格。
所以實(shí)際上來看一看,我存儲(chǔ)了哪些狀態(tài)信息
file_dsp: 這個(gè)肯定是必要的,不然你怎么對(duì)這個(gè)新連接進(jìn)行操作。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請(qǐng)求方法content_type : 響應(yīng)報(bào)文 中的 屬性content_length : 同上requ_res_path : 對(duì)端想請(qǐng)求的資源handle_loop里面,并且將總體流程已經(jīng)過了一遍handle_loop 就是一個(gè)事件循環(huán),我們整個(gè)程序的編程模型就是一個(gè) 事件驅(qū)動(dòng) 的編程體系,什么是事件驅(qū)動(dòng),可以查閱相關(guān)資料,如 UNP 等書籍。在這個(gè)事件循環(huán)中,我們使用兩個(gè)事件驅(qū)動(dòng)我們的流程 : 讀事件, 寫事件listen_thread 和 workers_thread,常理來說前者一個(gè)就夠了,后者可以酌情處理。listen_thread回到handle_loop的代碼中可以看到有一個(gè)獨(dú)立的代碼塊{},這個(gè)代碼塊的作用就是將我們之前創(chuàng)建的服務(wù)器套接字,添加到一個(gè)epoll實(shí)例中,準(zhǔn)備傳給listen_thread。在該epoll實(shí)例中,我們監(jiān)聽了它的讀事件,以及錯(cuò)誤事件 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);
}
先說前兩個(gè)
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) {
這是一個(gè)永不停止的循環(huán),除非在外部傳入了一個(gè)信號(hào)CTRL+C,其實(shí)沒什么意義,不過還是寫了
//這是監(jiān)聽的阻塞地點(diǎn),在此處會(huì)返回有多少個(gè)事件發(fā)生了,當(dāng)然這里只有一個(gè)
int is_work = epoll_wait(listen_epfd, &new_client, 1, 2000);
int sock = 0;
// 如果不是因?yàn)槌瑫r(shí)才到了這里
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;
// 分配新連接給各個(gè)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);
}
其實(shí)在上面的
accept和set_nonblock可以用一個(gè)系統(tǒng)調(diào)用來解決,accept4,而不需要使用兩個(gè)不同的系統(tǒng)調(diào)用來完成這個(gè)功能,具體可以查詢文檔。
listen_thread 的職責(zé)非常簡(jiǎn)單,就只是單純的接受創(chuàng)建新連接,設(shè)置一些屬性,并且分配給workers_thread,所以真正復(fù)雜的工作還是在后者身上整個(gè)的代碼有點(diǎn)冗長,但是邏輯十分清晰,大體可以分成讀寫兩部分
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 接口中的第二,三個(gè)參數(shù) , 代表著有事件改變狀態(tài)的新連接(new_apply[i]),和有多少個(gè)這樣的新連接(i)。代碼中寫的是(,&new_apply,1,)代表著我每次只想得到一個(gè),說明及替代方案在后面會(huì)提到,跳過也無所謂。
/* 讀事件 */
if (new_apply.events & EPOLLIN) { /* Reading Work */
/* handle_read 是接收并解析HTTP請(qǐng)求報(bào)文的地方 */
int err_code = handle_read(new_client);
/* 此處省略一個(gè)很重要的分片錯(cuò)誤處理 */
else if (err_code != HANDLE_READ_SUCCESS) {
/* Read Bad Things */
close(sock);
continue;
}
} // Read Event
以上便是簡(jiǎn)化的讀事件的處理,拋開來看,一切的核心就是handle_read這個(gè)函數(shù),后放會(huì)詳細(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其實(shí)就是清除一些現(xiàn)有狀態(tài),不然下次有別的連接占用的時(shí)候就會(huì)錯(cuò)亂了。
else { /* EPOLLRDHUG EPOLLERR EPOLLHUG */
close(sock);
}
} /* New Apply */
} /* main while */
return (void*)0;
}
if - else 分支組成,分別處理 讀事件,寫事件,錯(cuò)誤事件 前面提到一個(gè)名詞,叫做 HTTP狀態(tài)機(jī),指的就是狀態(tài)的轉(zhuǎn)換,在C語言中,可以使用enum來實(shí)現(xiàn)
typedef enum {
HANDLE_READ_SUCCESS = -(1 << 1),
HANDLE_READ_FAILURE = -(1 << 2),
...
}HANDLE_STATUS;
代表了,handle_read 是成功還是失敗,有一個(gè)額外的 MESSAGE_IMCOMPLETE 狀態(tài)也輸一這個(gè)范疇內(nèi),但是設(shè)計(jì)的時(shí)候出現(xiàn)了差錯(cuò),可以選擇將其放在里面。
MESSAGE_IMCOMPLETE 是為了應(yīng)對(duì)TCP分片問題,所以在顯示網(wǎng)絡(luò)中很常見,但是本地測(cè)試的時(shí)候可能不容易發(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能讓你輕松理解整個(gè)狀態(tài)機(jī)的邏輯
conn_client類型的指針,回想一下,這就是我們存儲(chǔ)每個(gè)新連接的各種信息的地方,返回值就是這個(gè)動(dòng)作的狀態(tài)了。從功能上看,這個(gè)函數(shù)主要的工作就是將handle_read拆分成兩大部分:
read_n)
socket中)GET 而言就是是否讀取到了一個(gè)空行\r\nPOST 來說就是是否一句Content-length屬性的值將 body 讀取完整了parse_reading)
Connectionread_n)static int read_n(conn_client * client)
實(shí)現(xiàn)一個(gè)read函數(shù)的加強(qiáng)版
__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)化,但我還是寫了,強(qiáng)迫癥吧, buf_index同理,read_number是本次讀的字符個(gè)數(shù),less_capacity是緩沖區(qū)的容量余量
while (1) {
/* 因?yàn)槭欠亲枞?,所以要不停地讀,直到`read`返回-1,且errno為EAGAIN */
less_capacity = CONN_BUF_SIZE - buf_index;
if (less_capacity <= 1) {/* Overflow Protection */
/* 萬一這本地的緩沖區(qū)容量不夠了,就刷新進(jìn) conn_client 中 */
buf[buf_index] = '\0'; /* Flush the buf to the r_buf String */
/* 對(duì)于 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ū)成功 */
}
上面的代碼中,有一個(gè)APPEND宏,是用來簡(jiǎn)化代碼的,功能是
#define APPEND(str) str,(strlen(str)+1)
read_number = (int)read(fd, buf+buf_index, less_capacity);
/* 0代表對(duì)端關(guān)閉了連接或者說是已經(jīng)讀完了 EOF(對(duì)端調(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) {
/* 這個(gè)時(shí)候,我們?cè)撟龅木褪菍⒕彌_區(qū)的東西,存儲(chǔ)起來 */
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)鍵字是多線程編程里一個(gè)挺有用的一個(gè)關(guān)鍵字,具體可以查詢資料,簡(jiǎn)單來說,就是讓每個(gè)線程擁有一個(gè)自己的全局變量。
read_n之后,我們就(可能)獲取到了完整的數(shù)據(jù)了,接下來就是解析它們,引入一個(gè)狀態(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是一個(gè)結(jié)構(gòu)體,用來存儲(chǔ)狀態(tài)行所含有的三個(gè)信息: 請(qǐng)求方法, 請(qǐng)求資源, HTTP版本號(hào)
/* Get Request line */
err_code = deal_requ(client, &line_status);
/* 回想一下這個(gè)狀態(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;
}
對(duì)于deal_requ,deal_head來說,只是一個(gè)很簡(jiǎn)單的從大字符串中識(shí)別出小字符串,并存儲(chǔ)起來的問題,不想過多的敘述。在這個(gè)處理過程中,自己實(shí)現(xiàn)了一個(gè)get_line按行讀取的函數(shù),同樣會(huì)被后面的deal_head使用
deal_head中,可以按行進(jìn)行循環(huán)讀取(get_line),知道你發(fā)現(xiàn)空行,那么你就處理完成了,如果是POST方法,你還需要繼續(xù)讀取,直到讀取完它的body?,F(xiàn)在想想,conn_client這個(gè)結(jié)構(gòu)體中的那些屬性是干什么的,就是從這里解析出來的。額外的補(bǔ)充
這個(gè)經(jīng)驗(yàn)分享系列馬上就要到頭了,下一步的我也許就該畢業(yè)了