86、HTTP代理:正向代理、反向代理、代理服务器实现
代理这个东西,说白了就是中间人。你在客户端和服务器之间插一个节点,让流量先经过它。我刚开始接触网络编程时,总觉得代理是“多此一举”——直接连不好吗?后来在项目中踩过几次坑,才明白代理这玩意儿,用好了能解决大问题。
今天咱们就聊聊HTTP代理。我会从正向代理和反向代理的区别讲起,再手把手带你实现一个简单的代理服务器。嗯,代码我会给C语言的,毕竟咱们这个课程就是C语言网络编程嘛。
正向代理 vs 反向代理:一张图说清楚
先别急着写代码。咱们得先搞清楚,正向代理和反向代理到底有啥区别。我见过不少新手,甚至一些工作两三年的朋友,都搞混过这两个概念。
简单来说:
- 正向代理:代理在客户端一侧。客户端知道自己连的是代理,代理帮它去请求目标服务器。典型场景:翻墙、公司内网访问外网。
- 反向代理:代理在服务器一侧。客户端不知道自己连的是代理,它以为自己在直接访问目标服务器。典型场景:负载均衡、隐藏后端服务器。
我用一张SVG图来展示这个区别,你一看就明白。
看到了吧?正向代理是“替你去要”,反向代理是“替它给”。方向不同,用途也完全不同。
正向代理的实现思路
咱们先来实现一个简单的正向代理。说白了,正向代理就是一个“请求转发器”。客户端把请求发给代理,代理再原封不动地转发给目标服务器,然后把响应返回给客户端。
我个人习惯用C语言写这类东西,因为性能好,控制力强。下面是一个简化版的实现框架:
// 正向代理核心流程
// 1. 客户端连接代理服务器
// 2. 代理解析客户端请求,提取目标主机和端口
// 3. 代理连接目标服务器
// 4. 代理转发请求到目标服务器
// 5. 代理接收目标服务器的响应
// 6. 代理转发响应回客户端
#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
// 解析HTTP请求,提取Host字段
int parse_http_request(const char *request, char *host, int *port) {
// 查找 "Host:" 字段
const char *host_start = strstr(request, "Host: ");
if (host_start == NULL) return -1;
host_start += 6; // 跳过 "Host: "
const char *host_end = strstr(host_start, "\r\n");
if (host_end == NULL) return -1;
// 复制主机名
size_t len = host_end - host_start;
strncpy(host, host_start, len);
host[len] = '\0';
// 检查是否包含端口号
char *colon = strchr(host, ':');
if (colon) {
*colon = '\0';
*port = atoi(colon + 1);
} else {
*port = 80; // 默认HTTP端口
}
return 0;
}
// 转发数据:从src读到dest
void forward_data(int src_fd, int dest_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
while ((bytes_read = read(src_fd, buffer, BUFFER_SIZE)) > 0) {
write(dest_fd, buffer, bytes_read);
}
}
// 处理一个客户端连接
void handle_client(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
// 读取客户端请求
bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
if (bytes_read <= 0) {
close(client_fd);
return;
}
buffer[bytes_read] = '\0';
// 解析请求,获取目标主机和端口
char host[256];
int port;
if (parse_http_request(buffer, host, &port) != 0) {
const char *error = "HTTP/1.1 400 Bad Request\r\n\r\n";
write(client_fd, error, strlen(error));
close(client_fd);
return;
}
// 连接目标服务器
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
struct hostent *server = gethostbyname(host);
if (server == NULL) {
close(client_fd);
return;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
memcpy(&server_addr.sin_addr.s_addr, server->h_addr, server->h_length);
server_addr.sin_port = htons(port);
if (connect(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
close(client_fd);
close(server_fd);
return;
}
// 转发请求到目标服务器
write(server_fd, buffer, bytes_read);
// 转发响应回客户端
forward_data(server_fd, client_fd);
close(server_fd);
close(client_fd);
}
小提示:上面的代码只处理了HTTP请求。如果是HTTPS,需要用CONNECT方法建立隧道。我在项目中遇到过这个问题,当时没注意,结果代理HTTPS网站全挂了。后来加了个CONNECT处理分支才搞定。
反向代理的实现思路
反向代理和正向代理正好反过来。客户端不知道代理的存在,它以为自己直接连的是目标服务器。反向代理收到请求后,根据规则把请求分发到后端服务器。
反向代理的核心价值在于:
- 负载均衡:把请求分散到多台后端服务器
- 安全隔离:隐藏后端服务器的真实IP
- 缓存加速:代理层可以缓存静态资源
- SSL终结:在代理层处理HTTPS,后端用HTTP通信
下面是一个简单的反向代理实现:
// 反向代理核心流程
// 1. 客户端连接反向代理(以为连的是目标服务器)
// 2. 反向代理根据规则选择后端服务器
// 3. 反向代理连接选中的后端服务器
// 4. 转发请求并返回响应
// 后端服务器列表
typedef struct {
char ip[16];
int port;
} backend_server;
backend_server backends[] = {
{"192.168.1.10", 8080},
{"192.168.1.11", 8080},
{"192.168.1.12", 8080}
};
int backend_count = 3;
int current_index = 0;
// 简单的轮询负载均衡
backend_server* select_backend() {
backend_server *selected = &backends[current_index];
current_index = (current_index + 1) % backend_count;
return selected;
}
// 处理反向代理请求
void handle_reverse_proxy(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
// 读取客户端请求
bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
if (bytes_read <= 0) {
close(client_fd);
return;
}
buffer[bytes_read] = '\0';
// 选择后端服务器
backend_server *backend = select_backend();
// 连接后端服务器
int backend_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in backend_addr;
memset(&backend_addr, 0, sizeof(backend_addr));
backend_addr.sin_family = AF_INET;
backend_addr.sin_addr.s_addr = inet_addr(backend->ip);
backend_addr.sin_port = htons(backend->port);
if (connect(backend_fd, (struct sockaddr *)&backend_addr, sizeof(backend_addr)) < 0) {
const char *error = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
write(client_fd, error, strlen(error));
close(client_fd);
return;
}
// 转发请求到后端
write(backend_fd, buffer, bytes_read);
// 转发后端响应回客户端
forward_data(backend_fd, client_fd);
close(backend_fd);
close(client_fd);
}
注意:上面的轮询算法是最简单的。生产环境中,我建议用加权轮询或最少连接数算法。我曾经在一个高并发项目里只用简单轮询,结果某台服务器配置低,直接被压垮了。后来改成加权轮询,问题就解决了。
代理服务器的完整实现
把上面两个功能合并,再加一个主循环,就是一个完整的代理服务器了。我习惯把正向和反向做成可配置的,启动时通过参数指定模式。
// 主函数:启动代理服务器
int main(int argc, char *argv[]) {
int proxy_fd, client_fd;
struct sockaddr_in proxy_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建监听socket
proxy_fd = socket(AF_INET, SOCK_STREAM, 0);
if (proxy_fd < 0) {
perror("socket");
exit(1);
}
// 设置地址复用
int opt = 1;
setsockopt(proxy_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 绑定端口
memset(&proxy_addr, 0, sizeof(proxy_addr));
proxy_addr.sin_family = AF_INET;
proxy_addr.sin_addr.s_addr = INADDR_ANY;
proxy_addr.sin_port = htons(8888); // 代理监听端口
if (bind(proxy_fd, (struct sockaddr *)&proxy_addr, sizeof(proxy_addr)) < 0) {
perror("bind");
exit(1);
}
// 开始监听
listen(proxy_fd, 10);
printf("代理服务器已启动,监听端口: 8888\n");
// 主循环
while (1) {
client_fd = accept(proxy_fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept");
continue;
}
// 这里根据配置选择正向或反向模式
// 正向代理:handle_client(client_fd);
// 反向代理:handle_reverse_proxy(client_fd);
// 为了演示,这里用正向代理
handle_client(client_fd);
}
close(proxy_fd);
return 0;
}
正向代理和反向代理的对比
| 特性 | 正向代理 | 反向代理 |
|---|---|---|
| 代理位置 | 客户端一侧 | 服务器一侧 |
| 客户端感知 | 知道代理存在 | 不知道代理存在 |
| 典型用途 | 翻墙、访问控制、缓存 | 负载均衡、安全防护、SSL终结 |
| 配置方式 | 客户端配置代理地址 | DNS指向代理地址 |
| 隐藏身份 | 隐藏客户端身份 | 隐藏服务器身份 |
避坑指南
做代理服务器,有几个坑我踩过,分享给你:
- 连接泄漏:每次处理完请求,一定要close掉socket。我曾经漏了一个close,结果服务器运行两天后文件描述符耗尽,所有新连接都失败。
- 超时处理:后端服务器可能挂掉,或者响应很慢。一定要给connect和read设置超时,否则一个慢请求会拖垮整个代理。
- 缓冲区大小:HTTP请求头可能很大,特别是带Cookie的时候。我建议缓冲区至少设8KB,或者用动态增长的方式。
- 并发处理:上面的例子是单线程的,一次只能处理一个请求。生产环境一定要用多线程或事件驱动模型(如epoll)。
核心要点:
- 正向代理:代理在客户端侧,客户端知道代理存在
- 反向代理:代理在服务器侧,客户端不知道代理存在
- 实现代理的核心就是“转发”:转发请求、转发响应
- 生产环境要考虑并发、超时、连接管理等问题
好了,关于HTTP代理的内容就聊到这儿。代码虽然简单,但核心逻辑都在里面了。你可以在本地跑一跑,试试用浏览器配置正向代理,看看请求是怎么被转发的。嗯,动手实践才是最好的学习方式。