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

为何使用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)
问题原因
  1. 请求格式不兼容:Go服务是标准HTTP服务,要求客户端发送完整HTTP请求,但Elixir仅发送了路径字符串/?param=1,服务端无法解析请求,不会返回响应,导致客户端recv阻塞超时。
  2. 数据包协议不匹配:Elixir客户端设置了packet: 4(4字节长度前缀的数据包格式),但Go的HTTP服务使用流模式处理请求,双方协议不匹配,服务端无法正确读取请求内容。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 13:13:11