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_find 和 cache_store 只管理缓存。职责单一,不容易出bug。
- 缓存过期策略:我用了最简单的60秒过期。实际生产环境会用LRU或者LFU,但原理一样——空间换时间。
- 请求解析:这里只取了GET后面的URL路径。真实场景要解析Host头、Cookie等,但核心思路不变。
- 错误处理:后端挂了就返回502。我曾经在生产环境遇到过代理没做超时控制,结果后端慢,代理线程全卡死。所以一定要加超时,代码里没写,但你要记住。
缓存命中率怎么提上去?
说白了,缓存不是所有请求都适合。静态资源(图片、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的配置,会发现它们都是在这个基础上加了很多高级功能——负载均衡、健康检查、动态路由等等。
嗯,今天的代码你拿回去跑一跑。先启动一个后端服务(随便什么语言都行),再启动这个代理,然后浏览器访问代理端口。看看请求是怎么被转发的,缓存是怎么生效的。动手试一遍,比看十遍文章都管用。