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

Linux C语言HTTP协议编程实践

2023-08-103.2k 阅读

一、HTTP 协议基础

HTTP(Hyper - Text Transfer Protocol)即超文本传输协议,它是一种应用层协议,用于分布式、协作式和超媒体信息系统。HTTP 协议采用客户端 - 服务器架构模式,客户端发起请求,服务器响应请求。

1.1 HTTP 请求

HTTP 请求由三部分组成:请求行、请求头和请求体。

请求行:包含请求方法、请求 URI 和协议版本。常见的请求方法有 GET、POST、PUT、DELETE 等。例如,一个 GET 请求行可能是这样:GET /index.html HTTP/1.1,这里GET是请求方法,/index.html是请求的资源路径,HTTP/1.1是协议版本。

请求头:包含了关于客户端环境和请求的附加信息。例如,User - Agent头字段表明客户端的类型,Accept头字段指定客户端能够接受的响应内容类型。以下是一些常见请求头示例:

User - Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 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

请求体:对于 GET 请求,请求体通常为空。而对于 POST 请求,请求体中会包含客户端发送给服务器的数据,比如表单数据。例如,一个登录表单的 POST 请求体可能是:username = admin&password = 123456

1.2 HTTP 响应

HTTP 响应同样由三部分组成:状态行、响应头和响应体。

状态行:包含协议版本、状态码和状态消息。状态码表示请求的处理结果,常见的状态码有 200(成功)、404(未找到)、500(服务器内部错误)等。例如:HTTP/1.1 200 OK,这里HTTP/1.1是协议版本,200是状态码,OK是状态消息。

响应头:提供了关于响应的附加信息,如Content - Type指定响应内容的类型,Content - Length指定响应体的长度。示例如下:

Content - Type: text/html; charset=UTF - 8
Content - Length: 1234

响应体:包含了服务器返回给客户端的实际数据,比如 HTML 页面内容、JSON 数据等。

二、Linux 下的网络编程基础

在 Linux 环境中进行 C 语言的 HTTP 协议编程,需要先掌握基本的网络编程知识。

2.1 套接字(Socket)

套接字是网络编程的基础,它是一种抽象层,用于在不同设备间进行通信。在 Linux 中,套接字编程主要通过sys/socket.h头文件提供的函数实现。

创建套接字:使用socket()函数创建一个套接字。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain参数指定协议族,常见的有AF_INET(IPv4)和AF_INET6(IPv6)。type参数指定套接字类型,如SOCK_STREAM(面向连接的流套接字,常用于 TCP 协议)和SOCK_DGRAM(无连接的数据报套接字,常用于 UDP 协议)。protocol参数通常设为 0,表示使用默认协议。例如,创建一个 IPv4 的 TCP 套接字:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    return -1;
}

绑定套接字:使用bind()函数将套接字绑定到一个特定的地址和端口。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

对于 IPv4,addr参数是一个struct sockaddr_in类型的结构体,addrlen是该结构体的长度。以下是绑定示例:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
    perror("bind failed");
    close(sockfd);
    return -1;
}

监听套接字:对于服务器端的 TCP 套接字,需要使用listen()函数开始监听连接请求。其原型如下:

#include <sys/socket.h>
int listen(int sockfd, int backlog);

backlog参数指定等待连接队列的最大长度。例如:

if (listen(sockfd, 5) == -1) {
    perror("listen failed");
    close(sockfd);
    return -1;
}

接受连接:服务器使用accept()函数接受客户端的连接请求。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

addr参数用于获取客户端的地址信息,addrlen是该地址结构体的长度。例如:

struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd == -1) {
    perror("accept failed");
    close(sockfd);
    return -1;
}

连接服务器:客户端使用connect()函数连接到服务器。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

同样,addr参数是服务器的地址信息,addrlen是其长度。例如:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
    perror("connect failed");
    close(sockfd);
    return -1;
}

2.2 数据传输

在建立连接后,就可以进行数据的发送和接收。

发送数据:使用send()函数发送数据。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

buf是要发送的数据缓冲区,len是数据长度,flags通常设为 0。例如:

char *msg = "Hello, server!";
ssize_t bytes_sent = send(connfd, msg, strlen(msg), 0);
if (bytes_sent == -1) {
    perror("send failed");
    close(connfd);
    return -1;
}

接收数据:使用recv()函数接收数据。其原型如下:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

buf是用于接收数据的缓冲区,len是缓冲区的长度,flags通常设为 0。例如:

char buffer[1024];
ssize_t bytes_received = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received == -1) {
    perror("recv failed");
    close(connfd);
    return -1;
}
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);

三、构建简单的 HTTP 服务器

了解了 HTTP 协议和 Linux 网络编程基础后,我们可以开始构建一个简单的 HTTP 服务器。

3.1 服务器框架

首先,我们创建一个 TCP 套接字并绑定到指定端口,然后开始监听连接请求。当有客户端连接时,接受连接并处理客户端的 HTTP 请求。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 8080
#define MAX_CLIENTS 10

