游戏算法推荐
  • 【十】表查找解
    【十】表查找解
    查找表又可分为静态查找表和动态查找表。借助静态查找表可引申出顺序查找算法、折半查找算法、分块查找算法等;而记住动态查找表,也可以通过构建二叉排序树、平衡二叉树等实现查找操作。
  • 【九】动态内存管理
    【九】动态内存管理
    动态内存管理机制,主要包含两方面内容,用户申请内存空间时,系统如何分配;用户使用内存空间完成后,系统如何及时回收。
  • 【八】图和图存储结构
    【八】图和图存储结构
    玩转数据结构的图,就必须稳扎稳打,死抠图结构的每一个知识点,每一行代码,只有这样,才有彻底学会图存储结构的可能。
  • 【七】树和树存储结构
    【七】树和树存储结构
    树存储结构中,最常用的还是二叉树,本章就二叉树的存储结构、二叉树的前序、中序、后序以及层次遍历、线索二叉树、哈夫曼树等,详细介绍二叉树。
  • 【六】数组和广义表
    【六】数组和广义表
    数组存储结构,99%的编程语言都包含的存储结构,用于存储不可再分的单一数据;而广义表不同,它还可以存储子广义表。
  • 【五】字符串和串存储结构
    【五】字符串和串存储结构
    字符串之间的逻辑关系也是“一对一”,用线性表的思维不难想出,串存储结构也有顺序存储和链式存储。
  • 【四】栈(Stack)和队列(Queue)
    【四】栈(Stack)和队列(Queue)
    栈和队列,严格意义上来说,也属于线性表,因为它们也都用于存储逻辑关系为"一对一"的数据,但由于它们比较特殊,因此将其单独作为一章,做重点讲解。
  • 【三】数据结构线性表
    【三】数据结构线性表
    线性表,数据结构中最简单的一种存储结构,专门用于存储逻辑关系为"一对一"的数据。线性表,基于数据在实际物理空间中的存储状态,又可细分为顺序表(顺序存储结构)和链表(链式存储结构)。

利用多线程和C++实现一个简单的HTTP服务器

5881

前言:服务器是现代软件不可或缺的一部分,而服务器的技术也是非常复杂和有趣的方向。随着操作系统不断地发展,服务器的底层架构也在不断变化。本文介绍一种使用 C++ 和 多线程实现的简单 HTTP 服务器。


首先我们先来看一下如何创建一个服务器。

