Updated to a working websocket implementation of webclient.

This commit is contained in:
Griatch 2014-06-26 23:37:03 +02:00
parent ca1e36da5f
commit d59500f574
6 changed files with 133 additions and 121 deletions

View file

@ -42,20 +42,21 @@ TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS SSL_PORTS = settings.SSL_PORTS
SSH_PORTS = settings.SSH_PORTS SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSOCKET_PORTS = settings.WEBSOCKET_PORTS WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
TELNET_INTERFACES = settings.TELNET_INTERFACES TELNET_INTERFACES = settings.TELNET_INTERFACES
SSL_INTERFACES = settings.SSL_INTERFACES SSL_INTERFACES = settings.SSL_INTERFACES
SSH_INTERFACES = settings.SSH_INTERFACES SSH_INTERFACES = settings.SSH_INTERFACES
WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES WEBSERVER_INTERFACES = settings.WEBSERVER_INTERFACES
WEBSOCKET_INTERFACES = settings.WEBSOCKET_INTERFACES WEBSOCKET_CLIENT_INTERFACE = settings.WEBSOCKET_CLIENT_INTERFACE
WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_ENABLED = settings.WEBSOCKET_ENABLED and WEBSOCKET_PORTS and WEBSOCKET_INTERFACES WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
AMP_HOST = settings.AMP_HOST AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT AMP_PORT = settings.AMP_PORT
@ -257,16 +258,31 @@ if WEBSERVER_ENABLED:
if WEBCLIENT_ENABLED: if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata # create ajax client processes at /webclientdata
from src.server.portal.webclient import WebClient from src.server.portal.webclient import WebClient
webclient = WebClient() webclient = WebClient()
webclient.sessionhandler = PORTAL_SESSIONS webclient.sessionhandler = PORTAL_SESSIONS
web_root.putChild("webclientdata", webclient) web_root.putChild("webclientdata", webclient)
webclientstr = "/client" webclientstr = "\n + client (ajax only)"
#from src.server.portal import websocket if WEBSOCKET_CLIENT_ENABLED:
#factory = protocol.ServerFactory() # start websocket client port for the webclient
#websocketclient = websocket.WebSocketProtocol() from src.server.portal import websocket_client
#websocketclient.sessionhandler = PORTAL_SESSIONS from src.utils.txws import WebSocketFactory
#web_root.putChild("websocket", websocketclient)
interface = WEBSOCKET_CLIENT_INTERFACE
port = WEBSOCKET_CLIENT_PORT
ifacestr = ""
if interface not in ('0.0.0.0', '::'):
ifacestr = "-%s" % interface
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = websocket_client.WebSocketClient
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
websocket_service.setName('EvenniaWebSocket%s' % pstring)
PORTAL.services.addService(websocket_service)
webclientstr = webclientstr[:-11] + "(%s:%s)" % (WEBSOCKET_CLIENT_URL, port)
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
proxy_service = internet.TCPServer(proxyport, proxy_service = internet.TCPServer(proxyport,
@ -274,30 +290,8 @@ if WEBSERVER_ENABLED:
interface=interface) interface=interface)
proxy_service.setName('EvenniaWebProxy%s' % pstring) proxy_service.setName('EvenniaWebProxy%s' % pstring)
PORTAL.services.addService(proxy_service) PORTAL.services.addService(proxy_service)
print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport) print " webproxy%s:%s (<-> %s)%s" % (ifacestr, proxyport, serverport, webclientstr)
if WEBSOCKET_ENABLED:
# websocket support is experimental!
# start websocket ports for real-time web communication
from src.server.portal import websocket
from src.utils.txws import WebSocketFactory
for interface in WEBSOCKET_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(WEBSOCKET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in WEBSOCKET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = protocol.ServerFactory()
factory.protocol = websocket.WebSocketProtocol
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
websocket_service.setName('EvenniaWebSocket%s' % pstring)
PORTAL.services.addService(websocket_service)
print ' websocket%s: %s' % (ifacestr, port)
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES: for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start # external plugin services to start

View file

@ -1,8 +1,9 @@
""" """
Websockets Protocol Websocket-webclient
This implements WebSockets (http://en.wikipedia.org/wiki/WebSocket) This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
used together with src/web/media/javascript/evennia_websocket_webclient.js.
Thanks to Ricard Pillosu whose Evennia plugin inspired this module. Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
@ -10,13 +11,13 @@ Communication over the websocket interface is done with normal text
communication. A special case is OOB-style communication; to do this communication. A special case is OOB-style communication; to do this
the client must send data on the following form: the client must send data on the following form:
OOB{oobfunc:[[args], {kwargs}], ...} OOB{"func1":[args], "func2":[args], ...}
where the tuple/list is sent json-encoded. The initial OOB-prefix where the dict is JSON encoded. The initial OOB-prefix
is used to identify this type of communication, all other data is used to identify this type of communication, all other data
is considered plain text (command input). is considered plain text (command input).
Example of call from javascript client: Example of call from a javascript client:
websocket = new WeSocket("ws://localhost:8021") websocket = new WeSocket("ws://localhost:8021")
var msg1 = "WebSocket Test" var msg1 = "WebSocket Test"
@ -33,9 +34,10 @@ from src.utils.logger import log_trace
from src.utils.utils import to_str from src.utils.utils import to_str
from src.utils.text2html import parse_html from src.utils.text2html import parse_html
class WebSocketProtocol(Protocol, Session):
class WebSocketClient(Protocol, Session):
""" """
This is called when the connection is first established Implements the server-side of the Websocket connection.
""" """
def connectionMade(self): def connectionMade(self):

View file

@ -58,18 +58,22 @@ UPSTREAM_IPS = ['127.0.0.1']
# with server load. Set the minimum and maximum number of threads it # with server load. Set the minimum and maximum number of threads it
# may use as (min, max) (must be > 0) # may use as (min, max) (must be > 0)
WEBSERVER_THREADPOOL_LIMITS = (1, 20) WEBSERVER_THREADPOOL_LIMITS = (1, 20)
# Start the evennia ajax client on /webclient # Start the evennia webclient. This requires the webserver to be running and
# (the webserver must also be running) # offers the fallback ajax-based webclient backbone for browsers not supporting
# the websocket one.
WEBCLIENT_ENABLED = True WEBCLIENT_ENABLED = True
# Activate Websocket support. If this is on, the default webclient will use this # Activate Websocket support for modern browsers. If this is on, the
# before going for the ajax implementation # default webclient will use this and only use the ajax version of the browser
WEBSOCKET_ENABLED = True # is too old to support websockets. Requires WEBCLIENT_ENABLED.
# Ports to use for Websockets. If this is changed, you must also update WEBSOCKET_CLIENT_ENABLED = True
# src/web/media/javascript/evennia_websocket_webclient.js to match. # Server-side websocket port to open for the webclient.
WEBSOCKET_PORTS = [8001] WEBSOCKET_CLIENT_PORT = 8001
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. # Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
WEBSOCKET_INTERFACES = ['0.0.0.0'] WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
# Activate SSH protocol (SecureShell) # Actual URL for webclient component to reach the websocket. The first
# port number in the WEBSOCKET_PORTS list will be automatically appended.
WEBSOCKET_CLIENT_URL = "ws://localhost"
# Activate SSH protocol communication (SecureShell)
SSH_ENABLED = False SSH_ENABLED = False
# Ports to use for SSH # Ports to use for SSH
SSH_PORTS = [8022] SSH_PORTS = [8022]

View file

@ -2,30 +2,39 @@
Evennia websocket webclient (javascript component) Evennia websocket webclient (javascript component)
The client is composed of several parts: The client is composed of two parts:
templates/webclient.html - the main page src/server/portal/websocket_client.py - the portal-side component
webclient/views.py - the django view serving the template (based on urls.py pattern)
src/server/portal/websockets.py - the server component talking to the client
this file - the javascript component handling dynamic content this file - the javascript component handling dynamic content
This implements an mud client for use with Evennia, using jQuery messages sent to the client is one of two modes:
for simplicity. OOB("func1",args, "func2",args, ...) - OOB command executions, this will
call unique javascript functions
messages sent to the client is one of three modes: func1(args), func2(args) etc.
OOB(func,(args), func,(args), ...) - OOB command executions text - any other text is considered a normal text output in the main output window.
text - any other text is considered a normal text to echo
*/ */
// jQuery must be imported by the calling html page before this script // If on, allows client user to send OOB messages to server by
// There are plenty of help on using the jQuery library on http://jquery.com/ // prepending with ##OOB{}, for example ##OOB{"echo":[1,2,3,4]}
var OOB_debug = true
// Server communications // Custom OOB functions
// Set this to the value matching settings.WEBSOCKET_PORTS // functions defined here can be called by name by the server. For
var wsurl = "ws://localhost:8001"; // example the OOB{"echo":arguments} will trigger a function named
// echo(arguments).
function echo(message) {
// example echo function.
doShow("out", "ECHO return: " + message) }
// Webclient code
function webclient_init(){ function webclient_init(){
// initializing the client once the html page has loaded // called when client is just initializing
websocket = new WebSocket(wsurl); websocket = new WebSocket(wsurl);
websocket.onopen = function(evt) { onOpen(evt) }; websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) }; websocket.onclose = function(evt) { onClose(evt) };
@ -34,91 +43,100 @@ function webclient_init(){
} }
function onOpen(evt) { function onOpen(evt) {
// client is just connecting // called when client is first connecting
$("#connecting").remove(); // remove the "connecting ..." message $("#connecting").remove(); // remove the "connecting ..." message
msg_display("sys", "Using websockets - connected to " + wsurl + ".") doShow("sys", "Using websockets - connected to " + wsurl + ".")
setTimeout(function () { setTimeout(function () {
$("#playercount").fadeOut('slow', webclient_set_sizes); $("#playercount").fadeOut('slow', doSetSizes);
}, 10000); }, 10000);
} }
function onClose(evt) { function onClose(evt) {
// client is closing // called when client is closing
CLIENT_HASH = 0; CLIENT_HASH = 0;
alert("Mud client connection was closed cleanly."); alert("Mud client connection was closed cleanly.");
} }
function onMessage(evt) { function onMessage(evt) {
// outgoing message from server // called when the Evennia is sending data to client
var inmsg = evt.data var inmsg = evt.data
if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") { if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") {
// dynamically call oob methods, if available // dynamically call oob methods, if available
try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB } try {var oobarray = JSON.parse(inmsg.slice(3));} // everything after OOB }
catch(err) { catch(err) {
// not JSON packed - a normal text // not JSON packed - a normal text
msg_display('out', err + " " + inmsg); doShow('out', err + " " + inmsg);
return; return;
} }
for (var ind in oobarray) { for (var ind in oobarray) {
try { window[oobarray[ind][0]](oobarray[ind][1]) } try { window[oobarray[ind][0]](oobarray[ind][1]) }
catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") } catch(err) { doShow("err", "Could not execute OOB function " + oobtuple[0] + "(" + oobtuple[1] + ")!") }
} }
} }
else { else {
// normal message // normal message
msg_display('out', inmsg); } doShow('out', inmsg); }
} }
function onError(evt) { function onError(evt) {
// client error message // called on a server error
msg_display('err', "Error: Server returned an error. Try reloading the page."); doShow('err', "Error: Server returned an error. Try reloading the page.");
} }
function doSend(){ function doSend(){
// sending data from client to server // relays data from client to Evennia.
// If OOB_debug is set, allows OOB test data on the
// form ##OOB{func:args}
outmsg = $("#inputfield").val(); outmsg = $("#inputfield").val();
history_add(outmsg); history_add(outmsg);
HISTORY_POS = 0; HISTORY_POS = 0;
$('#inputform')[0].reset(); // clear input field $('#inputform')[0].reset(); // clear input field
if (outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") { if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
// test OOB messaging // test OOB messaging
doOOB(JSON.parse(outmsg.slice(5))); } doOOB(JSON.parse(outmsg.slice(5))); }
else { else {
// normal output
websocket.send(outmsg); } websocket.send(outmsg); }
} }
function doOOB(oobdict){ function doOOB(oobdict){
// Handle OOB communication from client side // Send OOB data from client to Evennia.
// Takes a dict on form {funcname:[args], funcname: [args], ... ] // Takes input on form {funcname:[args], funcname: [args], ... }
msg_display("out", "into doOOB: " + oobdict)
msg_display("out", "stringify: " + JSON.stringify(oobdict))
var oobmsg = JSON.stringify(oobdict); var oobmsg = JSON.stringify(oobdict);
websocket.send("OOB" + oobmsg); websocket.send("OOB" + oobmsg);
} }
function doShow(type, msg){
// // Add msg to the main output window.
// OOB functions
//
function echo(message) {
msg_display("out", "ECHO return: " + message) }
//
// Display messages
function msg_display(type, msg){
// Add a div to the message window.
// type gives the class of div to use. // type gives the class of div to use.
// The default types are
// "out" (normal output) or "err" (red error message)
$("#messagewindow").append( $("#messagewindow").append(
"<div class='msg "+ type +"'>"+ msg +"</div>"); "<div class='msg "+ type +"'>"+ msg +"</div>");
// scroll message window to bottom // scroll message window to bottom
$('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight}); $('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight});
} }
// Input history mechanism
function doSetSizes() {
// Sets the size of the message window
var win_h = $(document).height();
//var win_w = $('#wrapper').width();
var inp_h = $('#inputform').outerHeight(true);
//var inp_w = $('#inputsend').outerWidth(true);
$("#messagewindow").css({'height': win_h - inp_h - 1});
//$("#inputfield").css({'width': win_w - inp_w - 20});
}
//
// Input code
//
// Input history
var HISTORY_MAX_LENGTH = 21 var HISTORY_MAX_LENGTH = 21
var HISTORY = new Array(); var HISTORY = new Array();
@ -223,6 +241,8 @@ $.fn.appendCaret = function() {
}); });
}; };
// Input jQuery callbacks
$(document).keydown( function(event) { $(document).keydown( function(event) {
// Get the pressed key (normalized by jQuery) // Get the pressed key (normalized by jQuery)
var code = event.which, var code = event.which,
@ -233,7 +253,7 @@ $(document).keydown( function(event) {
// Special keys recognized by client // Special keys recognized by client
//msg_display("out", "key code pressed: " + code); // debug //doShow("out", "key code pressed: " + code); // debug
if (code == 13) { // Enter Key if (code == 13) { // Enter Key
doSend(); doSend();
@ -252,36 +272,24 @@ $(document).keydown( function(event) {
// handler to avoid double-clicks until the ajax request finishes // handler to avoid double-clicks until the ajax request finishes
//$("#inputsend").one("click", webclient_input) //$("#inputsend").one("click", webclient_input)
function webclient_set_sizes() { // Callback function - called when the browser window resizes
// Sets the size of the message window $(window).resize(doSetSizes);
var win_h = $(document).height();
//var win_w = $('#wrapper').width();
var inp_h = $('#inputform').outerHeight(true);
//var inp_w = $('#inputsend').outerWidth(true);
$("#messagewindow").css({'height': win_h - inp_h - 1}); // Callback function - called when page is closed or moved away from.
//$("#inputfield").css({'width': win_w - inp_w - 20}); //$(window).bind("beforeunload", webclient_close);
} //
// Callback function - called when page has finished loading (kicks the client into gear)
// Callback function - called when page has finished loading (gets things going)
$(document).ready(function(){ $(document).ready(function(){
// remove the "no javascript" warning, since we obviously have javascript // remove the "no javascript" warning, since we obviously have javascript
$('#noscript').remove(); $('#noscript').remove();
// set sizes of elements and reposition them // set sizes of elements and reposition them
webclient_set_sizes(); doSetSizes();
// a small timeout to stop 'loading' indicator in Chrome // a small timeout to stop 'loading' indicator in Chrome
setTimeout(function () { setTimeout(function () {
webclient_init(); webclient_init();
}, 500); }, 500);
// set an idle timer to avoid proxy servers to time out on us (every 3 minutes) // set an idle timer to avoid proxy servers to time out on us (every 3 minutes)
setInterval(function() { setInterval(function() {
webclient_input("idle", true); websocket.send("idle");
}, 60000*3); }, 60000*3);
}); });
// Callback function - called when the browser window resizes
$(window).resize(webclient_set_sizes);
// Callback function - called when page is closed or moved away from.
//$(window).bind("beforeunload", webclient_close);

View file

@ -19,9 +19,10 @@
<script language="javascript" type="text/javascript"> <script language="javascript" type="text/javascript">
if ("WebSocket" in window) { if ("WebSocket" in window) {
<!-- Importing the Evennia websocket webclient component (requires jQuery) --> <!-- Importing the Evennia websocket webclient component (requires jQuery) -->
var wsurl = "{{websocket_url}}";
document.write("\<script src=\"/media/javascript/evennia_websocket_webclient.js\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")} document.write("\<script src=\"/media/javascript/evennia_websocket_webclient.js\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")}
else { else {
<!-- Importing the Evennia ajax webclient component (requires jQuery) --> <!-- No websocket support in browser. Importing the Evennia ajax webclient component (requires jQuery) -->
document.write("\<script src=\"/media/javascript/evennia_ajax_webclient.js\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")} document.write("\<script src=\"/media/javascript/evennia_ajax_webclient.js\" type=\"text/javascript\" charset=\"utf-8\"\>\</script\>")}
</script> </script>
{% else %} {% else %}

View file

@ -6,7 +6,6 @@
# tuple. # tuple.
# #
from django.db import models
from django.conf import settings from django.conf import settings
from src.utils.utils import get_evennia_version from src.utils.utils import get_evennia_version
@ -30,6 +29,9 @@ WEBSITE = ['Flatpages', 'News', 'Sites']
# The main context processor function # The main context processor function
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED
WSURL = "%s:%s" % (settings.WEBSOCKET_CLIENT_URL, settings.WEBSOCKET_CLIENT_PORT)
def general_context(request): def general_context(request):
""" """
@ -44,6 +46,7 @@ def general_context(request):
'evennia_setupapps': GAME_SETUP, 'evennia_setupapps': GAME_SETUP,
'evennia_connectapps': CONNECTIONS, 'evennia_connectapps': CONNECTIONS,
'evennia_websiteapps':WEBSITE, 'evennia_websiteapps':WEBSITE,
"webclient_enabled" : settings.WEBCLIENT_ENABLED, "webclient_enabled" : WEBCLIENT_ENABLED,
"websocket_enabled" : settings.WEBSOCKET_ENABLED "websocket_enabled" : WEBSOCKET_CLIENT_ENABLED,
"websocket_url" : WSURL
} }