├── test ├── test_helper.exs └── reagent_test.exs ├── .gitignore ├── examples └── echo.ex ├── lib ├── reagent │ ├── profile.ex │ ├── behaviour.ex │ ├── connection.ex │ └── listener.ex └── reagent.ex ├── mix.exs ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /ebin 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | procs.profile 7 | total.profile 8 | -------------------------------------------------------------------------------- /test/reagent_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ReagentTest do 2 | use ExUnit.Case 3 | 4 | test "the truth" do 5 | assert(true) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/echo.ex: -------------------------------------------------------------------------------- 1 | defmodule Echo do 2 | use Reagent 3 | 4 | def handle(conn) do 5 | case conn |> Socket.Stream.recv! do 6 | nil -> 7 | :closed 8 | 9 | data -> 10 | conn |> Socket.Stream.send!(data) 11 | 12 | handle(conn) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/reagent/profile.ex: -------------------------------------------------------------------------------- 1 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | # Version 2, December 2004 3 | # 4 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 5 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | # 7 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 8 | 9 | defmodule Reagent.Profile do 10 | def start do 11 | :eprof.start 12 | :eprof.start_profiling [Kernel.self] 13 | end 14 | 15 | def stop do 16 | :eprof.stop_profiling() 17 | :eprof.log('procs.profile') 18 | :eprof.analyze(:procs) 19 | :eprof.log('total.profile') 20 | :eprof.analyze(:total) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Reagent.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :reagent, 6 | version: "0.1.14", 7 | deps: deps(), 8 | package: package(), 9 | description: "You need more reagents to conjure this server" ] 10 | end 11 | 12 | # Configuration for the OTP application 13 | def application do 14 | [ applications: [:exts, :socket] ] 15 | end 16 | 17 | defp deps do 18 | [ { :socket, "~> 0.3" }, 19 | { :exts, "~> 0.3" }, 20 | { :ex_doc, "~> 0.14", only: [:dev] } ] 21 | end 22 | 23 | defp package do 24 | [ maintainers: ["meh"], 25 | licenses: ["WTFPL"], 26 | links: %{"GitHub" => "https://github.com/meh/reagent"} ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"datastructures": {:hex, :datastructures, "0.2.8", "cd9f53bed18f1d9526689a9559676c682b5a3ba230c1e3de187182f4ff86899c", [:mix], []}, 2 | "derp": {:git, "git://github.com/meh/derp.git", "7ed055d860292cb590d6505f44876c0311eef546", []}, 3 | "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 5 | "exts": {:hex, :exts, "0.3.4", "ec2af096ab4494c087a9615fffe9d5ba8a0fa0ccc465b340ee522ed7aff79078", [:mix], [{:datastructures, "~> 0.2", [hex: :datastructures, optional: false]}]}, 6 | "socket": {:hex, :socket, "0.3.10", "a615c308e681de78595ec98189a27aa2ef9b0fe8ce400c560d8185b366f13eae", [:mix], []}} 7 | -------------------------------------------------------------------------------- /lib/reagent/behaviour.ex: -------------------------------------------------------------------------------- 1 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | # Version 2, December 2004 3 | # 4 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 5 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | # 7 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 8 | 9 | defmodule Reagent.Behaviour do 10 | alias Reagent.Listener 11 | alias Reagent.Connection 12 | 13 | @doc """ 14 | Accept a client connection from the listener. 15 | """ 16 | @callback accept(Listener.t) :: { :ok, Socket.t } | { :error, term } 17 | 18 | @doc """ 19 | Start the process that will handle the connection, either define this or `handle/1`. 20 | """ 21 | @callback start(Connection.t) :: :ok | { :ok, pid } | { :error, term } 22 | 23 | @doc """ 24 | Handle the connection, either define this or `start/1`. 25 | """ 26 | @callback handle(Connection.t) :: :ok | { :error, term } 27 | 28 | @doc false 29 | def start_link(pool, listener) do 30 | Kernel.spawn_link __MODULE__, :run, [pool, listener] 31 | end 32 | 33 | @doc """ 34 | Uses the reagent behaviour and defines the default callbacks. 35 | """ 36 | defmacro __using__(_opts) do 37 | quote location: :keep do 38 | @behaviour unquote(__MODULE__) 39 | 40 | def accept(listener) do 41 | listener.socket |> Socket.accept 42 | end 43 | 44 | defoverridable accept: 1 45 | 46 | def start(conn) do 47 | :ok 48 | end 49 | 50 | defoverridable start: 1 51 | 52 | def handle(conn) do 53 | nil 54 | end 55 | 56 | defoverridable handle: 1 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/reagent.ex: -------------------------------------------------------------------------------- 1 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | # Version 2, December 2004 3 | # 4 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 5 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | # 7 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 8 | 9 | defmodule Reagent do 10 | @doc """ 11 | Uses the reagent behaviour and defines the default callbacks. 12 | """ 13 | defmacro __using__(_opts) do 14 | quote location: :keep do 15 | use Reagent.Behaviour 16 | end 17 | end 18 | 19 | @doc """ 20 | Start a listener with the given module and the given descriptor. 21 | """ 22 | @spec start(module, Keyword.t) :: { :ok, pid } | { :error, term } 23 | def start(module, descriptor) do 24 | start(Keyword.merge(descriptor, module: module)) 25 | end 26 | 27 | @doc """ 28 | Start a listener with the given descriptor. 29 | """ 30 | @spec start(Keyword.t) :: { :ok, pid } | { :error, term } 31 | def start(descriptor) do 32 | Reagent.Listener.start(descriptor) 33 | end 34 | 35 | @doc """ 36 | Start a listener with the given module and the given descriptor and link the 37 | process. 38 | """ 39 | @spec start_link(module, Keyword.t) :: { :ok, pid } | { :error, term } 40 | def start_link(module, descriptor) do 41 | start_link(Keyword.merge(descriptor, module: module)) 42 | end 43 | 44 | @doc """ 45 | Start a listener with the given descriptor and link the process. 46 | """ 47 | @spec start_link(Keyword.t) :: { :ok, pid } | { :error, term } 48 | def start_link(descriptor) do 49 | Reagent.Listener.start_link(descriptor) 50 | end 51 | 52 | @doc """ 53 | Wait for the accept ack. 54 | """ 55 | @spec wait(timeout) :: :ok | { :timeout, timeout } 56 | def wait(timeout \\ :infinity) do 57 | receive do 58 | { Reagent, :ack } -> 59 | :ok 60 | after timeout -> 61 | { :timeout, timeout } 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/reagent/connection.ex: -------------------------------------------------------------------------------- 1 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | # Version 2, December 2004 3 | # 4 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 5 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | # 7 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 8 | 9 | defmodule Reagent.Connection do 10 | alias __MODULE__, as: C 11 | 12 | defstruct [:socket, :id, :listener] 13 | 14 | @opaque t :: %Reagent.Connection{} 15 | 16 | require Record 17 | 18 | @doc false 19 | def new(descriptor) do 20 | id = make_ref() 21 | socket = Keyword.fetch! descriptor, :socket 22 | listener = Keyword.fetch! descriptor, :listener 23 | 24 | %C{socket: socket, id: id, listener: listener} 25 | end 26 | 27 | @doc """ 28 | Get the `Connection` id. 29 | """ 30 | @spec id(t) :: reference 31 | def id(%__MODULE__{id: id}) do 32 | id 33 | end 34 | 35 | @doc """ 36 | Get the `Connection` listener. 37 | """ 38 | @spec listener(t) :: Reagent.Listener.t 39 | def listener(%__MODULE__{listener: listener}) do 40 | listener 41 | end 42 | 43 | @doc """ 44 | Get the environment for the connection. 45 | """ 46 | @spec env(t) :: term 47 | def env(self) do 48 | Reagent.Listener.env_for(self.listener, self.id) 49 | end 50 | 51 | @doc """ 52 | Set the environment for the connection. 53 | """ 54 | @spec env(t, term) :: term 55 | def env(self, value) do 56 | Reagent.Listener.env_for(self.listener, self.id, value) 57 | end 58 | 59 | @doc """ 60 | Check if the connection is secure or not. 61 | """ 62 | @spec secure?(t) :: boolean 63 | def secure?(%C{socket: socket}) when socket |> is_port, do: false 64 | def secure?(%C{socket: socket}) when socket |> Record.is_record(:sslsocket), do: true 65 | 66 | @doc """ 67 | Get the SSL next negotiated protocol. 68 | """ 69 | @spec negotiated_protocol(t) :: nil | String.t 70 | def negotiated_protocol(%C{socket: socket}) when socket |> Record.is_record(:sslsocket) do 71 | socket |> Socket.SSL.negotiated_protocol 72 | end 73 | 74 | def negotiated_protocol(_) do 75 | nil 76 | end 77 | 78 | defimpl Socket.Protocol do 79 | use Socket.Helpers 80 | 81 | defwrap equal?(self, other) 82 | 83 | defwrap accept(self) 84 | defwrap accept(self, options) 85 | 86 | defwrap options(self, options) 87 | defwrap packet(self, type) 88 | defwrap process(self, pid) 89 | 90 | defwrap active(self) 91 | defwrap active(self, mode) 92 | defwrap passive(self) 93 | 94 | defwrap local(self) 95 | defwrap remote(self) 96 | 97 | defwrap close(self) 98 | end 99 | 100 | defimpl Socket.Stream.Protocol do 101 | use Socket.Helpers 102 | 103 | defwrap send(self, data) 104 | defwrap file(self, path, options) 105 | 106 | defwrap recv(self) 107 | defwrap recv(self, length_or_options) 108 | defwrap recv(self, length, options) 109 | 110 | defwrap close(self) 111 | 112 | defwrap shutdown(self) 113 | defwrap shutdown(self, how) 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | reagent - socket acceptor pool 2 | ============================== 3 | **reagent** is a socket acceptor pool for Elixir that leverages the 4 | [socket](https://github.com/meh/elixir-socket) library and its protocols to 5 | provide an easy way to implement servers. 6 | 7 | Getting started 8 | --------------- 9 | To define a reagent you first have to define a module using the reagent 10 | behaviour. This will define some basic functions you can extend and other 11 | helpers on the module and will make it startable as a reagent. 12 | 13 | ```elixir 14 | defmodule Test do 15 | use Reagent.Behaviour 16 | end 17 | ``` 18 | 19 | When you want to start a server running the defined reagent, you have to call 20 | `Reagent.start`. It takes as first parameter the module implementing the 21 | behaviour and as second parameter a listener descriptor. 22 | 23 | Listener descriptors contain the definition of the listener, including port, 24 | whether they're secure or not, other socket options and starting environment. 25 | 26 | Reagent behaviour 27 | ----------------- 28 | A reagent to do anything useful has to either implement `handle/1` or `start/1`. 29 | 30 | `handle/1` is called by the default `start/1` and it gets called as a 31 | replacement for the acceptor process. It gets called with a 32 | `Reagent.Connection` record. 33 | 34 | This is usually useful to implement simple protocols when you don't need a full 35 | blown `gen_server` or similar to handle a connection. 36 | 37 | If you want more complex connection handling you can define `start/1`, it gets 38 | called with a `Reagent.Connection` record as well and must return `{ :ok, pid 39 | }` or `{ :error, reason }`. The returned process will be made owner of the 40 | socket and be used as reference for the connection itself. 41 | 42 | You can also define `accept/1` which gets called with the `Reagent.Listener` 43 | and allows you more fine grained socket acception. 44 | 45 | Simple example 46 | -------------- 47 | ```elixir 48 | defmodule Echo do 49 | use Reagent 50 | 51 | def handle(conn) do 52 | case conn |> Socket.Stream.recv! do 53 | nil -> 54 | :closed 55 | 56 | data -> 57 | conn |> Socket.Stream.send! data 58 | 59 | handle(conn) 60 | end 61 | end 62 | end 63 | ``` 64 | 65 | This is a simple implementation of an echo server. 66 | 67 | To start it on port 8080 just run `Reagent.start Echo, port: 8080`. 68 | 69 | Complex example 70 | --------------- 71 | ```elixir 72 | defmodule Echo do 73 | use Reagent 74 | 75 | def start(connection) do 76 | GenServer.start __MODULE__, connection, [] 77 | end 78 | 79 | use GenServer 80 | 81 | def init(connection) do 82 | { :ok, connection } 83 | end 84 | 85 | # this message is sent when the socket has been completely accepted and the 86 | # process has been made owner of the socket, you don't need to wait for it 87 | # when implementing handle because it's internally handled 88 | def handle_info({ Reagent, :ack }, connection) do 89 | connection |> Socket.active! 90 | 91 | { :noreply, connection } 92 | end 93 | 94 | def handle_info({ :tcp, _, data }, connection) do 95 | connection |> Socket.Stream.send! data 96 | 97 | { :noreply, connection } 98 | end 99 | 100 | def handle_info({ :tcp_closed, _ }, connection) do 101 | { :stop, :normal, connection } 102 | end 103 | end 104 | ``` 105 | 106 | This is the implementation of a full-blown `gen_server` based echo server 107 | (which is obviously overkill). 108 | 109 | As with the simple example you just start it with `Reagent.start Echo, port: 110 | 8080`. 111 | -------------------------------------------------------------------------------- /lib/reagent/listener.ex: -------------------------------------------------------------------------------- 1 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | # Version 2, December 2004 3 | # 4 | # DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 5 | # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 6 | # 7 | # 0. You just DO WHAT THE FUCK YOU WANT TO. 8 | 9 | defmodule Reagent.Listener do 10 | defstruct socket: nil, id: nil, module: nil, port: nil, secure: nil, options: [], 11 | env: nil, acceptors: nil, connections: nil, waiting: nil 12 | 13 | @opaque t :: %Reagent.Listener{} 14 | 15 | use GenServer 16 | use Data 17 | 18 | @doc """ 19 | Get the id. 20 | """ 21 | @spec id(t) :: reference 22 | def id(%__MODULE__{id: id}) do 23 | id 24 | end 25 | 26 | @doc """ 27 | Get the port. 28 | """ 29 | @spec port(t) :: term 30 | def port(%__MODULE__{port: port}) do 31 | port 32 | end 33 | 34 | @doc """ 35 | Get the environment for the listener. 36 | """ 37 | @spec env(pid | t) :: term 38 | def env(%__MODULE__{} = self) do 39 | self.env |> Dict.get(self.id) 40 | end 41 | 42 | def env(id) do 43 | GenServer.call(id, :env) |> Dict.get(id) 44 | end 45 | 46 | @doc """ 47 | Set the environment for the listener. 48 | """ 49 | @spec env(pid | t, reference | term) :: term 50 | def env(%__MODULE__{} = self, value) do 51 | self.env |> Dict.put(self.id, value) 52 | 53 | value 54 | end 55 | 56 | def env(id, value) do 57 | GenServer.call(id, :env) |> Dict.put(id, value) 58 | 59 | value 60 | end 61 | 62 | @doc false 63 | def env_for(self, conn) do 64 | self.env |> Dict.get(conn) 65 | end 66 | 67 | @doc false 68 | def env_for(self, conn, value) do 69 | self.env |> Dict.put(conn, value) 70 | 71 | value 72 | end 73 | 74 | @doc """ 75 | Check if the connection is secure or not. 76 | """ 77 | @spec secure?(t) :: boolean 78 | def secure?(%__MODULE__{secure: nil}), do: false 79 | def secure?(%__MODULE__{}), do: true 80 | 81 | @doc """ 82 | Get the certificate of the listener. 83 | """ 84 | @spec cert(t) :: String.t 85 | def cert(%__MODULE__{secure: nil}), do: nil 86 | def cert(%__MODULE__{secure: sec}), do: sec[:cert] 87 | 88 | @doc false 89 | def start(descriptor) do 90 | GenServer.start __MODULE__, descriptor 91 | end 92 | 93 | @doc false 94 | def start_link(descriptor) do 95 | GenServer.start_link __MODULE__, descriptor 96 | end 97 | 98 | @doc false 99 | def init(descriptor) do 100 | if Keyword.get(descriptor, :profile) do 101 | Reagent.Profile.start 102 | end 103 | 104 | id = Kernel.self 105 | module = Keyword.fetch! descriptor, :module 106 | port = Keyword.fetch! descriptor, :port 107 | secure = Keyword.get descriptor, :secure 108 | acceptors = Keyword.get descriptor, :acceptors, 100 109 | options = Keyword.get descriptor, :options, [] 110 | 111 | socket = if secure do 112 | Socket.SSL.listen port, to_options(options, secure) 113 | else 114 | Socket.TCP.listen port, to_options(options) 115 | end 116 | 117 | case socket do 118 | { :ok, socket } -> 119 | Process.flag :trap_exit, true 120 | 121 | dict = Exts.Dict.new(access: :public) 122 | 123 | Seq.each Keyword.get(descriptor, :env), fn 124 | { key, value } -> 125 | dict |> Dict.put(key, value) 126 | end 127 | 128 | listener = %__MODULE__{ 129 | socket: socket, 130 | id: id, 131 | module: module, 132 | port: port, 133 | secure: secure, 134 | options: options, 135 | env: dict, 136 | acceptors: MapSet.new, 137 | connections: Map.new, 138 | waiting: :queue.new } 139 | 140 | GenServer.cast Kernel.self, { :acceptors, acceptors } 141 | 142 | { :ok, listener } 143 | 144 | { :error, reason } -> 145 | { :stop, reason } 146 | end 147 | end 148 | 149 | defp to_options(options) do 150 | options |> Keyword.merge(mode: :passive) 151 | end 152 | 153 | defp to_options(options, secure) do 154 | options |> Keyword.merge(secure) |> Keyword.merge(mode: :passive) 155 | end 156 | 157 | @doc false 158 | def terminate(_reason, self) do 159 | self.socket |> Socket.close 160 | end 161 | 162 | @doc false 163 | def handle_call(:env, _from, %__MODULE__{env: env} = listener) do 164 | { :reply, env, listener } 165 | end 166 | 167 | def handle_call(:wait, from, self) do 168 | case Keyword.fetch(self.options, :max_connections) do 169 | :error -> 170 | { :reply, :ok, self } 171 | 172 | { :ok, max } -> 173 | if Data.count(self.connections) >= max do 174 | { :noreply, %__MODULE__{self | waiting: :queue.in(from, self.waiting)} } 175 | else 176 | { :reply, :ok, self } 177 | end 178 | end 179 | end 180 | 181 | @doc false 182 | def handle_cast({ :acceptors, number }, self) when number > 0 do 183 | pids = Enum.map(1 .. number, fn _ -> 184 | { :ok, pid } = Task.start_link __MODULE__, :acceptor, [self]; pid 185 | end) |> Enum.into(MapSet.new) 186 | 187 | { :noreply, %__MODULE__{self | acceptors: Set.union(self.acceptors, pids)} } 188 | end 189 | 190 | def handle_cast({ :acceptors, number }, self) when number < 0 do 191 | { keep, drop } = Enum.split(self.acceptors, -number) 192 | 193 | Enum.each drop, fn pid -> 194 | Process.exit pid, :drop 195 | end 196 | 197 | { :noreply, %__MODULE__{self | acceptors: keep |> Enum.into(MapSet.new)} } 198 | end 199 | 200 | def handle_cast({ :accepted, pid, conn }, self) do 201 | { :noreply, %__MODULE__{self | connections: self.connections |> Dict.put(Process.monitor(pid), conn)} } 202 | end 203 | 204 | @doc false 205 | def handle_info({ :EXIT, pid, _reason }, self) do 206 | acceptors = self.acceptors |> Set.delete(pid) 207 | |> Set.add(Kernel.spawn_link(__MODULE__, :acceptor, [self])) 208 | 209 | { :noreply, %__MODULE__{self | acceptors: acceptors } } 210 | end 211 | 212 | def handle_info({ :DOWN, ref, _type, _object, _info }, self) do 213 | connection = self.connections |> Dict.get(ref) 214 | connections = self.connections |> Dict.delete(ref) 215 | 216 | connection |> Socket.close 217 | Dict.delete(self.env, connection.id) 218 | 219 | case :queue.out(self.waiting) do 220 | { :empty, queue } -> 221 | { :noreply, %__MODULE__{self | connections: connections, waiting: queue} } 222 | 223 | { { :value, from }, queue } -> 224 | GenServer.reply(from, :ok) 225 | 226 | { :noreply, %__MODULE__{self | connections: connections, waiting: queue} } 227 | end 228 | end 229 | 230 | @doc false 231 | def acceptor(self) do 232 | wait(self) 233 | 234 | case self.module.accept(self) do 235 | { :ok, socket } -> 236 | conn = Reagent.Connection.new(listener: self, socket: socket) 237 | 238 | case self.module.start(conn) do 239 | :ok -> 240 | self.module.handle(conn) 241 | 242 | { :ok, pid } -> 243 | socket |> Socket.process!(pid) 244 | pid |> send({ Reagent, :ack }) 245 | 246 | GenServer.cast self.id, { :accepted, pid, conn } 247 | 248 | acceptor(self) 249 | 250 | { :error, reason } -> 251 | exit reason 252 | end 253 | 254 | { :error, reason } -> 255 | exit reason 256 | end 257 | end 258 | 259 | defp wait(self) do 260 | GenServer.call self.id, :wait 261 | end 262 | 263 | defimpl Socket.Protocol do 264 | use Socket.Helpers 265 | 266 | defwrap equal?(self, other) 267 | 268 | defwrap accept(self) 269 | defwrap accept(self, options) 270 | 271 | defwrap options(self, options) 272 | defwrap packet(self, type) 273 | defwrap process(self, pid) 274 | 275 | defwrap active(self) 276 | defwrap active(self, mode) 277 | defwrap passive(self) 278 | 279 | defwrap local(self) 280 | defwrap remote(self) 281 | 282 | defwrap close(self) 283 | end 284 | 285 | defimpl Socket.Stream.Protocol do 286 | use Socket.Helpers 287 | 288 | defwrap send(self, data) 289 | defwrap file(self, path, options) 290 | 291 | defwrap recv(self) 292 | defwrap recv(self, length_or_options) 293 | defwrap recv(self, length, options) 294 | 295 | defwrap close(self) 296 | 297 | defwrap shutdown(self) 298 | defwrap shutdown(self, how) 299 | end 300 | end 301 | --------------------------------------------------------------------------------