C语言编写高并发Http文件上传下载服务器

时间:2024-03-22 15:02:35

前言

前段时间学习tinyhttpd和libevent开源库。
别人的代码写的再好终究是别人的,自以为看懂了,等到自己真正写的时候就会发现有各种问题。于是准备参考libevent里面最最最基础的功能(捡了芝麻丢了西瓜?),自己写一个event,用于熟悉libevent的I/O多路复用思想。然后再加上http。就有了这篇博客的Http高并发文件上传下载服务器(这里其实是伪高并发,下文会具体描述,原谅我的标题党,哈哈)。
共享代码给大家。希望可以帮助初学者熟悉http协议和libevent基础知识。如有问题,请大家不吝指出,谢谢!

项目效果图

  1. 服务器启动
    C语言编写高并发Http文件上传下载服务器
  2. 使用客户端(浏览器)访问效果,这里是chrome
    C语言编写高并发Http文件上传下载服务器

项目介绍

这里只是简单介绍下,具体细节请大家看代码,文章最后贴有代码地址。

环境介绍

系统平台: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

先写这么多了,以后有时间再补充吧。(耐不住懒啊 ~)大家有问题请留言。


源码地址:

CSDN下载: https://download.csdn.net/download/yu1121jm/10695695
Github: https://github.com/binbyu/httpd