凌峰创科服务平台

linux c 代理服务器

代理服务器的核心思想是作为一个“中间人”,客户端连接到代理服务器,然后代理服务器代替客户端去访问真正的目标服务器,并将目标服务器的响应返回给客户端。

linux c 代理服务器-图1
(图片来源网络,侵删)

这个过程可以分解为以下几个步骤:

  1. 监听:代理服务器在一个指定的端口上监听客户端的连接请求。
  2. 接受连接:当有客户端连接时,服务器接受这个连接,并创建一个新的 socket 与客户端通信。
  3. 解析请求:从客户端读取数据(通常是 HTTP 请求),并解析出目标服务器的地址(域名和端口)。
  4. 连接目标:代理服务器使用解析出的地址,与真正的目标服务器建立一个新的连接。
  5. 数据转发
    • 将客户端的原始请求(去掉一些头部信息,如 Connection: keep-alive)转发给目标服务器。
    • 将目标服务器返回的响应数据,原封不动地转发回给客户端。
  6. 关闭连接:当客户端或任何一端关闭连接时,代理服务器需要关闭相关的 socket,释放资源。

一个简单的 HTTP 代理服务器示例

下面是一个用 C 语言实现的、功能完整的 HTTP 代理服务器代码,它支持 GETCONNECT(用于 HTTPS 代理)方法。

#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>
#define BUFFER_SIZE 4096
#define PROXY_PORT 8888
// 函数声明
void handle_client(int client_socket);
void forward_data(int source_socket, int dest_socket);
int main() {
    int server_fd, client_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    // 1. 创建 socket 文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置 socket 选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
    address.sin_port = htons(PROXY_PORT); // 设置代理端口
    // 2. 绑定 socket 到指定端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 开始监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Proxy server listening on port %d...\n", PROXY_PORT);
    // 4. 主循环,接受客户端连接
    while ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
        printf("Accepted connection from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
        // 在实际应用中,这里应该创建一个子线程来处理客户端,
        // 以避免 accept() 阻塞其他连接,为简化示例,我们直接处理。
        handle_client(client_socket);
        close(client_socket);
        printf("Connection closed.\n");
    }
    if (client_socket < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    close(server_fd);
    return 0;
}
// 处理单个客户端连接
void handle_client(int client_socket) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;
    // 从客户端读取请求数据
    bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_read <= 0) {
        printf("Client closed connection or read error.\n");
        return;
    }
    buffer[bytes_read] = '\0';
    printf("--- Received Request ---\n%s\n--- End of Request ---\n", buffer);
    // 解析请求,获取目标主机和端口
    char method[16], url[256], protocol[16];
    sscanf(buffer, "%15s %255s %15s", method, url, protocol);
    char host[256];
    int port = 80; // 默认 HTTP 端口
    // 处理 CONNECT 方法 (HTTPS 代理)
    if (strcmp(method, "CONNECT") == 0) {
        sscanf(url, "%255[^:]:%d", host, &port);
        printf("HTTPS Tunnel to %s:%d\n", host, port);
        // 向客户端发送 200 Connection Established 响应
        const char *response = "HTTP/1.1 200 Connection Established\r\n\r\n";
        send(client_socket, response, strlen(response), 0);
        // 建立与目标服务器的 TCP 隧道,直接转发数据
        int target_socket = connect_to_host(host, port);
        if (target_socket < 0) {
            perror("Failed to connect to target for HTTPS");
            return;
        }
        printf("Tunnel established. Forwarding data...\n");
        forward_data(client_socket, target_socket);
        close(target_socket);
        return;
    }
    // 处理普通 HTTP 请求 (GET, POST, etc.)
    char host_header[256];
    char *host_line = strstr(buffer, "Host: ");
    if (host_line) {
        sscanf(host_line, "Host: %255[^\r\n]", host);
        // 尝试从 Host 头部获取端口
        char *port_str = strchr(host, ':');
        if (port_str) {
            *port_str = '\0'; // 分割主机名和端口
            port = atoi(port_str + 1);
        }
    } else {
        printf("Error: Host header not found in request.\n");
        return;
    }
    printf("Forwarding HTTP request to %s:%d\n", host, port);
    // 建立与目标服务器的连接
    int target_socket = connect_to_host(host, port);
    if (target_socket < 0) {
        perror("Failed to connect to target for HTTP");
        return;
    }
    // 修改原始请求,移除 Connection 和 Proxy-Connection 头部
    char *modified_request = modify_http_request(buffer);
    send(target_socket, modified_request, strlen(modified_request), 0);
    free(modified_request);
    // 转发目标服务器的响应回客户端
    forward_data(target_socket, client_socket);
    close(target_socket);
}
// 辅助函数:连接到目标主机
int connect_to_host(const char *host, int port) {
    struct hostent *host_entry;
    struct sockaddr_in server_addr;
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return -1;
    }
    host_entry = gethostbyname(host);
    if (!host_entry) {
        fprintf(stderr, "gethostbyname failed for host: %s\n", host);
        close(sock);
        return -1;
    }
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    memcpy(&server_addr.sin_addr, host_entry->h_addr, host_entry->h_length);
    server_addr.sin_port = htons(port);
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sock);
        return -1;
    }
    return sock;
}
// 辅助函数:修改 HTTP 请求头部
char* modify_http_request(const char* original_request) {
    // 简单实现:移除 "Connection: keep-alive" 和 "Proxy-Connection: keep-alive"
    // 注意:这只是一个简化示例,实际解析 HTTP 头部更复杂
    char *modified = strdup(original_request);
    char *line = modified;
    char *new_request = malloc(BUFFER_SIZE);
    new_request[0] = '\0';
    while (line != NULL) {
        char *next_line = strchr(line, '\n');
        if (next_line) *next_line = '\0';
        // 跳过不需要的头部
        if (strncmp(line, "Connection:", 11) == 0 ||
            strncmp(line, "Proxy-Connection:", 17) == 0) {
            // 跳过这一行
        } else {
            strcat(new_request, line);
            strcat(new_request, "\r\n");
        }
        line = (next_line) ? next_line + 1 : NULL;
    }
    strcat(new_request, "\r\n"); // 添加空行,结束头部
    free(modified);
    return new_request;
}
// 辅助函数:双向转发数据
void forward_data(int source_socket, int dest_socket) {
    fd_set read_fds;
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;
    while (1) {
        FD_ZERO(&read_fds);
        FD_SET(source_socket, &read_fds);
        FD_SET(dest_socket, &read_fds);
        // 使用 select 等待任一 socket 可读
        if (select(dest_socket + 1, &read_fds, NULL, NULL, NULL) < 0) {
            perror("select");
            break;
        }
        // 检查 source_socket 是否可读
        if (FD_ISSET(source_socket, &read_fds)) {
            bytes_read = recv(source_socket, buffer, BUFFER_SIZE, 0);
            if (bytes_read <= 0) {
                // 连接关闭或出错
                break;
            }
            send(dest_socket, buffer, bytes_read, 0);
        }
        // 检查 dest_socket 是否可读
        if (FD_ISSET(dest_socket, &read_fds)) {
            bytes_read = recv(dest_socket, buffer, BUFFER_SIZE, 0);
            if (bytes_read <= 0) {
                // 连接关闭或出错
                break;
            }
            send(source_socket, buffer, bytes_read, 0);
        }
    }
}

