WebSockex - An Elixir WebSocket client

Just because your behaviour is internally powered by GenServer doesn’t mean it has to expose everything GenServer does. So for example, if the timeout option doesn’t make sense in WebSockex, you don’t need to support it in your own behaviour.

Hey @michalmuskala and @sasajuric, thanks for the input. I feel like I understood what you’re saying at the time that I made my decision. But none of what I am thinking brings back a good enough reason to not use GenServer. Even if there was, in it’s current state WebSockex has no reason not to be a GenServer.

So I either completely forgot what it was I tried to avoid or I was wrong and my reasons weren’t good enough in the first place.

Let me think on it for a couple of days. If anything comes to mind, I’ll let you know.

Glancing at your code, one reason which comes to mind is that with async mode of connecting, you’ll invoke init_ack in the middle of your initialization. This is somewhat clumsy to support with GenServer, especially at the library level, when you’re not aware whether the server process will be named or not.

One solution might be to start the task from init/1 which will establish the connection, perform the initial handshake, and then give the ownership to the parent with controlling_process.

I’m not very familiar with websocket details, so maybe I’m missing some possible caveats, but I think this might take off. The main benefit here is that your initialization then becomes straightforward, because you don’t need to ack in the middle, and get to keep async connecting and handshake.

WebSockex 0.3.0 Released!

Breaking Changes

  • The parameters for the handle_connect/2 callback have been reversed. The order is now (conn, state).

Enhancements

  • Added initial connection timeouts. (:socket_connect_timeout and :socket_recv_timeout)
    • Can be used as start or start_link option or as a Conn.new option.
    • :socket_connect_timeout - The timeout for opening a TCP connection.
    • :socket_recv_timeout - The timeout for receiving a HTTP response header.
  • start and start_link can now take a Conn struct in place of a url.
  • Added the ability to handle system messages while opening a connection.
  • Added the ability to handle parent exit messages while opening a connection.
  • Improve :sys.get_status, :sys.get_state, :sys.replace_state functions.
    • These are undocumented, but are meant primarily for debugging.

Bug Fixes

  • Ensure terminate/2 callback is called consistently.
  • Ensure when termination when a parent exit signal is received.
  • Add the system_code_change function so that the code_change callback is actually used.

Take a look at the v0.2.0..v0.3.0 diff or the release on hex.pm for more information.

3 Likes

I started down the path of starting in a different process and ended up realizing that I would need to use a gen_statem. :persevere:

As a result, I feel like the code is kind of a mess right now. But thinking about structuring it for an already defined gen module has helped out. It’s still something I’m trying to do, but wanted to get another release out.

2 Likes

Hi guys, I try to use WebSockex to connect to Socket.io server but I was unable to maintain heartbeat, Is it possible to do that?

1 Like

There’s no reason it can’t, but it doesn’t do so automatically. What exactly are you having a problem with?

1 Like

WebSockex 0.3.1 Released!

Enhancements

  • Handle system messages and parent exits while closing the connection.
  • The output from :sys.get_status didn’t look pretty in :observer; now it does!
  • format_status/2 now an optional callback.

Bug Fixes

  • SSL frames sent right after connecting will now be handled instead of being left in a dead Task mailbox.
  • Fixed some places where dialyzer told me I had my specs screwed up.

Take a look at the v0.3.0..v0.3.1 diff or the release on hex.pm for more information.

2 Likes

Just see Engine.io protocol now able to ping, thanks.

1 Like

Glad you figured it out. If there’s anything else that you have problems with feel free to ask.

1 Like

WebSockex 0.4.0 Released!

Just as a side note, I think the most exciting thing about this release is the addition of OTP based debugging capabilities. The debug messages themselves are useful but not perfect. If you would like more hooks or better output feel free to open an issue.

Breaking Changes

  • send_frame/2 is now synchronous and returns an error when connection is opening or closing.

Enhancements

  • Added debug printing for the :sys module.
  • Rework Conn.new to accept other protocols.
  • Added an InvalidFrameError for frames unrecognized by Frame.encode_frame.
  • Go through the disconnect cycle when there’s an error while with the {:reply, frame, state} callback response.
  • Send a close frame with the close code 1011 when there is an unexpected error. (Like an Exception in the middle of a callback)
  • Add a more specific error when the :websockex application hasn’t been started yet.
  • Added a :name options for local registration.

Bug Fixes

  • Fix a couple of places where the call stack wasn’t being properly tail-call optimized.-

Take a look at the v0.3.1...v0.4.0 diff or the release on hex.pm for more information

3 Likes

Hi, I’m using the last version, and I this work really great.

But I have a question probably a silly one how can I close the connection and terminate the process with :normal. I’m going to have a pool of clients and this is one of the things that I need. Other thing that I’m doing is handling disconnects inside the client so we reconnect automatically without terminating the process but that’s other thing we want.

Thanks in advance.

1 Like

Yeah, I thought about this and I couldn’t come up with a good API solution that I liked.

For now, you can use exit(:normal) in the terminate callback.

If you want to open an issue, I will either add documentation or come up with a better solution for the next release.

1 Like

OK thanks, could you summarize when the terminate callback is called?

1 Like

Hopefully anytime that the process is going to exit for whatever reason.

If there is a time that terminate isn’t called then it is considered a bug.

1 Like

@Azolo Is there a OS configuration necessary to be able to create 1K websockets connecting to the same Phoenix instance. I am getting this error

(MatchError) no match of right hand side value:

{:error, %WebSockex.ConnError{original: :closed}}

after about 200 processes created with this code

opts = [

  {:debug, [:trace]},
  {:socket_connect_timeout, Keyword.get(
    opts, :socket_connect_timeout, @socket_connect_timeout)},
  {:socket_recv_timeout, Keyword.get(
      opts, :socket_recv_timeout, @socket_recv_timeout)},
]

    WebSockex.start_link(conn_info, __MODULE__, state, [ {:debug, [:trace]} | opts] )
1 Like

I would guess that is probably an OS file descriptor limit, but other than that I don’t know.

Mt best guess would be to try increasing the maximum file descriptors on the server and client using ulimit -n.

1 Like

@Azolo thank you for the prompt response… I increased the number of “open files” to 200K but still no luck.
I changed the code to display a specific error in
websockex/websockex/conn.ex

 91   @spec socket_send(__MODULE__.t, binary) :: :ok | {:error, reason :: term}
 92   def socket_send(conn, message) do
 93     case conn.conn_mod.send(conn.socket, message) do
 94       :ok -> :ok
 95       {:error, error} -> {:error, %WebSockex.ConnError{original: "ERR_A"}}
 96     end
 97   end

so it looks like this is triggered in a “SEND”.
How would you recommend to debug the

“conn.conn_mod.send(conn.socket, message)” call?

===== Fragment of code which you can test

  5   def start(server, users) do
  6     all_users = 1..users
  7
  8     Logger.info("Simulator: registering #{users} users...")
  9     all_users
 10     |> Enum.map(fn i ->
 11       Task.async(fn ->
 12         {:ok, client} = WebSockex.start_link("ws://0.0.0.0:4000/socket/websocket", [])

 15       end)
 16     end)
 17     |> Enum.each(&Task.await/1)
1 Like

Ulimit does only increase the softlimit which can never be higher than the hard limit. The hardlimit needs to be increased first, on most systems you can do so via sysctl AFAIR.

Im in mobile so it’s hard to do a proper check right now.

3 Likes

How were you able to connect to the socket.io client? Do you have any sample code?

2 Likes