如何在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文件、获取响应头和输出:
- 在
Cargo.toml添加依赖:
[dependencies] fastcgi-client = "0.8" tokio = { version = "1.0", features = ["full"] }
- 完整交互代码:
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(¶ms_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
相关产品推荐
相关产品推荐

