Linux C语言HTTP协议编程实践
一、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 协议来保证数据传输的安全性,通过多线程或异步编程来提高服务器的并发处理能力等。