86、HTTP代理:正向代理、反向代理、代理服务器实现

代理这个东西,说白了就是中间人。你在客户端和服务器之间插一个节点,让流量先经过它。我刚开始接触网络编程时,总觉得代理是“多此一举”——直接连不好吗?后来在项目中踩过几次坑,才明白代理这玩意儿,用好了能解决大问题。

今天咱们就聊聊HTTP代理。我会从正向代理和反向代理的区别讲起,再手把手带你实现一个简单的代理服务器。嗯,代码我会给C语言的,毕竟咱们这个课程就是C语言网络编程嘛。

正向代理 vs 反向代理:一张图说清楚

先别急着写代码。咱们得先搞清楚,正向代理和反向代理到底有啥区别。我见过不少新手,甚至一些工作两三年的朋友,都搞混过这两个概念。

简单来说:

  • 正向代理:代理在客户端一侧。客户端知道自己连的是代理,代理帮它去请求目标服务器。典型场景:翻墙、公司内网访问外网。
  • 反向代理:代理在服务器一侧。客户端不知道自己连的是代理,它以为自己在直接访问目标服务器。典型场景:负载均衡、隐藏后端服务器。

我用一张SVG图来展示这个区别,你一看就明白。

正向代理 vs 反向代理 正向代理 客户端 正向代理 目标服务器 请求 转发请求 返回响应 转发响应 反向代理 客户端 反向代理 后端服务器1 后端服务器2 请求(以为直接连服务器) 分发 响应 返回响应 客户端 代理 服务器

看到了吧?正向代理是“替你去要”,反向代理是“替它给”。方向不同,用途也完全不同。

正向代理的实现思路

咱们先来实现一个简单的正向代理。说白了,正向代理就是一个“请求转发器”。客户端把请求发给代理,代理再原封不动地转发给目标服务器,然后把响应返回给客户端。

我个人习惯用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代理的内容就聊到这儿。代码虽然简单,但核心逻辑都在里面了。你可以在本地跑一跑,试试用浏览器配置正向代理,看看请求是怎么被转发的。嗯,动手实践才是最好的学习方式。

公众号:蓝海资料掘金营,微信deep3321