如何编译和运行

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

  2. 编译:打开终端,使用 gcc 进行编译。

    linux c 代理服务器-图2
    (图片来源网络,侵删)
    gcc -o proxy_server proxy_server.c
  3. 运行:以普通用户身份运行即可(不需要 root 权限,除非你使用小于 1024 的端口)。

    ./proxy_server

    你会看到输出:Proxy server listening on port 8888...

  4. 配置客户端:现在你需要将你的浏览器或其他网络应用的代理设置为 0.0.1(或你的服务器 IP)和 8888

  5. 测试:打开浏览器,访问任何网站(如 http://example.com),代理服务器的终端会打印出连接信息和转发的请求数据。

    linux c 代理服务器-图3
    (图片来源网络,侵删)

代码详解

主函数 (main)

  • socket(): 创建一个 TCP socket。
  • setsockopt(): 设置 SO_REUSEADDR 选项,允许服务器快速重启,避免 "Address already in use" 错误。
  • bind(): 将 socket 绑定到本机的 8888 端口。
  • listen(): 开始监听传入的连接,3 是最大待处理连接数。
  • accept(): 阻塞等待客户端连接,当有连接时,它返回一个新的 client_socket,用于与该客户端通信。
  • handle_client(): 将处理客户端连接的任务交给一个单独的函数。
  • close(): 关闭 socket。

客户端处理 (handle_client)

这是代理的核心逻辑。

  • recv(): 从客户端读取完整的 HTTP 请求。
  • sscanf(): 解析请求行,获取 method (如 GET, CONNECT)、urlprotocol
  • CONNECT 方法处理 (HTTPS):
    • CONNECT example.com:443 HTTP/1.1 这种请求表示客户端想建立一个到 example.com:443 的隧道,用于 HTTPS。
    • 代理服务器不关心内容,只需与目标服务器建立 TCP 连接。
    • 成功后,代理服务器向客户端返回 HTTP/1.1 200 Connection Established,表示隧道已建立。
    • 之后,所有从客户端收到的数据都会被原封不动地转发到目标服务器,反之亦然,这就是“TCP 隧道”。
  • 普通 HTTP 请求处理 (GET, POST 等):
    • Host 头部解析出目标主机名和端口。
    • 调用 connect_to_host() 与目标服务器建立连接。
    • 调用 modify_http_request() 清理原始请求(主要是移除代理相关的头部)。
    • 将修改后的请求发送给目标服务器。
    • 调用 forward_data() 将目标服务器的响应转发回客户端。

辅助函数

  • connect_to_host(): 封装了通过主机名获取 IP 地址并建立 TCP 连接的过程。
  • modify_http_request(): 一个简化的 HTTP 请求修改器,真正的代理需要更健壮的头部解析和修改逻辑,例如处理 Content-LengthTransfer-Encoding 等。
  • forward_data(): 使用 select 系统调用来高效地双向转发数据,它会同时监视两个 socket,当任何一个有数据可读时,就读取并发送给另一个,当一端关闭连接时,循环结束。

进一步改进的方向

这个示例是一个很好的起点,但一个生产级的代理服务器还需要考虑:

  1. 多线程/多进程:当前的实现是单线程的,accept() 之后会阻塞处理一个客户端,无法处理其他并发请求,应该使用 pthreadfork 为每个客户端创建一个新的处理线程/进程。
  2. I/O 多路复用:对于高并发,使用 epoll (Linux) 或 kqueue (BSD) 比 select 更高效。
  3. 更健壮的 HTTP 解析:当前使用 sscanf 和字符串查找来解析 HTTP 请求非常脆弱,应该使用一个真正的 HTTP 解析库(如 libcurl 的内部组件)或自己编写更健壮的解析器。
  4. 缓冲区管理:处理大数据传输时,需要小心处理缓冲区溢出和不完整的消息。
  5. 身份验证:支持用户名和密码的代理认证。
  6. 功能增强:支持 FTPSOCKS5 等其他代理协议,或者实现内容缓存、访问控制列表等功能。
分享:
扫描分享到社交APP
上一篇
下一篇