WebSockets on LAMP stack Part 2

How to use WebSockets on a LAMP stack Part 2

In the previous part of this tutorial we saw how to build a WebSocket server in Python and how to send commands to a UNIX socket from the command line. In this part we will develop a PHP application to do that. I am not going to use any framework because I am trying to demonstrate some basic concepts. So lets get started. Some changes are required to the Python application to make it easier to read the output form the PHP application. The list of connected clients is going to start with “=== START” and is going to end with “=== END”. The SEND MESSAGE command is going to output OK or ERROR and the error message.

main.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import sys
"""
Include the library path in Pythons search path. Could also be done by setting the
PYTHONPATH environment variable but I rather keep it visible in the source code.
"""
sys.path.append("./txWebSocket/")
from websocket import WebSocketSite, WebSocketHandler, WebSocketFrame
from twisted.internet import reactor, protocol
from twisted.protocols import basic
from twisted.python import log

"""
The class helps keep track of the WebSockets and their id.
"""
class WebSocketTracker:

    def __init__(self):
        self.websockets=list()

    def addWebSocket(self,ws):
        self.websockets.append(ws)

    def removeWebSocket(self,ws):
        self.websockets.remove(ws)

    def findWebSocketWithSession(self,sessionId):
        for ws in self.websockets:
            if(ws.sessionId==sessionId):
                return ws
        return None
"""
Each WebSocket Connection instantiates this class.
"""
class WebSocket(WebSocketHandler):

    def __init__(self,transport,request):
        WebSocketHandler.__init__(self,transport,request)
        self.sessionId=None

    """
    Receive a WebSocket frame from a Web Browser most likely.
    """
    def frameReceived(self, frame):
        message=json.loads(frame)

    """
    Triggered when a new WebSocket connection is established. Make sure that it has an id in the URL
    parameters that will help us identify the connection. If it does register the connection otherwise disconnect.
    """
    def connectionMade(self):
        global websocketTracker
        log.msg("Connection Open")
        websocketTracker.addWebSocket(self)
        print self.request.args

        if "id" not in self.request.args:
            self.transport.loseConnection()
            return
        self.sessionId=self.request.args['id'][0]

    """
    Send a WebSocket Fame by converting the message parameter into a JSON string.
    """
    def sendMessageJSON(self,message):
        ws=WebSocketFrame(WebSocketFrame.TEXT,json.dumps(message))
        self.transport.write(ws)

    """
    Send a WebSocket Frame.
    """
    def sendMessage(self,message):
        ws=WebSocketFrame(WebSocketFrame.TEXT,message)
        self.transport.write(ws)

    """
    Connection was Lost. Remove the WebSocket from the registry.
    """
    def connectionLost(self, reason):
        print "Connection Lost"
        global websocketTracker
        websocketTracker.removeWebSocket(self)

"""
This class get instantiated when a client connects to the UNIX socket.
"""
class UnixSocketProtocol(basic.LineReceiver):

    """
    Set the line delimiter to \n instead of \r\n
    """
    def __init__(self):
		self.delimiter="\n"

    """
    Receive a command and process it.
    """
    def lineReceived(self, line):
        print line
        if(self.startsWith(line,"LIST WEBSOCKETS")):
            global webSocketTracker
            self.sendLine("=== START ===")
            for ws in websocketTracker.websockets:
                peer=ws.transport.getPeer()
                self.sendLine("{0} {1}:{2!s}".format(ws.sessionId,peer.host,peer.port))
            self.sendLine("=== END ===")
        if(self.startsWith(line,"SEND MESSAGE")):
            sm=line.split(" ",4)
            sessionId=sm[2]
            message=sm[3]
            ws=websocketTracker.findWebSocketWithSession(sessionId)
            if(ws!=None):
                ws.sendMessage(message)
                self.sendLine("OK")
            else:
                self.sendLine("ERROR Client Not Found");

    """
    Helper function to check if a line begins with a string. It would have been better to make it static and move it to
    a Utilities class.
    """
    def startsWith(self,line,start):
        if(len(line)<len(start)):
            return False
        elif(line[0:len(start)]==start):
            return True
        return False

"""
The entry point of our program. Creates a WebServer with a WebSocket handler and the UNIX socket listener.
"""
if __name__=="__main__":
    global sessionName,websocketTracker
    log.startLogging(sys.stdout)
    websocketTracker=WebSocketTracker()

    site = WebSocketSite(None)
    site.addHandler('/websocket', WebSocket)
    reactor.listenTCP(8080, site)

    sf=protocol.ServerFactory()
    sf.protocol=UnixSocketProtocol
    reactor.listenUNIX("/tmp/unix_socket",sf)

    reactor.run()

The PHP application for this project is going to be a bit raw. Also have in mind that we have not solve any security issues like authentication yet. We will discuss possible solutions to this problem in a future tutorial.

PHP Directory structure

.
|-classes
| |-ServiceSocket.class.php
|-config.php
|-setup.php
|-index.php
|-admin.php
|-sendMessage.php

The config file has some basic configuration that we may need to change.

config.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
	$config=array();
	//Path to the UNIX socket
	$config["service_socket"]="/tmp/unix_socket";
	//WebSocket host
	$config['ws_host']=$_SERVER["SERVER_ADDR"];
	//WebSocket port
	$config['ws_port']="8080";
?>

The setup.php file is going to initialize the ServiceSocket.

setup.php

