88、反向代理服务器:实现简单的反向代理、请求转发、缓存

说实话,反向代理这东西,刚入行时我总觉得它很神秘。不就是个中间人吗?后来在线上环境被流量教做人之后,才明白它有多重要。今天咱们就手写一个极简版的反向代理,把请求转发、缓存这些核心逻辑拆开揉碎了讲清楚。

反向代理到底在干什么?

你想想看,用户访问你的网站,实际上请求先落到反向代理上,再由它转发给后端真实服务器。用户根本不知道后端有几台机器、IP是什么。这就是反向代理的核心——隐藏后端,统一入口

我在项目中遇到过最典型的场景:一个老系统只支持HTTP,但前端非要HTTPS。怎么办?加一层反向代理,让它做SSL卸载,后端继续跑HTTP。你看,问题就这么解决了。

核心职责:
  • 接收客户端请求
  • 根据规则转发给后端
  • 缓存静态资源或响应结果
  • 负载均衡(本例简化,只做单点转发)

先画个图,理清流程

下面这张图展示了最简单的反向代理工作流。客户端、代理、后端三者之间的关系一目了然。

客户端 反向代理 后端服务器 请求 转发 响应 返回 缓存区(内存)

嗯,流程不复杂。客户端发请求给代理,代理检查缓存,有就直接返回;没有就转发给后端,拿到响应后存一份到缓存,再返回给客户端。

代码实现:一个能跑的反向代理

我用C语言写了一个精简版,去掉了很多工程细节,但核心逻辑都在。你拿过去改改就能用。

// reverse_proxy.c —— 极简反向代理
#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 <fcntl.h>
#include <time.h>

#define PROXY_PORT 8080
#define BACKEND_IP "127.0.0.1"
#define BACKEND_PORT 3000
#define BUFFER_SIZE 4096
#define CACHE_SIZE 10

// 缓存条目
typedef struct {
    char url[256];
    char response[BUFFER_SIZE];
    int resp_len;
    time_t timestamp;
    int valid;
} CacheEntry;

CacheEntry cache[CACHE_SIZE];
int cache_count = 0;

// 简单哈希,用于缓存查找
int cache_find(const char *url) {
    for (int i = 0; i < CACHE_SIZE; i++) {
        if (cache[i].valid && strcmp(cache[i].url, url) == 0) {
            // 60秒过期
            if (time(NULL) - cache[i].timestamp < 60) {
                return i;
            } else {
                cache[i].valid = 0;
                return -1;
            }
        }
    }
    return -1;
}

void cache_store(const char *url, const char *data, int len) {
    int idx = cache_count % CACHE_SIZE;
    cache[idx].valid = 1;
    strncpy(cache[idx].url, url, sizeof(cache[idx].url)-1);
    memcpy(cache[idx].response, data, len);
    cache[idx].resp_len = len;
    cache[idx].timestamp = time(NULL);
    cache_count++;
}

// 转发请求到后端
int forward_to_backend(const char *request, char *response, int *resp_len) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) return -1;

    struct sockaddr_in backend_addr;
    backend_addr.sin_family = AF_INET;
    backend_addr.sin_port = htons(BACKEND_PORT);
    inet_pton(AF_INET, BACKEND_IP, &backend_addr.sin_addr);

    if (connect(sock, (struct sockaddr*)&backend_addr, sizeof(backend_addr)) < 0) {
        close(sock);
        return -1;
    }

    send(sock, request, strlen(request), 0);
    *resp_len = recv(sock, response, BUFFER_SIZE-1, 0);
    response[*resp_len] = '\0';
    close(sock);
    return 0;
}

