我无法在这里提供一个完整的、可以直接运行的tinyproxy源码,我可以为你提供一个核心功能的、可运行的、教学用的C语言代理服务器,这个示例将包含tinyproxy最基本的功能:作为HTTP代理,接收客户端请求,转发到目标服务器,并将响应返回给客户端。

(图片来源网络,侵删)
通过这个示例,你可以理解代理服务器的工作原理和核心代码结构,并以此为基础进行扩展,逐步实现一个更完整的tinyproxy。
核心概念
一个简单的HTTP代理服务器主要扮演一个“中间人”的角色,其工作流程如下:
- 监听: 代理服务器在指定的端口(如8888)上等待客户端连接。
- 接收请求: 客户端(如浏览器)配置好代理后,会向代理服务器的IP和端口发送一个标准的HTTP请求。
GET http://www.example.com/index.html HTTP/1.1 Host: www.example.com User-Agent: ... - 解析请求: 代理服务器需要解析这个请求,提取出三个关键信息:
- 方法:
GET,POST等。 - 目标主机:
www.example.com。 - 目标路径:
/index.html。
- 方法:
- 建立新连接: 代理服务器根据解析出的目标主机,与真正的目标服务器(
www.example.com的80端口)建立一个TCP连接。 - 转发请求: 代理服务器将客户端的原始请求(或者修改后的请求)转发给目标服务器,需要注意的是,需要将请求中的完整URL
http://www.example.com/index.html改为相对路径/index.html,并设置正确的Host头。GET /index.html HTTP/1.1 Host: www.example.com User-Agent: ... - 接收响应: 目标服务器处理请求后,将HTTP响应(包括响应头和响应体)发送回代理服务器。
- 转发响应: 代理服务器将收到的完整HTTP响应原封不动地转发给最初的客户端。
- 关闭连接: 代理服务器可以关闭与客户端和目标服务器的连接,或者根据HTTP头的
Connection字段决定是否保持连接。
教学用代理服务器代码 (simple_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 <netdb.h>
#include <signal.h>
#include <sys/wait.h>
#define BUFFER_SIZE 4096
#define PROXY_PORT 8888
// 函数声明
void handle_client(int client_socket);
void error_exit(const char *msg);
void sigchld_handler(int sig);
int main(int argc, char *argv[]) {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 注册SIGCHLD信号处理函数,用于回收僵尸进程
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
error_exit("sigaction");
}
// 1. 创建TCP套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
error_exit("socket");
}
// 设置套接字选项,允许地址重用,避免"Address already in use"错误
int opt = 1;
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
error_exit("setsockopt");
}
// 2. 绑定地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
server_addr.sin_port = htons(PROXY_PORT);
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
error_exit("bind");
}
// 3. 开始监听
if (listen(server_socket, 10) < 0) {
error_exit("listen");
}
printf("Simple Proxy Server is running on port %d...\n", PROXY_PORT);
// 4. 循环接受客户端连接
while (1) {
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket < 0) {
perror("accept");
continue; // 继续接受下一个连接
}
printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 5. 为每个客户端创建一个子进程来处理
pid_t pid = fork();
if (pid < 0) {
perror("fork");
close(client_socket);
} else if (pid == 0) { // 子进程
close(server_socket); // 子进程不需要监听套接字
handle_client(client_socket);
close(client_socket);
exit(EXIT_SUCCESS); // 子进程处理完毕后退出
} else { // 父进程
close(client_socket); // 父进程不需要客户端套接字
}
}
close(server_socket);
return 0;
}
// 处理客户端请求的核心函数
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE];
char request_line[BUFFER_SIZE];
char method[BUFFER_SIZE], url[BUFFER_SIZE], protocol[BUFFER_SIZE];
char host[BUFFER_SIZE], path[BUFFER_SIZE];
int target_port = 80;
int target_socket;
// 1. 从客户端读取第一行请求 (e.g., GET http://example.com/ HTTP/1.1)
ssize_t bytes_read = recv(client_socket, request_line, BUFFER_SIZE - 1, 0);
if (bytes_read <= 0) {
return; // 客户端关闭连接或出错
}
request_line[bytes_read] = '\0';
printf("Request from client: %s", request_line);
// 2. 解析请求行
if (sscanf(request_line, "%s %s %s", method, url, protocol) != 3) {
fprintf(stderr, "Failed to parse request line.\n");
return;
}
// 3. 从URL中提取主机和路径
// 查找 "://" 来分离协议部分
char *host_start = strstr(url, "://");
if (host_start == NULL) {
fprintf(stderr, "Invalid URL format.\n");
return;
}
host_start += 3; // 跳过 "://"
// 查找路径或端口的开始位置
char *path_start = strchr(host_start, '/');
char *port_start = strchr(host_start, ':');
if (path_start == NULL && port_start == NULL) {
strcpy(host, host_start);
strcpy(path, "/");
} else if (port_start != NULL && (path_start == NULL || port_start < path_start)) {
// 存在端口号
strncpy(host, host_start, port_start - host_start);
host[port_start - host_start] = '\0';
target_port = atoi(port_start + 1);
strcpy(path, path_start ? path_start : "/");
} else {
// 存在路径,没有端口号
strncpy(host, host_start, path_start - host_start);
host[path_start - host_start] = '\0';
strcpy(path, path_start);
}
printf("Parsed -> Host: %s, Port: %d, Path: %s\n", host, target_port, path);
// 4. 连接目标服务器
struct hostent *target_host = gethostbyname(host);
if (target_host == NULL) {
fprintf(stderr, "Cannot resolve hostname: %s\n", host);
return;
}
struct sockaddr_in target_addr;
target_socket = socket(AF_INET, SOCK_STREAM, 0);
if (target_socket < 0) {
error_exit("target socket");
}
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(target_port);
memcpy(&target_addr.sin_addr, target_host->h_addr, target_host->h_length);
if (connect(target_socket, (struct sockaddr *)&target_addr, sizeof(target_addr)) < 0) {
perror("connect to target");
close(target_socket);
return;
}
printf("Connected to target server %s:%d\n", host, target_port);
// 5. 转发请求到目标服务器
// 需要修改原始请求,将绝对URL改为相对路径,并确保Host头存在
char new_request_line[BUFFER_SIZE];
snprintf(new_request_line, BUFFER_SIZE, "%s %s %s\r\n", method, path, protocol);
// 发送修改后的请求行
send(target_socket, new_request_line, strlen(new_request_line), 0);
// 转发剩余的请求头
while (1) {
bytes_read = recv(client_socket, buffer, BUFFER_SIZE, 0);
if (bytes_read <= 0) break;
// 检查是否到达请求头结束的空行 (\r\n\r\n)
if (bytes_read >= 4 && strncmp(buffer, "\r\n\r\n", 4) == 0) {
send(target_socket, buffer, 4, 0); // 转发空行,结束请求头
break;
}
// 确保Host头存在,如果不存在则添加一个
if (strncasecmp(buffer, "Host:", 5) == 0) {
// 如果已经有Host头,直接转发
send(target_socket, buffer, bytes_read, 0);
} else if (bytes_read == 2 && strncmp(buffer, "\r\n", 2) == 0) {
// 如果第一个空行就到了,说明没有Host头,需要添加
char host_header[BUFFER_SIZE];
snprintf(host_header, BUFFER_SIZE, "Host: %s\r\n\r\n", host);
send(target_socket, host_header, strlen(host_header), 0);
break;
} else {
// 转发其他头
send(target_socket, buffer, bytes_read, 0);
}
}
// 如果请求体存在,继续转发
// (对于GET请求,通常没有请求体,对于POST/PUT等,需要继续转发)
// 为了简化,这里只处理了请求头,实际应用中需要处理请求体。
// 我们假设客户端在发送完请求头后会立即发送请求体(如果有)
// 而我们上面的循环在遇到第一个空行后就退出了,所以请求体会被留在client_socket的缓冲区中
// 更健壮的做法是先完整接收客户端的整个请求,再一次性发送给目标服务器
// 但为了代码清晰,我们采用流式处理,即边收边发。
// 6. 转发响应回客户端
while ((bytes_read = recv(target_socket, buffer, BUFFER_SIZE, 0)) > 0) {
send(client_socket, buffer, bytes_read, 0);
}
printf("Finished handling connection from client.\n");
close(target_socket);
}
// 错误处理函数
void error_exit(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
// 信号处理函数,用于回收子进程,防止僵尸进程
void sigchld_handler(int sig) {
// 使用WNOHANG选项,防止waitpid阻塞
while (waitpid(-1, NULL, WNOHANG) > 0);
}
如何编译和运行
-
保存代码: 将上面的代码保存为
simple_proxy.c。
(图片来源网络,侵删) -
编译: 打开终端,使用gcc进行编译。
gcc -o simple_proxy simple_proxy.c
-
运行: 执行编译后的程序。
./simple_proxy
你会看到输出:
Simple Proxy Server is running on port 8888... -
配置客户端:
(图片来源网络,侵删)- 浏览器配置: 在你的浏览器(如Chrome, Firefox)的设置中,找到“网络设置”或“代理设置”。
- 选择“手动配置代理”。
- 填写代理服务器地址为
0.0.1(如果你的代理程序和浏览器在同一台机器上),端口为8888。 - 保存设置。
-
测试: 在浏览器中访问任何网站(
http://www.google.com或http://httpbin.org/ip),浏览器会通过你的代理服务器进行访问,你会在代理服务器的终端窗口中看到类似下面的日志:Accepted connection from 127.0.0.1:54321 Request from client: GET http://www.google.com/ HTTP/1.1 User-Agent: Mozilla/5.0 ... ... Parsed -> Host: www.google.com, Port: 80, Path: / Connected to target server www.google.com:80 Finished handling connection from client.
如何扩展成真正的 "tinyproxy"
这个简单的示例离一个功能完整的tinyproxy还有很远的距离,以下是一些可以继续扩展的方向:
-
配置文件:
- 添加一个配置文件(如
tinyproxy.conf),让用户可以配置监听端口、允许/禁止访问的域名、是否启用日志等。 - 使用
lex/yacc或手动编写解析器来读取和解析配置文件。
- 添加一个配置文件(如
-
日志系统:
- 记录每个客户端的请求(源IP、目标URL、时间戳、状态码等)。
- 支持不同级别的日志(如
error,info,debug)。
-
访问控制:
- 实现基于IP地址的访问控制列表(ACL),允许或拒绝特定IP的连接请求。
- 实现基于域名的过滤,禁止访问某些“黑名单”网站。
-
更高级的协议支持:
- HTTPS: 这是最大的挑战,代理服务器需要处理HTTPS连接,这涉及到SSL/TLS握手。
- 隧道模式: 当客户端请求
https://...时,代理服务器只需建立一个TCP隧道,将数据原封不动地在客户端和目标服务器之间转发,这相对简单。 - 中间人攻击模式: 代理服务器自己充当SSL客户端,与目标服务器建立安全连接,同时充当SSL服务器,与客户端建立安全连接,这样可以解密和检查HTTPS流量,但这会破坏SSL的信任链,并且需要处理客户端的证书验证警告,tinyproxy默认使用隧道模式。
- 隧道模式: 当客户端请求
- HTTPS: 这是最大的挑战,代理服务器需要处理HTTPS连接,这涉及到SSL/TLS握手。
-
性能和并发:
- 多线程模型: 使用
pthread创建线程池来处理客户端连接,比进程模型更轻量。 - I/O多路复用: 使用
select,poll或更高效的epoll(Linux) /kqueue(BSD) 来实现一个单线程、高并发的模型,能处理成千上万的连接。 - 内存优化: 避免在内存中缓存整个请求或响应,特别是对于大文件上传或下载,应采用流式处理。
- 多线程模型: 使用
-
功能增强:
- 反向代理: 根据请求的域名或路径,将请求转发到不同的后端服务器集群。
- 透明代理: 让客户端无需配置代理,所有流量都自动通过代理(需要网络设备配合)。
- 连接复用: 支持 HTTP/1.1 的
Keep-Alive,减少建立TCP连接的开销。
这个教学示例是一个很好的起点,通过理解它,你已经掌握了代理服务器的核心网络编程逻辑,你可以选择上述任何一个方向进行深入研究,逐步为你的代理服务器添加新功能。
