Browse Source

Add WebSocket demo that uses asynchronous messages from the server.

William Greathouse 11 years ago
parent
commit
aa7c2a1b8c
3 changed files with 641 additions and 0 deletions
  1. 36 0
      examples/ws_server/Makefile
  2. 316 0
      examples/ws_server/docroot/index.html
  3. 289 0
      examples/ws_server/ws_server.c

+ 36 - 0
examples/ws_server/Makefile

@@ -0,0 +1,36 @@
+# 
+# Copyright (c) 2013 No Face Press, LLC
+# License http://opensource.org/licenses/mit-license.php MIT License
+#
+
+#This makefile is used to test the other Makefiles
+
+
+PROG = ws_server
+SRC = ws_server.c
+
+TOP = ../..
+CIVETWEB_LIB = libcivetweb.a
+
+CFLAGS = -I$(TOP)/include $(COPT)
+LIBS = -lpthread
+
+include $(TOP)/resources/Makefile.in-os
+
+ifeq ($(TARGET_OS),LINUX) 
+	LIBS += -ldl
+endif
+
+all: $(PROG)
+
+$(PROG): $(CIVETWEB_LIB) $(SRC)
+	$(CC) -o $@ $(CFLAGS) $(LDFLAGS) $(SRC) $(CIVETWEB_LIB) $(LIBS)
+
+$(CIVETWEB_LIB):
+	$(MAKE) -C $(TOP) clean lib WITH_WEBSOCKET=1
+	cp $(TOP)/$(CIVETWEB_LIB) .
+
+clean:
+	rm -f $(CIVETWEB_LIB) $(PROG)
+
+.PHONY: all clean

+ 316 - 0
examples/ws_server/docroot/index.html

