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

Elixir新手求助:如何用Cowboy 2.9.0实现WebSocket反向代理?

Hey there! Since you're new to Elixir and trying to set up a WebSocket reverse proxy with the specific versions you mentioned (cowboy 2.9.0, plug_cowboy 2.6.0, plug 1.14.0), let's walk through this step by step with actionable code and explanations.

Step 1: Create a WebSocket Proxy Handler Module

We'll build a module that implements Cowboy's WebSocket behaviour. This module will handle upgrading the client's HTTP request to WebSocket, connecting to your target server (ws://127.0.0.1:8080/abc), and forwarding messages between the two connections.

We'll use gun (the HTTP client that Cowboy relies on under the hood) to manage the connection to the target server.

defmodule MyApp.WebSocketProxy do
  @behaviour :cowboy_websocket

  # Target WebSocket server details
  @target_host '127.0.0.1'
  @target_port 8080
  @target_path "/abc"

  # Initialize the HTTP request and upgrade to WebSocket
  @impl true
  def init(req, _state) do
    # Pass through the client's WebSocket headers to the target server
    client_headers = :cowboy_req.headers(req)

    # Set up connection options for the target server
    gun_opts = [
      headers: client_headers,
      protocols: [:http],
      timeout: 5000
    ]

    # Attempt to connect to the target server
    case :gun.open(@target_host, @target_port, gun_opts) do
      {:ok, conn} ->
        # Send a WebSocket upgrade request to the target
        :gun.ws_upgrade(conn, @target_path, client_headers)

        # Wait for the upgrade response from the target
        receive do
          {:gun_upgrade, ^conn, _ref, ["websocket"], _headers} ->
            # Upgrade successful—enter WebSocket loop with the target connection
            {:cowboy_websocket, req, %{conn: conn}}

          {:gun_response, ^conn, _ref, status, _headers, _body} ->
            # Target rejected the upgrade—return the status code to the client
            req = :cowboy_req.reply(status, req)
            {:ok, req, %{}}

          {:gun_error, ^conn, _ref, _reason} ->
            # Connection error with target—return 502 Bad Gateway
            req = :cowboy_req.reply(502, req)
            {:ok, req, %{}}
        after
          5000 ->
            # Timeout connecting to target—return 504 Gateway Timeout
            req = :cowboy_req.reply(504, req)
            {:ok, req, %{}}
        end

      {:error, _reason} ->
        # Failed to connect to target—return 502
        req = :cowboy_req.reply(502, req)
        {:ok, req, %{}}
    end
  end

  # Post-WebSocket initialization setup
  @impl true
  def websocket_init(state) do
    # Monitor the target server connection to detect disconnections
    Process.monitor(state.conn)
    {:ok, state}
  end

  # Handle messages from the client
  @impl true
  def websocket_handle({:text, msg}, state) do
    # Forward text messages to the target server
    :gun.ws_send(state.conn, {:text, msg})
    {:ok, state}
  end

  def websocket_handle({:binary, msg}, state) do
    # Forward binary messages to the target server
    :gun.ws_send(state.conn, {:binary, msg})
    {:ok, state}
  end

  def websocket_handle(_unexpected_frame, state) do
    # Ignore any unhandled frame types
    {:ok, state}
  end

  # Handle messages from the target server or system events
  @impl true
  def websocket_info({:gun_ws, conn, _ref, frame}, %{conn: conn} = state) do
    # Forward messages from the target server back to the client
    {:reply, frame, state}
  end

  def websocket_info({:gun_error, conn, _ref, _reason}, %{conn: conn} = state) do
    # Target server had an error—close the client connection
    {:stop, :remote_error, state}
  end

  def websocket_info({:DOWN, _ref, :process, conn, _reason}, %{conn: conn} = state) do
    # Target server disconnected—close the client connection
    {:stop, :remote_closed, state}
  end

  def websocket_info(_unexpected_info, state) do
    # Ignore unhandled system messages
    {:ok, state}
  end

  # Clean up when the WebSocket connection terminates
  @impl true
  def terminate(_reason, _req, %{conn: conn}) do
    # Close the connection to the target server
    :gun.close(conn)
    :ok
  end

  def terminate(_reason, _req, _state) do
    :ok
  end
end

Step 2: Configure Your Plug Router

Update your router to forward WebSocket requests to /api to our new proxy handler.

defmodule MyApp.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  # Route WebSocket requests to /api to our proxy handler
  forward "/api", to: MyApp.WebSocketProxy, via: :websocket
end

Step 3: Verify Dependencies

Ensure your mix.exs has the correct dependencies (you mentioned these versions already, but double-check):

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  defp deps do
    [
      {:plug_cowboy, "~> 2.6.0"},
      {:plug, "~> 1.14.0"},
      {:cowboy, "~> 2.9.0"}
    ]
  end
end

Step 4: Start the Server

Make sure your application starts the Plug Cowboy server. Update lib/my_app/application.ex:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start the Cowboy server on port 4000
      {Plug.Cowboy, scheme: :http, plug: MyApp.Router, options: [port: 4000]}
    ]

    # Start the supervisor
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Testing the Proxy

You can test this using a tool like wscat:

  1. Start your target WebSocket server on ws://127.0.0.1:8080/abc
  2. Install wscat via npm: npm install -g wscat
  3. Connect to your proxy: wscat -c ws://127.0.0.1:4000/api
  4. Send messages—they should be forwarded to the target server, and any responses from the target should come back to your client.

A few notes for you as a new Elixir developer:

  • We use Process.monitor/1 to detect if the target server connection drops, so we can cleanly close the client connection.
  • gun is asynchronous, so we use receive to wait for the WebSocket upgrade response from the target.
  • The handler handles both text and binary WebSocket frames, which covers most common use cases.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 10:16:34