
1. 理解HTTP代理服务器的核心概念HTTP代理服务器就像网络世界的中转站它位于客户端比如你的浏览器和目标服务器之间。当你想访问某个网站时请求不会直接发送到目标网站而是先经过代理服务器由代理服务器帮你获取内容后再返回给你。这种架构带来几个明显的好处隐私保护目标网站看到的是代理服务器的IP而不是你的真实IP访问控制企业或学校可以用它限制某些网站的访问性能优化通过缓存机制减少重复请求的响应时间内容过滤过滤掉不安全或不合规的内容在Proxy Lab中我们要实现的代理需要处理三种核心功能基础请求转发Part 1多线程并发处理Part 2LRU缓存机制Part 32. 搭建基础代理服务器框架我们先从最简单的顺序代理开始。这个阶段的代理只需要能接收HTTP请求并正确转发即可不需要考虑性能优化。关键代码结构如下#include csapp.h #define MAX_CACHE_SIZE 1049000 #define MAX_OBJECT_SIZE 102400 void doit(int clientfd) { char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; char hostname[MAXLINE], path[MAXLINE], port[MAXLINE]; int serverfd; rio_t rio; // 读取请求行 Rio_readinitb(rio, clientfd); Rio_readlineb(rio, buf, MAXLINE); sscanf(buf, %s %s %s, method, uri, version); // 只处理GET请求 if (strcasecmp(method, GET)) { printf(501 Not Implemented: %s method\n, method); return; } // 解析URI获取目标服务器信息 parse_uri(uri, hostname, path, port); // 连接目标服务器 serverfd Open_clientfd(hostname, port); // 转发请求并返回响应 forward_request(serverfd, path, hostname); return_response(serverfd, clientfd); Close(serverfd); }URI解析是这里的重点我们需要从类似http://www.example.com:8080/path/to/resource的URI中提取主机名www.example.com端口8080默认80路径/path/to/resource解析函数可以这样实现void parse_uri(char *uri, char *hostname, char *path, char *port) { char *ptr strstr(uri, //); ptr ptr ? ptr 2 : uri; // 跳过http:// char *port_ptr strchr(ptr, :); char *path_ptr strchr(ptr, /); if (port_ptr (!path_ptr || port_ptr path_ptr)) { // 处理显式端口号情况 *port_ptr \0; strcpy(hostname, ptr); *port_ptr :; sscanf(port_ptr 1, %[^/], port); } else { // 默认80端口 strcpy(port, 80); if (path_ptr) { *path_ptr \0; strcpy(hostname, ptr); *path_ptr /; } else { strcpy(hostname, ptr); strcpy(path, /); } } if (path_ptr) strcpy(path, path_ptr); else strcpy(path, /); }3. 实现多线程并发处理单线程代理性能太低我们需要引入多线程。这里采用经典的每个连接一个线程模型但要注意线程安全和资源管理。关键修改点在main函数int main(int argc, char **argv) { int listenfd, *connfd; char hostname[MAXLINE], port[MAXLINE]; socklen_t clientlen; struct sockaddr_storage clientaddr; pthread_t tid; if (argc ! 2) { fprintf(stderr, usage: %s port\n, argv[0]); exit(1); } listenfd Open_listenfd(argv[1]); while (1) { clientlen sizeof(clientaddr); connfd Malloc(sizeof(int)); // 避免竞争 *connfd Accept(listenfd, (SA *)clientaddr, clientlen); Getnameinfo((SA *)clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0); printf(Accepted connection from (%s, %s)\n, hostname, port); Pthread_create(tid, NULL, thread, connfd); } } void *thread(void *vargp) { int connfd *((int *)vargp); Pthread_detach(pthread_self()); Free(vargp); doit(connfd); Close(connfd); return NULL; }这里有几个关键点需要注意使用Malloc动态分配connfd避免线程间共享变量线程立即detach自己这样不需要主线程join记得释放动态分配的内存和关闭文件描述符4. 实现LRU缓存机制缓存是提升代理性能的关键。我们采用LRU最近最少使用策略当缓存满时淘汰最久未使用的对象。缓存数据结构设计typedef struct { char url[MAXLINE]; // 缓存键 char content[MAX_OBJECT_SIZE]; // 缓存内容 size_t size; // 内容大小 time_t last_used; // 最后使用时间 sem_t mutex; // 保护该缓存项的锁 } CacheItem; typedef struct { CacheItem items[MAX_CACHE_ITEM]; // 缓存项数组 int count; // 当前缓存项数 sem_t mutex; // 保护整个缓存的锁 } Cache;缓存查找和更新逻辑int find_cache(Cache *cache, const char *url, char *content, size_t *size) { P(cache-mutex); int found -1; for (int i 0; i cache-count; i) { if (strcmp(cache-items[i].url, url) 0) { P(cache-items[i].mutex); memcpy(content, cache-items[i].content, cache-items[i].size); *size cache-items[i].size; cache-items[i].last_used time(NULL); V(cache-items[i].mutex); found i; break; } } V(cache-mutex); return found; } void add_cache(Cache *cache, const char *url, const char *content, size_t size) { if (size MAX_OBJECT_SIZE) return; P(cache-mutex); // 查找空位或LRU项 int slot -1, lru 0; time_t oldest time(NULL); for (int i 0; i MAX_CACHE_ITEM; i) { if (i cache-count) { if (cache-items[i].last_used oldest) { oldest cache-items[i].last_used; lru i; } } else if (slot -1) { slot i; } } if (slot -1) { // 缓存已满 slot lru; cache-total_size - cache-items[slot].size; } // 更新缓存 P(cache-items[slot].mutex); strcpy(cache-items[slot].url, url); memcpy(cache-items[slot].content, content, size); cache-items[slot].size size; cache-items[slot].last_used time(NULL); cache-total_size size; V(cache-items[slot].mutex); if (slot cache-count) cache-count; V(cache-mutex); }5. 线程安全的读写锁实现为了优化缓存性能我们需要实现读写锁允许多个线程同时读缓存但写缓存时需要独占访问。读写锁数据结构typedef struct { sem_t lock; // 保护readcnt的锁 sem_t writelock; // 写锁 int readcnt; // 当前读者数量 } rwlock_t;对应的操作void rwlock_init(rwlock_t *rw) { Sem_init(rw-lock, 0, 1); Sem_init(rw-writelock, 0, 1); rw-readcnt 0; } void rwlock_acquire_read(rwlock_t *rw) { P(rw-lock); rw-readcnt; if (rw-readcnt 1) // 第一个读者获取写锁 P(rw-writelock); V(rw-lock); } void rwlock_release_read(rwlock_t *rw) { P(rw-lock); rw-readcnt--; if (rw-readcnt 0) // 最后一个读者释放写锁 V(rw-writelock); V(rw-lock); } void rwlock_acquire_write(rwlock_t *rw) { P(rw-writelock); } void rwlock_release_write(rwlock_t *rw) { V(rw-writelock); }6. 完整代理工作流程整合所有组件后代理的工作流程如下监听指定端口等待客户端连接为每个连接创建新线程线程中读取客户端请求检查是否为GET请求解析URI获取目标服务器信息检查缓存命中则直接返回缓存内容未命中则继续连接目标服务器并转发请求接收目标服务器响应如果响应可缓存如成功响应且大小合适存入缓存将响应返回给客户端关闭连接关键函数doit的完整实现void doit(int clientfd) { char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; char hostname[MAXLINE], path[MAXLINE], port[MAXLINE]; char cache_key[MAXLINE]; int serverfd; rio_t rio_client, rio_server; // 读取请求行 Rio_readinitb(rio_client, clientfd); if (!Rio_readlineb(rio_client, buf, MAXLINE)) return; sscanf(buf, %s %s %s, method, uri, version); // 只处理GET请求 if (strcasecmp(method, GET)) { clienterror(clientfd, method, 501, Not Implemented); return; } // 解析URI parse_uri(uri, hostname, path, port); // 生成缓存键 snprintf(cache_key, MAXLINE, %s:%s%s, hostname, port, path); // 检查缓存 char cache_content[MAX_OBJECT_SIZE]; size_t cache_size; if (find_cache(global_cache, cache_key, cache_content, cache_size) 0) { Rio_writen(clientfd, cache_content, cache_size); return; } // 连接目标服务器 if ((serverfd Open_clientfd(hostname, port)) 0) { clienterror(clientfd, hostname, 404, Not Found); return; } // 转发请求 char request[MAXLINE]; snprintf(request, MAXLINE, GET %s HTTP/1.0\r\n, path); Rio_writen(serverfd, request, strlen(request)); snprintf(request, MAXLINE, Host: %s\r\n, hostname); Rio_writen(serverfd, request, strlen(request)); snprintf(request, MAXLINE, %s, user_agent_hdr); Rio_writen(serverfd, request, strlen(request)); snprintf(request, MAXLINE, Connection: close\r\n); Rio_writen(serverfd, request, strlen(request)); snprintf(request, MAXLINE, Proxy-Connection: close\r\n\r\n); Rio_writen(serverfd, request, strlen(request)); // 读取响应并可能缓存 char response[MAX_OBJECT_SIZE]; size_t total 0; Rio_readinitb(rio_server, serverfd); while ((n Rio_readlineb(rio_server, buf, MAXLINE)) ! 0) { if (total n MAX_OBJECT_SIZE) { memcpy(response total, buf, n); total n; } Rio_writen(clientfd, buf, n); } // 如果响应可缓存则存入 if (total MAX_OBJECT_SIZE) { add_cache(global_cache, cache_key, response, total); } Close(serverfd); }7. 常见问题与调试技巧在实现过程中我遇到了几个典型问题请求转发不完整忘记转发某些必要的请求头如Host头。解决方法是在转发请求时确保包含所有必要头信息。缓存污染多个线程同时修改缓存导致数据不一致。通过引入读写锁和细粒度的缓存项锁来解决。内存泄漏忘记释放动态分配的内存和关闭文件描述符。解决方法是严格检查每个malloc/free和open/close的配对。性能瓶颈最初的实现在高并发下性能不佳。通过以下优化提升性能使用更高效的哈希算法查找缓存实现零拷贝的数据转发优化锁的粒度调试时可以使用的技巧使用netcat手动发送HTTP请求测试代理用curl -v --proxy http://localhost:PORT URL测试添加详细的日志输出请求/响应流程使用Valgrind检查内存错误8. 进阶优化方向完成基础功能后还可以考虑以下优化支持HTTPS现代代理需要处理加密流量可以通过CONNECT方法实现。智能缓存策略根据响应头中的Cache-Control决定是否缓存而不仅基于大小。连接池复用后端连接减少TCP握手开销。负载均衡在多个后端服务器间分配请求。压缩传输对特定内容类型进行压缩减少带宽占用。访问控制实现基于IP或用户的访问限制。这些优化可以显著提升代理服务器的实用性但也会增加代码复杂度。建议在基础功能稳定后再逐步添加。