c语言写tinyproxy

99ANYc3cd6
预计阅读时长 38 分钟
位置: 首页 C语言 正文

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

c语言写tinyproxy
(图片来源网络,侵删)

通过这个示例,你可以理解代理服务器的工作原理和核心代码结构,并以此为基础进行扩展,逐步实现一个更完整的tinyproxy。


核心概念

一个简单的HTTP代理服务器主要扮演一个“中间人”的角色,其工作流程如下:

  1. 监听: 代理服务器在指定的端口(如8888)上等待客户端连接。
  2. 接收请求: 客户端(如浏览器)配置好代理后,会向代理服务器的IP和端口发送一个标准的HTTP请求。
    GET http://www.example.com/index.html HTTP/1.1
    Host: www.example.com
    User-Agent: ...
  3. 解析请求: 代理服务器需要解析这个请求,提取出三个关键信息:
    • 方法: GET, POST等。
    • 目标主机: www.example.com
    • 目标路径: /index.html
  4. 建立新连接: 代理服务器根据解析出的目标主机,与真正的目标服务器(www.example.com的80端口)建立一个TCP连接。
  5. 转发请求: 代理服务器将客户端的原始请求(或者修改后的请求)转发给目标服务器,需要注意的是,需要将请求中的完整URL http://www.example.com/index.html 改为相对路径 /index.html,并设置正确的Host头。
    GET /index.html HTTP/1.1
    Host: www.example.com
    User-Agent: ...
  6. 接收响应: 目标服务器处理请求后,将HTTP响应(包括响应头和响应体)发送回代理服务器。
  7. 转发响应: 代理服务器将收到的完整HTTP响应原封不动地转发给最初的客户端。
  8. 关闭连接: 代理服务器可以关闭与客户端和目标服务器的连接,或者根据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);
}

如何编译和运行

  1. 保存代码: 将上面的代码保存为 simple_proxy.c

    c语言写tinyproxy
    (图片来源网络,侵删)
  2. 编译: 打开终端,使用gcc进行编译。

    gcc -o simple_proxy simple_proxy.c
  3. 运行: 执行编译后的程序。

    ./simple_proxy

    你会看到输出:

    Simple Proxy Server is running on port 8888...
  4. 配置客户端:

    c语言写tinyproxy
    (图片来源网络,侵删)
    • 浏览器配置: 在你的浏览器(如Chrome, Firefox)的设置中,找到“网络设置”或“代理设置”。
    • 选择“手动配置代理”。
    • 填写代理服务器地址为 0.0.1(如果你的代理程序和浏览器在同一台机器上),端口为 8888
    • 保存设置。
  5. 测试: 在浏览器中访问任何网站(http://www.google.comhttp://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还有很远的距离,以下是一些可以继续扩展的方向:

  1. 配置文件:

    • 添加一个配置文件(如 tinyproxy.conf),让用户可以配置监听端口、允许/禁止访问的域名、是否启用日志等。
    • 使用 lex/yacc 或手动编写解析器来读取和解析配置文件。
  2. 日志系统:

    • 记录每个客户端的请求(源IP、目标URL、时间戳、状态码等)。
    • 支持不同级别的日志(如 error, info, debug)。
  3. 访问控制:

    • 实现基于IP地址的访问控制列表(ACL),允许或拒绝特定IP的连接请求。
    • 实现基于域名的过滤,禁止访问某些“黑名单”网站。
  4. 更高级的协议支持:

    • HTTPS: 这是最大的挑战,代理服务器需要处理HTTPS连接,这涉及到SSL/TLS握手。
      • 隧道模式: 当客户端请求 https://... 时,代理服务器只需建立一个TCP隧道,将数据原封不动地在客户端和目标服务器之间转发,这相对简单。
      • 中间人攻击模式: 代理服务器自己充当SSL客户端,与目标服务器建立安全连接,同时充当SSL服务器,与客户端建立安全连接,这样可以解密和检查HTTPS流量,但这会破坏SSL的信任链,并且需要处理客户端的证书验证警告,tinyproxy默认使用隧道模式。
  5. 性能和并发:

    • 多线程模型: 使用 pthread 创建线程池来处理客户端连接,比进程模型更轻量。
    • I/O多路复用: 使用 select, poll 或更高效的 epoll (Linux) / kqueue (BSD) 来实现一个单线程、高并发的模型,能处理成千上万的连接。
    • 内存优化: 避免在内存中缓存整个请求或响应,特别是对于大文件上传或下载,应采用流式处理。
  6. 功能增强:

    • 反向代理: 根据请求的域名或路径,将请求转发到不同的后端服务器集群。
    • 透明代理: 让客户端无需配置代理,所有流量都自动通过代理(需要网络设备配合)。
    • 连接复用: 支持 HTTP/1.1 的 Keep-Alive,减少建立TCP连接的开销。

这个教学示例是一个很好的起点,通过理解它,你已经掌握了代理服务器的核心网络编程逻辑,你可以选择上述任何一个方向进行深入研究,逐步为你的代理服务器添加新功能。

-- 展开阅读全文 --
头像
dede field if 条件判断如何正确使用?
« 上一篇 今天
C语言中if与else if的执行逻辑是怎样的?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]