1.简介
当使用C语言开发一个http服务器时就要做到对http协议进行封装和解码,为此需要特别清楚http协议响应报文与请求报文的构成:
- http请求报文
请求方法 | sp | url | sp | 协议版本 | \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上,因为登录页面有可能被浏览器缓存,如果有人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码。