[C/C++后端开发学习] 11 实现一个简单的HTTP服务器

之前我们基于已经Reactor模型实现了一个简单的websocket服务器,在此基础上再实现一个简单的HTTP服务器小框架。实际上,最终我们会实现一个支持websocket的HTTP服务器。具体功能包括:首先要实现GET html页面、图片、pdf文档等;其次是实现POST方法并完成一个简单的表单提交功能。

实现GET方法

关于HTTP报文的消息结构都包含哪些元素可以参考这里

一个GET请求报文的示例如下:

GET /index.html HTTP/1.1
Host: 192.168.0.103
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

GET方法没有消息体,因此不需要解析其请求数据,只需解析其开头的请求行和请求头部。

对应的响应报文实例如下:

HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 02:13:47 GMT
Content-Type: text/html;charset=utf-8
Content-Length: 253

<!DOCTYPE html>
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>This is an awesome HTML</h1>
    </body>
</html>

也很简单,按格式构造状态行和必要的消息头后,再间隔一个空行并发送页面数据就可以了。这里的数据可能是由代码构造的html文本,也可能是一个文件,比如jpg格式的图片或pdf文档。因此,必须通过Content-Type指定消息体的格式客户端才能识别,同时指定Content-Length后客户端才能知道需要接收多少数据。

约定GET时URI的格式

GET方法一般是用于请求某一个具体的页面或文件资源,当然也可以用于获取XML或JSON格式的数据。为了简单起见,我们目前在做URI资源名称的解析时,仅支持GET方法请求服务器上已存在的文件资源,暂时包括.html、jpg、.pdf、ico四种。这些资源都单独放在程序工作目录下的一个public/目录下,但我们规定GET报文的URI中不需要体现public目录,比如请求index.html页面,那么开头的请求行只需这么写即可:GET /index.html HTTP/1.1,由程序自动转换为去获取public/index.html文件。

状态机与websocket协议兼容

为了兼容之前的websocket协议,我们需要先增加虚拟机中的状态以实现HTTP的请求和接收。

显然websocket协议的握手过程也属于HTTP请求与响应的一部分,二者都是使用GET方法进行请求,我们只需根据请求报文的头部来判断这是不是一个websocket握手请求,如果是则执行websocket握手过程的响应代码,如果不是则执行普通HTTP协议的响应代码。这里先简单地以头部中是否含有``字段来识别是否为websocket的握手请求,大致代码思路如下:

	/* 根据方法进行处理并设置下一个状态 */
    switch(http->req.method)
    {
   
        case HTTP_METHOD_GET:
            if(strlen(http->ws.ws_key) > 0)  // 获取到了websocket的key,进行 websocket 握手
            {
   
                ret = ws_handshake(http);
                http->status = WS_DATATRANSFER;   // 握手后则进入通信状态
            }
            else    // 否则按普通 http 请求处理
            {
   
                ret = http_request_get(http);
                http->status = HTTP_RESPONSE;	  // 
            }
            break;
    .......

状态机实现的大致思路如下图所示:

在这里插入图片描述

实现几个辅助函数

开始HTTP请求与响应的主体代码编写前,需要先实现一些辅助函数。此处仅列出几个比较关键的辅助函数,主要位于HTTP协议相关的模块代码。

1)解析消息头,获取方法、URI以及需要的头部字段。由于HTTP协议中消息头的结束标志位是一个空行,因此我们用\r\n\r\n作为消息头的结束字符串。在读取到\r\n\r\n前,我们不知道头部到底有多长。对此我在代码中作简单处理,为消息头部专门使用一个缓冲区,指定一个最大头部长度,如果头部接收过程中缓冲区已满却还没有收到结束符\r\n\r\n,则返回错误。

/* 解析消息头,获取方法、URI以及需要的头部字段 */
static int http_resolveReqHeader(http_service_interface *http)
{
   
    if(http == NULL) return -1;

    char linebuf[256];
    char* value;
    int level = 0;
    int ret;

    /* 1 - 从第1行提取出http方法*/
    memset(linebuf, 0, 256);
    level = readline(http->req.header, level, linebuf);
    if(strstr(linebuf, "GET"))
        http->req.method = HTTP_METHOD_GET;
    else if(strstr(linebuf, "POST"))
        http->req.method = HTTP_METHOD_POST;
    else if(strstr(linebuf, "PUT"))
        http->req.method = HTTP_METHOD_PUT;
    else
        http->req.method = HTTP_METHOD_NOTALLOWED;    // 从第一行提取不到方法或不支持的方法

    /* 2 - 尝试提取请求的资源路径 */
    if(value = strchr(linebuf, '/'))
    {
   
        memset(http->req.resource, 0, HTTP_MAX_RESOURCE_NAME);
        ret = http_getResourceName(http->req.resource, HTTP_MAX_RESOURCE_NAME, value + 1);
        if(ret >= 0) printf("# resource requested[%d]: %s\n", ret, http->req.resource);
    }

    /******* 3 - 提取可能有用的头部字段 *******/ // this piece of code is awful, but I will improve it
    /* 都需要提取哪些头部字段目前通过手动加到这里 */
    // 3.1 尝试提取 ws 的key字段,后面依次判断是否进行 ws 握手
    ret = http_getHeaderValueString(http->req.header, "Sec-WebSocket-Key", http->ws.ws_key);
    if(ret >= 0) printf("# WS key[%d]: %s\n\n", ret, http->ws.ws_key);

    // 3.2 尝试提取 Content-Length 字段
    if(http->req.method != HTTP_METHOD_GET)
    {
   
        ret = http_getHeaderValueInt(http->req.header, "Content-Length", &http->req.contentlength);
        if(ret >= 0 && http->req.contentlength > 0) 
        {
      // 根据消息体长度申请一块堆内存来单独存放消息体,此处暂不管申请是否成功
            http->req.body = (char*)malloc(http->req.contentlength + 1); // 加1若需要的话作为结束符
            if(http->req.body) 
                memset(http->req.body, 0, http->req.contentlength + 1);

            
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值