summaryrefslogtreecommitdiff
path: root/sample
diff options
context:
space:
mode:
authorDmitry Ilyin <dima@doty.ru>2022-09-12 22:16:56 +0300
committerGitHub <noreply@github.com>2022-09-12 22:16:56 +0300
commite8313084f9e8b064433cb10eb9a79bf87407fab6 (patch)
tree9a98db8cb69dc07017512e0f28f9eb9bfe3b3a5c /sample
parentbb41229ff4dbd62084994c6b0b2052a321fd0ccf (diff)
downloadlibevent-e8313084f9e8b064433cb10eb9a79bf87407fab6.tar.gz
Add minimal WebSocket server implementation for evhttp (#1322)
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.
Diffstat (limited to 'sample')
-rw-r--r--sample/include.am4
-rw-r--r--sample/ws-chat-server.c244
-rw-r--r--sample/ws-chat.html98
3 files changed, 346 insertions, 0 deletions
diff --git a/sample/include.am b/sample/include.am
index b8dd400e..6c0e7b5e 100644
--- a/sample/include.am
+++ b/sample/include.am
@@ -12,6 +12,7 @@ SAMPLES = \
sample/http-connect \
sample/signal-test \
sample/time-test \
+ sample/ws-chat-server \
sample/watch-timing
if OPENSSL
@@ -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
diff --git a/sample/ws-chat-server.c b/sample/ws-chat-server.c
new file mode 100644
index 00000000..8761df31
--- /dev/null
+++ b/sample/ws-chat-server.c
@@ -0,0 +1,244 @@
+#include <event2/buffer.h>
+#include <event2/bufferevent.h>
+#include <event2/event.h>
+#include <event2/http.h>
+#include <event2/ws.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+
+#ifdef _WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <windows.h>
+#include <getopt.h>
+#include <io.h>
+
+#ifndef stat
+#define stat _stat
+#endif
+#ifndef fstat
+#define fstat _fstat
+#endif
+#ifndef open
+#define open _open
+#endif
+#ifndef close
+#define close _close
+#endif
+#ifndef O_RDONLY
+#define O_RDONLY _O_RDONLY
+#endif
+
+#else /* !_WIN32 */
+
+#ifdef EVENT__HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+#ifdef EVENT__HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef EVENT__HAVE_NETINET_IN6_H
+#include <netinet/in6.h>
+#endif
+#include <unistd.h>
+
+#endif /* _WIN32 */
+
+#define log_d(...) fprintf(stderr, __VA_ARGS__)
+
+typedef struct client {
+ struct evws_connection *evws;
+ char name[INET6_ADDRSTRLEN];
+ TAILQ_ENTRY(client) next;
+} client_t;
+typedef TAILQ_HEAD(clients_s, client) clients_t;
+static clients_t clients;
+
+static void
+broadcast_msg(char *msg)
+{
+ struct client *client;
+
+ TAILQ_FOREACH (client, &clients, next) {
+ evws_send(client->evws, msg, strlen(msg));
+ }
+ log_d("%s\n", msg);
+}
+
+static void
+on_msg_cb(struct evws_connection *evws, int type, const unsigned char *data,
+ size_t len, void *arg)
+{
+ struct client *self = arg;
+ char buf[4096];
+ const char *msg = (const char *)data;
+
+ snprintf(buf, sizeof(buf), "%.*s", (int)len, msg);
+ if (len == 5 && memcmp(buf, "/quit", 5) == 0) {
+ evws_close(evws, WS_CR_NORMAL);
+ snprintf(buf, sizeof(buf), "'%s' left the chat", self->name);
+ } else if (len > 6 && strncmp(msg, "/name ", 6) == 0) {
+ const char *new_name = (const char *)msg + 6;
+ int name_len = len - 6;
+
+ snprintf(buf, sizeof(buf), "'%s' renamed itself to '%.*s'", self->name,
+ name_len, new_name);
+ snprintf(
+ self->name, sizeof(self->name) - 1, "%.*s", name_len, new_name);
+ } else {
+ snprintf(buf, sizeof(buf), "[%s] %.*s", self->name, (int)len, msg);
+ }
+
+ broadcast_msg(buf);
+}
+
+static void
+on_close_cb(struct evws_connection *evws, void *arg)
+{
+ client_t *client = arg;
+ log_d("'%s' disconnected\n", client->name);
+ TAILQ_REMOVE(&clients, client, next);
+ free(arg);
+}
+
+static const char *
+nice_addr(const char *addr)
+{
+ if (strncmp(addr, "::ffff:", 7) == 0)
+ addr += 7;
+
+ return addr;
+}
+
+static void
+addr2str(struct sockaddr *sa, char *addr, size_t len)
+{
+ const char *nice;
+ unsigned short port;
+ size_t adlen;
+
+ if (sa->sa_family == AF_INET) {
+ struct sockaddr_in *s = (struct sockaddr_in *)sa;
+ port = ntohs(s->sin_port);
+ evutil_inet_ntop(AF_INET, &s->sin_addr, addr, len);
+ } else { // AF_INET6
+ struct sockaddr_in6 *s = (struct sockaddr_in6 *)sa;
+ port = ntohs(s->sin6_port);
+ evutil_inet_ntop(AF_INET6, &s->sin6_addr, addr, len);
+ nice = nice_addr(addr);
+ if (nice != addr) {
+ size_t len = strlen(addr) - (nice - addr);
+ memmove(addr, nice, len);
+ addr[len] = 0;
+ }
+ }
+ adlen = strlen(addr);
+ snprintf(addr + adlen, len - adlen, ":%d", port);
+}
+
+
+static void
+on_ws(struct evhttp_request *req, void *arg)
+{
+ struct client *client;
+ evutil_socket_t fd;
+ struct sockaddr_storage addr;
+ socklen_t len;
+
+ client = calloc(sizeof(*client), 1);
+ client->evws = evws_new_session(req, on_msg_cb, client);
+ fd = bufferevent_getfd(evws_connection_get_bufferevent(client->evws));
+
+ len = sizeof(addr);
+ getpeername(fd, (struct sockaddr *)&addr, &len);
+
+ addr2str((struct sockaddr *)&addr, client->name, sizeof(client->name));
+ log_d("New client joined from %s\n", client->name);
+
+ evws_connection_set_closecb(client->evws, on_close_cb, client);
+ TAILQ_INSERT_TAIL(&clients, client, next);
+}
+
+static void
+on_html(struct evhttp_request *req, void *arg)
+{
+ int fd = -1;
+ struct evbuffer *evb;
+ struct stat st;
+
+ evhttp_add_header(
+ evhttp_request_get_output_headers(req), "Content-Type", "text/html");
+ if ((fd = open("ws-chat.html", O_RDONLY)) < 0) {
+ perror("open");
+ goto err;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ /* Make sure the length still matches, now that we
+ * opened the file :/ */
+ perror("fstat");
+ goto err;
+ }
+
+
+ evb = evbuffer_new();
+ evbuffer_add_file(evb, fd, 0, st.st_size);
+ evhttp_send_reply(req, HTTP_OK, NULL, evb);
+ evbuffer_free(evb);
+ return;
+
+err:
+ evhttp_send_error(req, HTTP_NOTFOUND, NULL);
+}
+
+#ifndef EVENT__HAVE_STRSIGNAL
+static inline const char *
+strsignal(evutil_socket_t sig)
+{
+ return "Signal";
+}
+#endif
+
+static void
+signal_cb(evutil_socket_t fd, short event, void *arg)
+{
+ printf("%s signal received\n", strsignal(fd));
+ event_base_loopbreak(arg);
+}
+
+int
+main(int argc, char **argv)
+{
+ struct event_base *base;
+ struct event *sig_int;
+ struct evhttp *http_server;
+
+ TAILQ_INIT(&clients);
+
+ base = event_base_new();
+
+ sig_int = evsignal_new(base, SIGINT, signal_cb, base);
+ event_add(sig_int, NULL);
+
+ http_server = evhttp_new(base);
+ evhttp_bind_socket_with_handle(http_server, "0.0.0.0", 8080);
+
+ evhttp_set_cb(http_server, "/", on_html, NULL);
+ evhttp_set_cb(http_server, "/ws", on_ws, NULL);
+
+ log_d("Server runs\n");
+ event_base_dispatch(base);
+
+ log_d("Active connections: %d\n", evhttp_get_connection_count(http_server));
+ evhttp_free(http_server);
+
+ event_free(sig_int);
+ event_base_free(base);
+ libevent_global_shutdown();
+}
diff --git a/sample/ws-chat.html b/sample/ws-chat.html
new file mode 100644
index 00000000..f4083f80
--- /dev/null
+++ b/sample/ws-chat.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Chat Example</title>
+<script type="text/javascript">
+window.onload = function () {
+ var conn;
+ var msg = document.getElementById("msg");
+ var log = document.getElementById("log");
+
+ function appendLog(item) {
+ var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
+ log.appendChild(item);
+ if (doScroll) {
+ log.scrollTop = log.scrollHeight - log.clientHeight;
+ }
+ }
+
+ document.getElementById("form").onsubmit = function () {
+ if (!conn) {
+ return false;
+ }
+ if (!msg.value) {
+ return false;
+ }
+ conn.send(msg.value);
+ msg.value = "";
+ return false;
+ };
+
+ if (window["WebSocket"]) {
+ conn = new WebSocket("ws://" + document.location.host + ":8080/ws");
+ conn.onclose = function (evt) {
+ var item = document.createElement("div");
+ item.innerHTML = "<b>Connection closed.</b>";
+ appendLog(item);
+ };
+ conn.onmessage = function (evt) {
+ var messages = evt.data.split('\n');
+ for (var i = 0; i < messages.length; i++) {
+ var item = document.createElement("div");
+ item.innerText = messages[i];
+ appendLog(item);
+ }
+ };
+ } else {
+ var item = document.createElement("div");
+ item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
+ appendLog(item);
+ }
+};
+</script>
+<style type="text/css">
+html {
+ overflow: hidden;
+}
+
+body {
+ overflow: hidden;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ background: gray;
+}
+
+#log {
+ background: white;
+ margin: 0;
+ padding: 0.5em 0.5em 0.5em 0.5em;
+ position: absolute;
+ top: 0.5em;
+ left: 0.5em;
+ right: 0.5em;
+ bottom: 3em;
+ overflow: auto;
+}
+
+#form {
+ padding: 0 0.5em 0 0.5em;
+ margin: 0;
+ position: absolute;
+ bottom: 1em;
+ left: 0px;
+ width: 100%;
+ overflow: hidden;
+}
+
+</style>
+</head>
+<body>
+<div id="log"></div>
+<form id="form">
+ <input type="submit" value="Send" />
+ <input type="text" id="msg" size="64" autofocus />
+</form>
+</body>
+</html>