Skip to content
New issue

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

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to implement a websocket client? #1548

Open
tilongzs opened this issue Jan 26, 2024 · 6 comments
Open

How to implement a websocket client? #1548

tilongzs opened this issue Jan 26, 2024 · 6 comments

Comments

@tilongzs
Copy link

I found that websocket seem to only provide the implementation of the server side, and example of the server side are provided in "ws-chat-server.c". May I ask how to implement the client?
I know many third parties have encapsulated libevent to implement websocket clients, but since "ws.h" has already implemented the server, why not continue implementing the client.
Thanks.

@widgetii
Copy link
Member

How would you see API for such functionality from the library user perspective? I have a couple in-house WS client implementations on top of libevent, but I'm not sure if they fit for broader audience

@tilongzs
Copy link
Author

“If anyone forces you to go one mile, go with them two miles.”
Libevent has its own websocket server code, but does not provide client code. I can't understand why only half of it is provided.
These APIs are convenient for users and will certainly not satisfy all users, but I think the "connection, sending, receiving and error callback" functions are APIs that any network library should provide. I don't like http. It contains too much redundant data and protocol exchanges. It is only forced to be used according to work requirements, and I have no interest in encapsulating the protocol upgrade process of websocket.
I also already have the server and client code for websocket implemented through libevent. I just think that implementing libevent yourself would be better than what I did.

@liudongmiao
Copy link
Contributor

@tilongzs, If you're familiar with websocket, it should be easy based on http client.

client send handshake request

  1. create a http request, like evhttp_request_new
  2. send all the headers required by websocket, refer https://datatracker.ietf.org/doc/html/rfc6455#section-1.2
    • Upgrade, should be websocket
    • Connection, should be Upgrade
    • Sec-WebSocket-Key, a 16-byte encoded in base64
    • Sec-WebSocket-Version, should be 13
  3. send the request

client handle handshake response

  1. status: 101
  2. validate Sec-WebSocket-Accept headers (You can skip it for test)
  3. upgrade to websocket

upgrade to websocket

Once http upgrade to websocket, you should send and receive in the http's bufferevent directly.

  1. evhttp_connection_get_bufferevent get the bufferevent of the evhttp_connection
  2. read the bufferevent, wrap in websocket frame, I'd suggest use bufferevent_filter_new.
     ev_tls = evhttp_connection_get_bufferevent(evcon); // get the ev of the connection
     ev_websocket = bufferevent_filter_new(ev_tls, wss_input_filter, wss_output_filter, 0, NULL, ev_tls);
     // use filter to handle websocket frame, don't use `BEV_OPT_CLOSE_ON_FREE`
     // as if evcon is closed, it will close ev_tls, then ev_websocket if invalid
     evhttp_connection_set_closecb(evcon, wss_close_cb, ev_websocket);
     // set the callback to close `ev_websocket` when `evcon` is closed
  3. send the websocket request, you can use wss_output_filter or just send it to ev_tls
     struct wss_frame_data {
         char header[MAX_WS_HEADER_SIZE];
         char buffer[WSS_PAYLOAD_SIZE];
     } wss_frame_data;
    // check your wss_payload_size
    // copy your data to `buffer`
    // build websocket frame ... for simple, you can leave mask_key as 0, the ignore mask, it's valid in websocket spec.
    // send it out
  4. receive and parse the websocket response, you use the wss_input_filter or parse it from ev_tls
     uint8_t header[MAX_WS_HEADER_SIZE]; // websocket header
     length = evbuffer_get_length(src);
     evbuffer_copyout(src, header, MIN(MAX_WS_HEADER_SIZE, length));
     // parse the headers, and validate the headers, in the end, you'll get header_size, and payload_size
     // drop the header_size from src
     evbuffer_drain(src, header_size); 
     // handle output (for server, you should unmask them)

a simple websocket header

Please note: this header has no 64-bits payload support.
IMO, 16 bits payload should be enough in practice, and continuation frame should be better than a very large frame.

/**
 * fop: fin:1, rsv:3, opcode: 4
 * mlen: mask: 1, length: 7
 */
#define FOP_MASK struct {   \
    uint8_t fop;            \
    uint8_t mlen;           \
}

typedef struct ws_header {
    union {
        struct {
            uint16_t unused;
            FOP_MASK;
        };
        struct {
            FOP_MASK;
            uint16_t elen;
        } extend;
    };
    uint32_t mask;
} ws_header;

@liudongmiao
Copy link
Contributor

@tilongzs @widgetii @cculianu @1inker @widgetii @yogo1212 and @azat For websocket, we should think about how libevent would offer websocket.

  • generate sec-websocket-key? requiers base64, used by client to generate, and server to validate.
  • generate sec-websocket-accept? requires sha1, used by client to validate, and server to generate.
  • handle websocket handshake? leave the sec-websocket-key / sec-websocket-accept for user code, or offer them in libevent?
    • IMO, libevent shouldn't offer base64 / sha1, user choose their own from mbedtls or openssl should be better.
  • parse websocket frame? use bufferevent_filter_new with BEV_NEED_MORE is easy.
    • support mask? and custom fin, rsv, op, ...?
  • build / make websocket frame?
    • support mask? and custom fin, rsv, op, ...?
  • should 64-bits payload be supported?
    • if it's less than 2**16, libevent / user code can buffer such a small frame.
    • otherwise, libevent / user code should remember the mask key in the frame lifecycle.
  • handle ping automatically?
  • support custom extension?

IMO, if libevent choose to offer websocket, both client and server should be offered.

@yogo1212
Copy link
Contributor

@liudongmiao

IMO, if libevent choose to offer websocket, both client and server should be offered.

You seem to have a decent amount of domain knowledge. I don't think there'll be answers to all your questions about API design up front.
Instead, you could start putting together a minimal implementation. That'll make it easier to reason about collaboratively 🙂

@liudongmiao
Copy link
Contributor

@liudongmiao

IMO, if libevent choose to offer websocket, both client and server should be offered.

You seem to have a decent amount of domain knowledge. I don't think there'll be answers to all your questions about API design up front. Instead, you could start putting together a minimal implementation. That'll make it easier to reason about collaboratively 🙂

@yogo1212 I have implemented them on the top of libevent 2.1.12-stable, both client and server.
However, I'm thinking about the API way in libevent, what should be part of the libevent api, and what shouldn't be.
BTW, I'm switching from libwebsockets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants