C語言編寫高併發Http檔案上傳下載伺服器
前言
前段時間學習tinyhttpd和libevent開源庫。
別人的程式碼寫的再好終究是別人的,自以為看懂了,等到自己真正寫的時候就會發現有各種問題。於是準備參考libevent裡面最最最基礎的功能(撿了芝麻丟了西瓜?),自己寫一個event,用於熟悉libevent的I/O多路複用思想。然後再加上http。就有了這篇部落格的Http高併發檔案上傳下載伺服器(這裡其實是偽高併發,下文會具體描述,原諒我的標題黨,哈哈)。
共享程式碼給大家。希望可以幫助初學者熟悉http協議和libevent基礎知識。如有問題,請大家不吝指出,謝謝!
專案效果圖
- 伺服器啟動
- 使用客戶端(瀏覽器)訪問效果,這裡是chrome
專案介紹
這裡只是簡單介紹下,具體細節請大家看程式碼,文章最後貼有程式碼地址。
環境介紹
系統平臺:windows
開發工具:vs2010
開發語言:C
程式結構之:event相關
單執行緒,使用I/O多路複用實現併發。main函式進來後直接呼叫http_startup()。
int main()
{
UINT16 port = 80;
http_startup(&port);
return 0;
}
http_startup()裡面建立一個socket用於listen,然後把這個socket扔到event裡面,設定回撥函式為accept_callback,等待客戶端(這裡就是各種瀏覽器)連線。所貼程式碼為了邏輯清晰,去掉了一些程式碼。
int http_startup(uint16_t *port)
{
SOCKET fd;
event_t ev = {0};
network_listen(port, &fd);
ev.fd = fd;
ev.type = EV_READ | EV_PERSIST;
ev.callback = accept_callback;
event_add(&ev);
// dispatch裡面就是個死迴圈,保證程式不退出
event_dispatch();
closesocket(fd);
return SUCC;
}
下面貼上event的核心,也就是event_dispatch()
為了邏輯清晰,也去掉了一些程式碼。
這裡就是所謂的偽高併發之一了(後面還有之二):
由於是windows系統沒有epoll,為了簡單使用了select模型。儘管重新定義了FD_SETSIZE為1024,但是還是無關痛癢。1024個連線就滿了,而且select是輪詢機制,效率受限。
開始準備使用iocp,一來api的名字太難看了,就懶得研究了。二來我就是用來寫個demo練練手,select也能湊合著用。
偽高併發之二:
網路I/O使用的是阻塞I/O,比如recv,會阻塞。上傳檔案時每次讀取BUFFER_UNIT個數據,測試時log打印發現還是會偶爾阻塞一會。把BUFFER_UNIT改小的可能會有所改善,但是也不是解決辦法。應該改成非阻塞I/O。這裡也不討論這個問題了。
#define BUFFER_UNIT 4096
ret_code_t event_dispatch()
{
fd_set readfds;
fd_set writefds;
fd_set exceptfds;
struct timeval timeout = { 0, 500000 };
int ret;
uint32_t i;
while (TRUE)
{
_active_size = 0;
memcpy(&readfds, &_readfds, sizeof(_readfds.fd_count) + _readfds.fd_count * sizeof(SOCKET));
memcpy(&writefds, &_writefds, sizeof(_writefds.fd_count) + _writefds.fd_count * sizeof(SOCKET));
memcpy(&exceptfds, &_exceptfds, sizeof(_exceptfds.fd_count) + _exceptfds.fd_count * sizeof(SOCKET));
ret = select(0, &readfds, &writefds, &exceptfds, &timeout);
switch (ret)
{
case 0: // the time limit expired
break;
case SOCKET_ERROR: // an error occurred
log_error("{%s:%d} an error occurred at select. WSAGetLastError=%d", __FUNCTION__, __LINE__, WSAGetLastError());
return FAIL;
default: // the total number of socket handles that are ready
for (i=0; i<readfds.fd_count; i++)
{
_active_ns[_active_size++] = find_rbnode(readfds.fd_array[i], &_read_evs);
}
for (i=0; i<writefds.fd_count; i++)
{
_active_ns[_active_size++] = find_rbnode(writefds.fd_array[i], &_write_evs);
}
for (i=0; i<exceptfds.fd_count; i++)
{
_active_ns[_active_size++] = find_rbnode(exceptfds.fd_array[i], &_except_evs);
}
break;
}
for (i = 0; i < _active_size; i++)
{
_active_ns[i]->ev->callback(_active_ns[i]->ev);
}
}
return SUCC;
}
再下面就是event_add()和event_del()。這三個函式基本上就是event驅動模型的全部了,貼程式碼。老規矩,去掉部分空指標判斷的程式碼。影響閱讀程式碼邏輯
ret_code_t event_add(event_t *ev)
{
struct rbnode_t k;
struct rbnode_t *n = NULL;
struct rbtree_t *t = NULL;
fd_set *s = NULL;
if (ev->type & EV_READ)
{
t = &_read_evs;
s = &_readfds;
}
else if (ev->type & EV_WRITE)
{
t = &_write_evs;
s = &_writefds;
}
else if (ev->type & EV_EXCEPT)
{
t = &_except_evs;
s = &_exceptfds;
}
k.ev = ev;
n = RB_FIND(rbtree_t, t, &k);
if (n)
{
log_warn("{%s:%d} event is already exist, fd=%d", __FUNCTION__, __LINE__, ev->fd);
return EXIS;
}
n = create_rbnode(ev);
RB_INSERT(rbtree_t, t, n);
FD_SET(ev->fd, s);
return SUCC;
}
實際程式碼裡面由於業務邏輯,這段程式碼與所貼不一致,哈哈
static int event_del(uint32_t fd, struct rbtree_t *t, fd_set *s)
{
struct rbnode_t k;
struct rbnode_t *n = NULL;
event_t e = { 0 };
e.fd = fd;
k.ev = &e;
n = RB_FIND(rbtree_t, t, &k);
if (n)
{
RB_REMOVE(rbtree_t, t, n);
FD_CLR(fd, s);
release_rbnode(n);
}
return SUCC;
}
程式結構之:http相關
本程式目前能支援的客戶端請求有三種:
第一種 客戶端(瀏覽器)上傳檔案類 POST請求
判斷邏輯如下:uri以 /upload 開頭,
本來開始是沒有後面的 ?path=,後來發現伺服器不知道儲存到哪級目錄下面,於是就加上了這個。
如下url:
http://localhost/upload?path=
http://localhost/upload?path=Debug/
使用form表單提交,html上傳程式碼如下:
html程式碼都是服務端按邏輯生成的。
<form action="/upload?path=Debug/" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
第二種 獲取檔案列表類 GET請求
程式碼裡面的response_home_page()函式。
判斷邏輯如下:request頭裡面的uri以 / 結尾,如下url:
http://localhost/
http://localhost/Debug/
http://localhost/Debug/httpd.tlog/
第三種 獲取檔案內容類 GET請求
程式碼裡面的response_send_file_page()函式。
判斷邏輯如下:非以上兩種情況,如下url:
http://localhost/event.c
http://localhost/Debug/event.obj
http://localhost/Debug/httpd.tlog/link.write.1.tlog
先寫這麼多了,以後有時間再補充吧。(耐不住懶啊 ~)大家有問題請留言。