news 2026/7/16 17:41:39

《深入理解计算机系统》实验八Proxy Lab:从零构建一个支持缓存与并发的HTTP代理服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《深入理解计算机系统》实验八Proxy Lab:从零构建一个支持缓存与并发的HTTP代理服务器

1. 理解HTTP代理服务器的核心概念

HTTP代理服务器就像网络世界的中转站,它位于客户端(比如你的浏览器)和目标服务器之间。当你想访问某个网站时,请求不会直接发送到目标网站,而是先经过代理服务器,由代理服务器帮你获取内容后再返回给你。

这种架构带来几个明显的好处:

  • 隐私保护:目标网站看到的是代理服务器的IP,而不是你的真实IP
  • 访问控制:企业或学校可以用它限制某些网站的访问
  • 性能优化:通过缓存机制减少重复请求的响应时间
  • 内容过滤:过滤掉不安全或不合规的内容

在Proxy Lab中,我们要实现的代理需要处理三种核心功能:

  1. 基础请求转发(Part 1)
  2. 多线程并发处理(Part 2)
  3. LRU缓存机制(Part 3)

2. 搭建基础代理服务器框架

我们先从最简单的顺序代理开始。这个阶段的代理只需要能接收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; }

这里有几个关键点需要注意:

  1. 使用Malloc动态分配connfd,避免线程间共享变量
  2. 线程立即detach自己,这样不需要主线程join
  3. 记得释放动态分配的内存和关闭文件描述符

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. 完整代理工作流程

整合所有组件后,代理的工作流程如下:

  1. 监听指定端口,等待客户端连接
  2. 为每个连接创建新线程
  3. 线程中读取客户端请求:
    • 检查是否为GET请求
    • 解析URI获取目标服务器信息
  4. 检查缓存:
    • 命中则直接返回缓存内容
    • 未命中则继续
  5. 连接目标服务器并转发请求
  6. 接收目标服务器响应:
    • 如果响应可缓存(如成功响应且大小合适),存入缓存
    • 将响应返回给客户端
  7. 关闭连接

关键函数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. 常见问题与调试技巧

在实现过程中,我遇到了几个典型问题:

  1. 请求转发不完整:忘记转发某些必要的请求头,如Host头。解决方法是在转发请求时确保包含所有必要头信息。

  2. 缓存污染:多个线程同时修改缓存导致数据不一致。通过引入读写锁和细粒度的缓存项锁来解决。

  3. 内存泄漏:忘记释放动态分配的内存和关闭文件描述符。解决方法是严格检查每个malloc/free和open/close的配对。

  4. 性能瓶颈:最初的实现在高并发下性能不佳。通过以下优化提升性能:

    • 使用更高效的哈希算法查找缓存
    • 实现零拷贝的数据转发
    • 优化锁的粒度

调试时可以使用的技巧:

  • 使用netcat手动发送HTTP请求测试代理
  • curl -v --proxy http://localhost:PORT URL测试
  • 添加详细的日志输出请求/响应流程
  • 使用Valgrind检查内存错误

8. 进阶优化方向

完成基础功能后,还可以考虑以下优化:

  1. 支持HTTPS:现代代理需要处理加密流量,可以通过CONNECT方法实现。

  2. 智能缓存策略:根据响应头中的Cache-Control决定是否缓存,而不仅基于大小。

  3. 连接池:复用后端连接减少TCP握手开销。

  4. 负载均衡:在多个后端服务器间分配请求。

  5. 压缩传输:对特定内容类型进行压缩减少带宽占用。

  6. 访问控制:实现基于IP或用户的访问限制。

这些优化可以显著提升代理服务器的实用性,但也会增加代码复杂度。建议在基础功能稳定后再逐步添加。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 17:40:43

串口接口电路设计:从基础原理到工程实践

1. 串口接口电路概述&#xff1a;从基础到实战串口通信作为电子设备间最古老也最可靠的通信方式之一&#xff0c;至今仍在工业控制、嵌入式系统和各类电子设备中广泛应用。不同于现代高速总线接口&#xff0c;串口以其简单、稳定和低成本的特性&#xff0c;在特定应用场景中展现…

作者头像 李华
网站建设 2026/7/16 17:34:11

LFM2.5-Embedding-350M-bf16代码实现深度解析:双向编码器架构详解

LFM2.5-Embedding-350M-bf16代码实现深度解析&#xff1a;双向编码器架构详解 【免费下载链接】LFM2.5-Embedding-350M-bf16 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/LFM2.5-Embedding-350M-bf16 想要在Apple Silicon设备上高效运行多语言文本嵌入…

作者头像 李华
网站建设 2026/7/16 17:31:59

【计算机网络】从零搭建移动热点:协议交互与Wireshark捕包实战解析

1. 移动热点基础概念与搭建准备移动热点本质上是一台设备&#xff08;如笔记本电脑&#xff09;将自己的网络连接&#xff08;有线/Wi-Fi/蜂窝数据&#xff09;通过无线信号共享给其他设备的功能。Windows系统自带的移动热点功能&#xff0c;实际上是在物理网卡基础上虚拟出一个…

作者头像 李华
网站建设 2026/7/16 17:31:27

从访问树到共享矩阵:LSSS矩阵构造的两种经典算法解析

1. 线性秘密共享方案&#xff08;LSSS&#xff09;基础概念 在密码学领域&#xff0c;线性秘密共享方案&#xff08;Linear Secret Sharing Scheme&#xff0c;简称LSSS&#xff09;是实现细粒度访问控制的核心工具。简单来说&#xff0c;它就像是一把被拆分成多个碎片的钥匙—…

作者头像 李华
网站建设 2026/7/16 17:29:32

2026大湾区EMBA择校测评:战略管理赋能,解锁民企升级新路径

不少大湾区民企老板读EMBA都会陷入两难困境&#xff1a;要么高额学费只换来镀金头衔&#xff0c;课程脱离实业经营无法落地&#xff1b;要么圈层杂乱、人脉资源含金量低&#xff0c;难以对接产业与跨境资源。为帮实体企业家精准避坑&#xff0c;本文基于四大核心维度&#xff0…

作者头像 李华