int main() 
{ 
    int server_fd;
    struct sockaddr_in server_addr;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    int on = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    if (server_fd < 0) { 
        perror("create socket error"); 
        goto EXIT;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family       = AF_INET;
    server_addr.sin_port         = htons(8888);
    server_addr.sin_addr.s_addr  = htonl(INADDR_ANY);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { 
        perror("bind address error"); 
        goto EXIT;
    }

    if (listen(server_fd, 511) < 0) { 
        perror("listen port error"); 
        goto EXIT;
    }

    while(1) {
        int connfd = accept(server_fd, nullptr, nullptr);
        if (connfd < 0) 
        { 
            perror("accept error"); 
            continue;
        }
        // 处理
    }

    close(server_fd);

    return 0;
EXIT:
    exit(1); 
}

我们看到根据操作系统提供的 API,创建一个 TCP 服务器非常简单 ,只需要调用几个函数就行。最后进程会阻塞在 accept 等待连接的到来,我们在一个死循环中串行地处理每个请求。显然,这样的效率肯定非常低,因为如果我们使用传统的 read / write 函数的话,它是会引起进程阻塞的,这样就会导致多个请求需要排队进行处理。我们在此基础上利用多线程提高一下效率。

std::thread threads[MAX_THREAD];
std::condition_variable condition_variable;
std::deque<int> requests;
std::mutex mutex;

for (int i = 0; i < MAX_THREAD; i++) 
{
    threads[i] = std::thread(worker, &mutex, &condition_variable, &requests);
}

多线程就会涉及到并发 / 同步的问题,所以需要使用互斥变量和条件变量来处理这些问题。上面的代码创建了几个线程,然后在每个线程中执行 worker 函数来处理请求,除此之外,用 requests 变量来表示请求队列,该变量会由主线程和子线程一起访问。具体是由主线程生产任务,子线程消费。在了解子线程逻辑之前先看看主线程代码的改动。

while(1) 
{
      int connfd = accept(server_fd, nullptr, nullptr);
      if (connfd < 0) 
      { 
          perror("accept error"); 
          continue;
      }
      {
          std::lock_guard<std::mutex> lock(mutex);
          requests.push_back(connfd);
          condition_variable.notify_one();
      }
}

我们看到当主线程收到请求时,自己不处理,而是添加到请求队列让子线程处理,因为子线程没有任务处理时会自我阻塞,所以主线程需要唤醒一个线程来处理新的请求。接下来看看子线程的逻辑。

void worker(std::mutex *mutex,
            std::condition_variable *condition_variable,
            std::deque<int> *requests) {
    int connfd;
    while (true) {
        {
            std::unique_lock<std::mutex> lock(*mutex);
            // 没有任务则等待,否则取出任务处理
            while ((*requests).size() == 0)
            {
                (*condition_variable).wait(lock);
            }
            connfd = (*requests).front();
            (*requests).pop_front();
        }

        char buf[4096];
        int ret;
        while (1) {
            memset(buf, 0, sizeof(buf));
            int bytes = read(connfd, buf, sizeof(buf)); 
            if (bytes <= 0) {
                close(connfd);
            } else {
                write(connfd, buf, bytes);
            }
        } 
    }
}

子线程不断从任务队列中取出任务,具体来说就是连接对应的文件描述符,然后不断读取里面的数据,最后返回给客户端。但是这样的功能显然没有太大意义,所以我们基于这个基础上实现一个 HTTP 服务,让它可以处理 HTTP 请求。当然我们手写一个优秀的 HTTP 解析器并非易事,所以我们直接使用开源的就好,这里选择的是 llhttp,这是 Node.js 所使用的 HTTP 解析器。这里就不具体罗列细节,大概介绍一下 llhttp 的用法。

void worker(std::mutex *mutex,
            std::condition_variable *condition_variable,
            std::deque<int> *requests) {
    int connfd;
    while (true) {
        {
            std::unique_lock<std::mutex> lock(*mutex);
            // 没有任务则等待,否则取出任务处理
            while ((*requests).size() == 0)
            {
                (*condition_variable).wait(lock);
            }
            connfd = (*requests).front();
            (*requests).pop_front();
        }

        char buf[4096];
        int ret;
        while (1) {
            memset(buf, 0, sizeof(buf));
            int bytes = read(connfd, buf, sizeof(buf)); 
            if (bytes <= 0) {
                close(connfd);
            } else {
                write(connfd, buf, bytes);
            }
        } 
    }
}

HTTP_Parser 是我自己实现的 HTTP Parser Wrapper,主要是对 llhttp 的封装,我们看到 HTTP_Parser 里有很多回调钩子,对应的就是 llhttp 提供的,另外 HTTP_Parser 支持调用方传入钩子,也就是 parser_callback 所定义的。当 llhttp 回调 HTTP_Parser 时,HTTP_Parser 在合适的时机就会调用 parser_callback 里的回调,比如在解析完 HTTP Header 时,或者解析完整个报文时。具体的解析过程是当调用方收到数据时,执行 parse 函数,然后 llhttp 就会不断地调用我们传入的钩子。了解了 HTTP 解析器的大致使用,我们来看看怎么在项目里使用。

typedef void (*p_on_headers_complete)(on_headers_complete_info, parser_callback);
typedef void (*p_on_body_complete)(on_body_complete_info, parser_callback);
typedef void (*p_on_body)(on_body_info, parser_callback);

struct parser_callback {
    void * data;
    p_on_headers_complete on_headers_complete;
    p_on_body on_body;
    p_on_body_complete on_body_complete;
};

class HTTP_Parser {
    public:
        HTTP_Parser(llhttp_type type, parser_callback callbacks = {});
        int on_message_begin(llhttp_t* parser);
        int on_status(llhttp_t* parser, const char* at, size_t length);
        int on_url(llhttp_t* parser, const char* at, size_t length);
        int on_header_field(llhttp_t* parser, const char* at, size_t length);
        int on_header_value(llhttp_t* parser, const char* at, size_t length);
        int on_headers_complete(llhttp_t* parser);
        int on_body(llhttp_t* parser, const char* at, size_t length);
        int on_message_complete(llhttp_t* parser);
        int parse(const char* data, int len);
        int finish();
        void print();
};

这里只列出关键的代码,当我们收到数据时,我们通过 parser.parse(buf, ret) 调用 llhttp 进行解析,llhttp 就会不断地回调钩子函数,当解析完一个报文后,on_body_complete 回调就会被执行,在这里我们就可以对 HTTP 请求进行响应,比如这里返回一个 200 的响应报文,然后关闭连接。因为通过 llhttp 我们可以拿到具体的请求 url,所以我们还可以进一步拓展,根据 url 进行不同的处理。

利用多线程和C++实现一个简单的HTTP服务器_C++语言-游民部落(gamecolg.com)

到此为止,就实现了一个 HTTP 服务器了 ,在早期的时候,服务器也是采用这种多进程 / 多线程的处理方式,现在有了多路复用等技术后,很多服务器都是基于事件驱动来实现了。但是主线程接收请求,分发给子线程处理这种思想在有些服务器也还是存在的,比如 Node.js,只不过 Node.js 中是进程间进行传递。


特别声明:本文仅供交流学习 , 版权归属原作者,并不代表游民部落赞同其观点和对其真实性负责。若文章无意侵犯到您的知识产权,损害了您的利益,烦请与我们联系vmaya_gz@126.com,我们将在24小时内进行修改或删除。

相关推荐:

Unity3D引擎推荐
  • 【二十八】游戏UI之UI界面管理
    【二十八】游戏UI之UI界面管理
    游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理游戏UI界面之UI管理
  • 【二十七】游戏UI之透视相机模式规划
    【二十七】游戏UI之透视相机模式规划
    游戏UI界面之透视相机模式规划游戏UI界面之透视相机模式规划游戏UI界面之透视相机模式规划游戏UI界面之透视相机模式规划游戏UI界面之透视相机模式规划游戏UI界面之透视相机模式规划
  • 【二十六】游戏UI之正交相机模式规划
    【二十六】游戏UI之正交相机模式规划
    游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面之正交相机模式规划游戏UI界面
  • 【二十五】游戏UI之NGUI和UGUI简介
    【二十五】游戏UI之NGUI和UGUI简介
    游戏UI界面NGUI和UGUI简介游戏UI界面NGUI和UGUI简介游戏UI界面NGUI和UGUI简介游戏UI界面NGUI和UGUI简介游戏UI界面NGUI和UGUI简介游戏UI界面NGUI和UGUI简介
  • 【二十四】Lua与C、C++间模块交互
    【二十四】Lua与C、C++间模块交互
    Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互Lua与C、C++间模块交互
  • 【二十二】Lua游戏配置内存优化策略
    【二十二】Lua游戏配置内存优化策略
    Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略Lua游戏配置内存优化策略
  • 【二十三】Lua模块和Unity引擎C#模块间交互
    【二十三】Lua模块和Unity引擎C#模块间交互
    Lua模块和Unity引擎C#模块间交互Lua模块和Unity引擎C#模块间交互Lua模块和Unity引擎C#模块间交互Lua模块和Unity引擎C#模块间交互Lua模块和Unity引擎C#模块间交互Lua模块和Unity引擎C#模块间交互
  • 【二十一】Lua内存开销规划和优化策略
    【二十一】Lua内存开销规划和优化策略
    Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略Lua内存开销规划和优化策略