void handle_request(int connfd) {
    char buffer[1024] = {0};
    ssize_t bytes_read = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(connfd);
        return;
    }
    buffer[bytes_read] = '\0';
    // 这里开始解析 HTTP 请求
    // 目前简单处理,直接返回一个固定的响应
    const char *response = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body><h1>Hello, World!</h1></body></html>";
    ssize_t bytes_sent = send(connfd, response, strlen(response), 0);
    if (bytes_sent == -1) {
        perror("send failed");
    }
    close(connfd);
}

int main() {
    int sockfd, connfd;
    struct sockaddr_in servaddr, cliaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        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)) == -1) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    if (listen(sockfd, MAX_CLIENTS) == -1) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        socklen_t len = sizeof(cliaddr);
        connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
        if (connfd == -1) {
            perror("accept failed");
            continue;
        }
        handle_request(connfd);
    }
    close(sockfd);
    return 0;
}

3.2 解析 HTTP 请求

在上述代码的handle_request函数中,我们目前只是简单返回一个固定响应。现在我们来详细解析 HTTP 请求。

HTTP 请求的第一行是请求行,我们可以使用strtok函数将其按空格分割,获取请求方法、请求 URI 和协议版本。例如:

void handle_request(int connfd) {
    char buffer[1024] = {0};
    ssize_t bytes_read = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(connfd);
        return;
    }
    buffer[bytes_read] = '\0';

    char *request_line = strtok(buffer, "\r\n");
    if (request_line) {
        char *method = strtok(request_line, " ");
        char *uri = strtok(NULL, " ");
        char *version = strtok(NULL, " ");
        printf("Method: %s, URI: %s, Version: %s\n", method, uri, version);
    }

    // 解析请求头
    char *header;
    while ((header = strtok(NULL, "\r\n")) && strlen(header) > 0) {
        printf("Header: %s\n", header);
    }

    // 这里处理请求体,对于简单示例,暂不详细处理
    // 目前简单处理,直接返回一个固定的响应
    const char *response = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body><h1>Hello, World!</h1></body></html>";
    ssize_t bytes_sent = send(connfd, response, strlen(response), 0);
    if (bytes_sent == -1) {
        perror("send failed");
    }
    close(connfd);
}

3.3 根据请求返回不同响应

根据请求的 URI,我们可以返回不同的内容。例如,如果请求的是/index.html,我们返回一个 HTML 页面;如果请求的是/api/data,我们返回 JSON 数据。

void handle_request(int connfd) {
    char buffer[1024] = {0};
    ssize_t bytes_read = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(connfd);
        return;
    }
    buffer[bytes_read] = '\0';

    char *request_line = strtok(buffer, "\r\n");
    if (request_line) {
        char *method = strtok(request_line, " ");
        char *uri = strtok(NULL, " ");
        char *version = strtok(NULL, " ");
        printf("Method: %s, URI: %s, Version: %s\n", method, uri, version);
    }

    // 解析请求头
    char *header;
    while ((header = strtok(NULL, "\r\n")) && strlen(header) > 0) {
        printf("Header: %s\n", header);
    }

    const char *response;
    if (strcmp(uri, "/index.html") == 0) {
        response = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body><h1>Index Page</h1></body></html>";
    } else if (strcmp(uri, "/api/data") == 0) {
        response = "HTTP/1.1 200 OK\r\nContent - Type: application/json\r\n\r\n{\"message\":\"This is API data\"}";
    } else {
        response = "HTTP/1.1 404 Not Found\r\nContent - Type: text/html\r\n\r\n<html><body><h1>404 Not Found</h1></body></html>";
    }

    ssize_t bytes_sent = send(connfd, response, strlen(response), 0);
    if (bytes_sent == -1) {
        perror("send failed");
    }
    close(connfd);
}

四、构建 HTTP 客户端

了解了服务器端的实现,我们再来构建一个 HTTP 客户端。

4.1 客户端框架

客户端首先创建一个 TCP 套接字,然后连接到服务器,发送 HTTP 请求并接收服务器的响应。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080