int main() {
    int proxy_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in proxy_addr;
    proxy_addr.sin_family = AF_INET;
    proxy_addr.sin_port = htons(PROXY_PORT);
    proxy_addr.sin_addr.s_addr = INADDR_ANY;

    bind(proxy_fd, (struct sockaddr*)&proxy_addr, sizeof(proxy_addr));
    listen(proxy_fd, 5);
    printf("反向代理已启动,监听端口 %d\n", PROXY_PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);
        int client_fd = accept(proxy_fd, (struct sockaddr*)&client_addr, &addr_len);

        char request[BUFFER_SIZE] = {0};
        recv(client_fd, request, BUFFER_SIZE-1, 0);

        // 提取URL(简化:取第一行)
        char url[256] = {0};
        sscanf(request, "GET %s", url);
        if (strlen(url) == 0) {
            close(client_fd);
            continue;
        }

        // 查缓存
        int cache_idx = cache_find(url);
        if (cache_idx >= 0) {
            printf("[缓存命中] %s\n", url);
            send(client_fd, cache[cache_idx].response, cache[cache_idx].resp_len, 0);
            close(client_fd);
            continue;
        }

        // 转发
        char response[BUFFER_SIZE] = {0};
        int resp_len = 0;
        if (forward_to_backend(request, response, &resp_len) == 0) {
            printf("[转发] %s -> 后端\n", url);
            cache_store(url, response, resp_len);
            send(client_fd, response, resp_len, 0);
        } else {
            char *err = "HTTP/1.1 502 Bad Gateway\r\n\r\n";
            send(client_fd, err, strlen(err), 0);
        }
        close(client_fd);
    }
    close(proxy_fd);
    return 0;
}

这段代码的几个关键点

我个人习惯把缓存和转发逻辑拆开,这样后期加负载均衡也方便。你看代码里 forward_to_backend 只负责收发,cache_findcache_store 只管理缓存。职责单一,不容易出bug。

  • 缓存过期策略:我用了最简单的60秒过期。实际生产环境会用LRU或者LFU,但原理一样——空间换时间。
  • 请求解析:这里只取了GET后面的URL路径。真实场景要解析Host头、Cookie等,但核心思路不变。
  • 错误处理:后端挂了就返回502。我曾经在生产环境遇到过代理没做超时控制,结果后端慢,代理线程全卡死。所以一定要加超时,代码里没写,但你要记住。
小技巧:调试时可以把后端换成Python的SimpleHTTPServer,一行命令就能启动。这样你就能快速验证代理是否正常工作。

缓存命中率怎么提上去?

说白了,缓存不是所有请求都适合。静态资源(图片、CSS、JS)缓存效果最好。动态接口(登录、下单)千万别乱缓存,否则用户数据就串了。

我建议你在代理层加一个规则:URL后缀匹配 .jpg、.css、.js 的才缓存。其他请求直接透传。代码里加个判断就行,不复杂。

资源类型是否缓存缓存时间
HTML页面视情况短(0-5分钟)
CSS/JS长(1小时以上)
图片/字体很长(1天以上)
API接口一般不缓存

避坑指南

我曾经犯过一个低级错误:缓存key只用了URL路径,没考虑查询参数。结果 /api/data?user=1/api/data?user=2 返回了同一个缓存,用户数据全乱了。后来我把完整请求URI(包括query string)作为key,才解决。

另外,缓存一定要设置上限。我见过有人用链表存缓存,结果内存暴涨,OOM杀进程。用固定大小的数组或者LRU链表,加上淘汰机制,才是正道。

注意:上面的代码只是教学演示,没有处理并发。多线程环境下要对缓存加锁,或者用无锁队列。否则多个线程同时写缓存,数据就乱套了。

总结一下

反向代理的核心就三件事:接请求、查缓存、转发。你把这个模型吃透了,后面再看Nginx、HAProxy的配置,会发现它们都是在这个基础上加了很多高级功能——负载均衡、健康检查、动态路由等等。

嗯,今天的代码你拿回去跑一跑。先启动一个后端服务(随便什么语言都行),再启动这个代理,然后浏览器访问代理端口。看看请求是怎么被转发的,缓存是怎么生效的。动手试一遍,比看十遍文章都管用。


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