1. 程式人生 > >C語言編寫高併發Http檔案上傳下載伺服器

C語言編寫高併發Http檔案上傳下載伺服器

前言

前段時間學習tinyhttpd和libevent開源庫。
別人的程式碼寫的再好終究是別人的,自以為看懂了,等到自己真正寫的時候就會發現有各種問題。於是準備參考libevent裡面最最最基礎的功能(撿了芝麻丟了西瓜?),自己寫一個event,用於熟悉libevent的I/O多路複用思想。然後再加上http。就有了這篇部落格的Http高併發檔案上傳下載伺服器(這裡其實是偽高併發,下文會具體描述,原諒我的標題黨,哈哈)。
共享程式碼給大家。希望可以幫助初學者熟悉http協議和libevent基礎知識。如有問題,請大家不吝指出,謝謝!

專案效果圖

  1. 伺服器啟動
    在這裡插入圖片描述
  2. 使用客戶端(瀏覽器)訪問效果,這裡是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

先寫這麼多了,以後有時間再補充吧。(耐不住懶啊 ~)大家有問題請留言。

原始碼地址: