Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Websockets #17

Closed
wants to merge 2 commits into
from
Jump to file or symbol
Failed to load files and symbols.
+271 −7
Split
@@ -0,0 +1,22 @@
+defmodule Phoenix.Adapters.Cowboy do
+ def setup_options(module, options, dispatch_options) do
+ dispatch = Enum.concat [dispatch_options,
+ Dict.get(options, :dispatch, []),
+ [{:_, Plug.Adapters.Cowboy.Handler, { module, [] }}]]
+
+ Dict.put(options, :dispatch, [{:_, dispatch}])
+ end
+
+ defmacro __using__(_options) do
+ quote do
+ Module.register_attribute __MODULE__, :dispatch_options, accumulate: true,
+ persist: false
+ import unquote(__MODULE__)
+ end
+ end
+ defmacro cowboy_dispatch(path, handler, options \\ []) do
+ quote do
+ @dispatch_options {unquote(path), unquote(handler), unquote(options)}
+ end
+ end
+end
@@ -57,3 +57,10 @@ defmodule Phoenix.Examples.Controllers.Files do
end
end
+defmodule Phoenix.Examples.Controllers.Echo do
+ use Phoenix.Websocket.RawHandler
+
+ def stream(data, socket, _state) do
+ reply socket, data
+ end
+end
@@ -11,5 +11,7 @@ defmodule Router do
resources "users", Users do
resources "comments", Comments
end
+
+ raw_websocket "/echo", Eco
end
end
@@ -7,9 +7,12 @@ defmodule Phoenix.Router do
defmacro __using__(plug_adapter_options \\ []) do
quote do
use Phoenix.Router.Mapper
+ use Phoenix.Adapters.Cowboy
+ import Phoenix.Router.RawWebsocketMapper
+
+ import unquote(__MODULE__)
@before_compile unquote(__MODULE__)
use Plug.Builder
- import unquote(__MODULE__)
@options unquote(plug_adapter_options)
end
@@ -26,8 +29,10 @@ defmodule Phoenix.Router do
end
def start do
- IO.puts ">> Running #{__MODULE__} with Cowboy with #{inspect @options}"
- Plug.Adapters.Cowboy.http __MODULE__, [], @options
+ options = Phoenix.Adapters.Cowboy.setup_options(__MODULE__, @options, @dispatch_options)
+
+ IO.puts "Running #{__MODULE__} with Cowboy on port #{inspect options}"
+ Plug.Adapters.Cowboy.http __MODULE__, [], options
end
end
end
@@ -38,7 +43,6 @@ defmodule Phoenix.Router do
http_method = conn.method |> String.downcase |> binary_to_atom
split_path = Path.split_from_conn(conn)
-
request = Dispatcher.Request.new(conn: conn,
router: router,
http_method: http_method,
@@ -50,6 +54,17 @@ defmodule Phoenix.Router do
{:error, reason} -> Controller.error(conn, reason)
end
end
-end
+ def dispatch(conn, router, http_method, path) do
+ request = Dispatcher.Request.new(conn: conn,
+ router: router,
+ http_method: http_method,
+ path: path)
+ {:ok, pid} = Dispatcher.Client.start(request)
+ case Dispatcher.Client.dispatch(pid) do
+ {:ok, conn} -> {:ok, conn}
+ {:error, reason} -> Controller.error(conn, reason)
+ end
+ end
+end
@@ -0,0 +1,7 @@
+defmodule Phoenix.Router.RawWebsocketMapper do
+ defmacro raw_websocket(path, handler, options \\ []) do
+ quote do
+ cowboy_dispatch unquote(path), unquote(handler), unquote(options)
+ end
+ end
+end
@@ -0,0 +1,166 @@
+defmodule Phoenix.Websocket.RawHandler do
+ @moduledoc """
+ This module is a convenience for setting up a basic cowboy websocket.
+
+ ## Example
+
+ Below is an example of an EchoServer websocket.
+ defmodule Router do
+ use Phoenix.Router
+ raw_websocket "echo", Websocket
+ end
+
+ defmodule Websocket do
+ use Phoenix.Websocket.RawHandler
+ def receive(conn, data, state) do
+ conn.send(data)
+ end
+ end
+
+ There are 4 callbacks you can override to handle the common functions of a websocket.
+ start(transport, req, opts)
+ stream(data, conn, state)
+ info(data, conn, state)
+ closed(data, conn, state)
+
+ Each function is an alias for the websocket handler functions, for more detailed information on
+ connections and what you should/could return from these functions check out the cowboy websocket documentation.
+ http://ninenines.eu/docs/en/cowboy/HEAD/manual/cowboy_websocket_handler/
+
+ Things to keep in mind:
+ * Connections will die quite often for any number of reasons. Keep your session and state data in another process.
+ * This is a raw websocket and it may make sense to look into more robust solutions such as [bullet](https://github.com/extend/bullet)
+
+ """
+ defmacro __using__(opts \\ []) do
+
+ transport = Dict.get(opts, :transport, :tcp)
+ unless transport in [:tcp, :ssl] do
+ raise "Websocket transport needs to be :tcp or :ssl. Please refer to websocket documentation."
+ end
+
+ quote location: :keep do
+ @behaviour :cowboy_websocket_handler
+
+ defrecord Socket, conn: nil, pid: nil
+
+ import unquote(__MODULE__)
+
+ @doc false
+ def init({unquote(transport), :http}, req, opts) do
+ {:upgrade, :protocol, :cowboy_websocket, req, opts}
+ end
+
+ @doc false
+ def websocket_init(transport, req, opts) do
+ case start(transport, Socket.new(conn: req, pid: self()), opts) do
+ {:ok, state} -> {:ok, req, state}
+ {:ok, state, timeout} -> {:ok, req, state, timeout}
+ _ -> {:ok, req, :undefined_state}
+ end
+ end
+
+ @doc """
+ Handles initalization of the websocket
+
+ Possible returns:
+ :ok
+ {:ok, state}
+ {:ok, state, timeout} # Timeout defines how long it waits for activity from the client. Default: infinity.
+ """
+ def start(_transport, req, _opts) do
+ {:ok, req, :undefined_state}
+ end
+
+ @doc """
+ Handles handles recieving data from the client, default implementation does nothing.
+ """
+ def stream(data, req, state) do
+ {:ok, req, state}
+ end
+
+ @doc false
+ def websocket_handle(data, req, state) do
+ stream(data, Socket.new(conn: req, pid: self()), state)
+ {:ok, req, state}
+ end
+
+ @doc """
+ Handles handles recieving messages from erlang processes. Default returns
+ {:ok, state}
+ Possible Returns are identical to stream, all replies gets send to the client.
+ """
+ def info(conn, info, state) do
+ {:ok, state}
+ end
+
+ @doc false
+ def websocket_info({:send, frame, state}, req, _state) do
+ {:reply, frame, req, state}
+ end
+ @doc false
+ def websocket_info(:shutdown, req, state) do
+ {:shutdown, req, state}
+ end
+ @doc false
+ def websocket_info(:hibernate, req, state) do
+ {:ok, req, state, :hibernate}
+ end
+
+ @doc false
+ def websocket_info(data, req, state) do
+ info(data, Socket.new(conn: req, pid: self()), state)
+ {:ok, req, state}
+ end
+
+ @doc """
+ This is called right before the websocket is about to be closed.
+ Reason is defined as:
+ {:normal, :shutdown | :timeout} # Called when erlang closes connection
+ {:remote, :closed} # Called if the client formally closes connection
+ {:remote, close_code(), binary()}
+ {:error, :badencoding | :badframe | :closed | atom()} # Called for many reasons: tab closed, connection dropped.
+ """
+ def closed(_reason, req, state) do
+ :ok
+ end
+
+ @doc false
+ def websocket_terminate(reason, req, state) do
+ closed(reason, Socket.new(conn: req), state)
+ end
+
+ defoverridable [start: 3, stream: 3, info: 3, closed: 3]
+ end
+ end
+
+ @doc """
+ Sends a reply to the socket. Follow the cowboy websocket frame syntax
+ Frame is defined as
+ :close | :ping | :pong
+ {:text | :binary | :close | :ping | :pong, iodata()}
+ {:close, close_code(), iodata()}
+ Options:
+ :state
+ :hibernate # (true | false) if you want to hibernate the connection
+ close_code: 1000..4999
+ """
+ def reply(socket, frame, state \\ []) do
+ send(socket.pid, {:send, frame, state})
+ end
+
+ @doc """
+ Terminates a connection.
+ """
+ def terminate(socket) do
+ send(socket.pid, :shutdown)
+ end
+
+ @doc """
+ Hibernates the socket.
+ """
+ def hibernate(socket) do
+ send(socket.pid, :hibernate)
+ end
+end
+
View
@@ -1,5 +1,5 @@
-[ "cowboy": {:git, "git://github.com/extend/cowboy.git", "0ec713fc4b185c3cd0f6b2e7ec2c5f198361bddd", []},
- "cowlib": {:git, "git://github.com/extend/cowlib.git", "63298e8e160031a70efff86a1acde7e7db1fcda6", [ref: "0.4.0"]},
+[ "cowboy": {:git, "git://github.com/extend/cowboy.git", "e7afe1f381f93f0c4df3680ff55dae111e864c9f", []},
+ "cowlib": {:git, "git://github.com/extend/cowlib.git", "0d4ece08a7cce90a07cf33d4edad29bc324c7d90", [ref: "0.5.1"]},
"ex_doc": {:git, "git://github.com/elixir-lang/ex_doc.git", "ee01fe3efd7b8af1dbaa00946bad5817d57b5875", []},
"inflex": {:git, "git://github.com/nurugger07/inflex.git", "b1c8b1d2647867b9171dbf3e6a3a4ab77ad0617e", []},
"mime": {:git, "git://github.com/dynamo/mime.git", "db84370c53a67a7e58a4d0cfb026f4edc64a9367", []},
@@ -0,0 +1,24 @@
+defmodule Phoenix.Adapters.CowboyTest do
+ use ExUnit.Case, async: true
+ alias Phoenix.Adapters.Cowboy
+
+ test "only modifies dispatch option" do
+ options = ListDict.new port: 5000
+ assert Cowboy.setup_options(Pheonix, options, [])[:port] == 5000
+ end
+
+ test "add users dispatch directly to the options" do
+ options = Cowboy.setup_options(Phoenix, [dispatch: [{:hello}]], [])
+ assert {:hello} in options[:dispatch][:_]
+ end
+
+ test "add dispatch_options to the list" do
+ options = Cowboy.setup_options(Phoenix, [], [:my_dispatch])
+ assert :my_dispatch in options[:dispatch][:_]
+ end
+
+ test "adds default plug adapter and points to our module" do
+ options = Cowboy.setup_options(Phoenix, [], [])
+ assert options[:dispatch][:_] == [{:_, Plug.Adapters.Cowboy.Handler, {Phoenix, []}}]
+ end
+end
@@ -0,0 +1,21 @@
+defmodule Phoenix.Controller.WebsocketTest do
+ use ExUnit.Case, async: true
+ import Phoenix.Websocket.RawHandler
+
+ defrecord Socket, conn: nil, pid: nil
+
+ test "verify correct return from terminate" do
+ terminate(Socket.new(pid: self()))
+ assert_received :shutdown
+ end
+
+ test "verify correct return from hibernate" do
+ hibernate(Socket.new(pid: self))
+ assert_received :hibernate
+ end
+
+ test "verify basic reply" do
+ reply(Socket.new(pid: self), {:text, "hello"})
+ assert_received {:send, {:text, "hello"}, []}
+ end
+end