Linux C语言HTTP协议解析与请求处理
HTTP 协议基础
HTTP(HyperText Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议,是万维网数据通信的基础。HTTP 协议基于请求 - 响应模型,客户端向服务器发送请求,服务器根据请求返回相应的响应。
HTTP 请求
一个 HTTP 请求通常由三部分组成:请求行、请求头和请求体。
请求行:包含请求方法、请求 URI 和 HTTP 版本。常见的请求方法有 GET、POST、PUT、DELETE 等。例如:
GET /index.html HTTP/1.1
这里使用 GET 方法请求 /index.html
资源,使用的 HTTP 版本是 1.1。
请求头:包含一系列键值对,用于传递额外的信息,如客户端信息、请求内容类型等。例如:
Host: www.example.com
User - Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q = 0.9,image/avif,image/webp,*/*;q = 0.8
Accept - Language: en - US,en;q = 0.5
Accept - Encoding: gzip, deflate, br
Connection: keep - alive
Upgrade - Insecure - Requests: 1
Sec - Fetch - Dest: document
Sec - Fetch - Mode: navigate
Sec - Fetch - Site: none
Sec - Fetch - User: ?1
上述请求头中,Host
指明请求的主机名,User - Agent
标识客户端的浏览器和操作系统等信息,Accept
表示客户端能接受的响应内容类型等。
请求体:通常在 POST、PUT 等请求方法中使用,用于传递数据。例如,当提交表单时,表单数据会放在请求体中。
HTTP 响应
HTTP 响应也由三部分组成:状态行、响应头和响应体。
状态行:包含 HTTP 版本、状态码和状态消息。状态码表示请求的处理结果,常见的状态码有 200(成功)、404(未找到)、500(服务器内部错误)等。例如:
HTTP/1.1 200 OK
表示使用 HTTP 1.1 版本,请求成功。
响应头:同样包含一系列键值对,提供关于响应的额外信息,如内容类型、内容长度等。例如:
Content - Type: text/html; charset = UTF - 8
Content - Length: 1234
Date: Mon, 15 Aug 2023 12:00:00 GMT
Server: Apache/2.4.54 (Ubuntu)
Content - Type
说明响应内容的类型是 HTML 且字符编码为 UTF - 8,Content - Length
表示响应体的长度。
响应体:包含服务器返回的实际内容,如 HTML 页面、JSON 数据等。
在 Linux 环境下用 C 语言解析 HTTP 请求
在 Linux 环境中,我们可以使用 socket 编程来处理网络通信,进而解析 HTTP 请求。下面是一个简单的示例代码,用于接收并解析 HTTP 请求。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void parse_request(char *buffer) {
char *method = strtok(buffer, " ");
char *uri = strtok(NULL, " ");
char *http_version = strtok(NULL, "\r\n");
printf("Method: %s\n", method);
printf("URI: %s\n", uri);
printf("HTTP Version: %s\n", http_version);
char *header = strtok(NULL, "\r\n");
while (header != NULL) {
printf("Header: %s\n", header);
header = strtok(NULL, "\r\n");
}
}
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("Bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
if (listen(sockfd, 10) < 0) {
perror("Listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
socklen_t len = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if (connfd < 0) {
perror("Accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
char buffer[BUFFER_SIZE] = {0};
int n = read(connfd, buffer, sizeof(buffer));
buffer[n] = '\0';
printf("Received request:\n%s\n", buffer);
parse_request(buffer);
close(connfd);
close(sockfd);
return 0;
}
在上述代码中,parse_request
函数用于解析 HTTP 请求。它首先通过 strtok
函数从请求字符串中提取请求方法、URI 和 HTTP 版本。然后,通过循环提取请求头信息并打印。
main
函数创建一个 TCP 套接字,绑定到指定端口并监听连接。当有客户端连接时,接收客户端发送的 HTTP 请求数据,并调用 parse_request
函数进行解析。
处理 HTTP 请求中的参数
在 HTTP 请求中,GET 请求的参数通常包含在 URI 中,而 POST 请求的参数则在请求体中。下面我们分别来看如何处理这两种情况。
处理 GET 请求参数
对于 GET 请求,参数以 key = value
的形式附加在 URI 后面,通过 ?
与 URI 主体分隔,多个参数之间用 &
分隔。例如:/index.html?name = John&age = 30
。
void parse_get_params(char *uri) {
char *params = strchr(uri, '?');
if (params != NULL) {
params++;
char *token = strtok(params, "&");
while (token != NULL) {
char *key = strtok(token, "=");
char *value = strtok(NULL, "=");
printf("GET Parameter: %s = %s\n", key, value);
token = strtok(NULL, "&");
}
}
}
在 parse_get_params
函数中,首先通过 strchr
查找 ?
字符,找到后将指针移到参数部分。然后通过 strtok
以 &
为分隔符提取每个参数,再以 =
为分隔符提取参数的键和值。
处理 POST 请求参数
POST 请求参数在请求体中,并且通常编码为 application/x - www - form - urlencoded
格式,即 key1 = value1&key2 = value2
的形式。
void parse_post_params(char *buffer) {
char *content_length_str = strstr(buffer, "Content - Length: ");
if (content_length_str != NULL) {
content_length_str += strlen("Content - Length: ");
int content_length = atoi(content_length_str);
char *post_data_start = strstr(buffer, "\r\n\r\n");
if (post_data_start != NULL) {
post_data_start += 4;
char post_data[content_length + 1];
strncpy(post_data, post_data_start, content_length);
post_data[content_length] = '\0';
char *token = strtok(post_data, "&");
while (token != NULL) {
char *key = strtok(token, "=");
char *value = strtok(NULL, "=");
printf("POST Parameter: %s = %s\n", key, value);
token = strtok(NULL, "&");
}
}
}
}
在 parse_post_params
函数中,首先通过 strstr
查找 Content - Length
头字段,获取请求体的长度。然后找到请求体的起始位置(\r\n\r\n
之后),提取请求体数据。最后以 &
和 =
为分隔符解析参数。
构建 HTTP 响应
了解如何解析 HTTP 请求后,我们还需要学会构建 HTTP 响应。一个完整的 HTTP 响应需要包含状态行、响应头和响应体。
void send_http_response(int connfd, const char *status_line, const char *content_type, const char *response_body) {
char response[BUFFER_SIZE];
int content_length = strlen(response_body);
snprintf(response, sizeof(response), "%s\r\nContent - Type: %s\r\nContent - Length: %d\r\n\r\n%s",
status_line, content_type, content_length, response_body);
write(connfd, response, strlen(response));
}
在 send_http_response
函数中,使用 snprintf
函数构建 HTTP 响应字符串。它包含状态行、Content - Type
响应头、Content - Length
响应头以及响应体。最后通过 write
函数将响应发送给客户端。
示例:简单的 HTTP 服务器
下面是一个完整的简单 HTTP 服务器示例,它可以处理 GET 和 POST 请求,并返回相应的响应。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void parse_request(char *buffer, char **method, char **uri, char **http_version, char **post_data) {
*method = strtok(buffer, " ");
*uri = strtok(NULL, " ");
*http_version = strtok(NULL, "\r\n");
if (strcmp(*method, "POST") == 0) {
char *content_length_str = strstr(buffer, "Content - Length: ");
if (content_length_str != NULL) {
content_length_str += strlen("Content - Length: ");
int content_length = atoi(content_length_str);
char *post_data_start = strstr(buffer, "\r\n\r\n");
if (post_data_start != NULL) {
post_data_start += 4;
*post_data = (char *)malloc(content_length + 1);
strncpy(*post_data, post_data_start, content_length);
(*post_data)[content_length] = '\0';
}
}
}
}
void parse_get_params(char *uri) {
char *params = strchr(uri, '?');
if (params != NULL) {
params++;
char *token = strtok(params, "&");
while (token != NULL) {
char *key = strtok(token, "=");
char *value = strtok(NULL, "=");
printf("GET Parameter: %s = %s\n", key, value);
token = strtok(NULL, "&");
}
}
}
void parse_post_params(char *post_data) {
char *token = strtok(post_data, "&");
while (token != NULL) {
char *key = strtok(token, "=");
char *value = strtok(NULL, "=");
printf("POST Parameter: %s = %s\n", key, value);
token = strtok(NULL, "&");
}
}
void send_http_response(int connfd, const char *status_line, const char *content_type, const char *response_body) {
char response[BUFFER_SIZE];
int content_length = strlen(response_body);
snprintf(response, sizeof(response), "%s\r\nContent - Type: %s\r\nContent - Length: %d\r\n\r\n%s",
status_line, content_type, content_length, response_body);
write(connfd, response, strlen(response));
}
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("Bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
if (listen(sockfd, 10) < 0) {
perror("Listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
socklen_t len = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if (connfd < 0) {
perror("Accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
char buffer[BUFFER_SIZE] = {0};
int n = read(connfd, buffer, sizeof(buffer));
buffer[n] = '\0';
char *method, *uri, *http_version, *post_data = NULL;
parse_request(buffer, &method, &uri, &http_version, &post_data);
printf("Method: %s\n", method);
printf("URI: %s\n", uri);
printf("HTTP Version: %s\n", http_version);
if (strcmp(method, "GET") == 0) {
parse_get_params(uri);
send_http_response(connfd, "HTTP/1.1 200 OK", "text/plain", "GET request received");
} else if (strcmp(method, "POST") == 0) {
parse_post_params(post_data);
send_http_response(connfd, "HTTP/1.1 200 OK", "text/plain", "POST request received");
free(post_data);
} else {
send_http_response(connfd, "HTTP/1.1 405 Method Not Allowed", "text/plain", "Method not allowed");
}
close(connfd);
close(sockfd);
return 0;
}
在这个示例中,parse_request
函数不仅解析请求行和请求头,还会提取 POST 请求的请求体数据。parse_get_params
和 parse_post_params
函数分别处理 GET 和 POST 请求的参数。send_http_response
函数构建并发送 HTTP 响应。main
函数创建服务器套接字,接收请求,解析并根据请求方法进行相应处理。
HTTP 协议中的常见问题与处理
长连接与短连接
HTTP/1.0 默认使用短连接,即每次请求 - 响应完成后,客户端和服务器之间的连接就会关闭。而 HTTP/1.1 默认使用长连接,通过在请求头和响应头中设置 Connection: keep - alive
来实现。长连接可以减少连接建立和关闭的开销,提高性能。
在代码中,如果要支持长连接,可以在响应头中添加 Connection: keep - alive
,并且在处理完一个请求后不关闭连接,而是等待下一个请求。例如:
void send_http_response_with_keepalive(int connfd, const char *status_line, const char *content_type, const char *response_body) {
char response[BUFFER_SIZE];
int content_length = strlen(response_body);
snprintf(response, sizeof(response), "%s\r\nContent - Type: %s\r\nContent - Length: %d\r\nConnection: keep - alive\r\n\r\n%s",
status_line, content_type, content_length, response_body);
write(connfd, response, strlen(response));
}
然后在 main
函数中,处理完一个请求后不调用 close(connfd)
,而是继续循环接收新的请求。
重定向
重定向是指服务器返回一个特殊的状态码(如 301 永久重定向、302 临时重定向),并在响应头中包含 Location
字段,指示客户端去请求另一个 URL。
在 C 语言实现中,可以这样构建重定向响应:
void send_redirect_response(int connfd, const char *redirect_url) {
char response[BUFFER_SIZE];
snprintf(response, sizeof(response), "HTTP/1.1 302 Found\r\nLocation: %s\r\n\r\n", redirect_url);
write(connfd, response, strlen(response));
}
在处理请求时,如果需要重定向,调用 send_redirect_response
函数即可。
处理 HTTP 分块传输编码
在 HTTP 协议中,当服务器不知道响应体的长度时,可以使用分块传输编码。在这种情况下,响应体被分成多个块,每个块的开头是块的长度(以十六进制表示),后面跟着块的数据,最后以一个长度为 0 的块结束。
解析分块传输编码的响应体比较复杂,需要逐块读取数据。以下是一个简单的示例代码,用于解析分块传输编码的响应体:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 1024
void parse_chunked_response(char *buffer) {
char *ptr = buffer;
while (1) {
char chunk_length_str[10];
int i = 0;
while (*ptr != '\r' && *ptr != '\0') {
chunk_length_str[i++] = *ptr++;
}
chunk_length_str[i] = '\0';
ptr += 2; // 跳过 \r\n
int chunk_length = strtol(chunk_length_str, NULL, 16);
if (chunk_length == 0) {
break;
}
char chunk_data[chunk_length + 1];
strncpy(chunk_data, ptr, chunk_length);
chunk_data[chunk_length] = '\0';
printf("Chunk Data: %s\n", chunk_data);
ptr += chunk_length + 2; // 跳过块数据和 \r\n
}
}
int main() {
char buffer[BUFFER_SIZE] = "4\r\nabcd\r\n3\r\nxyz\r\n0\r\n\r\n";
parse_chunked_response(buffer);
return 0;
}
在上述代码中,parse_chunked_response
函数通过循环读取每个块的长度和数据,直到遇到长度为 0 的块。
总结
通过以上内容,我们详细了解了在 Linux 环境下使用 C 语言进行 HTTP 协议解析与请求处理的相关知识,包括 HTTP 协议基础、请求和响应的解析与构建、处理请求参数、以及常见问题的处理等。希望这些内容能帮助读者更好地理解和应用 HTTP 协议在 C 语言开发中的实践。
注意,实际应用中,还需要考虑更多的细节,如安全性、性能优化等。例如,在处理请求参数时,需要进行适当的验证和过滤,以防止 SQL 注入、XSS 等安全漏洞。同时,为了提高性能,可以采用多线程或异步 I/O 等技术来处理多个并发请求。