let there be websockets

This commit is contained in:
raz-varren
2016-08-09 00:48:14 +00:00
commit 1877a4f174
32 changed files with 3312 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
package main
import (
"encoding/json"
"log"
"net/http"
ss "github.com/raz-varren/sacrificial-socket"
)
func main() {
s := ss.NewServer()
s.On("join", join)
s.On("message", message)
http.Handle("/socket", s.WebHandler())
http.Handle("/", http.FileServer(http.Dir("webroot")))
log.Fatalln(http.ListenAndServe(":80", nil))
}
func join(s *ss.Socket, data []byte) {
//just one room at a time for the simple example
currentRooms := s.GetRooms()
for _, room := range currentRooms {
s.Leave(room)
}
s.Join(string(data))
s.Emit("joinedRoom", string(data))
}
type msg struct {
Room string
Message string
}
func message(s *ss.Socket, data []byte) {
var m msg
json.Unmarshal(data, &m)
s.Roomcast(m.Room, "message", m.Message)
}

View File

@@ -0,0 +1,27 @@
#container{
width: 400px;
margin: 30px auto 0px auto;
}
#messages{
height: 200px;
overflow-y: scroll;
}
.control-item{
margin-top: 15px;
}
#message-send-btn,
#clear-messages-btn{
width: 100%;
}
#message-list{
padding-left: 25px;
}
.message-item{
margin-bottom: 8px;
}

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<title>Sacrificial-Socket Chat Client</title>
<link type="text/css" rel="stylesheet" href="/css/app.css">
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"
>
</head>
<body>
<div id="container">
<div id="messages" class="well well-sm">
<ul id="message-list"></ul>
</div>
<div id="controls">
<div class="control-item">
<div id="clear-messages-btn" class="btn btn-default">Clear Messages</div>
</div>
<div class="control-item">
<label for="name-input">Name:</label>
<input id="name-input" class="form-control" type="text" value="stranger" />
</div>
<div class="control-item">
<label for="join-room-input">Join Chat Room:</label>
<div class="input-group">
<input id="join-room-input" class="form-control" type="text" value="global" />
<div class="input-group-btn">
<div id="join-room-btn" class="btn btn-info">Join</div>
</div>
</div>
</div>
<div class="control-item">
<textarea id="message-body-input" class="form-control" placeholder="Type message here"></textarea>
</div>
<div class="control-item">
<div id="message-send-btn" class="btn btn-success">Send Message</div>
</div>
</div>
</div>
<script type="text/javascript" src="/js/sacrificial-socket.js"></script>
<script type="text/javascript" src="/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
(function(SS){ 'use strict';
var msgCnt = document.querySelector('#messages'),
msgList = document.querySelector('#message-list'),
msgClear = document.querySelector('#clear-messages-btn'),
nameInput = document.querySelector('#name-input'),
joinInput = document.querySelector('#join-room-input'),
joinBtn = document.querySelector('#join-room-btn'),
msgBody = document.querySelector('#message-body-input'),
send = document.querySelector('#message-send-btn'),
currentRoom = null,
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
getTime = function(dt) {
var hours = dt.getHours(),
minutes = dt.getMinutes(),
ampm = hours >= 12 ? 'pm' : 'am';
hours %= 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
minutes = minutes < 10 ? '0'+minutes : minutes;
var strTime = hours + ':' + minutes + ' ' + ampm;
return strTime;
},
addMsg = function(msg){
var li = document.createElement('li'),
dt = new Date(),
dtString = months[dt.getMonth()]+' '+dt.getDate()+', '+dt.getFullYear();
li.innerText = dtString+' '+getTime(dt)+' - '+msg;
msgList.appendChild(li);
msgCnt.scrollTop = msgCnt.scrollHeight;
};
var ss = new SS('ws://'+window.location.host+'/socket');
ss.onConnect(function(){
ss.emit('join', joinInput.value);
});
ss.onDisconnect(function(){
alert('chat disconnected');
});
ss.on('joinedRoom', function(room){
currentRoom = room;
addMsg('joined room: '+room);
});
ss.on('message', function(msg){
addMsg(msg);
});
send.addEventListener('click', function(){
var msg = msgBody.value;
if(msg.length === 0) return;
ss.emit('message', {Room: currentRoom, Message: nameInput.value+' says: '+msg});
});
joinBtn.addEventListener('click', function(){
var room = joinInput.value;
if(room.length === 0) return;
ss.emit('join', joinInput.value);
});
msgClear.addEventListener('click', function(){
msgList.innerHTML = '';
});
})(window.SS);

View File

