Skip to content

Instantly share code, notes, and snippets.

@mitsuoka
Last active November 11, 2022 12:43
Show Gist options
  • Save mitsuoka/2969464 to your computer and use it in GitHub Desktop.
Save mitsuoka/2969464 to your computer and use it in GitHub Desktop.
Dart WebSocket chat server and client samples
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Bootstrap support for Dart scripts on the page as this script.
if (navigator.webkitStartDart) {
if (!navigator.webkitStartDart()) {
document.body.innerHTML = 'This build has expired. Please download a new Dartium at http://www.dartlang.org/dartium/index.html';
}
} else {
// TODO:
// - Support in-browser compilation.
// - Handle inline Dart scripts.
window.addEventListener("DOMContentLoaded", function (e) {
// Fall back to compiled JS. Run through all the scripts and
// replace them if they have a type that indicate that they source
// in Dart code.
//
// <script type="application/dart" src="..."></script>
//
var scripts = document.getElementsByTagName("script");
var length = scripts.length;
for (var i = 0; i < length; ++i) {
if (scripts[i].type == "application/dart") {
// Remap foo.dart to foo.dart.js.
if (scripts[i].src && scripts[i].src != '') {
var script = document.createElement('script');
script.src = scripts[i].src + '.js';
var parent = scripts[i].parentNode;
parent.replaceChild(script, scripts[i]);
}
}
}
}, false);
}
// ---------------------------------------------------------------------------
// Experimental support for JS interoperability
// ---------------------------------------------------------------------------
function SendPortSync() {
}
function ReceivePortSync() {
this.id = ReceivePortSync.id++;
ReceivePortSync.map[this.id] = this;
}
(function() {
// Serialize the following types as follows:
// - primitives / null: unchanged
// - lists: [ 'list', internal id, list of recursively serialized elements ]
// - maps: [ 'map', internal id, map of keys and recursively serialized values ]
// - send ports: [ 'sendport', type, isolate id, port id ]
//
// Note, internal id's are for cycle detection.
function serialize(message) {
var visited = [];
function checkedSerialization(obj, serializer) {
// Implementation detail: for now use linear search.
// Another option is expando, but it may prohibit
// VM optimizations (like putting object into slow mode
// on property deletion.)
var id = visited.indexOf(obj);
if (id != -1) return [ 'ref', id ];
var id = visited.length;
visited.push(obj);
return serializer(id);
}
function doSerialize(message) {
if (message == null) {
return null; // Convert undefined to null.
} else if (typeof(message) == 'string' ||
typeof(message) == 'number' ||
typeof(message) == 'boolean') {
return message;
} else if (message instanceof Array) {
return checkedSerialization(message, function(id) {
var values = new Array(message.length);
for (var i = 0; i < message.length; i++) {
values[i] = doSerialize(message[i]);
}
return [ 'list', id, values ];
});
} else if (message instanceof LocalSendPortSync) {
return [ 'sendport', 'nativejs', message.receivePort.id ];
} else if (message instanceof DartSendPortSync) {
return [ 'sendport', 'dart', message.isolateId, message.portId ];
} else {
return checkedSerialization(message, function(id) {
var keys = Object.getOwnPropertyNames(message);
var values = new Array(keys.length);
for (var i = 0; i < keys.length; i++) {
values[i] = doSerialize(message[keys[i]]);
}
return [ 'map', id, keys, values ];
});
}
}
return doSerialize(message);
}
function deserialize(message) {
return deserializeHelper(message);
}
function deserializeHelper(message) {
if (message == null ||
typeof(message) == 'string' ||
typeof(message) == 'number' ||
typeof(message) == 'boolean') {
return message;
}
switch (message[0]) {
case 'map': return deserializeMap(message);
case 'sendport': return deserializeSendPort(message);
case 'list': return deserializeList(message);
default: throw 'unimplemented';
}
}
function deserializeMap(message) {
var result = { };
var id = message[1];
var keys = message[2];
var values = message[3];
for (var i = 0, length = keys.length; i < length; i++) {
var key = deserializeHelper(keys[i]);
var value = deserializeHelper(values[i]);
result[key] = value;
}
return result;
}
function deserializeSendPort(message) {
var tag = message[1];
switch (tag) {
case 'nativejs':
var id = message[2];
return new LocalSendPortSync(ReceivePortSync.map[id]);
case 'dart':
var isolateId = message[2];
var portId = message[3];
return new DartSendPortSync(isolateId, portId);
default:
throw 'Illegal SendPortSync type: $tag';
}
}
function deserializeList(message) {
var values = message[2];
var length = values.length;
var result = new Array(length);
for (var i = 0; i < length; i++) {
result[i] = deserializeHelper(values[i]);
}
return result;
}
window.registerPort = function(name, port) {
var stringified = JSON.stringify(serialize(port));
var attrName = 'dart-port:' + name;
document.documentElement.setAttribute(attrName, stringified);
};
window.lookupPort = function(name) {
var attrName = 'dart-port:' + name;
var stringified = document.documentElement.getAttribute(attrName);
return deserialize(JSON.parse(stringified));
};
ReceivePortSync.id = 0;
ReceivePortSync.map = {};
ReceivePortSync.dispatchCall = function(id, message) {
// TODO(vsm): Handle and propagate exceptions.
var deserialized = deserialize(message);
var result = ReceivePortSync.map[id].callback(deserialized);
return serialize(result);
};
ReceivePortSync.prototype.receive = function(callback) {
this.callback = callback;
};
ReceivePortSync.prototype.toSendPort = function() {
return new LocalSendPortSync(this);
};
ReceivePortSync.prototype.close = function() {
delete ReceivePortSync.map[this.id];
};
if (navigator.webkitStartDart) {
window.addEventListener('js-sync-message', function(event) {
var data = JSON.parse(getPortSyncEventData(event));
var deserialized = deserialize(data.message);
var result = ReceivePortSync.map[data.id].callback(deserialized);
// TODO(vsm): Handle and propagate exceptions.
dispatchEvent('js-result', serialize(result));
}, false);
}
function LocalSendPortSync(receivePort) {
this.receivePort = receivePort;
}
LocalSendPortSync.prototype = new SendPortSync();
LocalSendPortSync.prototype.callSync = function(message) {
// TODO(vsm): Do a direct deepcopy.
message = deserialize(serialize(message));
return this.receivePort.callback(message);
}
function DartSendPortSync(isolateId, portId) {
this.isolateId = isolateId;
this.portId = portId;
}
DartSendPortSync.prototype = new SendPortSync();
function dispatchEvent(receiver, message) {
var string = JSON.stringify(message);
var event = document.createEvent('CustomEvent');
event.initCustomEvent(receiver, false, false, string);
window.dispatchEvent(event);
}
function getPortSyncEventData(event) {
return event.detail;
}
DartSendPortSync.prototype.callSync = function(message) {
var serialized = serialize(message);
var target = 'dart-port-' + this.isolateId + '-' + this.portId;
// TODO(vsm): Make this re-entrant.
// TODO(vsm): Set this up set once, on the first call.
var source = target + '-result';
var result = null;
var listener = function (e) {
result = JSON.parse(getPortSyncEventData(e));
};
window.addEventListener(source, listener, false);
dispatchEvent(target, [source, serialized]);
window.removeEventListener(source, listener, false);
return deserialize(result);
}
})();
library MIME;
// get MIME type (returns null if there is no such extension)
String mimeType(String extension) => _mimeMaps[extension];
// default MIME type mappings
Map _mimeMaps = const {
'abs': 'audio/x-mpeg',
'ai': 'application/postscript',
'aif': 'audio/x-aiff',
'aifc': 'audio/x-aiff',
'aiff': 'audio/x-aiff',
'aim': 'application/x-aim',
'art': 'image/x-jg',
'asf': 'video/x-ms-asf',
'asx': 'video/x-ms-asf',
'au': 'audio/basic',
'avi': 'video/x-msvideo',
'avx': 'video/x-rad-screenplay',
'bcpio': 'application/x-bcpio',
'bin': 'application/octet-stream',
'bmp': 'image/bmp',
'body': 'text/html',
'cdf': 'application/x-cdf',
'cer': 'application/x-x509-ca-cert',
'class': 'application/java',
'cpio': 'application/x-cpio',
'csh': 'application/x-csh',
'css': 'text/css',
'dart': 'application/vnd.dart',
'dib': 'image/bmp',
'doc': 'application/msword',
'docm': 'application/vnd.ms-word.document.macroEnabled.12',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dot': 'application/msword',
'dotm': 'application/vnd.ms-word.template.macroEnabled.12',
'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'dtd': 'application/xml-dtd',
'dv': 'video/x-dv',
'dvi': 'application/x-dvi',
'eps': 'application/postscript',
'etx': 'text/x-setext',
'exe': 'application/octet-stream',
'gif': 'image/gif',
'gtar': 'application/x-gtar',
'gz': 'application/x-gzip',
'hdf': 'application/x-hdf',
'hqx': 'application/mac-binhex40',
'htc': 'text/x-component',
'htm': 'text/html',
'html': 'text/html',
'ico': 'image/vnd.microsoft.icon',
'ief': 'image/ief',
'jad': 'text/vnd.sun.j2me.app-descriptor',
'jar': 'application/java-archive',
'java': 'text/plain',
'jnlp': 'application/x-java-jnlp-file',
'jpe': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpg': 'image/jpeg',
'js': 'text/javascript',
'jsf': 'text/plain',
'jspf': 'text/plain',
'kar': 'audio/x-midi',
'latex': 'application/x-latex',
'm3u': 'audio/x-mpegurl',
'mac': 'image/x-macpaint',
'man': 'application/x-troff-man',
'mathml': 'application/mathml+xml',
'me': 'application/x-troff-me',
'mid': 'audio/x-midi',
'midi': 'audio/x-midi',
'mif': 'application/x-mif',
'mov': 'video/quicktime',
'movie': 'video/x-sgi-movie',
'mp1': 'audio/x-mpeg',
'mp2': 'audio/x-mpeg',
'mp3': 'audio/x-mpeg',
'mp4': 'video/mp4',
'mpa': 'audio/x-mpeg',
'mpe': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpega': 'audio/x-mpeg',
'mpg': 'video/mpeg',
'mpv2': 'video/mpeg2',
'ms': 'application/x-wais-source',
'nc': 'application/x-netcdf',
'oda': 'application/oda',
'odb': 'application/vnd.oasis.opendocument.database',
'odc': 'application/vnd.oasis.opendocument.chart',
'odf': 'application/vnd.oasis.opendocument.formula',
'odg': 'application/vnd.oasis.opendocument.graphics',
'odi': 'application/vnd.oasis.opendocument.image',
'odm': 'application/vnd.oasis.opendocument.text-master',
'odp': 'application/vnd.oasis.opendocument.presentation',
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
'odt': 'application/vnd.oasis.opendocument.text',
'otg': 'application/vnd.oasis.opendocument.graphics-template',
'oth': 'application/vnd.oasis.opendocument.text-web',
'otp': 'application/vnd.oasis.opendocument.presentation-template',
'ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
'ott': 'application/vnd.oasis.opendocument.text-template',
'ogx': 'application/ogg',
'ogv': 'video/ogg',
'oga': 'audio/ogg',
'ogg': 'audio/ogg',
'spx': 'audio/ogg',
'flac': 'audio/flac',
'anx': 'application/annodex',
'axa': 'audio/annodex',
'axv': 'video/annodex',
'xspf': 'application/xspf+xml',
'pbm': 'image/x-portable-bitmap',
'pct': 'image/pict',
'pdf': 'application/pdf',
'pgm': 'image/x-portable-graymap',
'pic': 'image/pict',
'pict': 'image/pict',
'pls': 'audio/x-scpls',
'png': 'image/png',
'pnm': 'image/x-portable-anymap',
'pnt': 'image/x-macpaint',
'ppm': 'image/x-portable-pixmap',
'ppt': 'application/vnd.ms-powerpoint',
'pot': 'application/vnd.ms-powerpoint',
'pps': 'application/vnd.ms-powerpoint',
'ppa': 'application/vnd.ms-powerpoint',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'potx': 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'ppam': 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
'pptm': 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'potm': 'application/vnd.ms-powerpoint.template.macroEnabled.12',
'ppsm': 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
'ps': 'application/postscript',
'psd': 'image/x-photoshop',
'qt': 'video/quicktime',
'qti': 'image/x-quicktime',
'qtif': 'image/x-quicktime',
'ras': 'image/x-cmu-raster',
'rdf': 'application/rdf+xml',
'rgb': 'image/x-rgb',
'rm': 'application/vnd.rn-realmedia',
'roff': 'application/x-troff',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
'sh': 'application/x-sh',
'shar': 'application/x-shar',
// 'shtml': 'text/x-server-parsed-html',
'smf': 'audio/x-midi',
'sit': 'application/x-stuffit',
'snd': 'audio/basic',
'src': 'application/x-wais-source',
'sv4cpio': 'application/x-sv4cpio',
'sv4crc': 'application/x-sv4crc',
'svg': 'image/svg+xml',
'svgz': 'image/svg+xml',
'swf': 'application/x-shockwave-flash',
't': 'application/x-troff',
'tar': 'application/x-tar',
'tcl': 'application/x-tcl',
'tex': 'application/x-tex',
'texi': 'application/x-texinfo',
'texinfo': 'application/x-texinfo',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'tr': 'application/x-troff',
'tsv': 'text/tab-separated-values',
'txt': 'text/plain',
'ulw': 'audio/basic',
'ustar': 'application/x-ustar',
'vxml': 'application/voicexml+xml',
'xbm': 'image/x-xbitmap',
'xht': 'application/xhtml+xml',
'xhtml': 'application/xhtml+xml',
'xls': 'application/vnd.ms-excel',
'xlt': 'application/vnd.ms-excel',
'xla': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12',
'xltm': 'application/vnd.ms-excel.template.macroEnabled.12',
'xlam': 'application/vnd.ms-excel.addin.macroEnabled.12',
'xlsb': 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xml': 'application/xml',
'xpm': 'image/x-xpixmap',
'xsl': 'application/xml',
'xslt': 'application/xslt+xml',
'xul': 'application/vnd.mozilla.xul+xml',
'xwd': 'image/x-xwindowdump',
'vsd': 'application/x-visio',
'war': 'application/java-archive',
'wav': 'audio/x-wav',
'wbmp': 'image/vnd.wap.wbmp',
'wml': 'text/vnd.wap.wml',
'wmlc': 'application/vnd.wap.wmlc',
'wmls': 'text/vnd.wap.wmlscript',
'wmlscriptc': 'application/vnd.wap.wmlscriptc',
'wmv': 'video/x-ms-wmv',
'wrl': 'x-world/x-vrml',
'wspolicy': 'application/wspolicy+xml',
'Z': 'application/x-compress',
'z': 'application/x-compress',
'zip': 'application/zip'
};
<!--
JavaScript based WebSocketChat client.
This HTML file is used for WebSocketChatServer test.
Open this file from your Chrome browser.
Ref. http://www.websocket.org/echo.html
-->
<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Chat</title>
<script language="javascript" type="text/javascript">
var wsUri = "ws://localhost:8080/Chat";
var mode = "DISCONNECTED";
window.addEventListener("load", init, false);
function init() {
var consoleLog = document.getElementById("consoleLog");
var clearLogBut = document.getElementById("clearLogButton");
clearLogBut.onclick = clearLog;
var connectBut = document.getElementById("joinButton");
connectBut.onclick = doConnect;
var disconnectBut = document.getElementById("leaveButton");
disconnectBut.onclick = doDisconnect;
var sendBut = document.getElementById("sendButton");
sendBut.onclick = doSend;
var userName = document.getElementById("userName");
var sendMessage = document.getElementById("sendMessage");
}
function onOpen(evt) {
logToConsole("CONNECTED");
mode = "CONNECTED";
websocket.send("userName=" + userName.value);
}
function onClose(evt) {
logToConsole("DISCONNECTED");
mode = "DISCONNECTED";
}
function onMessage(evt) {
logToConsole('<span style="color: blue;">' + evt.data+'</span>');
}
function onError(evt) {
logToConsole('<span style="color: red;">ERROR:</span> ' + evt.data);
websocket.close();
}
function doConnect() {
if (mode == "CONNECTED") {
return;
}
if (window.MozWebSocket) {
logToConsole('<span style="color: red;"><strong>Info:</strong> This browser supports WebSocket using the MozWebSocket constructor</span>');
window.WebSocket = window.MozWebSocket;
}
else if (!window.WebSocket) {
logToConsole('<span style="color: red;"><strong>Error:</strong> This browser does not have support for WebSocket</span>');
return;
}
if (!userName.value) {
logToConsole('<span style="color: red;"><strong>Enter your name!</strong></span>');
return;
}
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
}
function doDisconnect() {
if (mode == "CONNECTED") {
}
websocket.close();
}
function doSend() {
if (sendMessage.value != "" && mode == "CONNECTED") {
websocket.send(sendMessage.value);
sendMessage.value = "";
}
}
function clearLog() {
while (consoleLog.childNodes.length > 0) {
consoleLog.removeChild(consoleLog.lastChild);
}
}
function logToConsole(message) {
var pre = document.createElement("p");
pre.style.wordWrap = "break-word";
pre.innerHTML = message;
consoleLog.appendChild(pre);
while (consoleLog.childNodes.length > 50) {
consoleLog.removeChild(consoleLog.firstChild);
}
consoleLog.scrollTop = consoleLog.scrollHeight;
}
</script>
<h2>WebSocket Chat Sample</h2>
<div id="chat">
<div id="chat-access">
<strong>Your Name:</strong><br>
<input id="userName" cols="40">
<br>
<button id="joinButton">Join</button>
<button id="leaveButton">Leave</button>
<br>
<br>
<strong>Message:</strong><br>
<textarea rows="5" id="sendMessage" style="font-size:small; width:250px"></textarea>
<br>
<button id="sendButton">Send</button>
<br>
<br>
</div>
<div id="chat-log"> <strong>Chat:</strong>
<div id="consoleLog" style="font-size:small; width:270px; border:solid;
border-width:1px; height:172px; overflow-y:scroll"></div>
<button id="clearLogButton" style="position: relative; top: 3px;">Clear log</button>
</div>
</div>
</html>
/*
Dart code sample : WebSocket chat client for Dartium
1. Save these files and dart.js into a folder named WebSocketChat.
2. From Dart editor, File > Open Folder and select this WebSocketChat folder.
3. Run WebSocketChatServer.dart as server.
4. Call the the server from Dartium: http://localhost:8080/chat
5. To establish WebSocket connection, enter your name and click 'join' button.
6. To chat, enter chat message and click 'send' button.
7. To close the connection, click 'leave' button
June 2012, by Cresc Corp.
February 2013, revised to incorporate re-designed dart:html library.
Ref: www.cresc.co.jp/tech/java/Google_Dart/DartLanguageGuide.pdf (in Japanese)
*/
import 'dart:html';
var wsUri = 'ws://localhost:8080/Chat';
var mode = 'DISCONNECTED';
WebSocket webSocket;
var userName;
var sendMessage;
var consoleLog;
void main() {
show('Dart WebSocket Chat Sample');
userName = document.query('#userName');
sendMessage = document.query('#sendMessage');
consoleLog = document.query('#consoleLog');
document.query('#clearLogButton').onClick.listen((e) {clearLog();});
document.query('#joinButton').onClick.listen((e) {doConnect();});
document.query('#leaveButton').onClick.listen((e) {doDisconnect();});
document.query('#sendButton').onClick.listen((e) {doSend();});
}
doConnect() {
if (mode == 'CONNECTED') {
return;
}
if (userName.value == '') {
logToConsole('<span style="color: red;"><strong>Enter your name!</strong></span>');
return;
}
webSocket = new WebSocket(wsUri);
webSocket.onOpen.listen(onOpen);
webSocket.onClose.listen(onClose);
webSocket.onMessage.listen(onMessage);
webSocket.onError.listen(onError);
}
doDisconnect() {
if (mode == 'CONNECTED') {
}
webSocket.close();
}
doSend() {
if (sendMessage.value != '' && mode == 'CONNECTED') {
webSocket.send(sendMessage.value);
sendMessage.value = '';
}
}
clearLog() {
while (consoleLog.nodes.length > 0) {
consoleLog.nodes.removeLast();
}
}
onOpen(open) {
logToConsole('CONNECTED');
mode = 'CONNECTED';
webSocket.send('userName=${userName.value}');
}
onClose(close) {
logToConsole('DISCONNECTED');
mode = 'DISCONNECTED';
}
onMessage(message) {
logToConsole('<span style="color: blue;">${message.data}</span>');
}
onError(error) {
logToConsole('<span style="color: red;">ERROR:</span> ${error}');
webSocket.close();
}
logToConsole(message) {
Element pre = new Element.tag('p');
pre.style.wordWrap = 'break-word';
pre.innerHtml = message;
consoleLog.nodes.add(pre);
while (consoleLog.nodes.length > 50) {
consoleLog.$dom_removeChild(consoleLog.nodes[0]);
}
pre.scrollIntoView();
}
show(String message) {
document.query('#status').text = message;
}
<!--
Dart based WebSocketChat client
Open this file from Dartium browser
-->
<!DOCTYPE html>
<meta charset="utf-8" />
<head>
<title>WebSocketChatClient</title>
</head>
<body>
<h1>WebSocketChatClient</h1>
<h2 id="status">dart is not running</h2>
<script type="application/dart" src="/chat/WebSocketChatClient.dart"></script>
<script src="/chat/dart.js"></script>
<div id="chat">
<div id="chat-access">
<strong>Your Name:</strong><br>
<input id="userName" cols="40">
<br>
<button id="joinButton">Join</button>
<button id="leaveButton">Leave</button>
<br>
<br>
<strong>Message:</strong><br>
<textarea rows="5" id="sendMessage" style="font-size:small; width:250px"></textarea>
<br>
<button id="sendButton">Send</button>
<br>
<br>
</div>
<div id="chat-log"> <strong>Chat:</strong>
<div id="consoleLog" style="font-size:small; width:270px; border:solid;
border-width:1px; height:172px; overflow-y:scroll"></div>
<button id="clearLogButton" style="position: relative; top: 3px;">Clear log</button>
</div>
</div>
<body>
</html>
/*
Dart code sample : WebSocket chat server
1. Run this WebSocketChatServer.dart as server.
2. Access the server from Dartium or Chrome browser:
http://localhost:8080/chat
This chat server distinguishes Dartium and returns Dart based client page.
For the request from Chrome, this server returns JS based client page.
3. To establish the WebSocket connection, enter your name and click 'join' button.
4. To chat, enter chat message and click 'send' button.
5. To close the connection, click 'leave' button
Source : http://blog.sethladd.com/2012/04/dart-server-supports-web-sockets.html
June 2012, modified by Cresc Corp.
Sept. 2012, modified to incorpolate catch syntax change
Oct. 2012, incorporated M1 changes
Feb. 2013, incorporated re-designed dart:io (v2) library
March 2013, incorporated API changes (WebSocket r19376 and String)
Ref: www.cresc.co.jp/tech/java/Google_Dart/DartLanguageGuide.pdf (in Japanese)
*/
import 'dart:io';
import 'dart:async';
import 'MIME.dart' as mime;
final HOST = "127.0.0.1";
final PORT = 8080;
final LOG_REQUESTS = true;
void main() {
WebSocketHandler webSocketHandler = new WebSocketHandler();
HttpRequestHandler httpRequestHandler = new HttpRequestHandler();
HttpServer.bind(HOST, PORT)
.then((HttpServer server) {
StreamController sc = new StreamController();
sc.stream.transform(new WebSocketTransformer())
.listen((WebSocket ws){
webSocketHandler.wsHandler(ws);
});
server.listen((HttpRequest request) {
if (request.uri.path == '/Chat') {
sc.add(request);
} else if (request.uri.path.startsWith('/chat')) {
httpRequestHandler.requestHandler(request);
} else {
new NotFoundHandler().onRequest(request.response);
}
});
});
print('${new DateTime.now().toString()} - Serving Chat on ${HOST}:${PORT}.');
}
// handle WebSocket events
class WebSocketHandler {
Map<String, WebSocket> users = {}; // Map of current users
wsHandler(WebSocket ws) {
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - New connection ${ws.hashCode} '
'(active connections : ${users.length + 1})');
}
ws.listen((message) {
processMessage(ws, message);
} ,
onDone:(){
processClosed(ws);
}
);
}
processMessage(WebSocket ws, String receivedMessage) {
try {
String sendMessage = '';
String userName;
userName = getUserName(ws);
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - Received message on connection'
' ${ws.hashCode}: $receivedMessage');
}
if (userName != null) {
sendMessage = '${timeStamp()} $userName >> $receivedMessage';
} else if (receivedMessage.startsWith("userName=")) {
userName = receivedMessage.substring(9);
if (users[userName] != null) {
sendMessage = 'Note : $userName already exists in this chat room. '
'Previous connection was deleted.\n';
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - Duplicated name, closed previous '
'connection ${users[userName].hashCode} (active connections : ${users.length})');
}
users[userName].send(preFormat('$userName has joind using another connection!'));
users[userName].close(); // close the previous connection
}
users[userName] = ws;
sendMessage = '${sendMessage}${timeStamp()} * $userName joined.';
}
sendAll(sendMessage);
} on Exception catch (err) {
print('${new DateTime.now().toString()} - Exception - ${err.toString()}');
}
}
processClosed(WebSocket ws){
try {
String userName = getUserName(ws);
if (userName != null) {
String sendMessage = '${timeStamp()} * $userName left.';
users.remove(userName);
sendAll(sendMessage);
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - Closed connection '
'${ws.hashCode} with ${ws.closeCode} for ${ws.closeReason}'
'(active connections : ${users.length})');
}
}
} on Exception catch (err) {
print('${new DateTime.now().toString()} Exception - ${err.toString()}');
}
}
String getUserName(WebSocket ws) {
String userName;
users.forEach((key, value) {
if (value == ws) userName = key;
});
return userName;
}
void sendAll(String sendMessage) {
users.forEach((key, value) {
value.send(preFormat(sendMessage));
});
}
}
String timeStamp() => new DateTime.now().toString().substring(11,16);
String preFormat(String s) {
StringBuffer b = new StringBuffer();
String c;
bool nbsp = false;
for (int i = 0; i < s.length; i++){
c = s[i];
if (c != ' ') nbsp = false;
if (c == '&') { b.write('&amp;');
} else if (c == '"') { b.write('&quot;');
} else if (c == "'") { b.write('&#39;');
} else if (c == '<') { b.write('&lt;');
} else if (c == '>') { b.write('&gt;');
} else if (c == '\n') { b.write('<br>');
} else if (c == ' ') {
if (!nbsp) {
b.write(' ');
nbsp = true;
}
else { b.write('&nbsp;');
}
}
else { b.write(c);
}
}
return b.toString();
}
// adapt this function to your logger
void log(String s) {
print(s);
}
// handle HTTP requests
class HttpRequestHandler {
void requestHandler(HttpRequest request) {
HttpResponse response = request.response;
try {
String fileName = request.uri.path;
if (fileName == '/chat') {
if (request.headers['user-agent'][0].contains('Dart')) {
fileName = 'WebSocketChatClient.html';
}
else { fileName = 'WebSocketChat.html';
}
new FileHandler().sendFile(request, response, fileName);
}
else if (fileName.startsWith('/chat/')){
fileName = request.uri.path.replaceFirst('/chat/', '');
new FileHandler().sendFile(request, response, fileName);
}
else { new NotFoundHandler().onRequest(response);
}
}
on Exception catch (err) {
print('Http request handler error : $err.toString()');
}
}
}
class FileHandler {
void sendFile(HttpRequest request, HttpResponse response, String fileName) {
try {
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - Requested file name : $fileName');
}
File file = new File(fileName);
if (file.existsSync()) {
String mimeType = mime.mime(fileName);
if (mimeType == null) mimeType = 'text/plain; charset=UTF-8';
response.headers.set('Content-Type', mimeType);
RandomAccessFile openedFile = file.openSync();
response.contentLength = openedFile.lengthSync();
openedFile.closeSync();
// Pipe the file content into the response.
file.openRead().pipe(response);
} else {
if (LOG_REQUESTS) {
log('${new DateTime.now().toString()} - File not found: $fileName');
}
new NotFoundHandler().onRequest(response);
}
} on Exception catch (err) {
print('${new DateTime.now().toString()} - File handler error : $err.toString()');
}
}
}
class NotFoundHandler {
static final String notFoundPageHtml = '''
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL or File was not found on this server.</p>
</body></html>''';
void onRequest(HttpResponse response){
response.statusCode = HttpStatus.NOT_FOUND;
response.headers.set('Content-Type', 'text/html; charset=UTF-8');
response.write(notFoundPageHtml);
response.close();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment