MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言HTTP协议解析与请求处理

2021-08-296.8k 阅读

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_paramsparse_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 等技术来处理多个并发请求。