You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rust中通过Unix Socket与php-fpm交互并传递数据

Rust与php-fpm的FastCGI交互实现指南

一、修正初始连接代码

你当前代码里的socket_path包含冗余引号,会导致系统找不到Socket文件,先修正:

use std::os::unix::net::UnixStream;
use std::io::{Read, Write};

fn main() {
    let socket_path = "/run/php-fpm/php-fpm.sock";

    let mut socket = match UnixStream::connect(socket_path) {
        Ok(sock) => sock,
        Err(e) => {
            println!("Couldn't connect: {e:?}");
            return;
        }
    };
    // 后续交互逻辑写在这里
}

二、使用fastcgi-client库实现完整交互(推荐)

直接用成熟的FastCGI客户端库可避免手动处理二进制协议的繁琐,以下是完整示例,包含执行PHP文件、获取响应头和输出:

  1. 在Cargo.toml添加依赖:
[dependencies]
fastcgi-client = "0.8"
tokio = { version = "1.0", features = ["full"] }
  1. 完整交互代码:
use fastcgi-client::{Client, Params, Request};
use std::os::unix::net::UnixStream;
use tokio::io::AsyncReadExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let socket_path = "/run/php-fpm/php-fpm.sock";
    let stream = UnixStream::connect(socket_path)?;
    let mut client = Client::new(stream);

    // 构造FastCGI请求参数,必须指定要执行的PHP文件路径
    let mut params = Params::new();
    params.insert("SCRIPT_FILENAME", "/var/www/html/test.php"); // 替换为你的PHP文件路径
    params.insert("REQUEST_METHOD", "GET");
    params.insert("CONTENT_TYPE", "text/html");

    // 发送请求,GET请求的STDIN内容为空
    let mut request = Request::new(params, "");
    let mut response = client.execute(request).await?;

    // 读取响应内容
    let mut output = Vec::new();
    response.read_to_end(&mut output).await?;

    // 分离响应头和页面内容(FastCGI响应头与内容以\r\n\r\n分隔)
    let output_str = String::from_utf8_lossy(&output);
    if let Some((headers_str, content)) = output_str.split_once("\r\n\r\n") {
        println!("=== 响应头 ===");
        for header in headers_str.split("\r\n") {
            println!("{header}");
        }
        println!("\n=== 输出内容 ===");
        println!("{content}");
    } else {
        println!("=== 输出内容 ===");
        println!("{output_str}");
    }

    Ok(())
}

三、手动实现FastCGI协议(原理学习用)

FastCGI是二进制协议,不能直接发送文本字符串,必须严格按照协议规定的二进制格式构造数据包。以下是核心步骤和代码示例:

1. 定义FastCGI核心常量与结构体

use std::os::unix::net::UnixStream;
use std::io::{Read, Write};

// FastCGI版本
const FCGI_VERSION: u8 = 1;
// 记录类型
const FCGI_BEGIN_REQUEST: u8 = 1;
const FCGI_PARAMS: u8 = 4;
const FCGI_STDIN: u8 = 5;
const FCGI_STDOUT: u8 = 6;
const FCGI_END_REQUEST: u8 = 3;
// 角色类型(PHP-FPM使用RESPONDER)
const FCGI_RESPONDER: u16 = 1;

// FastCGI头部结构
#[repr(C, packed)]
struct FcgiHeader {
    version: u8,
    type_: u8,
    request_id: [u8; 2], // 大端序
    content_length: [u8; 2], // 大端序
    padding_length: u8,
    reserved: u8,
}

// Begin请求的内容结构
#[repr(C, packed)]
struct FcgiBeginRequestBody {
    role: [u8; 2], // 大端序
    flags: u8,
    reserved: [u8; 5],
}

2. 构造并发送FCGI_BEGIN_REQUEST数据包

fn write_fcgi_header(stream: &mut UnixStream, type_: u8, request_id: u16, content_len: u16) -> std::io::Result<()> {
    let padding_len = (8 - (content_len % 8)) % 8;
    let header = FcgiHeader {
        version: FCGI_VERSION,
        type_,
        request_id: request_id.to_be_bytes(),
        content_length: content_len.to_be_bytes(),
        padding_length: padding_len as u8,
        reserved: 0,
    };
    // 将结构体转为字节数组写入socket
    let header_bytes = unsafe { std::slice::from_raw_parts(&header as *const _ as *const u8, std::mem::size_of::<FcgiHeader>()) };
    stream.write_all(header_bytes)?;
    Ok(())
}

fn send_begin_request(stream: &mut UnixStream, request_id: u16) -> std::io::Result<()> {
    // 构造Begin请求内容
    let body = FcgiBeginRequestBody {
        role: FCGI_RESPONDER.to_be_bytes(),
        flags: 0, // 0表示请求结束后关闭连接
        reserved: [0; 5],
    };
    let body_bytes = unsafe { std::slice::from_raw_parts(&body as *const _ as *const u8, std::mem::size_of::<FcgiBeginRequestBody>()) };
    
    // 写入头部
    write_fcgi_header(stream, FCGI_BEGIN_REQUEST, request_id, body_bytes.len() as u16)?;
    // 写入内容
    stream.write_all(body_bytes)?;
    // 写入填充字节
    let padding_len = (8 - (body_bytes.len() % 8)) % 8;
    stream.write_all(&vec![0; padding_len])?;
    Ok(())
}

