对http协议的应用层封装实现

1.简介

当使用C语言开发一个http服务器时就要做到对http协议进行封装和解码,为此需要特别清楚http协议响应报文与请求报文的构成:

  • http请求报文
请求方法spurlsp协议版本\r\n
首部字段名\r\n
首部字段名\r\n
\r\n
body
  • http响应报文
协议版本sp状态码sp状态原因\r\n
首部字段名\r\n
首部字段名\r\n
\r\n
body

2.http请求包与响应包的实现

  • 请求包
struct http_request {
    char *method;   //请求方法
    char *url;      //url
    char *version;  //版本号
    enum http_request_state current_state;  //为解码提供的一个标记变量
    struct request_header *request_headers; //保存报文头部的数组
    int request_headers_number;             //头部的数目
};

依赖数据结构定义如下:

enum http_request_state {
    REQUEST_STATUS,    //等待解析状态行
    REQUEST_HEADERS,   //等待解析headers
    REQUEST_BODY,      //等待解析请求body
    REQUEST_DONE       //解析完成
};
struct request_header {
    char *key;
    char *value;
};
  • 响应包
struct http_response {
    enum HttpStatusCode statusCode; //状态码
    char *statusMessage;            //状态消息
    char *contentType;              //文本类型
    char *body;                     //body主体
    struct response_header *response_headers;  //响应头部
    int response_headers_number;
    int keep_connected;
};

依赖数据结构定义如下:

enum HttpStatusCode {
    Unknown,
    OK = 200,
    MovedPermanently = 301,
    BadRequest = 400,
    NotFound = 404,
};
struct response_header {
    char *key;
    char *value;
};

3.对数据流进行解码转换为struct request数据结构

编码实现将TCP层收到的放在缓存中的字节流解码便于获取详细信息:
先假设TCP层的缓存如下结构体buffer所示:

struct buffer{
	char *data;     //实际的数据缓存
	int readIndex;  //表征可读部分的起始
	int writeIndex; //表征可写部分的起始
	int total_size; //缓冲的总大小
};

当套接字上接受到数据的时候,就调用相关回调函数将字节流读到如上结构体中,然后就是负责对以上结构的解码部分了。

  • 解析状态行
        //函数将buffer里面的数据解码存到request结构中
        struct buffer input;
        struct http_request httpRequest;
        if (httpRequest->current_state == REQUEST_STATUS) { /*解析状态行*/
            char *crlf = buffer_find_CRLF(input);//该函数返回字节流中第一个回车换行符的位置
            if (crlf) {
                int request_line_size = process_status_line(input->data + input->readIndex, crlf, httpRequest);
                //process_status_line函数去处理状态行,具体见下
                if (request_line_size) {//之后更新缓冲区的读指针的位置
                    input->readIndex += request_line_size;  // request line size
                    input->readIndex += 2;  //CRLF size
                    httpRequest->current_state = REQUEST_HEADERS;
                }
            }
        }
//处理状态行函数
int process_status_line(char *start, char *end, struct http_request *httpRequest) {
    int size = end - start;
    //method
    char *space = memmem(start, end - start, " ", 1);//状态行以空格为划分
    assert(space != NULL);
    int method_size = space - start;
    httpRequest->method = malloc(method_size + 1);
    strncpy(httpRequest->method, start, space - start);
    httpRequest->method[method_size + 1] = '\0';

    //url
    start = space + 1;
    space = memmem(start, end - start, " ", 1);
    assert(space != NULL);
    int url_size = space - start;
    httpRequest->url = malloc(url_size + 1);
    strncpy(httpRequest->url, start, space - start);
    httpRequest->url[url_size + 1] = '\0';

    //version
    start = space + 1;
    httpRequest->version = malloc(end - start + 1);
    strncpy(httpRequest->version, start, end - start);
    httpRequest->version[end - start + 1] = '\0';
    assert(space != NULL);
    return size;
}

函数很简单主要是处理状态行时要有以下套路,先利用memmem函数找到空格所在位置,为request变量的成员变量分配内存,然后将字符拷贝到指定位置。

  • 解析头部
//为解析所有头部字段应该外面还有一层循环,处理所有直到遇到空行
        else if (httpRequest->current_state == REQUEST_HEADERS) {
            char *crlf = buffer_find_CRLF(input);
            if (crlf) {
                /**
                 *    <start>-------<colon>:-------<crlf>
                 */
                char *start = input->data + input->readIndex;
                int request_line_size = crlf - start;
                char *colon = memmem(start, request_line_size, ": ", 2);//不要忘了空格
                if (colon != NULL) {
                    char *key = malloc(colon - start + 1);
                    strncpy(key, start, colon - start);
                    key[colon - start] = '\0';
                    char *value = malloc(crlf - colon - 2 + 1);
                    strncpy(value, colon + 2, crlf - colon - 2);
                    value[crlf - colon - 2] = '\0';

                    http_request_add_header(httpRequest, key, value);//添加进去

                    input->readIndex += request_line_size;  //request line size
                    input->readIndex += 2;  //CRLF size
                } else {
                    //读到这里说明:没找到,就说明这个是最后一行
                    input->readIndex += 2;  //CRLF size
                    httpRequest->current_state = REQUEST_DONE;
                }
            }
        }

4.编码和发送

待所有事件处理完毕之后,开始进行编码和发送

if (http_request_current_state(httpRequest) == REQUEST_DONE) {
        struct http_response *httpResponse = http_response_new();
        //为响应包动态分配内存
        //httpServer暴露的requestCallback回调
        if (httpServer->requestCallback != NULL) {
            httpServer->requestCallback(httpRequest, httpResponse);
            //上面调用用户自己编写的函数处理代码设置httpResponse包
        }
        struct buffer *buffer = buffer_new();
        http_response_encode_buffer(httpResponse, buffer);
        tcp_connection_send_buffer(tcpConnection, buffer);
        //发出去
        if (http_request_close_connection(httpRequest)) {
            tcp_connection_shutdown(tcpConnection);
        }
	http_request_reset(httpRequest);
    }

在用户层编码的时候需要注意的是,在以url中?分割URL和传输数据,多个参数用&连接;
例如:login.action?name=hyddd&password=idontknow&verify=%E4%BD%A0 %E5%A5%BD。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。

5.关于HTTP请求GET和POST的区别

  • (1) 数据提交的方式不同
    GET提交,请求的数据会附在URL之后(就是把数据放置在HTTP协议头<request-line>中)。具体分割方法见上面。 POST提交:把提交的数据放置在是HTTP包的包体<request-body>中。因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
  • (2) 传输数据的大小
    HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制, 而在实际开发中存在的限制。对于GET特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35),一般来说理论上没有长度限制,其限制取决于操作系统的支持。而对于post由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。
  • (3) 安全性
    POST的安全性要比GET的安全性高。因为通过GET提交数据,用户名和密码将明文出现在URL上,因为登录页面有可能被浏览器缓存,如果有人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值