Skip to content

Commit

Permalink
Add minimal WebSocket server implementation for evhttp (libevent#1322)
Browse files Browse the repository at this point in the history
This adds few functions to use evhttp-based webserver to handle incoming
WebSockets connections. We've tried to use both libevent and libwebsockets in
our application, but found that we need to have different ports at the same
time to handle standard HTTP and WebSockets traffic. This change can help to
stick only with libevent library.

Implementation was inspired by modified Libevent source code in ipush project
[1].

  [1]: https://github.com/sqfasd/ipush/tree/master/deps/libevent-2.0.21-stable

Also, WebSocket-based chat server was added as a sample.
  • Loading branch information
widgetii authored Sep 12, 2022
1 parent bb41229 commit e831308
Show file tree
Hide file tree
Showing 19 changed files with 1,606 additions and 5 deletions.
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,7 @@ set(HDR_PRIVATE
util-internal.h
openssl-compat.h
evconfig-private.h
sha1.h
compat/sys/queue.h)

set(HDR_COMPAT
Expand Down Expand Up @@ -854,6 +855,7 @@ set(HDR_PUBLIC
include/event2/tag_compat.h
include/event2/thread.h
include/event2/util.h
include/event2/ws.h
include/event2/visibility.h
${PROJECT_BINARY_DIR}/include/event2/event-config.h)

Expand Down Expand Up @@ -966,8 +968,12 @@ set(SRC_EXTRA
event_tagging.c
http.c
evdns.c
ws.c
sha1.c
evrpc.c)

set_source_files_properties(sha1.c PROPERTIES COMPILE_FLAGS
-D${CMAKE_C_BYTE_ORDER}=1)
add_definitions(-DHAVE_CONFIG_H)

# We use BEFORE here so we don't accidentally look in system directories
Expand Down Expand Up @@ -1134,6 +1140,7 @@ if (NOT EVENT__DISABLE_SAMPLES)

set(SAMPLES_WOPT
dns-example
ws-chat-server
http-server
)
foreach (SAMPLE ${SAMPLES_WOPT})
Expand Down Expand Up @@ -1217,6 +1224,7 @@ if (NOT EVENT__DISABLE_TESTS)
test/regress_et.c
test/regress_finalize.c
test/regress_http.c
test/regress_http.h
test/regress_listener.c
test/regress_main.c
test/regress_minheap.c
Expand All @@ -1225,6 +1233,8 @@ if (NOT EVENT__DISABLE_TESTS)
test/regress_testutils.h
test/regress_util.c
test/regress_watch.c
test/regress_ws.c
test/regress_ws.h
test/tinytest.c)

if (WIN32)
Expand Down
1 change: 1 addition & 0 deletions Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ INPUT = \
$(SRCDIR)/include/event2/tag.h \
$(SRCDIR)/include/event2/tag_compat.h \
$(SRCDIR)/include/event2/thread.h \
$(SRCDIR)/include/event2/ws.h \
$(SRCDIR)/include/event2/util.h \
$(SRCDIR)/include/event2/watch.h

Expand Down
3 changes: 3 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ EXTRAS_SRC = \
evdns.c \
event_tagging.c \
evrpc.c \
sha1.c \
ws.c \
http.c

if BUILD_WITH_NO_UNDEFINED
Expand Down Expand Up @@ -338,6 +340,7 @@ noinst_HEADERS += \
util-internal.h \
openssl-compat.h \
mbedtls-compat.h \
sha1.h \
ssl-compat.h \
wepoll.h

Expand Down
2 changes: 2 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,8 @@ AC_SUBST([LIBEVENT_GC_SECTIONS])

AM_CONDITIONAL([INSTALL_LIBEVENT], [test "$enable_libevent_install" = "yes"])

AC_C_BIGENDIAN([CFLAGS="$CFLAGS -DBIG_ENDIAN"], [CFLAGS="$CFLAGS -DLITTLE_ENDIAN"])

dnl Doxygen support
DX_HTML_FEATURE(ON)
DX_MAN_FEATURE(OFF)
Expand Down
9 changes: 8 additions & 1 deletion http-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ struct evhttp_cb {
/* both the http server as well as the rpc system need to queue connections */
TAILQ_HEAD(evconq, evhttp_connection);

/* WebSockets connections */
TAILQ_HEAD(evwsq, evws_connection);

/* each bound socket is stored in one of these */
struct evhttp_bound_socket {
TAILQ_ENTRY(evhttp_bound_socket) next;
Expand Down Expand Up @@ -151,8 +154,10 @@ struct evhttp {

TAILQ_HEAD(httpcbq, evhttp_cb) callbacks;

/* All live connections on this host. */
/* All live HTTP connections on this host. */
struct evconq connections;
/* All live WebSockets sessions on this host. */
struct evwsq ws_sessions;
int connection_max;
int connection_cnt;

Expand Down Expand Up @@ -221,6 +226,8 @@ void evhttp_start_write_(struct evhttp_connection *);
void evhttp_response_code_(struct evhttp_request *, int, const char *);
void evhttp_send_page_(struct evhttp_request *, struct evbuffer *);

struct bufferevent * evhttp_start_ws_(struct evhttp_request *req);

/* [] has been stripped */
#define _EVHTTP_URI_HOST_HAS_BRACKETS 0x02

Expand Down
32 changes: 32 additions & 0 deletions http.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
#include "event2/http_struct.h"
#include "event2/http_compat.h"
#include "event2/util.h"
#include "event2/ws.h"
#include "event2/listener.h"
#include "log-internal.h"
#include "util-internal.h"
Expand Down Expand Up @@ -3191,6 +3192,31 @@ evhttp_send_reply_chunk_with_cb(struct evhttp_request *req, struct evbuffer *dat
evhttp_write_buffer(evcon, cb, arg);
}

struct bufferevent *
evhttp_start_ws_(struct evhttp_request *req)
{
struct evhttp_connection *evcon = req->evcon;
struct bufferevent *bufev;

evhttp_response_code_(req, HTTP_SWITCH_PROTOCOLS, "Switching Protocols");

if (req->evcon == NULL)
return NULL;

evhttp_make_header(req->evcon, req);
evhttp_write_buffer(req->evcon, NULL, NULL);

TAILQ_REMOVE(&evcon->requests, req, next);

bufev = evcon->bufev;
evcon->bufev = NULL;
evcon->closecb = NULL;

evhttp_request_free(req);
evhttp_connection_free(evcon);
return bufev;
}

void
evhttp_send_reply_chunk(struct evhttp_request *req, struct evbuffer *databuf)
{
Expand Down Expand Up @@ -3961,6 +3987,7 @@ evhttp_new_object(void)
TAILQ_INIT(&http->sockets);
TAILQ_INIT(&http->callbacks);
TAILQ_INIT(&http->connections);
TAILQ_INIT(&http->ws_sessions);
TAILQ_INIT(&http->virtualhosts);
TAILQ_INIT(&http->aliases);

Expand Down Expand Up @@ -4005,6 +4032,7 @@ evhttp_free(struct evhttp* http)
{
struct evhttp_cb *http_cb;
struct evhttp_connection *evcon;
struct evws_connection *evws;
struct evhttp_bound_socket *bound;
struct evhttp* vhost;
struct evhttp_server_alias *alias;
Expand All @@ -4023,6 +4051,10 @@ evhttp_free(struct evhttp* http)
evhttp_connection_free(evcon);
}

while ((evws = TAILQ_FIRST(&http->ws_sessions)) != NULL) {
evws_connection_free(evws);
}

while ((http_cb = TAILQ_FIRST(&http->callbacks)) != NULL) {
TAILQ_REMOVE(&http->callbacks, http_cb, next);
mm_free(http_cb->what);
Expand Down
58 changes: 58 additions & 0 deletions include/event2/ws.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifndef EVENT2_WS_H_INCLUDED_
#define EVENT2_WS_H_INCLUDED_

struct evws_connection;

#define WS_CR_NONE 0
#define WS_CR_NORMAL 1000
#define WS_CR_PROTO_ERR 1002
#define WS_CR_DATA_TOO_BIG 1009

#define WS_TEXT_FRAME 0x1
#define WS_BINARY_FRAME 0x2

typedef void (*ws_on_msg_cb)(
struct evws_connection *, int type, const unsigned char *, size_t, void *);
typedef void (*ws_on_close_cb)(struct evws_connection *, void *);

/** Opens new WebSocket session from HTTP request.
@param req a request object
@param cb the callback function that gets invoked on receiving message
with len bytes length. In case of receiving text messages user is responsible
to make a string with terminating \0 (with copying-out data) or use text data
other way in which \0 is not required
@param arg an additional context argument for the callback
@return a pointer to a newly initialized WebSocket connection or NULL
on error
@see evws_close()
*/
EVENT2_EXPORT_SYMBOL
struct evws_connection *evws_new_session(
struct evhttp_request *req, ws_on_msg_cb, void *arg);

/** Sends data over WebSocket connection */
EVENT2_EXPORT_SYMBOL
void evws_send(
struct evws_connection *evws, const char *packet_str, size_t str_len);

/** Closes a WebSocket connection with reason code */
EVENT2_EXPORT_SYMBOL
void evws_close(struct evws_connection *evws, uint16_t reason);

/** Sets a callback for connection close. */
EVENT2_EXPORT_SYMBOL
void evws_connection_set_closecb(
struct evws_connection *evws, ws_on_close_cb, void *);

/** Frees a WebSocket connection */
EVENT2_EXPORT_SYMBOL
void evws_connection_free(struct evws_connection *evws);

/**
* Return the bufferevent that an evws_connection is using.
*/
EVENT2_EXPORT_SYMBOL
struct bufferevent *evws_connection_get_bufferevent(
struct evws_connection *evws);

#endif
1 change: 1 addition & 0 deletions include/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ EVENT2_EXPORT = \
include/event2/tag_compat.h \
include/event2/thread.h \
include/event2/util.h \
include/event2/ws.h \
include/event2/visibility.h

if OPENSSL
Expand Down
4 changes: 4 additions & 0 deletions sample/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ SAMPLES = \
sample/http-connect \
sample/signal-test \
sample/time-test \
sample/ws-chat-server \
sample/watch-timing

if OPENSSL
Expand Down Expand Up @@ -74,3 +75,6 @@ sample_http_connect_SOURCES = sample/http-connect.c
sample_http_connect_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la
sample_watch_timing_SOURCES = sample/watch-timing.c
sample_watch_timing_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la -lm
sample_ws_chat_server_SOURCES = sample/ws-chat-server.c
sample_ws_chat_server_LDADD = $(LIBEVENT_GC_SECTIONS) libevent.la -lm
EXTRA_DIST+=sample/ws-chat.html
Loading

0 comments on commit e831308

Please sign in to comment.