gwsocket(1)

πŸ“¦ version 0.4, in alpine 3.24

πŸ’– NAME πŸ’–

okay bestie, welcome to the gwsocket manual page ✨ gwsocket is a standalone, simple, yet powerful rfc6455 compliant WebSocket Server. periodt πŸ’…


✨ SYNOPSIS ✨

gwsocket -p [--addr][--origin][gwsocket options ...]

πŸ¦‹ DESCRIPTION πŸ¦‹

gwsocket is a free (MIT Licensed), standalone WebSocket Server 🫢 It sits between your application and the client’s browser, giving you bidirectional communication between those two with ease and flexibility. it’s giving middleman, but make it iconic πŸ’–

🌸 Start the Server 🌸

You can run gwsocket without passing any options to the command line. By default, it will listen on port 7890 on all the interfaces, e.g., 127.0.0.1, ::1, etc. For instance:

# gwsocket --access-log=/tmp/access.log
Note: See a basic client-side example at the bottom of the man page. πŸ‘€

πŸ‘‘ OPTIONS πŸ‘‘

  • -p, --port β€” Specifies the port to bind. πŸŽ€
  • -h, --help β€” Command line help. 🫢
  • -V, --version β€” Display version information and exit. ✨
  • --access-log=<path/file> β€” Specifies the path/file for the access log. πŸ“
  • --addr=<addr> β€” Specifies the address to bind. πŸ¦‹
  • --echo-mode β€” Set the server to echo all received messages. πŸ”
  • --max-frame-size=<bytes> β€” Maximum size of a websocket frame. This includes received frames from the client and messages through the named pipe. πŸ“¦
  • --origin=<origin> β€” Ensure clients send the specified origin header upon the WebSocket handshake. πŸ”
  • --pipein=<path/file> β€” Creates a named pipe (FIFO) that reads from on the given path/file. 🧚
  • --pipeout=<path/file> β€” Creates a named pipe (FIFO) that writes to the given path/file. 🧚
  • --std β€” Enable --stdin and --stdout. By default named pipes are used for I/O. This adds the option to use stdin / stdout as well. For example, this allows:
./gwsocket --std > log.txt

tail -F /var/log/syslog | ./gwsocket --std

--stdin and --stdout are added for fine grained control. πŸ’…

  • --stdin β€” Send stdin to the websocket. 🌸
  • --stdout β€” Send received websocket data to stdout. 🌈
  • --strict β€” Parse messages using strict mode. See below for more details. πŸ‘€
  • --ssl-cert=<cert.crt> β€” Path to TLS/SSL certificate. πŸ”’
  • --ssl-key=<priv.key> β€” Path to TLS/SSL private key. πŸ”‘
  • --unix-socket=<addr> β€” Specify UNIX-domain socket address to bind server to. 🧡

πŸŽ€ CONFIGURATION πŸŽ€

  • --enable-debug β€” Compile with debugging symbols and turn off compiler optimizations. πŸ› οΈ
  • --with-openssl β€” Compile gwsocket with OpenSSL support for its WebSocket server. πŸ”

πŸ’… DATA MODES πŸ’…

In order to establish a channel between your application and the client’s browser, gwsocket provides two methods that allow the user to send data in and out. The first one is through the use of the standard input (stdin) and the standard output (stdout) followed by the payload. See options below for more details. πŸ‘‘

1. stdin/stdout

The standard input/output is the simplest way of sending/receiving data to/from a client. However, it’s limited to broadcasting messages to all clients. To send messages to or receive from a specific client, use the strict mode in section 2. πŸ’–

1.1 Sending Data to all Clients β€” stdout πŸš€

If you need to broadcast data from your application to all clients connected to gwsocket, then the simplest way of doing it is by piping your application output into a named pipe (also known as FIFO) that gwsocket makes use of. Once gwsocket receives the payload, then it will automatically broadcast the message to all connected clients. ✨

1.1. Examples πŸ“

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int fd;
    char *myfifo = "/tmp/wspipein.fifo";
    const char *msg = "Message to broadcast";

    fd = open(myfifo, O_WRONLY);
    write(fd, msg, strlen(msg));
    close(fd);

    return 0;
}
Note: You can send as many bytes PIPE_BUF can hold. If a message is greater than PIPE_BUF, it would send the rest on a second message or third, and so on. See strict mode below for more control over messages. 🫢

1.2 Receiving Data from Clients β€” stdin 🌸

When a client sends a message to the server, it is possible to capture that message in your application. To do this, your application simply needs to read from a named pipe. By default, gwsocket creates a FIFO under /tmp/wspipeout.fifo. πŸŽ€

1.2. Examples πŸ“

Receiving data can be as simple as doing a # cat /tmp/wspipeout.fifo or you can do it in the language of your choice. See examples below. πŸ¦‹

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>

static void read_message (int fd, fd_set set) {
  int bytes = 0;
  char buf[PIPE_BUF] = { 0 };

  FD_ZERO (&set);
  FD_SET (fd, &set);

  if ((select (fd + 1, &set, NULL, NULL, NULL)) < 1)
    exit (1);
  if (!FD_ISSET (fd, &set))
    return;

  if (read (fd, buf, PIPE_BUF) > 0)
    printf ("%s0, buf);
}

int main (void) {
  fd_set set;
  char *fifo = "/tmp/wspipeout.fifo";
  int fd = 0;

  if ((fd = open (fifo, O_RDWR | O_NONBLOCK)) < 0)
    exit (1);
  while (1)
    read_message(fd, set);

  return 0;
}
Note: Make sure the reader in your application is set as non-blocking to get a constant feed. πŸ’…
Tip: If you need to know which client sent the message, for example, in a chat application, please see the strict mode below. πŸ’¬

2. Strict Mode

gwsocket implements its own tiny protocol for sending/receiving data. In contrast to the stdin/stdout mode, the strict mode allows you to send/receive data to/from specific connected clients as well as to keep track of who opened/closed a WebSocket connection. It also gives you the ability to pack and send as much data as you would like on a single message. πŸ‘‘

2. Data Format

The message header is a fixed-size header. The first 12 bytes (uint32_t) are packed in network byte order and contain the β€œmeta-data” of the message we are sending/receiving. The rest of it is the actual message. ✨

0            1               2                3
+---------------------------------------------+
|        Client Socket Id (listener)          |
+---------------------------------------------+
|    Message Type (binary: 0x2 / text: 0x1)   |
+---------------------------------------------+
|             Payload length                  |
+---------------------------------------------+
|              Payload Data                   |
+---------------------------------------------+

2.1 Sending Data β€” Strict Mode

If you need to send a message to a specific client, then you can do so by specifying the client id in the message header. If set to 0, the message will be broadcasted to all clients. The first 4 bytes are reserved for the client id or listener. The following 4 bytes are reserved for the message type. 0x01 for a text message, and 0x02 for a binary message. And the last 4 bytes are reserved for the payload’s length. πŸŽ€

Once the header has been written to the pipe, you may now write the message. πŸ’Œ

2.1. Examples πŸ“

First, start the server in strict-mode.

# gwsocket --strict-mode
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

size_t pack_uint32(void* buf, uint32_t val) {
  uint32_t v32 = htonl(val);
  memcpy(buf, &v32, sizeof(uint32_t));
  return sizeof(uint32_t);
}

int main() {
  char *p = calloc (sizeof(uint32_t) * 3, sizeof(char)), *ptr;
  const char *msg = "Message to broadcast";
  const char *fifo = "/tmp/wspipein.fifo";
  int fd;

  ptr = p;
  ptr += pack_uint32(ptr, 0);
  ptr += pack_uint32(ptr, 0x01);
  ptr += pack_uint32(ptr, strlen(msg));

  fd = open(fifo, O_WRONLY);
  write(fd, p, sizeof(uint32_t) * 3);
  write(fd, msg, strlen(msg));
  close(fd);
  free (p);

  return 0;
}

2.2 Receiving Data from Clients β€” Strict Mode

Now, to get a message from a specific client and route it to another client, you just need to do the opposite of sending data. First you unpack the header from network byte order to host byte order and then read the payload. πŸ”„

2.2. Examples πŸ“

First, start the server in strict-mode.

# gwsocket --strict-mode
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#include <int.h>

static size_t unpack_uint32 (const void *b, uint32_t * val) {
  uint32_t v32 = 0;
  memcpy (&v32, b, sizeof (uint32_t));
  *val = ntohl (v32);
  return sizeof (uint32_t);
}

static void read_message (int fd, fd_set set) {
  int bytes = 0;
  uint32_t size = 0, listener = 0, type = 0;
  char hdr[PIPE_BUF] = { 0 }, buf[PIPE_BUF] = {0};
  char *ptr = NULL;

  FD_ZERO (&set);
  FD_SET (fd, &set);

  if ((select (fd + 1, &set, NULL, NULL, NULL)) < 1)
    exit (1);
  if (!FD_ISSET (fd, &set))
    return;

  if (hdr[0] == ' ') {
    if (read (fd, hdr, sizeof (uint32_t) * 3) < 1)
      return;
  }

  ptr = hdr;
  ptr += unpack_uint32(ptr, &listener);
  ptr += unpack_uint32(ptr, &type);
  ptr += unpack_uint32(ptr, &size);

  if (read (fd, buf, size) < 1)
    return;

  printf ("client: %d, msg: %s0, listener, buf);
}

int main (void) {
  fd_set set;
  char *fifo = "/tmp/wspipeout.fifo";
  int fd = 0;

  if ((fd = open (fifo, O_RDWR | O_NONBLOCK)) < 0)
    exit (1);
  while (1)
    read_message(fd, set);

  return 0;
}
Note: If you read/write to a stream, be aware that they do not necessarily read/write the full amount of data you have requested. Your application will need to handle the case where only a single byte is read or written. Examples above do not handle this. πŸ’€

πŸŽ€ OBLIGATORY CLIENT EXAMPLE πŸŽ€

Okay bestie, this is the part where we actually do the thing. Here's the basic setup, client and server side, and it's honestly not that deep I promise. ✨

First up β€” start the server and put it in echo mode. That's it. That's the whole server step:

# gwsocket --echo-mode

Now for the client side, we're gonna build a lil' HTML page that talks to it. No cap, this is the main character of the example:

<!DOCTYPE html>
<html lang="en">
<style>
pre {
    background: #EEE;
    border: 1px solid #CCC;
    padding: 10px;
}
#page-wrapper {
    border-top: 5px solid #69c773;
    margin: 1em auto;
    width: 950px;
}
</style>
<script>
window.onload = function() {
    function $(selector) {
        return document.querySelector(selector);
    }
    var socket = new WebSocket('ws://localhost:7890');
    socket.onopen = function(event) {
        $('#messages').innerHTML = 'Connected<br>';
    };
    socket.onmessage = function(event) {
        $('#messages').innerHTML += 'Received:<br>' + event.data +
'<br>';
    };
    socket.onclose = function(event) {
        $('#messages').innerHTML = 'Disconnected ' + event.reason;
    };
    $('#submit').onclick = function(e) {
        socket.send($('input').value);
        $('#messages').innerHTML += 'Sent:<br>' + $('input').value +
'<br>';
        $('input').value = '';
    };
};
</script>

<div id="page-wrapper">
    <pre id="messages">Connecting...</pre>
    <input id="message" required>
    <button id="submit">Send Message</button>
</div>

Quick vibe check on what's happening in there, since I love a guided tour πŸ™ˆ:

  • The socket connects to ws://localhost:7890 β€” that's the line where the friendship begins.
  • On onopen the page drops 'Connected<br>' into the #messages element, so you know it's alive.
  • On onmessage it appends 'Received:<br>' + event.data + '<br>' β€” every message the server echoes back shows up in the log.
  • On onclose it shows 'Disconnected ' + event.reason so you're never left wondering what happened.
  • Clicking #submit calls socket.send() with whatever's in the input, appends 'Sent:<br>' + <that value> + '<br>', then clears the input field. Clean. Iconic. No notes.

The markup below is just the #page-wrapper div holding a pre#messages starting at Connecting..., a required input with id message, and the Send Message button with id submit. Chef's kiss. πŸ’…


πŸ’€ BUGS πŸ’€

If you think you've found a bug β€” and like, valid, it happens β€” please send an email to hello [@at] goaccess.io. That's the whole ask. Be nice about it. 🫢


πŸ‘‘ AUTHOR πŸ‘‘

Gerardo Orellana. For more details about it, or new releases, please visit http://gwsocket.io β€” go show some love bestie, the link is right there. 🌈


Okay that's the whole page, we made it to the end together πŸ¦‹πŸ’– This one's the gwsocket(1) page from Linux, dated MARCH 2023 β€” so if you're reading this and wondering when it was written, there's your timestamp, periodt. Go forth and socket responsibly! β­πŸŽ€

πŸ“¦ where this page comes from