├── .gitignore ├── README.md ├── chat.pl ├── daemon.pl ├── debug.pl ├── stress_client.pl └── upstart └── chat.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SWI-Prolog based chat server 2 | 3 | This repository provides a demonstration for using the recently added 4 | websocket support for realising a chat server. To use it, you must 5 | install *[SWI-Prolog](http://www.swi-prolog.org) 7.1.23 or later*. Then, 6 | you can load `debug.pl` and run 7 | 8 | ?- server. 9 | 10 | This will start the server at port 3050 in debug mode, showing a 11 | graphical window with the running server threads and debug messages in 12 | the console. Load `chat.pl` if you want to run the demo interactively, 13 | but without noisy debugging messages. 14 | 15 | ## Running as a service 16 | 17 | The script `daemon.pl` is provided to start the server as a Unix daemon 18 | process. Run `./daemon.pl --help` for a brief help message. 19 | 20 | ### Starting as an upstart job 21 | 22 | The service can be started as an (Ubuntu) upstart job by copying 23 | `upstart/chat.conf` to `/etc/init` editing `/etc/init/chat.conf` to 24 | reflect the installation directory. After that, run this command to 25 | start the server: 26 | 27 | % sudo service chat start 28 | 29 | ## Status 30 | 31 | The chatserver itself is really simple. Tested with the server running 32 | on Linux using firefox and chromium as clients. Anne Ogborn confirmed it 33 | also works using Windows 7 as server and IE 11.0 as client. 34 | -------------------------------------------------------------------------------- /chat.pl: -------------------------------------------------------------------------------- 1 | /* Part of SWI-Prolog 2 | 3 | Author: Jan Wielemaker 4 | E-mail: J.Wielemaker@cs.vu.nl 5 | WWW: http://www.swi-prolog.org 6 | Copyright (C): 2014, VU University Amsterdam 7 | 8 | This program is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU General Public License 10 | as published by the Free Software Foundation; either version 2 11 | of the License, or (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public 19 | License along with this library; if not, write to the Free Software 20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 21 | 22 | As a special exception, if you link this library with other files, 23 | compiled with a Free Software compiler, to produce an executable, this 24 | library does not by itself cause the resulting executable to be covered 25 | by the GNU General Public License. This exception does not however 26 | invalidate any other reasons why the executable file might be covered by 27 | the GNU General Public License. 28 | */ 29 | 30 | :- module(chat_server, 31 | [ server/0, 32 | server/1, % ?Port 33 | create_chat_room/0 34 | ]). 35 | :- use_module(library(http/thread_httpd)). 36 | :- use_module(library(http/http_dispatch)). 37 | :- use_module(library(http/websocket)). 38 | :- use_module(library(http/html_write)). 39 | :- use_module(library(http/js_write)). 40 | :- use_module(library(http/hub)). 41 | :- use_module(library(debug)). 42 | 43 | 44 | /** A scalable websocket based chat server in SWI-Prolog 45 | 46 | Chat servers are an example of services that require mixed initiative 47 | and may be used to serve many connections. One way to implement is using 48 | long-polling: the browser asks for events from the server. The server 49 | waits until there is an event or it times out after -say- 1 minute, 50 | after which the server replies there are no events and the client tries 51 | again. The long polling structure can be implemented in the SWI-Prolog 52 | server architecture, but it is rather expensive because it implies a 53 | Prolog thread for each blocking call. 54 | 55 | This demo application implements a chatroom using _websockets_. The 56 | implementation uses library(http/hub), which bundles the responsibility 57 | for multiple websockets in a small number of threads by using I/O 58 | multiplexing based on wait_for_input/3. As a user of hub.pl, life is 59 | fairly straighforward: 60 | 61 | - Create a hub using hub_create/3 and a thread that 62 | listens to chat events and broadcasts the changes. 63 | 64 | - Serve a web page that provides the chat frontend. The frontend 65 | contains JavaScript that establishes a websocket on /chat. If 66 | a websocket is obtained, hand it to to the room using 67 | hub_add/2 68 | */ 69 | 70 | 71 | %% server is det. 72 | %% server(?Port) is det. 73 | % 74 | % Create the chat room and start the server. The default port is 75 | % 3050. 76 | 77 | server :- 78 | server(3050). 79 | 80 | server(Port) :- 81 | ( debugging(chat), 82 | current_prolog_flag(gui, true) 83 | -> prolog_ide(thread_monitor) 84 | ; true 85 | ), 86 | create_chat_room, 87 | http_server(http_dispatch, [port(Port)]). 88 | 89 | % setup the HTTP location. The first (/) loads the application. The 90 | % loaded application will create a websocket using /chat. Normally, 91 | % http_upgrade_to_websocket/3 runs call(Goal, WebSocket) and closes the 92 | % connection if Goal terminates. Here, we use guarded(false) to tell the 93 | % server we will take responsibility for the websocket. 94 | 95 | :- http_handler(root(.), chat_page, []). 96 | :- http_handler(root(chat), 97 | http_upgrade_to_websocket( 98 | accept_chat, 99 | [ guarded(false), 100 | subprotocols([chat]) 101 | ]), 102 | [ id(chat_websocket) 103 | ]). 104 | 105 | chat_page(_Request) :- 106 | reply_html_page( 107 | title('SWI-Prolog chat demo'), 108 | \chat_page). 109 | 110 | %% chat_page// 111 | % 112 | % Generate the web page. 113 | 114 | chat_page --> 115 | style, 116 | html([ h1('YAWSBCR: Yet Another ...'), 117 | div([ id(chat) 118 | ], []), 119 | input([ placeholder('Type a message and hit RETURN'), 120 | id(input), 121 | onkeypress('handleInput(event)') 122 | ], []) 123 | ]), 124 | script. 125 | 126 | %% style// 127 | % 128 | % Emit the style sheet. Typically, this comes from a static file. 129 | % We generate it inline here to keep everything in one file. 130 | % Second best would be to use a quasi quotation, but the library 131 | % does not provide a CSS quasi quotation (yet). As CSS does 132 | % contains few special characters, this is bearable. 133 | 134 | style --> 135 | html(style([ 'body,html { height:100%; overflow: hidden; }\n', 136 | '#chat { height: calc(100% - 150px); overflow-y:scroll; \c 137 | border: solid 1px black; padding:5px; }\n', 138 | '#input { width:100%; border:solid 1px black; \c 139 | padding: 5px; box-sizing: border-box; }\n' 140 | ])). 141 | 142 | %% script// 143 | % 144 | % Generate the JavaScript that establishes the websocket and 145 | % handles events on the websocket. 146 | 147 | script --> 148 | { http_link_to_id(chat_websocket, [], WebSocketURL) 149 | }, 150 | js_script({|javascript(WebSocketURL)|| 151 | function handleInput(e) { 152 | if ( !e ) e = window.event; // IE 153 | if ( e.keyCode == 13 ) { 154 | var msg = document.getElementById("input").value; 155 | sendChat(msg); 156 | document.getElementById("input").value = ""; 157 | } 158 | } 159 | 160 | var connection; 161 | 162 | function openWebSocket() { 163 | connection = new WebSocket("ws://"+window.location.host+WebSocketURL, 164 | ['chat']); 165 | 166 | connection.onerror = function (error) { 167 | console.log('WebSocket Error ' + error); 168 | }; 169 | 170 | connection.onmessage = function (e) { 171 | var chat = document.getElementById("chat"); 172 | var msg = document.createElement("div"); 173 | msg.appendChild(document.createTextNode(e.data)); 174 | var child = chat.appendChild(msg); 175 | child.scrollIntoView(false); 176 | }; 177 | } 178 | 179 | function sendChat(msg) { 180 | connection.send(msg); 181 | } 182 | 183 | window.addEventListener("DOMContentLoaded", openWebSocket, false); 184 | |}). 185 | 186 | 187 | %% accept_chat(+WebSocket) is det. 188 | % 189 | % Normally, the goal called by http_upgrade_to_websocket/3 190 | % processes all communication with the websocket in a read/write 191 | % loop. In this case however, we tell http_upgrade_to_websocket/3 192 | % that we will take responsibility for the websocket and we hand 193 | % it to the chat room. 194 | 195 | accept_chat(WebSocket) :- 196 | hub_add(chat, WebSocket, _Id). 197 | 198 | %% create_chat_room 199 | % 200 | % Create our actual chat room. 201 | 202 | :- dynamic 203 | utterance/1, % messages 204 | visitor/1. % joined visitors 205 | 206 | create_chat_room :- 207 | hub_create(chat, Room, _{}), 208 | thread_create(chatroom(Room), _, [alias(chatroom)]). 209 | 210 | %% chatroom(+Room) 211 | % 212 | % Realise the chatroom main loop: listen for an event, update the 213 | % state and possibly broadcast status updates. 214 | 215 | chatroom(Room) :- 216 | thread_get_message(Room.queues.event, Message), 217 | handle_message(Message, Room), 218 | chatroom(Room). 219 | 220 | handle_message(Message, Room) :- 221 | websocket{opcode:text} :< Message, !, 222 | assertz(utterance(Message)), 223 | hub_broadcast(Room.name, Message). 224 | handle_message(Message, _Room) :- 225 | hub{joined:Id} :< Message, !, 226 | assertz(visitor(Id)), 227 | forall(utterance(Utterance), 228 | hub_send(Id, Utterance)). 229 | handle_message(Message, _Room) :- 230 | hub{left:Id} :< Message, !, 231 | retractall(visitor(Id)). 232 | handle_message(Message, _Room) :- 233 | debug(chat, 'Ignoring message ~p', [Message]). 234 | -------------------------------------------------------------------------------- /daemon.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swipl 2 | 3 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4 | Script to start the SWI-Prolog chat server as a Unix daemon process 5 | based on library(http/http_unix_daemon). The server is started as a 6 | deamon using 7 | 8 | % ./daemon.pl port=Port [option ...] 9 | 10 | See library(http/http_unix_daemon) for details. Using ./deamon.pl --help 11 | for a brief help message. 12 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 13 | 14 | :- set_prolog_flag(verbose, silent). 15 | :- use_module(library(http/http_unix_daemon)). 16 | :- use_module(library(broadcast)). 17 | :- use_module(chat). 18 | 19 | % http_daemon/0 processes the commandline options, creates the requested 20 | % daemon setup and starts the HTTP server. It is not allowed to create 21 | % any threads before calling http_daemon/0 and therefore we use the 22 | % http_daemon broadcast event `http(pre_server_start)` to start our chat 23 | % thread. 24 | 25 | :- initialization http_daemon. 26 | 27 | :- listen(http(pre_server_start), create_chat_room). 28 | -------------------------------------------------------------------------------- /debug.pl: -------------------------------------------------------------------------------- 1 | :- use_module(library(debug)). 2 | 3 | % This is a demo. Be chatty. Comment for silent operation 4 | % :- debug(websocket). 5 | % :- debug(chatroom(wait)). % quite noisy 6 | :- debug(chatroom(event)). % new events 7 | :- debug(chatroom(door)). % visitors joining and leaving 8 | :- debug(chatroom(broadcast)). % messages sent 9 | :- debug(chatroom(thread)). % give threads a name 10 | :- debug(chat). % this demo 11 | :- debug_message_context(+time). % add timestamp to debug message 12 | 13 | :- use_module(chat). 14 | -------------------------------------------------------------------------------- /stress_client.pl: -------------------------------------------------------------------------------- 1 | :- module(chat_client, 2 | [ chat_client/1, 3 | clients/1 4 | ]). 5 | :- use_module(library(debug)). 6 | :- use_module(library(http/websocket)). 7 | 8 | :- debug(chatter(_)). 9 | 10 | clients(N) :- 11 | clients('http://localhost:3050/chat', N). 12 | 13 | clients(Url, N) :- 14 | forall(between(1, N, _), 15 | client(Url)). 16 | 17 | client(URL) :- 18 | Mean is 0.2 + random_float * 10, 19 | StDev is Mean/2, 20 | gensym(client_, Base), 21 | format(atom(Alias), '~w_~2f', [Base, Mean]), 22 | thread_create(chat_client(_{ url:URL, 23 | timeout:_{mean:Mean, stdev:StDev} 24 | }), 25 | _, [detached(true), alias(Alias)]). 26 | 27 | chat_client(Options) :- 28 | http_open_websocket(Options.url, WebSocket, []), 29 | chatter(WebSocket, Options). 30 | 31 | chatter(WebSocket, Options) :- 32 | timeout(Options, Timeout), 33 | debug(chatter(receiver), 'Waiting for ~w seconds', [Timeout]), 34 | wait_for_input([WebSocket], Ready, Timeout), 35 | ( Ready == [] 36 | -> message(Options, Message), 37 | ws_send(WebSocket, Message), 38 | chatter(WebSocket, Options) 39 | ; ws_receive(WebSocket, Message), 40 | debug(chatter(receiver), 'Got ~p', [Message]), 41 | ( _{opcode:close, data:end_of_file} :< Message 42 | -> catch(ws_close(WebSocket, 1011, end_of_file), Error, 43 | print_message(warning, Error)) 44 | ; chatter(WebSocket, Options) 45 | ) 46 | ). 47 | 48 | timeout(Options, Timeout) :- 49 | ( _{ mean:Mean, stdev:StDev } :< Options.get(timeout) 50 | -> true 51 | ; Mean is 10, 52 | StDev is 5 53 | ), 54 | gausian_random(Mean, StDev, Timeout0), 55 | Timeout is max(0, Timeout0). 56 | 57 | message(_Options, text(Message)) :- 58 | gensym(hello_, Message). 59 | 60 | 61 | %% gausian_random(+Mean, +StDev, -Value) 62 | % 63 | % @see http://www.design.caltech.edu/erik/Misc/Gaussian.html 64 | 65 | gausian_random(Mean, StDev, Value) :- 66 | Value is Mean + StDev * sqrt(-2*log(random_float)) * cos(1*pi*random_float). 67 | -------------------------------------------------------------------------------- /upstart/chat.conf: -------------------------------------------------------------------------------- 1 | # chat - SWI-Prolog chat server 2 | # 3 | # The SWI-Prolog chat demo server 4 | 5 | description "Chat server" 6 | 7 | start on runlevel [2345] 8 | stop on runlevel [!2345] 9 | 10 | respawn 11 | respawn limit 5 60 12 | umask 022 13 | 14 | console log 15 | chdir /home/prolog/src/swi-chat 16 | 17 | script 18 | export LANG=en_US.utf8 19 | ./daemon.pl --no-fork --port=3050 --user=www-data --pidfile=/var/run/chat.pid --workers=4 --syslog=chat 20 | end script 21 | --------------------------------------------------------------------------------