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:
- Start your target WebSocket server on
ws://127.0.0.1:8080/abc - Install
wscatvia npm:npm install -g wscat - Connect to your proxy:
wscat -c ws://127.0.0.1:4000/api - 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/1to detect if the target server connection drops, so we can cleanly close the client connection. gunis asynchronous, so we usereceiveto 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