3. 发送FCGI_PARAMS(传递PHP执行参数)

FastCGI参数是键值对,每个键值对的长度用前缀表示:长度<128用1字节,否则用4字节(最高位设为1)。以下是参数写入函数:

fn write_param(stream: &mut Vec<u8>, key: &str, value: &str) -> std::io::Result<()> {
    // 写入键长度
    let key_len = key.len();
    if key_len < 128 {
        stream.write_all(&[key_len as u8])?;
    } else {
        stream.write_all(&((key_len | 0x80000000) as u32).to_be_bytes())?;
    }
    // 写入值长度
    let value_len = value.len();
    if value_len < 128 {
        stream.write_all(&[value_len as u8])?;
    } else {
        stream.write_all(&((value_len | 0x80000000) as u32).to_be_bytes())?;
    }
    // 写入键和值
    stream.write_all(key.as_bytes())?;
    stream.write_all(value.as_bytes())?;
    Ok(())
}

fn send_params(stream: &mut UnixStream, request_id: u16) -> std::io::Result<()> {
    let mut params_buf = Vec::new();
    // 写入核心参数
    write_param(&mut params_buf, "SCRIPT_FILENAME", "/var/www/html/test.php")?;
    write_param(&mut params_buf, "REQUEST_METHOD", "GET")?;
    
    // 发送PARAMS头部和内容
    write_fcgi_header(stream, FCGI_PARAMS, request_id, params_buf.len() as u16)?;
    stream.write_all(&params_buf)?;
    let padding_len = (8 - (params_buf.len() % 8)) % 8;
    stream.write_all(&vec![0; padding_len])?;
    
    // 发送空的PARAMS表示参数结束
    write_fcgi_header(stream, FCGI_PARAMS, request_id, 0)?;
    Ok(())
}

4. 发送FCGI_STDIN(请求体,GET请求为空)

fn send_stdin(stream: &mut UnixStream, request_id: u16) -> std::io::Result<()> {
    // 发送空的STDIN表示请求体结束
    write_fcgi_header(stream, FCGI_STDIN, request_id, 0)?;
    Ok(())
}

5. 读取并解析响应

fn read_response(stream: &mut UnixStream) -> std::io::Result<String> {
    let mut output = String::new();
    loop {
        let mut header_buf = [0; 8];
        stream.read_exact(&mut header_buf)?;
        
        let header = FcgiHeader {
            version: header_buf[0],
            type_: header_buf[1],
            request_id: [header_buf[2], header_buf[3]],
            content_length: [header_buf[4], header_buf[5]],
            padding_length: header_buf[6],
            reserved: header_buf[7],
        };
        
        let content_len = u16::from_be_bytes(header.content_length) as usize;
        let padding_len = header.padding_length as usize;
        
        match header.type_ {
            FCGI_STDOUT => {
                let mut content_buf = vec![0; content_len];
                stream.read_exact(&mut content_buf)?;
                output.push_str(&String::from_utf8_lossy(&content_buf));
                // 跳过填充字节
                let mut padding_buf = vec![0; padding_len];
                stream.read_exact(&mut padding_buf)?;
            }
            FCGI_END_REQUEST => {
                // 读取结束内容后退出循环
                let mut content_buf = vec![0; content_len];
                stream.read_exact(&mut content_buf)?;
                let mut padding_buf = vec![0; padding_len];
                stream.read_exact(&mut padding_buf)?;
                break;
            }
            _ => {
                // 忽略其他类型的记录
                let mut skip_buf = vec![0; content_len + padding_len];
                stream.read_exact(&mut skip_buf)?;
            }
        }
    }
    Ok(output)
}

6. 整合所有步骤

fn main() -> std::io::Result<()> {
    let socket_path = "/run/php-fpm/php-fpm.sock";
    let mut stream = UnixStream::connect(socket_path)?;
    
    let request_id = 1;
    send_begin_request(&mut stream, request_id)?;
    send_params(&mut stream, request_id)?;
    send_stdin(&mut stream, request_id)?;
    
    let response = read_response(&mut stream)?;
    // 分离响应头和内容
    if let Some((headers, content)) = response.split_once("\r\n\r\n") {
        println!("=== 响应头 ===");
        println!("{headers}");
        println!("\n=== 输出内容 ===");
        println!("{content}");
    } else {
        println!("=== 输出内容 ===");
        println!("{response}");
    }
    
    Ok(())
}

关键原理说明

你之前的核心误解是把FastCGI当成了文本协议——FastCGI所有通信都是二进制格式,每个数据包必须包含固定结构的头部,内容也必须严格遵循协议定义的字节顺序和格式,不能直接发送"FCGI_BEGIN_REQUEST"这类文本字符串。

内容的提问来源于stack exchange,提问作者DFG

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 18:55:24