为何使用Elixir的gen_tcp连接Unix套接字时持续超时?
问题描述
尝试用Go实现基于Unix Socket的HTTP服务端,搭配Elixir客户端做IPC通信,但Elixir端在请求到达服务端前持续出现超时错误。
服务端代码(Go)
package main import ( "fmt" "net" "net/http" "os" ) func main() { socket := "/tmp/ipc_test.sock" os.Remove(socket) listener, err := net.Listen("unix", socket) if err != nil { fmt.Printf("Error listening on unix socket: %s\n", err) return } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { param := r.URL.Query().Get("param") fmt.Printf("%s", param) fmt.Fprint(w, param) }) fmt.Printf("Server listening on %s\n", socket) http.Serve(listener, nil) }
客户端代码(Elixir)
defmodule IpcTest do def make_request(socket, request) do :gen_tcp.send(socket, request) {:ok, response} = :gen_tcp.recv(socket, 0) :gen_tcp.close(socket) response end def print_responses(responses) do responses |> Enum.each(&IO.puts/1) end def run(num_requests) do 1..num_requests |> Enum.map(fn i -> request = "/?param=#{i}" Task.async(fn -> {:ok, socket} = :gen_tcp.connect({:local, "/tmp/ipc_test.sock"}, 0, [:binary, packet: 4, active: false]) make_request(socket, request) end) end) |> Enum.map(&Task.await/1) end end
运行情况
服务端启动正常:
> go run src/main.go Server listening on /tmp/js_runner.sock
客户端执行超时:
> iex -S mix > IpcTest.run 1 ** (exit) exited in: Task.await(%Task{mfa: {:erlang, :apply, 2}, owner: #PID<0.237.0>, pid: #PID<0.263.0>, ref: #Reference<0.3740412160.2895708166.190144>}, 5000) ** (EXIT) time out (elixir 1.14.2) lib/task.ex:830: Task.await/2 (elixir 1.14.2) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2 iex:2: (file)
问题原因
- 请求格式不兼容:Go服务是标准HTTP服务,要求客户端发送完整HTTP请求,但Elixir仅发送了路径字符串
/?param=1,服务端无法解析请求,不会返回响应,导致客户端recv阻塞超时。 - 数据包协议不匹配:Elixir客户端设置了
packet: 4(4字节长度前缀的数据包格式),但Go的HTTP服务使用流模式处理请求,双方协议不匹配,服务端无法正确读取请求内容。 - Socket路径不一致:服务端启动输出的路径是
/tmp/js_runner.sock,但客户端连接的是/tmp/ipc_test.sock,属于配置笔误,会导致连接失败(当前超时主要由前两个问题导致)。
修复方案
方案1:适配HTTP协议修正Elixir客户端
修改客户端发送完整HTTP请求,同时移除packet: 4配置,匹配HTTP的流模式:
defmodule IpcTest do def make_request(socket, request) do # 构造完整HTTP GET请求 full_request = "GET #{request} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" :gen_tcp.send(socket, full_request) {:ok, response} = :gen_tcp.recv(socket, 0) :gen_tcp.close(socket) # 提取响应体(跳过HTTP头) case String.split(response, "\r\n\r\n", parts: 2) do [_header, body] -> body _ -> response end end def print_responses(responses) do responses |> Enum.each(&IO.puts/1) end def run(num_requests) do 1..num_requests |> Enum.map(fn i -> request = "/?param=#{i}" Task.async(fn -> {:ok, socket} = # 移除packet:4,使用默认流模式 :gen_tcp.connect({:local, "/tmp/ipc_test.sock"}, 0, [:binary, active: false]) make_request(socket, request) end) end) |> Enum.map(&Task.await/1) end end
方案2:Go服务端改为自定义TCP协议(无需HTTP)
如果不需要HTTP协议,可将Go服务改为处理带4字节长度前缀的请求,匹配Elixir客户端的配置:
package main import ( "fmt" "net" "os" ) func main() { socket := "/tmp/ipc_test.sock" os.Remove(socket) listener, err := net.Listen("unix", socket) if err != nil { fmt.Printf("Error listening on unix socket: %s\n", err) return } defer listener.Close() fmt.Printf("Server listening on %s\n", socket) for { conn, err := listener.Accept() if err != nil { fmt.Printf("Accept error: %s\n", err) continue } go handleConn(conn) } } func handleConn(conn net.Conn) { defer conn.Close() // 读取4字节长度前缀 lenBuf := make([]byte, 4) _, err := conn.Read(lenBuf) if err != nil { fmt.Printf("Read length error: %s\n", err) return } length := int(lenBuf[0])<<24 | int(lenBuf[1])<<16 | int(lenBuf[2])<<8 | int(lenBuf[3]) // 读取请求内容 reqBuf := make([]byte, length) _, err = conn.Read(reqBuf) if err != nil { fmt.Printf("Read request error: %s\n", err) return } param := string(reqBuf) fmt.Printf("Received: %s\n", param) // 发送带长度前缀的响应 response := []byte(param) respLen := make([]byte, 4) respLen[0] = byte(len(response) >> 24) respLen[1] = byte(len(response) >> 16) respLen[2] = byte(len(response) >> 8) respLen[3] = byte(len(response)) conn.Write(respLen) conn.Write(response) }
对应Elixir客户端需发送带长度前缀的请求:
def make_request(socket, request) do req_bytes = to_string(request) |> String.to_bytes() len = byte_size(req_bytes) # 添加4字节大端长度前缀 len_bytes = <<len::32>> :gen_tcp.send(socket, len_bytes <> req_bytes) {:ok, response} = :gen_tcp.recv(socket, 0) :gen_tcp.close(socket) response end
额外修正:统一Socket路径
修改Go服务端的输出语句,确保路径与客户端一致:
fmt.Printf("Server listening on %s\n", socket) // 输出/tmp/ipc_test.sock
内容的提问来源于stack exchange,提问作者Moon
相关产品推荐
相关产品推荐