1
2
3
4
5
6
<?php
	require_once dirname(__FILE__)."/config.php";
	require_once dirname(__FILE__)."/classes/ServiceSocket.class.php";
	$serviceSocket=new ServiceSocket($config["service_socket"]);
 ?>

The ServiceSocket class is the one class that is going to do the heavy leafing by communicating with the underline UNIX socket.

ServiceSocket.class.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php
class ServiceSocket {
	function __construct($socketPath) {
		$this->socketPath=$socketPath;
		$this->socket=socket_create(AF_UNIX, SOCK_STREAM, 0);
		$ok=socket_connect($this->socket,$this->socketPath);
		if($ok==false) {
			error_log(socket_strerror($this->socket));
			die();
		}
		$this->buffer="";
		socket_set_nonblock($this->socket);
	}

	function getLine() {
		while(true) {
			$pos=strpos($this->buffer,"\n");
			if($pos!==false) {
				$line=substr($this->buffer,0,$pos);
				$this->buffer=substr($this->buffer,$pos+1);
				return $line;
			}

			$data=socket_read($this->socket,100);
			if($data==false)
				continue;
			$this->buffer.=$data;
		}
		return $line;

	}

	function listConnections() {
		socket_write($this->socket,"LIST WEBSOCKETS\n");
		$line=$this->getLine();
		$ret=array();
		if(substr($line,0,3)=="===") {
			while(true) {
				$line=$this->getLine();
				if(substr($line,0,3)=="===")
					break;
				$cline=explode(" ",$line);
				$ret[]=array("clientId"=>$cline[0],"host"=>$cline[1]);
			}
		}
		return $ret;
	}

	function sendMessage($clientId,$jsonMessage) {
		socket_write($this->socket,"SEND MESSAGE ".$clientId." ".$jsonMessage."\n");
		$line=$this->getLine();
		if($line=="OK") {
			return true;
		}else {
			return explode(" ",$line,2)[1];
		}
	}

}
?>

The index.php is pretty much the same like the index.html with the difference that the WebSocket host and port is coming from the config file.

index.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 <?php
 	require_once dirname(__FILE__)."/setup.php"
?>
<!DOCTYPE html>
<html>
	<head>
	</head>
	<body>
		<h2>Message Board</h2>
		<div><h3>Board Id:<h3><span id="boardId"></span></div>
		<div id="message_board">
		</div>
		<script type="text/javascript">
		function generateId() {
			var ret="";
			var charset="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
			for(var i=0;i<10;i++) {
				ret+=charset.charAt(Math.random()*charset.length);
			}
			return ret;
		}
		randomId=generateId();
		var ele=document.getElementById("boardId");
		ele.innerText=randomId;
		ws = new WebSocket("ws://<?=$config["ws_host"].":".$config["ws_port"];?>/websocket?id="+randomId); //?id="+randomId
		ws.onmessage=function(event){
			console.log(event.data);
			var message=JSON.parse(event.data);
			var messageElement=document.createElement("div");
			messageElement.innerText=message.message;
			document.getElementById("message_board").appendChild(messageElement);
		}
		</script>
	</body>
</html>

The admin.php page is the one that is going to display the connected WebSockets and allow us to to send messages to the clients. The page needs to be reloaded to show any changes to list of connected clients but this is not a problem that we will solve for now.

admin.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
 <?php
 	require_once dirname(__FILE__)."/setup.php";
 	$clients=$serviceSocket->listConnections();
  ?>
  <!DOCTYPE html>
  <html>
  	<head>
  	</head>
  	<body>
  		<h2>Clients</h2>
  		<select id="clients">
 <?php
  			foreach($clients as $client) {
 				?>
 				<option value="<?=$client["clientId"];?>"><?=$client["clientId"];?> <?=$client["host"];?></option>
 				<?php
 			}
 ?>
 		</select>
  		<h2>Message</h2>
 		<textarea id="message"></textarea>
 		<button id="send_message">Send Message</button>

  		<script type="text/javascript">
 		var ele=document.getElementById("send_message");
 		ele.addEventListener("click",function() {
 			var clientId=document.getElementById("clients").value;
 			var message=document.getElementById("message").value;
 			var req = new XMLHttpRequest();
 			req.open("POST", "/sendMessage.php");
 			req.onreadystatechange=function() {
 				if(req.readyState === 4 && req.status === 200) {
 					var data=JSON.parse(req.responseText)
 					if(data.error==true) {
 						alert(data.message);
 					}else {
 						alert("Message Send");
 					}
 				}
 			}

 			req.send(JSON.stringify({"clientId":clientId,"message":message}));
 		});
  		</script>
  	</body>
  </html>

The sendMessage.php is the script that will receive a JSON message form the admin.php, forward the message to the UNIX socket and return a response.

sendMessage.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
require_once dirname(__FILE__)."/setup.php";
$data=file_get_contents("php://input");
$data=json_decode($data);
$clientId=$data->clientId;
$message=$data->message;
$socketMessage=array("message"=>$message);
$resp=array("error"=>false,"message"=>"");
$res=$serviceSocket->sendMessage($clientId,json_encode($socketMessage));
if($res!==true) {
	$resp["error"]=true;
	$resp["message"]=$res;
}
print json_encode($resp);
?>

I am going to write some commends in the code and upload everything in a Github repository as soon as I find some time. Again this project is for demonstrating some basic concepts. Taking those concepts into production is a different story that is beyond the point of this article, so be careful to address all security concerns before you do so. In the next part I am going to discuss how to you could handle authentication.