@@ -0,0 +1,316 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+	<meta charset='UTF-8'>
+	
+	<title>Websocket Meters</title>
+    <!-- 
+        Version 0.1 Contributed by William Greathouse    9-Sep-2013
+        Simple demo of WebSocket connection use. Not a great example of web coding,
+        but it is functional.
+
+        The meter displays are adapted from CSS-TRICKS Progress Bars by Chris Coyier 
+        at http://css-tricks.com/css3-progress-bars/
+    -->
+	<style>
+        body {
+            background: #222;
+        }
+        h1 {
+            color: white;
+            text-align: center;
+        }
+        button {
+            width: 225px;
+            height: 30px;
+            margin: auto 10px;
+            background-color: #ccc;
+            -moz-border-radius: 5px;
+            -webkit-border-radius: 5px;
+            border-radius:6px;
+            color: blue;
+            font-size: 20px;
+        }
+        button:hover {
+            background-color: #888;
+        }
+        button:hover:disabled {
+            background-color: #ccc;
+        }
+        button:disabled {
+            color: lightgray;
+        }
+
+        .button_container {
+            width:550px;
+            display:block;
+            margin-left:auto;
+            margin-right:auto;
+        }
+
+		.meter { 
+			height: 20px;  /* Can be anything */
+			position: relative;
+			background: #555;
+			-moz-border-radius: 25px;
+			-webkit-border-radius: 25px;
+			border-radius: 25px;
+			padding: 10px;
+			-webkit-box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3);
+			-moz-box-shadow   : inset 0 -1px 1px rgba(255,255,255,0.3);
+			box-shadow        : inset 0 -1px 1px rgba(255,255,255,0.3);
+		}
+		.meter > span {
+			display: block;
+			height: 100%;
+			   -webkit-border-top-right-radius: 20px;
+			-webkit-border-bottom-right-radius: 20px;
+			       -moz-border-radius-topright: 20px;
+			    -moz-border-radius-bottomright: 20px;
+			           border-top-right-radius: 20px;
+			        border-bottom-right-radius: 20px;
+			    -webkit-border-top-left-radius: 20px;
+			 -webkit-border-bottom-left-radius: 20px;
+			        -moz-border-radius-topleft: 20px;
+			     -moz-border-radius-bottomleft: 20px;
+			            border-top-left-radius: 20px;
+			         border-bottom-left-radius: 20px;
+			background-color: rgb(43,194,83);
+			background-image: -webkit-gradient(
+			  linear,
+			  left bottom,
+			  left top,
+			  color-stop(0, rgb(43,194,83)),
+			  color-stop(1, rgb(84,240,84))
+			 );
+			background-image: -moz-linear-gradient(
+			  center bottom,
+			  rgb(43,194,83) 37%,
+			  rgb(84,240,84) 69%
+			 );
+			-webkit-box-shadow: 
+			  inset 0 2px 9px  rgba(255,255,255,0.3),
+			  inset 0 -2px 6px rgba(0,0,0,0.4);
+			-moz-box-shadow: 
+			  inset 0 2px 9px  rgba(255,255,255,0.3),
+			  inset 0 -2px 6px rgba(0,0,0,0.4);
+			box-shadow: 
+			  inset 0 2px 9px  rgba(255,255,255,0.3),
+			  inset 0 -2px 6px rgba(0,0,0,0.4);
+			position: relative;
+			overflow: hidden;
+		}
+		.meter > span:after, .animate > span > span {
+			content: "";
+			position: absolute;
+			top: 0; left: 0; bottom: 0; right: 0;
+			background-image: 
+			   -webkit-gradient(linear, 0 0, 100% 100%, 
+			      color-stop(.25, rgba(255, 255, 255, .2)), 
+			      color-stop(.25, transparent), color-stop(.5, transparent), 
+			      color-stop(.5, rgba(255, 255, 255, .2)), 
+			      color-stop(.75, rgba(255, 255, 255, .2)), 
+			      color-stop(.75, transparent), to(transparent)
+			   );
+			background-image: 
+				-moz-linear-gradient(
+				  -45deg, 
+			      rgba(255, 255, 255, .2) 25%, 
+			      transparent 25%, 
+			      transparent 50%, 
+			      rgba(255, 255, 255, .2) 50%, 
+			      rgba(255, 255, 255, .2) 75%, 
+			      transparent 75%, 
+			      transparent
+			   );
+			z-index: 1;
+			-webkit-background-size: 50px 50px;
+			-moz-background-size: 50px 50px;
+			-webkit-animation: move 2s linear infinite;
+			   -webkit-border-top-right-radius: 20px;
+			-webkit-border-bottom-right-radius: 20px;
+			       -moz-border-radius-topright: 20px;
+			    -moz-border-radius-bottomright: 20px;
+			           border-top-right-radius: 20px;
+			        border-bottom-right-radius: 20px;
+			    -webkit-border-top-left-radius: 20px;
+			 -webkit-border-bottom-left-radius: 20px;
+			        -moz-border-radius-topleft: 20px;
+			     -moz-border-radius-bottomleft: 20px;
+			            border-top-left-radius: 20px;
+			         border-bottom-left-radius: 20px;
+			overflow: hidden;
+		}
+		
+		.animate > span:after {
+			display: none;
+		}
+		
+		@-webkit-keyframes move {
+		    0% {
+		       background-position: 0 0;
+		    }
+		    100% {
+		       background-position: 50px 50px;
+		    }
+		}
+		
+		.orange > span {
+			background-color: #f1a165;
+			background-image: -moz-linear-gradient(top, #f1a165, #f36d0a);
+			background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #f1a165),color-stop(1, #f36d0a));
+			background-image: -webkit-linear-gradient(#f1a165, #f36d0a); 
+		}
+		
+		.red > span {
+			background-color: #f0a3a3;
+			background-image: -moz-linear-gradient(top, #f0a3a3, #f42323);
+			background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #f0a3a3),color-stop(1, #f42323));
+			background-image: -webkit-linear-gradient(#f0a3a3, #f42323);
+		}
+		
+		.nostripes > span > span, .nostripes > span:after {
+			-webkit-animation: none;
+			background-image: none;
+		}
+
+        #output {
+            background-color: #ccc;
+            height: 240px;
+            overflow-y: auto;
+        }
+
+	</style>
+</head>
+
+<body>
+	<div id="page_wrap">
+		
+	  <h1>Meter Updates via WebSocket</h1>
+	  
+	  <p/>
+
+		<div class="meter">
+			<span id="meter1" style="width: 25%"></span>
+		</div>
+
+        <p/>
+		
+		<div class="meter orange nostripes">
+			<span id="meter2" style="width: 33.3%"></span>
+		</div>
+
+        <p/>
+		
+		<div class="meter red">
+			<span id="meter3" style="width: 80%"></span>
+		</div>
+
+        <p/>
+
+	</div>
+    <div class="button_container">
+        <div>
+            <button id="connection" onclick="toggleConnection(this)">WebSocket Connect</button>
+            <button id="update" disabled onclick="toggleUpdate(this)">Disable Update</button>
+        </div>
+    </div>
+    <p/>
+    <div id="output"></div>
+	
+</body>
+
+<script language="javascript" type="text/javascript">
+    var connection; // websocket connection
+
+    function writeToScreen (message) {
+        var div = document.createElement('div');
+        var output = document.getElementById('output');
+        div.innerHTML = message;
+        output.appendChild(div);
+        output.scrollTop = output.scrollHeight;
+    }
+
+    function ws_connect() {
+        // check for websocket support
+        // for Internet Explorer < 10 there are options for websocket support that
+        // could be integrated into production code, but for now, we are expecting 
+        // browser support to be present for this demo
+        if ('WebSocket' in window) {
+
+            writeToScreen('Connecting');
+            connection = new WebSocket('ws://' + window.location.host + '/meters');
+            connection.onopen = function(ev) {
+                document.getElementById("connection").innerHTML = "WebSocket Disconnect";
+                document.getElementById("update").disabled=false;
+                document.getElementById("update").innerHTML = "Disable Update";
+                writeToScreen('CONNECTED');
+                var message = 'update on';
+                writeToScreen('SENT: ' + message);
+                connection.send(message);
+            };
+
+            connection.onclose = function(ev) {
+                document.getElementById("update").disabled=true;
+                document.getElementById("update").innerHTML = "Enable Update";
+                document.getElementById("connection").innerHTML = "WebSocket Connect";
+                writeToScreen('DISCONNECTED');
+            };
+
+            connection.onmessage = function(ev) {
+                if (ev.data.substr(0,5) == "meter")
+                {
+                    var target = ev.data.split(":")[0];
+                    var meter = document.getElementById(target);
+                    var data = ev.data.split(":")[1].split(",");
+                    var percent = (data[0]*100)/data[1];
+                    meter.style.width = percent+"%";
+                }
+                else
+                    writeToScreen('RECEIVED: ' + ev.data);
+            };
+
+            connection.onerror = function(ev) {
+                alert("WebSocket error");
+            };
+
+        } else {
+            alert("WebSocket is not available!!!\n" +
+                  "Demo will not function.");
+        }
+    }
+
+    // user connect/disconnect
+    function toggleConnection(el) {
+        var tag=el.innerHTML;
+        if (tag == "WebSocket Connect")
+        {
+            ws_connect();
+        }
+        else
+        {
+            connection.close();
+        }
+    }
+
+    // user turn updates on/off
+    function toggleUpdate(el) {
+        var tag=el.innerHTML;
+        var message;
+        if (tag == "Enable Update")
+        {
+            message = 'update on';
+            el.innerHTML = "Disable Update";
+        }
+        else
+        {
+            message = 'update off';
+            el.innerHTML = "Enable Update";
+        }
+        writeToScreen('SENT: ' + message);
+        connection.send(message);
+    }
+</script>
+
+</html>

+ 289 - 0
examples/ws_server/ws_server.c

@@ -0,0 +1,289 @@
+// Copyright (c) 2004-2012 Sergey Lyubka
+// This file is a part of civetweb project, http://github.com/sunsetbrew/civetweb
+//
+// v 0.1 Contributed by William Greathouse    9-Sep-2013
+
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "civetweb.h"
+
+// simple structure for keeping track of websocket connection
+struct ws_connection {
+    struct mg_connection    *conn;
+    int     update;
+    int     closing;
+};
+
+// time base and structure periodic updates to client for demo
+#define BASETIME 100000 /* 0.1 seconds */
+struct progress {
+    int     limit;
+    int     increment;
+    int     period;
+    int     value;
+};
+
+// up to 16 independent client connections
+#define CONNECTIONS 16
+static struct ws_connection ws_conn[CONNECTIONS];
+
+
+// ws_server_thread()
+// Simple demo server thread. Sends periodic updates to connected clients
+static void *ws_server_thread(void *parm)
+{
+    int wsd = (long)parm;
+    struct mg_connection    *conn = ws_conn[wsd].conn;
+    int timer = 0;
+    char tstr[32];
+    int i;
+    struct progress meter[] = {
+        /* first meter 0 to 1000, by 5 every 0.1 second */
+        { 1000, 5, 1, 0 },
+        /* second meter 0 to 500, by 10 every 0.5 second */
+        { 500, 10, 5, 0 },
+        /* third meter 0 to 100, by 10 every 1.0 second */
+        { 100, 10, 10, 0},
+        /* end of list */
+        { 0, 0, 0, 0}
+    };
+
+    fprintf(stderr, "ws_server_thread %d\n", wsd);
+
+    /* Send initial meter updates */
+    for (i=0; meter[i].period != 0; i++)
+    {
+        if (meter[i].value >= meter[i].limit)
+            meter[i].value = 0;
+        if (meter[i].value >= meter[i].limit)
+            meter[i].value = meter[i].limit;
+        sprintf(tstr, "meter%d:%d,%d", i+1, 
+                meter[i].value, meter[i].limit);
+        mg_websocket_write(conn, WEBSOCKET_OPCODE_TEXT, tstr, strlen(tstr));
+    }
+
+    /* While the connection is open, send periodic updates */
+    while(!ws_conn[wsd].closing)
+    {
+        usleep(100000); /* 0.1 second */
+        timer++;
+
+        /* Send meter updates */
+        if (ws_conn[wsd].update)
+        {
+            for (i=0; meter[i].period != 0; i++)
+            {
+                if (timer%meter[i].period == 0)
+                {
+                    if (meter[i].value >= meter[i].limit)
+                        meter[i].value = 0;
+                    else
+                        meter[i].value += meter[i].increment;
+                    if (meter[i].value >= meter[i].limit)
+                        meter[i].value = meter[i].limit;
+                    // if we are closing, server should not send new data
+                    if (!ws_conn[wsd].closing)
+                    {
+                        sprintf(tstr, "meter%d:%d,%d", i+1, 
+                                meter[i].value, meter[i].limit);
+                        mg_websocket_write(conn, WEBSOCKET_OPCODE_TEXT, tstr, strlen(tstr));
+                    }
+                }
+            }
+        }
+
+        /* Send periodic PING to assure websocket remains connected, except if we are closing */
+        if (timer%100 == 0 && !ws_conn[wsd].closing)
+            mg_websocket_write(conn, WEBSOCKET_OPCODE_PING, NULL, 0);
+    }
+
+    fprintf(stderr, "ws_server_thread %d exiting\n", wsd);
+
+    // reset connection information to allow reuse by new client
+    ws_conn[wsd].conn = NULL;
+    ws_conn[wsd].update = 0;
+    ws_conn[wsd].closing = 2;
+
+    return NULL;
+}
+
+// websocket_connect_handler()
+// On new client connection, find next available server connection and store
+// new connection information. If no more server connections are available
+// tell civetweb to not accept the client request.
+static int websocket_connect_handler(const struct mg_connection *conn) {
+    int i;
+
+    fprintf(stderr, "connect handler\n");
+
+    for(i=0; i < CONNECTIONS; ++i)
+    {
+        if (ws_conn[i].conn == NULL)
+        {
+            fprintf(stderr, "...prep for server %d\n", i);
+            ws_conn[i].conn = (struct mg_connection *)conn;
+            ws_conn[i].closing = 0;
+            ws_conn[i].update = 0;
+            break;
+        }
+    }
+    if (i >= CONNECTIONS)
+    {
+        fprintf(stderr, "Refused connection: Max connections exceeded\n");
+        return 1;
+    }
+
+    return 0;
+}
+
+// websocket_ready_handler()
+// Once websocket negotiation is complete, start a server for the connection
+static void websocket_ready_handler(struct mg_connection *conn) {
+    int i;
+
+    fprintf(stderr, "ready handler\n");
+
+    for(i=0; i < CONNECTIONS; ++i)
+    {
+        if (ws_conn[i].conn == conn)
+        {
+            fprintf(stderr, "...start server %d\n", i);
+            mg_start_thread(ws_server_thread, (void *)(long)i);
+            break;
+        }
+    }
+}
+
+// websocket_close_handler()
+// When websocket is closed, tell the associated server to shut down
+static void websocket_close_handler(struct mg_connection *conn) {
+    int i;
+
+    //fprintf(stderr, "close handler\n");   /* called for every close, not just websockets */
+
+    for(i=0; i < CONNECTIONS; ++i)
+    {
+        if (ws_conn[i].conn == conn)
+        {
+            fprintf(stderr, "...close server %d\n", i);
+            ws_conn[i].closing = 1;
+        }
+    }
+}
+
+// Arguments:
+//   flags: first byte of websocket frame, see websocket RFC,
+//          http://tools.ietf.org/html/rfc6455, section 5.2
+//   data, data_len: payload data. Mask, if any, is already applied.
+static int websocket_data_handler(struct mg_connection *conn, int flags,
+                                  char *data, size_t data_len) {
+    int i;
+    int wsd;
+
+    for(i=0; i < CONNECTIONS; ++i)
+    {
+        if (ws_conn[i].conn == conn)
+        {
+            wsd = i;
+            break;
+        }
+    }
+    if (i >= CONNECTIONS)
+    {
+        fprintf(stderr, "Received websocket data from unknown connection\n");
+        return 1;
+    }
+
+    if (flags & 0x80)
+    {
+      flags &= 0x7f;
+      switch (flags)
+      {
+          case WEBSOCKET_OPCODE_CONTINUATION:
+              fprintf(stderr, "CONTINUATION...\n");
+              break;
+          case WEBSOCKET_OPCODE_TEXT:
+              fprintf(stderr, "TEXT: %-.*s\n", (int)data_len, data);
+              /*** interpret data as commands here ***/
+              if (strncmp("update on", data, data_len)== 0)
+              {
+                  /* turn on updates */
+                  ws_conn[wsd].update = 1;
+                  /* echo back */
+                  mg_websocket_write(conn, WEBSOCKET_OPCODE_TEXT, data, data_len);
+              }
+              else if (strncmp("update off", data, data_len)== 0)
+              {
+                  /* turn off updates */
+                  ws_conn[wsd].update = 0;
+                  /* echo back */
+                  mg_websocket_write(conn, WEBSOCKET_OPCODE_TEXT, data, data_len);
+              }
+              break;
+          case WEBSOCKET_OPCODE_BINARY:
+              fprintf(stderr, "BINARY...\n");
+              break;
+          case WEBSOCKET_OPCODE_CONNECTION_CLOSE:
+              fprintf(stderr, "CLOSE...\n");
+              /* If client initiated close, respond with close message in acknowlegement */
+              if (!ws_conn[wsd].closing)
+              {
+                  mg_websocket_write(conn, WEBSOCKET_OPCODE_CONNECTION_CLOSE, data, data_len);
+                  ws_conn[wsd].closing = 1; /* we should not send addional messages when close requested/acknowledged */
+              }
+              return 0; /* time to close the connection */
+              break;
+          case WEBSOCKET_OPCODE_PING:
+              /* client sent PING, respond with PONG */
+              mg_websocket_write(conn, WEBSOCKET_OPCODE_PONG, data, data_len);
+              break;
+          case WEBSOCKET_OPCODE_PONG:
+              /* received PONG to our PING, no action */
+              break;
+          default:
+              fprintf(stderr, "Unknown flags: %02x\n", flags);
+              break;
+      }
+    }
+
+    return 1;   /* keep connection open */
+}
+
+
+int main(void)
+{
+    char server_name[40];
+    struct mg_context *ctx;
+    struct mg_callbacks callbacks;
+    const char *options[] = {
+        "listening_ports", "8080",
+        "document_root", "docroot",
+        NULL
+    };
+
+    /* get simple greeting for the web server */
+    snprintf(server_name, sizeof(server_name), 
+            "Civetweb websocket server v. %s",
+            mg_version());
+
+    memset(&callbacks, 0, sizeof(callbacks));
+    callbacks.websocket_connect = websocket_connect_handler;
+    callbacks.websocket_ready = websocket_ready_handler;
+    callbacks.websocket_data = websocket_data_handler;
+    callbacks.connection_close = websocket_close_handler;
+
+    ctx = mg_start(&callbacks, NULL, options);
+
+    /* show the greeting and some basic information */
+    printf("%s started on port(s) %s with web root [%s]\n",
+            server_name, mg_get_option(ctx, "listening_ports"),
+            mg_get_option(ctx, "document_root"));
+
+    getchar();  // Wait until user hits "enter"
+    mg_stop(ctx);
+
+    return 0;
+}