代理服务器的核心思想是作为一个“中间人”,客户端连接到代理服务器,然后代理服务器代替客户端去访问真正的目标服务器,并将目标服务器的响应返回给客户端。

这个过程可以分解为以下几个步骤:
- 监听:代理服务器在一个指定的端口上监听客户端的连接请求。
- 接受连接:当有客户端连接时,服务器接受这个连接,并创建一个新的 socket 与客户端通信。
- 解析请求:从客户端读取数据(通常是 HTTP 请求),并解析出目标服务器的地址(域名和端口)。
- 连接目标:代理服务器使用解析出的地址,与真正的目标服务器建立一个新的连接。
- 数据转发:
- 将客户端的原始请求(去掉一些头部信息,如
Connection: keep-alive)转发给目标服务器。 - 将目标服务器返回的响应数据,原封不动地转发回给客户端。
- 将客户端的原始请求(去掉一些头部信息,如
- 关闭连接:当客户端或任何一端关闭连接时,代理服务器需要关闭相关的 socket,释放资源。
一个简单的 HTTP 代理服务器示例
下面是一个用 C 语言实现的、功能完整的 HTTP 代理服务器代码,它支持 GET 和 CONNECT(用于 HTTPS 代理)方法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define BUFFER_SIZE 4096
#define PROXY_PORT 8888
// 函数声明
void handle_client(int client_socket);
void forward_data(int source_socket, int dest_socket);
int main() {
int server_fd, client_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
// 1. 创建 socket 文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 socket 选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
address.sin_port = htons(PROXY_PORT); // 设置代理端口
// 2. 绑定 socket 到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 开始监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Proxy server listening on port %d...\n", PROXY_PORT);
// 4. 主循环,接受客户端连接
while ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
printf("Accepted connection from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 在实际应用中,这里应该创建一个子线程来处理客户端,
// 以避免 accept() 阻塞其他连接,为简化示例,我们直接处理。
handle_client(client_socket);
close(client_socket);
printf("Connection closed.\n");
}
if (client_socket < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
close(server_fd);
return 0;
}
// 处理单个客户端连接
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
// 从客户端读取请求数据
bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
if (bytes_read <= 0) {
printf("Client closed connection or read error.\n");
return;
}
buffer[bytes_read] = '\0';
printf("--- Received Request ---\n%s\n--- End of Request ---\n", buffer);
// 解析请求,获取目标主机和端口
char method[16], url[256], protocol[16];
sscanf(buffer, "%15s %255s %15s", method, url, protocol);
char host[256];
int port = 80; // 默认 HTTP 端口
// 处理 CONNECT 方法 (HTTPS 代理)
if (strcmp(method, "CONNECT") == 0) {
sscanf(url, "%255[^:]:%d", host, &port);
printf("HTTPS Tunnel to %s:%d\n", host, port);
// 向客户端发送 200 Connection Established 响应
const char *response = "HTTP/1.1 200 Connection Established\r\n\r\n";
send(client_socket, response, strlen(response), 0);
// 建立与目标服务器的 TCP 隧道,直接转发数据
int target_socket = connect_to_host(host, port);
if (target_socket < 0) {
perror("Failed to connect to target for HTTPS");
return;
}
printf("Tunnel established. Forwarding data...\n");
forward_data(client_socket, target_socket);
close(target_socket);
return;
}
// 处理普通 HTTP 请求 (GET, POST, etc.)
char host_header[256];
char *host_line = strstr(buffer, "Host: ");
if (host_line) {
sscanf(host_line, "Host: %255[^\r\n]", host);
// 尝试从 Host 头部获取端口
char *port_str = strchr(host, ':');
if (port_str) {
*port_str = '\0'; // 分割主机名和端口
port = atoi(port_str + 1);
}
} else {
printf("Error: Host header not found in request.\n");
return;
}
printf("Forwarding HTTP request to %s:%d\n", host, port);
// 建立与目标服务器的连接
int target_socket = connect_to_host(host, port);
if (target_socket < 0) {
perror("Failed to connect to target for HTTP");
return;
}
// 修改原始请求,移除 Connection 和 Proxy-Connection 头部
char *modified_request = modify_http_request(buffer);
send(target_socket, modified_request, strlen(modified_request), 0);
free(modified_request);
// 转发目标服务器的响应回客户端
forward_data(target_socket, client_socket);
close(target_socket);
}
// 辅助函数:连接到目标主机
int connect_to_host(const char *host, int port) {
struct hostent *host_entry;
struct sockaddr_in server_addr;
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return -1;
}
host_entry = gethostbyname(host);
if (!host_entry) {
fprintf(stderr, "gethostbyname failed for host: %s\n", host);
close(sock);
return -1;
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
memcpy(&server_addr.sin_addr, host_entry->h_addr, host_entry->h_length);
server_addr.sin_port = htons(port);
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock);
return -1;
}
return sock;
}
// 辅助函数:修改 HTTP 请求头部
char* modify_http_request(const char* original_request) {
// 简单实现:移除 "Connection: keep-alive" 和 "Proxy-Connection: keep-alive"
// 注意:这只是一个简化示例,实际解析 HTTP 头部更复杂
char *modified = strdup(original_request);
char *line = modified;
char *new_request = malloc(BUFFER_SIZE);
new_request[0] = '\0';
while (line != NULL) {
char *next_line = strchr(line, '\n');
if (next_line) *next_line = '\0';
// 跳过不需要的头部
if (strncmp(line, "Connection:", 11) == 0 ||
strncmp(line, "Proxy-Connection:", 17) == 0) {
// 跳过这一行
} else {
strcat(new_request, line);
strcat(new_request, "\r\n");
}
line = (next_line) ? next_line + 1 : NULL;
}
strcat(new_request, "\r\n"); // 添加空行,结束头部
free(modified);
return new_request;
}
// 辅助函数:双向转发数据
void forward_data(int source_socket, int dest_socket) {
fd_set read_fds;
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
while (1) {
FD_ZERO(&read_fds);
FD_SET(source_socket, &read_fds);
FD_SET(dest_socket, &read_fds);
// 使用 select 等待任一 socket 可读
if (select(dest_socket + 1, &read_fds, NULL, NULL, NULL) < 0) {
perror("select");
break;
}
// 检查 source_socket 是否可读
if (FD_ISSET(source_socket, &read_fds)) {
bytes_read = recv(source_socket, buffer, BUFFER_SIZE, 0);
if (bytes_read <= 0) {
// 连接关闭或出错
break;
}
send(dest_socket, buffer, bytes_read, 0);
}
// 检查 dest_socket 是否可读
if (FD_ISSET(dest_socket, &read_fds)) {
bytes_read = recv(dest_socket, buffer, BUFFER_SIZE, 0);
if (bytes_read <= 0) {
// 连接关闭或出错
break;
}
send(source_socket, buffer, bytes_read, 0);
}
}
}
如何编译和运行
-
保存代码:将上面的代码保存为
proxy_server.c。 -
编译:打开终端,使用
gcc进行编译。
(图片来源网络,侵删)gcc -o proxy_server proxy_server.c
-
运行:以普通用户身份运行即可(不需要 root 权限,除非你使用小于 1024 的端口)。
./proxy_server
你会看到输出:
Proxy server listening on port 8888... -
配置客户端:现在你需要将你的浏览器或其他网络应用的代理设置为
0.0.1(或你的服务器 IP)和8888。 -
测试:打开浏览器,访问任何网站(如
http://example.com),代理服务器的终端会打印出连接信息和转发的请求数据。
(图片来源网络,侵删)
代码详解
主函数 (main)
socket(): 创建一个 TCP socket。setsockopt(): 设置SO_REUSEADDR选项,允许服务器快速重启,避免 "Address already in use" 错误。bind(): 将 socket 绑定到本机的8888端口。listen(): 开始监听传入的连接,3是最大待处理连接数。accept(): 阻塞等待客户端连接,当有连接时,它返回一个新的client_socket,用于与该客户端通信。handle_client(): 将处理客户端连接的任务交给一个单独的函数。close(): 关闭 socket。
客户端处理 (handle_client)
这是代理的核心逻辑。
recv(): 从客户端读取完整的 HTTP 请求。sscanf(): 解析请求行,获取method(如GET,CONNECT)、url和protocol。CONNECT方法处理 (HTTPS):CONNECT example.com:443 HTTP/1.1这种请求表示客户端想建立一个到example.com:443的隧道,用于 HTTPS。- 代理服务器不关心内容,只需与目标服务器建立 TCP 连接。
- 成功后,代理服务器向客户端返回
HTTP/1.1 200 Connection Established,表示隧道已建立。 - 之后,所有从客户端收到的数据都会被原封不动地转发到目标服务器,反之亦然,这就是“TCP 隧道”。
- 普通 HTTP 请求处理 (
GET,POST等):- 从
Host头部解析出目标主机名和端口。 - 调用
connect_to_host()与目标服务器建立连接。 - 调用
modify_http_request()清理原始请求(主要是移除代理相关的头部)。 - 将修改后的请求发送给目标服务器。
- 调用
forward_data()将目标服务器的响应转发回客户端。
- 从
辅助函数
connect_to_host(): 封装了通过主机名获取 IP 地址并建立 TCP 连接的过程。modify_http_request(): 一个简化的 HTTP 请求修改器,真正的代理需要更健壮的头部解析和修改逻辑,例如处理Content-Length、Transfer-Encoding等。forward_data(): 使用select系统调用来高效地双向转发数据,它会同时监视两个 socket,当任何一个有数据可读时,就读取并发送给另一个,当一端关闭连接时,循环结束。
进一步改进的方向
这个示例是一个很好的起点,但一个生产级的代理服务器还需要考虑:
- 多线程/多进程:当前的实现是单线程的,
accept()之后会阻塞处理一个客户端,无法处理其他并发请求,应该使用pthread或fork为每个客户端创建一个新的处理线程/进程。 - I/O 多路复用:对于高并发,使用
epoll(Linux) 或kqueue(BSD) 比select更高效。 - 更健壮的 HTTP 解析:当前使用
sscanf和字符串查找来解析 HTTP 请求非常脆弱,应该使用一个真正的 HTTP 解析库(如libcurl的内部组件)或自己编写更健壮的解析器。 - 缓冲区管理:处理大数据传输时,需要小心处理缓冲区溢出和不完整的消息。
- 身份验证:支持用户名和密码的代理认证。
- 功能增强:支持
FTP、SOCKS5等其他代理协议,或者实现内容缓存、访问控制列表等功能。