int main() {
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    const char *request = "GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n";
    ssize_t bytes_sent = send(sockfd, request, strlen(request), 0);
    if (bytes_sent == -1) {
        perror("send failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    ssize_t bytes_read = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    buffer[bytes_read] = '\0';
    printf("Received:\n%s\n", buffer);

    close(sockfd);
    return 0;
}

4.2 处理响应

上述代码中,我们只是简单打印了接收到的服务器响应。实际上,我们需要像服务器解析请求一样,解析服务器的响应。

我们可以先解析状态行,获取状态码和状态消息,然后解析响应头,最后获取响应体。例如:

int main() {
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    const char *request = "GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n";
    ssize_t bytes_sent = send(sockfd, request, strlen(request), 0);
    if (bytes_sent == -1) {
        perror("send failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    ssize_t bytes_read = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    buffer[bytes_read] = '\0';

    char *status_line = strtok(buffer, "\r\n");
    if (status_line) {
        char *version = strtok(status_line, " ");
        char *status_code = strtok(NULL, " ");
        char *status_msg = strtok(NULL, " ");
        printf("Version: %s, Status Code: %s, Status Msg: %s\n", version, status_code, status_msg);
    }

    char *header;
    while ((header = strtok(NULL, "\r\n")) && strlen(header) > 0) {
        printf("Header: %s\n", header);
    }

    // 找到响应体开始位置
    char *body_start = strstr(buffer, "\r\n\r\n");
    if (body_start) {
        body_start += 4;
        printf("Body: %s\n", body_start);
    }

    close(sockfd);
    return 0;
}

五、处理复杂的 HTTP 场景

5.1 处理 POST 请求

在服务器端处理 POST 请求时,我们需要从请求体中获取数据。例如,假设 POST 请求体是表单数据,格式为key1 = value1&key2 = value2,我们可以解析这些数据。

void handle_request(int connfd) {
    char buffer[1024] = {0};
    ssize_t bytes_read = recv(connfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(connfd);
        return;
    }
    buffer[bytes_read] = '\0';

    char *request_line = strtok(buffer, "\r\n");
    if (request_line) {
        char *method = strtok(request_line, " ");
        char *uri = strtok(NULL, " ");
        char *version = strtok(NULL, " ");
        printf("Method: %s, URI: %s, Version: %s\n", method, uri, version);
    }

    // 解析请求头
    char *header;
    while ((header = strtok(NULL, "\r\n")) && strlen(header) > 0) {
        printf("Header: %s\n", header);
    }

    // 解析请求体
    char *content_length_header = strstr(buffer, "Content - Length:");
    if (content_length_header) {
        int content_length = atoi(content_length_header + strlen("Content - Length:") + 1);
        char *body = buffer + strlen(buffer) - content_length;
        printf("Body: %s\n", body);
        // 进一步解析表单数据
        char *token = strtok(body, "&");
        while (token) {
            char *key = strtok(token, "=");
            char *value = strtok(NULL, "=");
            printf("Key: %s, Value: %s\n", key, value);
            token = strtok(NULL, "&");
        }
    }

    const char *response = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body><h1>POST Request Handled</h1></body></html>";
    ssize_t bytes_sent = send(connfd, response, strlen(response), 0);
    if (bytes_sent == -1) {
        perror("send failed");
    }
    close(connfd);
}

5.2 处理 HTTP 重定向

当服务器返回 301 或 302 状态码时,客户端需要根据Location头字段的指示,重新发送请求到新的 URL。

在客户端代码中,我们可以这样处理:

int main() {
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    const char *request = "GET /redirect HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n";
    ssize_t bytes_sent = send(sockfd, request, strlen(request), 0);
    if (bytes_sent == -1) {
        perror("send failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[1024] = {0};
    ssize_t bytes_read = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read == -1) {
        perror("recv failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    buffer[bytes_read] = '\0';

    char *status_line = strtok(buffer, "\r\n");
    if (status_line) {
        char *version = strtok(status_line, " ");
        char *status_code = strtok(NULL, " ");
        if (strcmp(status_code, "301") == 0 || strcmp(status_code, "302") == 0) {
            char *location_header = strstr(buffer, "Location:");
            if (location_header) {
                char *new_url = location_header + strlen("Location:") + 1;
                // 这里简单处理,假设新 URL 还是在同一服务器
                char new_request[1024];
                snprintf(new_request, sizeof(new_request), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n", new_url);
                close(sockfd);
                sockfd = socket(AF_INET, SOCK_STREAM, 0);
                if (sockfd == -1) {
                    perror("socket creation failed");
                    exit(EXIT_FAILURE);
                }
                if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
                    perror("connect failed");
                    close(sockfd);
                    exit(EXIT_FAILURE);
                }
                bytes_sent = send(sockfd, new_request, strlen(new_request), 0);
                if (bytes_sent == -1) {
                    perror("send failed");
                    close(sockfd);
                    exit(EXIT_FAILURE);
                }
                bytes_read = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
                if (bytes_read == -1) {
                    perror("recv failed");
                    close(sockfd);
                    exit(EXIT_FAILURE);
                }
                buffer[bytes_read] = '\0';
                printf("Final Response:\n%s\n", buffer);
            }
        } else {
            // 处理其他状态码的响应
            //...
        }
    }

    close(sockfd);
    return 0;
}

通过以上步骤,我们在 Linux 环境下使用 C 语言实现了基本的 HTTP 协议编程,涵盖了服务器和客户端的开发,以及处理一些复杂的 HTTP 场景。这些知识和代码示例为进一步深入 HTTP 协议编程提供了坚实的基础。在实际应用中,还需要考虑安全性、性能优化等更多方面的问题。例如,可以使用 HTTPS 协议来保证数据传输的安全性,通过多线程或异步编程来提高服务器的并发处理能力等。