The Cache: Technology Expert's Forum
 
*
Welcome, Guest. Please login or register. July 22, 2019, 09:53:09 PM

Login with username, password and session length


Pages: [1]
  Print  
Author Topic: HTML5 WebSockets: A true Client / Server example  (Read 8985 times)
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« on: March 28, 2012, 10:11:37 AM »

Haven't put any really good chunks of code up for a while, so I thought I'd post my work on this stuff.

For those that aren't yet familiar, WebSockets is an HTML5 candidate specification that allows for a website to open a persistent, high speed, bi-directional, full duplex socket level connection with the server over a single TCP connection. There are restrictions regarding cross-platform stuff and it's not completely fleshed out yet, but it's an awesome addition. In fact there are two different protocols out right now - the older chr(0) + [content] + chr(255) (hixie75 et al) and the new hybi-10 protocol. I'll be demonstrating the hixie-75 because the iPad still uses it.

So in layman's terms: Rather than the stateless, query/response normal mechanism of the web and AJAX, this allows for a website to spontaneously get a message from the server.

This is not something that should be used everywhere. In fact, I think it's use for normal websites is limited at best. But for web apps it's incredible. As a simple example, consider a stock watcher app. You could register that you're watching AAPL and GOOG, the server sees movement in one of them and sends an update message to the client. The website sites completely quiescent - there's no communication between the two until the SERVER notices that something needs to be updated. The website updates instantly as the message is sent. It's very cool for web apps that need to be dynamic against the server, but the continual flow of AJAX-like "Is there anything for me?" packets would be completely debilitating to a server and unscalable.

The code I'm going to present is rather unlike my normal posts, where I go really verbose and literal - this is my actual code. So it won't be quite as readable as normal. But I'll do my best to create the project in a way that makes sense. I took this on because I have a need with something Nuts and I are doing for our cloud business. But what really pissed me off was the complete lack of examples of real server implementations for WebSockets. In fact, all of the examples out there are little more than AJAX-like chat bots. Behavior that could be replicated quite easily (and, in fact, more easily) with old-school techniques. So I wanted to do a much fuller example that incorporates both bi-directional messaging and a basic communication protocol - required for anything more intricate than just a chatbot.

What this code demonstrates:
  • a simple JS class with events for connection, send, receive, ack, disconnect and exceptions
  • jQuery based graphical movement based on those events
  • Simple client to server messages, with and without ack
  • "Query" style messages that request an answer from the server, with and without ack
  • Repetitive messaging from the server in the shape of a "Time Connected" message
  • Spontaneous messaging from the server to the client

For those that just want the code, here you go:
http://www.wickedsignals.com/websock/class.socketConnection.js
http://www.wickedsignals.com/websock/sockServer.php
http://www.wickedsignals.com/websock/sockClient.php

You'll also need
http://www.wickedsignals.com/websock/json2.js

.. which I use to stringify requests up to the server.

So without further ado - here we go.
« Last Edit: May 13, 2013, 04:13:53 PM by perkiset » Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #1 on: March 28, 2012, 10:22:06 AM »

I chose to make this look reasonable to pique the interest of people for jQuery and CSS3 as well. There are some really cool effects that can be had without ever getting into graphics files. I also added a couple nifty little jQuery and even tricks to hint at how we are moving when it comes to web apps.

Here's the HTML for our client:
Code:
<body>
<h1>Perk's Websock Server & JS Class</h1>
<div id="chat1" class="chatBlock">
<div class="statusBlock">
ConnexTime: <span id="conn1stat">Not Connected</span>
<input type="button" value="Connect" target="conn1" class="connButton" />
</div>
<div class="connLog"></div>
<div class="sendBlock">
Send:
<select class="sendType" target="conn1" size="1">
<option value="message">Message</option>
<option value="query" SELECTED>Query</option>
<option value="fail">Fail</option>
</select>
<input class="sendBox" type="text" target="conn1" value="time"/>
<select class="sendAck" target="conn1" size="1">
<option value="1">Want Ack</option>
<option value="0">No Ack</option>
</select>
<input type="button" value="Send" class="sendButton" target="conn1" />
</div>
</div>

<div id="chat2" class="chatBlock">
<div class="statusBlock">
ConnexTime: <span id="conn2stat">Not Connected</span>
<input type="button" value="Connect" target="conn2" class="connButton" />
</div>
<div class="connLog"></div>
<div class="sendBlock">
Send:
<select class="sendType" target="conn2" size="1">
<option value="message">Message</option>
<option value="query" SELECTED>Query</option>
<option value="fail">Fail</option>
</select>
<input class="sendBox" type="text" target="conn2" value="date"/>
<select class="sendAck" target="conn2" size="1">
<option value="1">Want Ack</option>
<option value="0">No Ack</option>
</select>
<input type="button" value="Send" class="sendButton" target="conn2" />
</div>
</div>

</body>

As you can see, not a lot there. If you post that to a server and request it it will look like:


Now let's add the CSS to pretty it up:
Code:

body { font-family: arial; font-size: 12px; font-weight: normal; margin: 0; padding: 20px; color: white; }
h1 { font-size: 24px; font-weight: bold; color: black; }
.chatBlock {
float: left; width: 380px; background-color: #b0b0b0; padding: 10px 20px 10px 20px; margin-right: 30px;
-moz-border-radius: 20px; -webkit-border-radius: 20px; border-style: solid; border-color: #404040; border-width: 1px;
box-shadow: 5px 5px 10px #909090; -webkit-box-shadow: 5px 5px 10px #909090;
}
.connected { background-color: #096311; }
.statusBlock { height: 24px; line-height: 24px; margin-bottom: 5px; font-weight: bold; }
.statusBlock span { font-weight: normal; }
.connButton { float: right; }
.connLog {
font-family: courier; background-color: white; color: #404040; padding: 5px;
border-style: solid; border-color: 404040; border-width: 1px 2px 2px 1px;
overflow: auto; width: 370px; height: 300px;
}
.sendBlock { height: 14px; line-height: 14px; margin-top: 5px; font-weight: bold; }
.sendBox { width: 120px; }
.sendButton { float: right; }
</style>

... and the site looks as we want it:


Note that one of the really cool features of WebSockets is that you can have as many open as you want. I've only tested out to 3 because I have a hard time seeing how I'd need lots and lots open. But you can do it. The clearest immediate example to me would be if you wanted to do something like MySQL communication - kind of a "DC Pair" - where there's one socket for requests and another for responses. This is a good plan when one or the other is going to be very noisy and you don't want the "lighter channel" to get blocked by excessive packets from the noisy one. I'm not doing anything like that - my communications are very quiet and short bursts, so a single socket does me just fine. All that said, we'll open two here both to show that it can be done and to illustrate some jQuery coolness in the process.

Now that the easy stuff is out of the way, let's move to the server and client.
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #2 on: March 28, 2012, 10:55:57 AM »

The server code is written in PHP. This is not the most blazing language - if you really need a screamer I'd suggest going to Python or even C. But for most applications this is more than adequate. In fact it's all I do - PHP is plenty fast.

Essentially the code is this:
  • A main routine that waits for a socket to connect/disconnect or change (read), times-out at a specific interval and then calls a function to see if there's anything that needs to be sent out to the connected clients
  • A function for initializing the socket server - initServer()
  • A function for handling received messages - receiveMsg()
  • A function for sending messages to the client - sendMsg()
  • A function to look for messages that NEED to be send to a client and then send them - checkOutMsgs()
  • Functions to manage the connection and disconnection of sockets, connect(), disconnect() and wsHandshake()
  • Utility functions, findConnection() (when a message comes in, identify that socket), wrapMsg() & unwrapMsg() (add / strip off the chr(0) and chr(255) required by the protocol) and console() for logging things IF the user chooses to (turned on and off with -q at the command line)
  • A simple class for managing each socket, cleverly named Socket.

The pieces of juice you want to get your arms around
The first interesting thing to look at is initServer(). This function is what creates the socket and binds it to an address on your server. Note that if you specify localhost or 127.0.0.1, you MUST then talk to the socket that way. It will not listen for you if you don't bind your publicly facing address. So in this case, I bind 10.10.123.4, which is the NAT'd address behind my firewall. You also need to choose a port. I chose 6789 - it's arbitrary. IMPORTANT: If your server sits behind a firewall, you will of course need to poke that port through it with NAT.

Now the main loop needs to be looked at. The real work is done with socket_select(). This extremely handy function looks at an array of sockets (passed in param 1) and sees if there has been any change. Note that this array will be screwed with, so do not pass your ACTUAL sockets array. Here you can see I create a copy, $changed, and pass that to socket_select. The last 2 params, $secsInterval and $usecsInterval are how long it will sit waiting for an update. If you do not put any variables here or pass 0, then socket_select will sit and block until something happens. This is fine for chatbots and is exactly how all the demonstration pieces I saw worked. But it is wrong for a server that must also look for messages to be sent back to the user spontaneously, because the code never has control.

So when any socket changes, the loop fires. if the socket is $master, then I know it's a connect event. If socket_accept doesn't work it means that the connection failed during TCP level handshaking. If it succeeds, we move to connect() where I add the new socket to my own array and create an object to manage that socket.

If it's not the $master socket, then I know it was a client event. If there are 0 bytes it was a disconnect, otherwise it's a read. If my socket object notices that we've not handshook with the client yet, then that is what the packet must be - otherwise it's a normal send/receive - calling the appropriate functions.

The handshake function is plumbing required by the protocol. I've received information from the client (that was sent by the WebSockets API itself, not even the programmer in Javascript) - I respond to it with a very specific header and we're all connected. The handshake header has got to be correct or your SOL.

Now that we're receiving data, we get into another notion - communication protocol. There are only about 19 billion ways this can be done. I demonstrate a very small protocol here where I have a msgID, msgType and msgData. This is wrapped in JSON for transmission between the server and the client. So the receiveMsg() function is all about that protocol. Unwrap the message, decode it from json, then see what we've got and act upon it. Also in that function is the ACK handler - if the sent packet has wantAck in it, then I'll send back that I got it.

The next interesting thing is checkOutMsgs(). This happens every time socket_select() times out. At the top of the code we can see the variables for seconds and useconds. I chose 1/2 second for my interval (0 seconds, 500000 micro seconds). The easiest part of this function is the sending of connection time to every client. But you'll notice a little code there to see if there's a file named the same as the connection object's frameworkID property. If there is, pick up the file, send it to the client and delete it. This is how I chose to demonstrate spontaneous messages. As you'll see in the client, the very first activity by a connecting client is to "register" themselves with a name. This is so that the server will know how to find them and send them messages. As you'll see I chose "connection1" and "connection2." So by creating a file, /www/sites/telemetry/utilities/connection1 with some text in it, that will be picked up and delivered to the client. This is where you'd add database connectivity, or other network methods, or shared memory or any number of ways to get messages out to the client. We'll see that in action in a bit.

Note that I could have forked this into a daemon, and if you really put something like this into production you'd probably want to. But for the sake of demonstration a normal executable is better. Also, if you don't add the -q option, then it will spit out interesting data about the connections while you're working with the client, which will really help overall understanding of the data flow.

#!/usr/local/bin/php
<?php

$GLOBALS
['quiet'] = (trim(strtolower($argv[1])) == '-q');

error_reporting(E_PARSE|E_ERROR);
set_time_limit(0);
ob_implicit_flush();
$secsInterval 0;
$usecsInterval 500000;

$master initServer('10.10.123.4'6789);
$sockets = array($master);
$connections = array();
$w $e null;

while(
true)
{
	
$changed $sockets;
	
socket_select($changed$w$e$secsInterval$usecsInterval);
	
foreach(
$changed as $socket)
	
{
	
	
if(
$socket == $master)
	
	
{
	
	
	
if ((
$client socket_accept($master)) < 0)
	
	
	
	
console("socket_accept() failed");
	
	
	
else
	
	
	
	
connect($client);
	
	
} else {
	
	
	
if(!@
socket_recv($socket$buffer40960))
	
	
	
	
disconnect($socket);
	
	
	
else
	
	
	
{
	
	
	
	
$connex findConnection($socket);
	
	
	
	
if (!
$connex->handshake)
	
	
	
	
	
wsHandshake($connex$buffer);
	
	
	
	
else
	
	
	
	
	
receiveMsg($connexunwrapMsg($buffer));
	
	
	
}
	
	
}
	
}
	
checkOutMsgs();
}

function 
initServer($address$port)
{
	
$master socket_create(AF_INETSOCK_STREAMSOL_TCP) or die("socket_create() failed");
	
socket_set_option($masterSOL_SOCKETSO_REUSEADDR1) or die("socket_option() failed");
	
socket_bind($master$address$port) or die("socket_bind() failed");
	
socket_listen($master20) or die("socket_listen() failed");
	
console('Server Started: ' date('Y-m-d H:i:s') . "\nListening on $address:$port");
	
return 
$master;
}

function 
receiveMsg($connex$msgStr)
{
	
$socket $connex->socket;
	
$msg json_decode($msgStr);
	
console('Received Packet');
	
console(print_r($msgtrue));

	
switch(
$msg->msgType)
	
{
	
	
case 
'register':
	
	
	
console('register ' $msg->msgData);
	
	
	
$connex->frameworkID $msg->msgData;
	
	
	
sendMsg($socket'''registered'$msg->msgData);
	
	
	
break;

	
	
case 
'message':
	
	
	
// Do something here;
	
	
	
if (
$msg->wantAck)
	
	
	
	
sendMsg($socket$msg->msgID'ack''');
	
	
	
break;

	
	
case 
'query':
	
	
	
switch (
strtolower($msg->msgData))
	
	
	
{
	
	
	
	
case 
'date':
	
	
	
	
	
sendMsg($socket'''response'date('m/d/Y'time()));
	
	
	
	
	
break;

	
	
	
	
case 
'time':
	
	
	
	
	
sendMsg($socket'''response'date('H:i:s'time()));
	
	
	
	
	
break;

	
	
	
	
default:
	
	
	
	
	
sendMsg($socket'''response'"What do you mean \"{$msg->msgData}\"?");

	
	
	
}
	
	
	
if (
$msg->wantAck)
	
	
	
	
sendMsg($socket$msg->msgID'ack''');
	
	
	
break;

	
	
default:
	
	
	
sendMsg($socket$msg->msgID'error'"Unknown msgType [{$msg->msgType}]");
	
}
}

function 
sendMsg($socket$msgID$msgType$msgData)
{
	
$outMsg['msgID'] = $msgID;
	
$outMsg['msgType'] = $msgType;
	
$outMsg['msgData'] = $msgData;
	
$msgStr json_encode($outMsg);
	
$msgStr wrapMsg($msgStr);
	
socket_write($socket$msgStr);
}

function 
checkOutMsgs()
{
	
global 
$connections;
	
$fPath '/www/sites/telemetry/utility';

	
foreach(
$connections as $connection)
	
{
	
	
if (
$connection->handshake)
	
	
{
	
	
	
$thisFile "$fPath/{$connection->frameworkID}";
	
	
	
if (
is_file($thisFile))
	
	
	
{
	
	
	
	
$buff file_get_contents($thisFile);
	
	
	
	
sendMsg($connection->socket'''message'$buff);
	
	
	
	
unlink($thisFile);
	
	
	
}

	
	
	
$elapse time() - $connection->connectedAt;
	
	
	
$now date('H:i:s'$elapse 25200);
	
	
	
if (
$now <> $connection->lastSent)
	
	
	
{
	
	
	
	
sendMsg($connection->socket'''servertime'$now);
	
	
	
	
$connection->lastSent $now;
	
	
	
}
	
	
}
	
}
}

function 
connect($socket)
{
	
global 
$sockets$connections;

	
$connex = new Connection();
	
$connex->id uniqid();
	
$connex->socket $socket;
	
$connex->connectedAt time();
	
array_push($connections$connex);
	
array_push($sockets$socket);
	
console($socket " CONNECTED!");
}

function 
disconnect($socket)
{
	
global 
$sockets$connections;

	
$found null;
	
$max count($connections);
	
for(
$i=0$i<$max$i++)
	
{
	
	
if(
$connections[$i]->socket == $socket)
	
	
{
	
	
	
$found $i;
	
	
	
break;
	
	
}
	
}

	
if(!
is_null($found))
	
	
array_splice($connections$found1);

	
$index array_search($socket$sockets);
	
socket_close($socket);
	
console($socket " DISCONNECTED!");

	
if(
$index >= 0)
	
	
array_splice($sockets$index1);
}

function 
wsHandshake($connex$buffer)
{
	
console("\nHandshake...\n$buffer");
	
if (
preg_match("/GET (.*) HTTP/"$buffer$match)) $resource $match[1];
	
if (
preg_match("/Host: (.*)\r\n/"$buffer$match)) $host $match[1];
	
if (
preg_match("/Origin: (.*)\r\n/"$buffer$match)) $origin $match[1];
	
if (
preg_match("/Sec-WebSocket-Key1: (.*)\r\n/"$buffer$match)) $strkey1 $match[1];
	
if (
preg_match("/Sec-WebSocket-Key2: (.*)\r\n/"$buffer$match)) $strkey2 $match[1];
	
if (
preg_match("/\r\n(.*?)\$/"$buffer$match)) $data $match[1];
	
$numkey1 preg_replace('/[^\d]*/'''$strkey1);
	
$numkey2 preg_replace('/[^\d]*/'''$strkey2);
	
$spaces1 strlen(preg_replace('/[^ ]*/'''$strkey1));
	
$spaces2 strlen(preg_replace('/[^ ]*/'''$strkey2));

	
// Here's the checksum...
	
if (
$spaces1 == || $spaces2 == || fmod($numkey1$spaces1) != || fmod($numkey2$spaces2) != 0)
	
{
	
	
socket_close($connex->socket);
	
	
console('Handshake failed');
	
	
return 
false;
	
}

	
$ctx hash_init('md5');
	
hash_update($ctxpack("N"$numkey1/$spaces1));
	
hash_update($ctxpack("N"$numkey2/$spaces2));
	
hash_update($ctx$data);
	
$hashData hash_final($ctx,true);

	
$headResp[] = 'HTTP/1.1 101 WebSocket Protocol Handshake';
	
$headResp[] = 'Upgrade: WebSocket';
	
$headResp[] = 'Connection: Upgrade';
	
$headResp[] = "Sec-WebSocket-Origin: $origin";
	
$headResp[] = "Sec-WebSocket-Location: ws://$host$resource";
	
$headResp[] = '';
	
$headResp[] = $hashData;
	
$hsHead implode("\r\n"$headResp);
	
socket_write($connex->socket$hsHeadstrlen($hsHead));

	
$connex->handshake true;
	
console("Handshake:\n$hsHead");
	
return 
true;
}

function 
findConnection($socket)
{
	
global 
$connections;

	
$found null;
	
foreach(
$connections as $connex)
	
{
	
	
if(
$connex->socket == $socket)
	
	
{
	
	
	
$found $connex;
	
	
	
break;
	
	
}
	
}
	
return 
$found;
}

function 
wrapMsg($msg) { return chr(0) . $msg chr(255); }
function 
unwrapMsg($msg) { return substr($msg1strlen($msg) - 2); }
function 
console($msg) { if (!$GLOBALS['quiet']) echo "$msg\n"; }

class 
Connection
{
	
var 
$id;
	
var 
$frameworkID;
	
var 
$socket;
	
var 
$handshake;
	
var 
$connectedAt;
	
var 
$lastSend;
}

?>
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #3 on: March 28, 2012, 11:12:22 AM »

This little class wraps up everything you need to create an manage a WebSockets connection. It is superior to use a class for this rather than just functions because you might want more than one. Also it's clearly easy to extend and include.

The vast majority of the class is management variables, event function variables and exception handling. The single most interesting line of the code is
Code:
this.socket = new WebSocket(this.url);
... which creates the WebSocket. After that I tie event handlers to it and wrap my own stuff around it to make it easier to work with. Then there's my protocol stuff (msgID, msgType, msgData) which have nothing inherently to do with WebSockets, they are simply the protocol that is agreed upon with my sockServer.php program. The S4 and generateID functions at the bottom do nothing more than help me generate a unique ID for each packet.

You might get thrown by phrases like:
Code:
this.socket.onopen = function() {
sender._connected = true;
if (sender._isFunc(sender.onConnect))
sender.onConnect.call(sender, this.readyState);
if (sender.connectionID > ' ')
{
sender.send('register', sender.connectionID, false);
}
}
... and that's understandable. Little blocks like this are functions that I've attached to the underlying WebSockets object, that must call (this class) within the context of (this class). That's what the .call() function does. This is most definitely the most confusing part of the code and, although necessary for it to work the way it does, also has nothing inherently to do with WebSockets - this is a Javascript thang. The code above essentially says: "Attach the following code to the onopen event of the WebSocket. Do not give it a name, just use the code. Set the calling object's "_connected" property to true. If the calling object has an onConnect function, call it within the context of the calling object (yuck). Second, if there's a connectionID property, then send it up to the server. This is how the server will know who I am for future messages that need to receive.

The send() method is worth a look because that's where I create a packet that conforms to my communication protocol (msgID, msgType, msgData), JSON it into a string and then post it up to the server. Note that unlike AJAX, I do not wait for any return message at all. Remember that WebSockets is full duplex - that means that I can send send send and receive receive receive all together all the time. It also, very importantly, means that I may receive things out of order. Which is why my wantAck function is important. An author using this class may need to know that certain messages were received by the server. Of course, the server could do a considerably more lush job of responding, but this basic send/ack protocol is adequate for many things that I need.

Code:
function socketConnection(_url)
{
this._connected = false;
this.socket = false;
this.url = _url;
this.connectionID = false;
this.onLog = null;
this.onConnect = null;
this.onReceive = null;
this.onAck = null;
this.onDisconnect = null;
this.onException = null;
this.debug = false;
}

socketConnection.prototype.connect = function()
{
if (this._connected)
{
if (this._isFunc(this.onException)) this.onException('socketConnection: already connected');
else throw'socketConnection: already connected';
return false;
}

try
{
this.socket = new WebSocket(this.url);
var sender = this;

this.socket.onopen = function() {
sender._connected = true;
if (sender._isFunc(sender.onConnect))
sender.onConnect.call(sender, this.readyState);
if (sender.connectionID > ' ')
{
sender.send('register', sender.connectionID, false);
}
}

this.socket.onmessage = function(msg)
{
try
{
var data = eval('(' + msg.data + ')');
if (data.msgType == 'error')
{
throw data.msgData;
} else if (data.msgType == 'ack')
{
if (sender._isFunc(sender.onAck))
sender.onAck.call(sender, data.msgID);
} else {
if (sender._isFunc(sender.onReceive))
sender.onReceive.call(sender, data.msgType, data.msgData);
}
} catch(ex) {
if (sender._isFunc(sender.onException))
sender.onException.call(sender, ex);
else throw ex;
}
}

this.socket.onclose = function() {
sender._connected = false;
if (sender._isFunc(sender.onDisconnect))
sender.onDisconnect.call(sender, this.readyState);
}

} catch(ex) {
if (this._isFunc(this.onException)) this.onException(ex);
else throw ex;
}
}

socketConnection.prototype.connected = function() { return this._connected; }

socketConnection.prototype.disconnect = function()
{
if (this._connected) this.socket.close();
return true;
}

socketConnection.prototype._isFunc = function(x) { return Object.prototype.toString.call(x) === "[object Function]"; }

socketConnection.prototype.send = function(msgType, msgData, wantAck)
{
if (!this._connected)
{
if (this._isFunc(this.onException))
this.onException("socketConnect is not connected");
else throw "socketConnect is not connected";
return false;
}

try
{
var out = new Object;
out.msgType = msgType;
out.msgData = msgData;
out.wantAck = wantAck;
out.msgID = (wantAck) ? this.generateID() : false;
var outStr = JSON.stringify(out);
this.socket.send(outStr);
return out.msgID;
} catch(except) {
if (this._isFunc(this.onException)) this.onException(except);
else throw except;
}
}

socketConnection.prototype._s4 = function() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); };
socketConnection.prototype.generateID = function() { return this._s4() + ':' + this._s4() + ':' + this._s4() + ':' + this._s4(); };
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #4 on: March 28, 2012, 11:13:30 AM »

I have completely shifted to hooking elements in the HTML with jQuery in the header, rather than having anything inline (just like my CSS). The real benefit of this is that PinkHat can modify the HTML to her hearts desire, I can modify the executable and look portion without ever getting in each other's hair. Another benefit is that all executable code is in one place: I do not need to go galloping through the HTML to see HOW a particular thing happened.

So the most important thing to check out here is the document ready piece. First off I create 2 instances of the socketConnection class, then bind events to it. I've also become a pretty big fan of nameless functional code. But in this case, since I want more than one object, I had to put nameless functional code that calls named code - so in the case of each connection, they both call the same functions to respond to events, but pass the "which" parameter so that the handling function knows who it is speaking to.

I also bind the Return key to the message box, so that a user can type / press return / type / press return.

For those less familiar with jQuery and CSS3 selectors, this line must be rather interesting:
Code:
var msgType = $('#chat' + which + ' .sendType').val();

Essentially that asks jQuery to "find every element that has an id of #chat(n) (where n is the which parameter) and those have children with the .sentType class and return the input value thereof."

This one's even cooler:
Code:
$('#chat' + which + ' .statusBlock > *').
filter('input').attr('value', (connecting) ? 'Disconnect' : 'Connect').end().
filter('span').html((connecting) ? '00:00:00' : 'Not Connected');
That looks for all the children of all items with a class of .statusBlock that are children of an element with the id #chat1 (or #chat2). Then it filters down for input types, sets the attribute "value" to either disconnect or connect, then steps out of that filter and filters into elements of type "span" and changes their HTML to 00:00:00. All in one line. It's an extremely efficient way to change a bunch of things on a page. If this is foreign to you, I strongly suggest you check out jQuery and CSS3 selectors because it will change your life.

Here's the code:
Code:
<script>
var conn1;
var conn2;

$(document).ready(function() {
var host = "ws://telemetry.ironmed.net:6789/";

conn1 = new socketConnection(host);
conn1.connectionID = 'connection1';
conn1.onConnect = function(readyState) { handleConnect(1, readyState); };
conn1.onReceive = function(msgType, msgData) { handleReceive(1, msgType, msgData); };
conn1.onAck = function(msgID) { handleAck(1, msgID); };
conn1.onException = function(msg) { handleException(1, msg); };
conn1.onDisconnect = function(readyState) { handleConnect(1, readyState); };

conn2 = new socketConnection(host);
conn2.connectionID = 'connection2';
conn2.onConnect = function(readyState) { handleConnect(2, readyState); };
conn2.onReceive = function(msgType, msgData) { handleReceive(2, msgType, msgData); };
conn2.onAck = function(msgID) { handleAck(2, msgID); };
conn2.onException = function(msg) { handleException(2, msg); };
conn2.onDisconnect = function(readyState) { handleConnect(2, readyState); };

$('.connButton').click(function(e)
{
var socket = ($(e.srcElement).attr('target') == 'conn1') ? conn1 : conn2;
if (socket.connected()) socket.disconnect();
else socket.connect();
});

$('.sendButton').click(function(e) {
var which = ($(e.srcElement).attr('target') == 'conn1') ? '1' : '2';
handleSend(which);
});

$('.sendBox').
keypress(function(e) {
if(e.keyCode == 13)
handleSend(($(e.srcElement).attr('target') == 'conn1') ? '1' : '2');
}).focus(function() {
this.select();
});

});

function handleConnect(which, readyState)
{
var connecting = (readyState == 1);

if (connecting) $('#chat' + which).addClass('connected');
else $('#chat' + which).removeClass('connected');

$('#chat' + which + ' .statusBlock > *').
filter('input').attr('value', (connecting) ? 'Disconnect' : 'Connect').end().
filter('span').html((connecting) ? '00:00:00' : 'Not Connected');
if (connecting)
$('#chat' + which + ' .connLog').html('');
connLog(which, (connecting) ? '(+) Connected' : '(x) Disconnected');
$('#chat' + which + ' .sendBox').focus();
}

function handleSend(which)
{
var socket = (which == '1') ? conn1 : conn2;
if (!socket.connected())
return alert('Socket ' + which + ' is not connected');

var msgType = $('#chat' + which + ' .sendType').val();
var msg = $('#chat' + which + ' .sendBox').val();
var wantAck = $('#chat' + which + ' .sendAck').val();
var msgID = socket.send(msgType, msg, wantAck);
connLog(which, '(&#8593;) [' + msgType + '] ' + msg);
if (wantAck == '1') connLog(which, '(&#8225;) ' + msgID);

$('#chat' + which + ' .sendBox').focus();
}

function handleReceive(which, msgType, msgData)
{
switch(msgType)
{
case 'servertime':
$('#conn' + which + 'stat').html(msgData);
break;

default:
connLog(which, '(&#8595;) [' + msgType + '] ' + msgData);
}
}

function handleAck(which, msgID)
{
connLog(which, '(&#8595;) [ack] ' + msgID);
}

function handleException(which, msg)
{
connLog(which, '(&#8252;) [err] ' + msg);
}

function connLog(which, msg)
{
var target = $('#chat' + which + ' .connLog').get(0);
$(target).append('<div>' + msg + '</div>');
$(target).prop('scrollTop', $(target).prop('scrollHeight'));
}

</script>
« Last Edit: March 28, 2012, 09:35:23 PM by perkiset » Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #5 on: March 28, 2012, 03:02:10 PM »

So after you've added everything into your HTML file, it will look like this:
Code:
<html>
<head>

<style>
body { font-family: arial; font-size: 12px; font-weight: normal; margin: 0; padding: 20px; color: white; }
h1 { font-size: 24px; font-weight: bold; color: black; }
.chatBlock {
float: left; width: 380px; background-color: #b0b0b0; padding: 10px 20px 10px 20px; margin-right: 30px;
-moz-border-radius: 20px; -webkit-border-radius: 20px; border-style: solid; border-color: #404040; border-width: 1px;
box-shadow: 5px 5px 10px #909090; -webkit-box-shadow: 5px 5px 10px #909090;
}
.connected { background-color: #096311; }
.statusBlock { height: 24px; line-height: 24px; margin-bottom: 5px; font-weight: bold; }
.statusBlock span { font-weight: normal; }
.connButton { float: right; }
.connLog {
font-family: courier; background-color: white; color: #404040; padding: 5px;
border-style: solid; border-color: 404040; border-width: 1px 2px 2px 1px;
overflow: auto; width: 370px; height: 300px;
}
.sendBlock { height: 14px; line-height: 14px; margin-top: 5px; font-weight: bold; }
.sendBox { width: 120px; }
.sendButton { float: right; }
</style>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
<script src="/js/json2.js"></script>
<script src="/js/class.socketConnection.js"></script>

<script>
var conn1;
var conn2;

$(document).ready(function() {
var host = "ws://telemetry.ironmed.net:6789/";

conn1 = new socketConnection(host);
conn1.connectionID = 'connection1';
conn1.onConnect = function(readyState) { handleConnect(1, readyState); };
conn1.onReceive = function(msgType, msgData) { handleReceive(1, msgType, msgData); };
conn1.onAck = function(msgID) { handleAck(1, msgID); };
conn1.onException = function(msg) { handleException(1, msg); };
conn1.onDisconnect = function(readyState) { handleConnect(1, readyState); };

conn2 = new socketConnection(host);
conn2.connectionID = 'connection2';
conn2.onConnect = function(readyState) { handleConnect(2, readyState); };
conn2.onReceive = function(msgType, msgData) { handleReceive(2, msgType, msgData); };
conn2.onAck = function(msgID) { handleAck(2, msgID); };
conn2.onException = function(msg) { handleException(2, msg); };
conn2.onDisconnect = function(readyState) { handleConnect(2, readyState); };

$('.connButton').click(function(e)
{
var socket = ($(e.srcElement).attr('target') == 'conn1') ? conn1 : conn2;
if (socket.connected()) socket.disconnect();
else socket.connect();
});

$('.sendButton').click(function(e) {
var which = ($(e.srcElement).attr('target') == 'conn1') ? '1' : '2';
handleSend(which);
});

$('.sendBox').
keypress(function(e) {
if(e.keyCode == 13)
handleSend(($(e.srcElement).attr('target') == 'conn1') ? '1' : '2');
}).focus(function() {
this.select();
});

});

function handleConnect(which, readyState)
{
var connecting = (readyState == 1);

if (connecting) $('#chat' + which).addClass('connected');
else $('#chat' + which).removeClass('connected');

$('#chat' + which + ' .statusBlock > *').
filter('input').attr('value', (connecting) ? 'Disconnect' : 'Connect').end().
filter('span').html((connecting) ? '00:00:00' : 'Not Connected');
if (connecting)
$('#chat' + which + ' .connLog').html('');
connLog(which, (connecting) ? '(+) Connected' : '(x) Disconnected');
$('#chat' + which + ' .sendBox').focus();
}

function handleSend(which)
{
var socket = (which == '1') ? conn1 : conn2;
if (!socket.connected())
return alert('Socket ' + which + ' is not connected');

var msgType = $('#chat' + which + ' .sendType').val();
var msg = $('#chat' + which + ' .sendBox').val();
var wantAck = $('#chat' + which + ' .sendAck').val();
var msgID = socket.send(msgType, msg, wantAck);
connLog(which, '(&#8593;) [' + msgType + '] ' + msg);
if (wantAck == '1') connLog(which, '(&#8225;) ' + msgID);

$('#chat' + which + ' .sendBox').focus();
}

function handleReceive(which, msgType, msgData)
{
switch(msgType)
{
case 'servertime':
$('#conn' + which + 'stat').html(msgData);
break;

default:
connLog(which, '(&#8595;) [' + msgType + '] ' + msgData);
}
}

function handleAck(which, msgID)
{
connLog(which, '(&#8595;) [ack] ' + msgID);
}

function handleException(which, msg)
{
connLog(which, '(&#8252;) [err] ' + msg);
}

function connLog(which, msg)
{
var target = $('#chat' + which + ' .connLog').get(0);
$(target).append('<div>' + msg + '</div>');
$(target).prop('scrollTop', $(target).prop('scrollHeight'));
}

</script>

</head>
<body>
<h1>Perk's Websock Server & JS Class</h1>
<div id="chat1" class="chatBlock">
<div class="statusBlock">
ConnexTime: <span id="conn1stat">Not Connected</span>
<input type="button" value="Connect" target="conn1" class="connButton" />
</div>
<div class="connLog"></div>
<div class="sendBlock">
Send:
<select class="sendType" target="conn1" size="1">
<option value="message">Message</option>
<option value="query" SELECTED>Query</option>
<option value="fail">Fail</option>
</select>
<input class="sendBox" type="text" target="conn1" value="time"/>
<select class="sendAck" target="conn1" size="1">
<option value="1">Want Ack</option>
<option value="0">No Ack</option>
</select>
<input type="button" value="Send" class="sendButton" target="conn1" />
</div>
</div>

<div id="chat2" class="chatBlock">
<div class="statusBlock">
ConnexTime: <span id="conn2stat">Not Connected</span>
<input type="button" value="Connect" target="conn2" class="connButton" />
</div>
<div class="connLog"></div>
<div class="sendBlock">
Send:
<select class="sendType" target="conn2" size="1">
<option value="message">Message</option>
<option value="query" SELECTED>Query</option>
<option value="fail">Fail</option>
</select>
<input class="sendBox" type="text" target="conn2" value="date"/>
<select class="sendAck" target="conn2" size="1">
<option value="1">Want Ack</option>
<option value="0">No Ack</option>
</select>
<input type="button" value="Send" class="sendButton" target="conn2" />
</div>
</div>

</body>
</html>

Note that you'll need to change the server and the client settings so that it's pointing at the right places before this will all work. Interesting point: Note that the "url" the WebSockets call points to is not a URL but simply a domain and a port. This is simple, but significant: This entire thing is outside the channel of normal web work. There's no connection at all. You are pointing at an address with a port, not unlike FTP or POP or TELNET rather than a fully qualified URL like AJAX. So the name of the server really doesn't matter - so long as your firewall points into the right machine and the right port, this will work. Remember to start your server. You should probably start the server without -q and without & so that you can see it respond to client requests. CTRL-C will kill it also.

And if you're at that point, then you click Connect on the left side box and hopefully you will be rewarded with ...


Then you'll want to play about and your screen will start to look more like this:



Your server will look something like this:


These are the things to try:
  • Send a "message" with no ack. Note that the server sees it but nothing is sent back.
  • Try the same with an ACK.
  • Send a "query" with "time" and wantAck
  • Send a "fail" with any string to see how exceptions are caught
  • Notice how, during all this time, the connected time is being delivered to the client and displayed at the top
  • And now for the coolest one: Go to the directory you've specified for file-messages. In my example it's /www/sites/telemetry/utility. use VI or edit or whatever to create a little text file named connection1. When you save it, check out the clients - instantly delivered. This is the one that gives me a stiffy.
  • CTRL-C your server. Watch how both clients instantly see it and disconnect.
  • Start your server back up and re-connect the clients. Note that they are un-perturbed by having been cut out at the knees previously.
  • Disconnect the clients using the disconnect button,  notice how the server sees that and responds correctly.
  • Lastly: start the server, connect the clients, walk away for a day. Come back tomorrow and notice that nothing has moved between the two, but you can still send any message and both client and server respond correctly. The persistence of these sockets is really good and strong.

So that's about it. I have a boatload of things I'll be doing with my new server framework - I hope this sparks your imagination.

Enjoy!
/perk
« Last Edit: March 28, 2012, 07:27:59 PM by perkiset » Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
Terradon
n00b
*
Offline Offline

Posts: 6


View Profile
« Reply #6 on: April 28, 2013, 02:22:30 PM »

The download links for the files don't work....
Does this tutorial still work, becase i really want to try it out!
Logged
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #7 on: May 01, 2013, 02:39:03 PM »

Hey Terradon, welcome to The Cache Smiley

Sorry, I must've moved some stuff around, didn't notice the links were broken. Here are working links:

http://www.wickedsignals.com/websock/class.socketConnection.js
http://www.wickedsignals.com/websock/sockServer.php
http://www.wickedsignals.com/websock/sockClient.php
http://www.wickedsignals.com/websock/json2.js

(Cheers mate, I've also repaired the links at the top)
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
Terradon
n00b
*
Offline Offline

Posts: 6


View Profile
« Reply #8 on: May 10, 2013, 01:23:26 PM »

Thanks for the repairing.
I dont want to ruin any party, but the links to the php-files aren't links to the php-source codes.... Need Help

thanks for sharing codes anyways:)
Logged
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #9 on: May 12, 2013, 02:49:29 PM »

Huh?

I need to look again (I'm not at the office at the moment) ... but I'm seeing them OK.

I'll look later and confirm they're OK.

Thanks man.
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
Terradon
n00b
*
Offline Offline

Posts: 6


View Profile
« Reply #10 on: May 13, 2013, 01:59:26 PM »

no problem, i do have patience:)
after all, it is free, so no complaints from my side:)
Logged
perkiset
Olde World Hacker
Administrator
Lifer
*****
Offline Offline

Posts: 10096



View Profile
« Reply #11 on: May 13, 2013, 04:28:51 PM »

Hey Terradon -

Looks like I must've edited the files at some point and "dereferenced" the HTML codes, so it was trying to process the PHPs rather than showing the code.

Please give this a try and let me know if it compiles and goes the way it's supposed to.

Thanks!
/EP
Logged

It is now believed, that after having lived in one compound with 3 wives and never leaving the house for 5 years, Bin Laden called the U.S. Navy Seals himself.
Terradon
n00b
*
Offline Offline

Posts: 6


View Profile
« Reply #12 on: May 16, 2013, 05:34:48 PM »

Thanks, I could copy the source now and have tried it out.
Unfortnately i did not got it working on our testwebsite.

What i did:
put all the files in 1 map.
tried different addresses/ports

Code:
//var host = "ws://www.yourdomain.com:6789/";
var host = "ws://www.helenaweb.nl:56789/";
//var host = "ws://127.0.0.1:56789/";

//$master = initServer('128.140.218.200', 56789);
$master = initServer('127.0.0.1', 56789);

//$fPath = '/www/sites/telemetry/utility'; //
$fPath = '/home/xxxxxxx/domains/helenaweb.nl/public_html/heltest/perkiset/files';

When starting sockServer.php in my browser (keeping the tab open and timelimit set on 300 seconds) i get this kind of messages:

Server Started: 2013-05-17 02:12:14 Listening on 127.0.0.1:56789

So the server seems to run, but the client does not connect, i always get an alert: Socket 2 is not connected. (appears always in second chat and always Socket 2, no matte which chat i use)
Also many times the message: "Disconnected" appears in the second (always!) chat.

This demo is still at helenaweb.nl/heltest/perkiset/

I know for production it should be started from commandline, but for testing it should be possible to run it from the browser too.

Perhaps yo have any idea why it does not work for me or how to trobleshoot it?

Thanks in advance for any hint:)
Logged
Terradon
n00b
*
Offline Offline

Posts: 6


View Profile
« Reply #13 on: May 16, 2013, 05:46:32 PM »

Now something is happening, some tekst appears on the screen on socketServer.php:

Server Started: 2013-05-17 02:40:24 Listening on 128.140.218.200:25000 Resource id #3 CONNECTED! Handshake... GET / HTTP/1.1 Host: www.helenaweb.nl:25000 User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:21.0) Gecko/20100101 Firefox/21.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: nl,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: http://www.helenaweb.nl Sec-WebSocket-Key: IRo2SWi4A9v9yzyjaBqqVQ== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Handshake failed Resource id #4 CONNECTED! Handshake... GET / HTTP/1.1 Host: www.helenaweb.nl:25000 User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:21.0) Gecko/20100101 Firefox/21.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: nl,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: http://www.helenaweb.nl Sec-WebSocket-Key: BypQLXDoj2NIMIRFGP3qrA== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Handshake failed Resource id #5 CONNECTED! Handshake... GET / HTTP/1.1 Host: www.helenaweb.nl:25000 User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:21.0) Gecko/20100101 Firefox/21.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: nl,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: http://www.helenaweb.nl Sec-WebSocket-Key: TxBYwuljatXin9/FH7haDA== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Handshake failed Resource id #6 CONNECTED! Handshake... GET / HTTP/1.1 Host: www.helenaweb.nl:25000 User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:21.0) Gecko/20100101 Firefox/21.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: nl,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: http://www.helenaweb.nl Sec-WebSocket-Key: kYe0NQXNf+JsewoEh0Zbuw== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Handshake failed Resource id #7 CONNECTED! Handshake... GET / HTTP/1.1 Host: www.helenaweb.nl:25000 User-Agent: Mozilla/5.0 (Windows NT 6.0; rv:21.0) Gecko/20100101 Firefox/21.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: nl,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip, deflate DNT: 1 Sec-WebSocket-Version: 13 Origin: http://www.helenaweb.nl Sec-WebSocket-Key: 3ykBn997NHOprcE21be5DA== Connection: keep-alive, Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Handshake failed

"handshake failed" message when in the client pushed on connect button....
Logged
Pages: [1]
  Print  
 
Jump to:  

Perkiset's Place Home   Best of The Cache   phpMyIDE: MySQL Stored Procedures, Functions & Triggers
Politics @ Perkiset's   Pinkhat's Perspective   
cache
mart
coder
programmers
ajax
php
javascript
Powered by MySQL Powered by PHP Powered by SMF 1.1.2 | SMF © 2006-2007, Simple Machines LLC
Seo4Smf v0.2 © Webmaster's Talks


Valid XHTML 1.0! Valid CSS!