@@ -0,0 +1,170 @@
(function(window){ 'use strict';
/**
* SS is the constructor for the sacrificial-socket client
*
* @class SS
* @constructor
* @param {String} url - The url to the sacrificial-socket server endpoint. The url must conform to the websocket URI Scheme ("ws" or "wss")
*/
var SS = function(url){
var self = this,
ws = new WebSocket(url, 'sac-sock'),
events = {},
headerStartCharCode = 1,
headerStartChar = String.fromCharCode(headerStartCharCode),
dataStartCharCode = 2,
dataStartChar = String.fromCharCode(dataStartCharCode);
//sorry, only supporting arraybuffer at this time
//maybe if there is demand for it, I'll add Blob support
ws.binaryType = 'arraybuffer';
//Parses all incoming messages and dispatches their payload to the appropriate eventName if one has been registered. Messages received for unregistered events will be ignored.
ws.onmessage = function(e){
var msg = e.data,
headers = {},
eventName = '',
data = '',
chr = null,
i, msgLen;
if(typeof msg === 'string'){
var dataStarted = false,
headerStarted = false;
for(i = 0, msgLen = msg.length; i < msgLen; i++){
chr = msg[i];
if(!dataStarted && !headerStarted && chr !== dataStartChar && chr !== headerStartChar){
eventName += chr;
}else if(!headerStarted && chr === headerStartChar){
headerStarted = true;
}else if(headerStarted && !dataStarted && chr !== dataStartChar){
headers[chr] = true;
}else if(!dataStarted && chr === dataStartChar){
dataStarted = true;
}else{
data += chr;
}
}
}else if(msg && msg instanceof ArrayBuffer && msg.byteLength !== undefined){
var dv = new DataView(msg),
headersStarted = false;
for(i = 0, msgLen = dv.byteLength; i < msgLen; i++){
chr = dv.getUint8(i);
if(chr !== dataStartCharCode && chr !== headerStartCharCode && !headersStarted){
eventName += String.fromCharCode(chr);
}else if(chr === headerStartCharCode && !headersStarted){
headersStarted = true;
}else if(headersStarted && chr !== dataStartCharCode){
headers[String.fromCharCode(chr)] = true;
}else if(chr === dataStartCharCode){
data = dv.buffer.slice(i+1);
break;
}
}
}
if(eventName.length === 0) return; //no event to dispatch
if(typeof events[eventName] === 'undefined') return;
events[eventName]((headers.J) ? JSON.parse(data) : data);
};
/**
* onConnect registers a callback to be run when the websocket connection is open.
*
* @method onConnect
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection opens.
*
*/
self.onConnect = function(callback){
ws.onopen = function(){ callback(self); };
};
/**
* onDisconnect registers a callback to be run when the websocket connection is closed.
*
* @method onDisconnect
* @param {Function} callback(SS) - The callback that will be executed when the websocket connection is closed.
*/
self.onDisconnect = function(callback){
ws.onclose = function(){ callback(self); };
};
/**
* on registers an event to be called when the client receives an emit from the server for
* the given eventName.
*
* @method on
* @param {String} eventName - The name of the event being registerd
* @param {Function} callback(payload) - The callback that will be ran whenever the client receives an emit from the server for the given eventName. The payload passed into callback may be of type String, Object, or ArrayBuffer
*
*/
self.on = function(eventName, callback){
events[eventName] = callback;
};
/**
* off unregisters an emit event
*
* @method off
* @param {String} eventName - The name of event being unregistered
*/
self.off = function(eventName){
if(events[eventName]){
delete events[eventName];
}
};
/**
* emit dispatches an event to the server
*
* @method emit
* @param {String} eventName - The event to dispatch
* @param {String|Object|ArrayBuffer} data - The data to be sent to the server. If data is a string then it will be sent as a normal string to the server. If data is an object it will be converted to JSON before being sent to the server. If data is an ArrayBuffer then it will be sent to the server as a uint8 binary payload.
*/
self.emit = function(eventName, data){
var rs = ws.readyState;
if(rs === 0){
console.warn("websocket is not open yet");
return;
}else if(rs === 2 || rs === 3){
console.error("websocket is closed");
return;
}
var msg = '';
if(data instanceof ArrayBuffer){
var ab = new ArrayBuffer(data.byteLength+eventName.length+1),
newBuf = new DataView(ab),
oldBuf = new DataView(data),
i = 0;
for(var evtLen = eventName.length; i < evtLen; i++){
newBuf.setUint8(i, eventName.charCodeAt(i));
}
newBuf.setUint8(i, dataStartCharCode);
i++;
for(var x = 0, xLen = oldBuf.byteLength; x < xLen; x++, i++){
newBuf.setUint8(i, oldBuf.getUint8(x));
}
msg = ab;
}else if(typeof data === 'object'){
msg = eventName+dataStartChar+JSON.stringify(data);
}else{
msg = eventName+dataStartChar+data;
}
ws.send(msg);
};
/**
* close will close the websocket connection, calling the "onDisconnect" event if one has been registered.
*
* @method close
*/
self.close = function(){
return ws.close();
};
};
window.SS = SS;
})(window);