HTML - WebSocket Sample - with PHP Server

兩個月前因為工作的關係,開始在網路上找如何實做 WebSocket。

這是關於 WebSocket 的介紹:
◎ http://www.websocket.org/
◎ http://zh.wikipedia.org/wiki/WebSocket

如果懶得看,讓我們從這張比較圖來了解最快!


簡單說以前網頁大多數的狀況是,使用者端要求新的資料,提供資料的伺服才吐新的資料給你,這樣如果要做到即時更新,變得使用者端要不斷地詢問 Server 有沒有新的資料,而這樣一去一回,浪費時間也浪費資源,也沒想像中的即時。因此 HTML5 多了這條路 WebSocket,WebSocket 概念是一開始使用者端跟 Server "握個手"之後,在沒說要斷線的情況下,任何一方就可以在任何時間主動扔資料給對方,如此是也,不用一去一回,也省去了一些重新建立連線的動作,又快又方便。

以上簡介,那接下來我在網路上搜尋的過程中,發現 Client 端建立 WebSocket 的範例很多,但是 Server 端處理 WebSocket 的範例卻很少,即使找到了,還不能直接 Run ,所以小的才 po 這篇文章,提供一個可以直接運作的版本 (程式有不少部分是網路上找來的,加以改善)。以 Server 傳時間給 Client 端顯示為範例。

首先是 Client 端,一般來說 Client 端為網頁,透過 javascript 建立連線,建立連線之後可以定義 onopen (連線成功)、onmessage (收到訊息)、onclose (結束連線)、onerror (錯誤時) 四個事件各要對應什麼 function 處理。如下 :

Fileclient.htmljquery-1.10.1.min.js

<html>
<head>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
<title>WebSocket</title>
<script type="text/javascript" src="jquery-1.10.1.min.js"></script>
<script type="text/javascript">

var socket; // WebSocket

$(document).ready(function() {
SetupWebSocket();
});

// 連線至提供資料的WebSocket
function SetupWebSocket()
{
var host = 'ws://localhost:12345/server.php';
socket = new WebSocket(host);
socket.onopen = function(e) { 
$('#ShowTime').html('WebSocket Connected!');
};
socket.onmessage = function(e) {
$('#ShowTime').html(e.data);
};
socket.onclose = function(e) {
alert('Disconnected - status ' + this.readyState);
};
}

</script>
</head>

<body>
<div id="ShowTime" Style="font-size:24px"></div>
</body>
</html>


OK! Client 簡單一點看比較清楚, 接下來是 Server 端,Server 端我使用PHP做,在此說明一下,WebSocket 在 Coding 上還是建立 Socket 等著被連接,差異是在連線之後,必須傳一個特定的回應訊息 ,以完成 handshake 握手協定。然後之後收送訊息時,收到的訊息會有個 Header,需知道怎麼拿掉 (unmask),以及 Server 送出訊息也要掛一個 Header (encode) 出去!我們直接看Code (很多):

Fileserver.php

<?php  /*  >php -q server.php  */

$debug = true;

error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

$sock = WebSocket("localhost",12345);
$sockets = array($sock);
$users = array();

while(true)
{
$read = $sockets;

$write = NULL;
$except = NULL;
if (socket_select($read, $write, $except, NULL) < 1)
continue;

if (in_array($sock, $read))
{
$newsock = socket_accept($sock);
connect($newsock);

$key = array_search($sock, $read);
unset($read[$key]);
}

foreach($read as $socket)
{
$bytes = @socket_recv($socket, $buffer, 2048, 0);

if($bytes == 0)
{
disconnect($socket);
}
else
{
$user = getuserbysocket($socket);
if(!$user->handshake)
{
dohandshake($user,$buffer);
}
else
{
console("\nprocess -> id: " . $user->id);
process($socket,$buffer);
}
}
}

}

//---------------------------------------------------------------

function doTest($socket)
{
while(true) {
console("[doTest] " . $socket);
$sendText = date('Y-m-d H:i:s');

// 如果送失敗就停止
if (!send($socket, $sendText)) {
echo "[doTest] Stop \n";
return;
}
sleep(1);
}
}

function process($socket,$msg)
{
// 訊息需要解碼
$action = unmask($msg);
console("< " . $action);
}

/**
 * Unmask a received recvMsg
 * @param $payload
 */
function unmask($recvMsg) 
{
// ord 回傳 ascii code
// 127 → 0x01111111
$length = ord($recvMsg[1]) & 127;

if($length == 126) 
{
$masks = substr($recvMsg, 4, 4);
$data = substr($recvMsg, 8);
}
elseif($length == 127) 
{
$masks = substr($recvMsg, 10, 4);
$data = substr($recvMsg, 14);
}
else 
{
$masks = substr($recvMsg, 2, 4);
$data = substr($recvMsg, 6);
}

$text = '';
for ($i = 0; $i < strlen($data); ++$i) 
{
$text .= $data[$i] ^ $masks[$i % 4];
}
return $text;
}

function send($client, $msg)
{
console("> " . $msg);
$sendMsg = encode($msg);
$result = socket_write($client, $sendMsg, strlen($sendMsg));

if ( !$result )
{
disconnect($client);
$client = false;
return false;
}
return true;
}


/**
 * Encode a text for sending to clients via ws://
 * @param $text
 */
function encode($text) 
{
$header = " ";
$header[0] = chr(0x81);
$header_length = 1;

//Payload length:  7 bits, 7+16 bits, or 7+64 bits
$dataLength = strlen($text);

//The length of the payload data, in bytes: if 0-125, that is the payload length.  
if($dataLength <= 125)
{
$header[1] = chr($dataLength);
$header_length = 2;
}
elseif ($dataLength <= 65535)
{
// If 126, the following 2 bytes interpreted as a 16
// bit unsigned integer are the payload length. 

$header[1] = chr(126);
$header[2] = chr($dataLength >> 8);
 $header[3] = chr($dataLength & 0xFF);
 $header_length = 4;
}
else
{
// If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the 
// most significant bit MUST be 0) are the payload length. 
$header[1] = chr(127);
$header[2] = chr(($dataLength & 0xFF00000000000000) >> 56);
$header[3] = chr(($dataLength & 0xFF000000000000) >> 48);
$header[4] = chr(($dataLength & 0xFF0000000000) >> 40);
$header[5] = chr(($dataLength & 0xFF00000000) >> 32);
$header[6] = chr(($dataLength & 0xFF000000) >> 24);
$header[7] = chr(($dataLength & 0xFF0000) >> 16);
$header[8] = chr(($dataLength & 0xFF00 ) >> 8);
$header[9] = chr( $dataLength & 0xFF );
$header_length = 10;
}
return $header . $text;
}


function WebSocket($address,$port)
{
$master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");
socket_bind($master, $address, $port)                    or die("socket_bind() failed");
socket_listen($master, 20)                               or die("socket_listen() failed");
echo "Server Started : " . date('Y-m-d H:i:s') . "\n";
echo "Master socket  : " . $master . "\n";
echo "Listening on   : " . $address . " port " . $port . "\n\n";
return $master;
}

function connect($socket)
{
global $sockets,$users;
$newUser = new User();
$newUser->id = uniqid();
$newUser->socket = $socket;
array_push($users,$newUser);
array_push($sockets,$socket);
console("id:" . $newUser->id . ", " . $socket . " CONNECTED!");
}

function disconnect($socket)
{
global $sockets,$users;
$found = null;
$n = count($users);
for($i=0; $i < $n; $i++)
{
if($users[$i]->socket == $socket)
{
$found = $i;
break;
}
}
if(!is_null($found))
{
array_splice($users, $found, 1);
}
$index = array_search($socket,$sockets);
socket_close($socket);
console($socket." DISCONNECTED!");
if($index >= 0)
{
array_splice($sockets, $index, 1);
}
}

function dohandshake($user,$buffer)
{
console("\nRequesting handshake...");
console($buffer);

list($resource,$host,$origin,$strkey,$data) = getheaders($buffer);
if (strlen($strkey) == 0) 
{
socket_close($user->socket);
console('failed');
return false;
}

$hash_data = base64_encode(sha1($strkey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));

$upgrade  = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n" .
     "Upgrade: webSocket\r\n" .
     "Connection: Upgrade\r\n" .
     "WebSocket-Origin: " . $origin . "\r\n" .
     "WebSocket-Location: ws://" . $host . $resource . "\r\n" .
     "Sec-WebSocket-Accept:" . $hash_data . "\r\n\r\n";

socket_write($user->socket, $upgrade, strlen($upgrade));
$user->handshake = true;
console($upgrade);
console("Done handshaking...\n\n");

doTest($user->socket);
return true;
}


function getheaders($req)
{
$r=$h=$o=$key=$data=null;
if(preg_match("/GET (.*) HTTP/"               ,$req,$match)){ $r=$match[1]; }
if(preg_match("/Host: (.*)\r\n/"              ,$req,$match)){ $h=$match[1]; }
if(preg_match("/Origin: (.*)\r\n/"            ,$req,$match)){ $o=$match[1]; }
if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/" ,$req,$match)){ $key=$match[1]; }
if(preg_match("/\r\n(.*?)\$/"                 ,$req,$match)){ $data=$match[1]; }

return array($r,$h,$o,$key,$data);
}

function getuserbysocket($socket)
{
global $users;
$found=null;
foreach($users as $user)
{
if($user->socket==$socket)
{
$found=$user;
break;
}
}
return $found;
}

function    wrap($msg = ""){ return chr(0) . $msg . chr(255); }
function  unwrap($msg = ""){ return substr($msg, 1, strlen($msg) - 2); }
function console($msg = ""){ global $debug; if($debug) { echo $msg . "\n"; } }

class User
{
var $id;
var $socket;
var $handshake;
}

?>


最後,看到這邊最重要的要說,怎麼跑這程式:

◎ 首先 Server 端為 server.php,透過 cmd 指令模式下,有設定好 php 路徑 (ex. path c:\php) ,然後到你放 server.php 檔案的路徑下執行指令 php server.php,讓 Server 以 daemon 的方式跑著。

◎ 再來 Client 端為 client.html,因為程式有用到 jquery,所以請去網路上找到這個檔 jquery-1.10.1.min.js,跟 client.html 放在一起,然後用瀏覽器把 client.html 開起來即可,記得 Server 要先跑起來 ...


附上檔案供下載:
https://docs.google.com/file/d/0B0kC-urN3g_jY2tBRWZEbl9EcmM/edit?usp=sharing

留言

  1. 拜謝大大用心整理,幫了我好大忙

    回覆刪除
  2. 感謝大大的 encode function 小的找這方面的資料找了許久, 謝謝大大救援

    回覆刪除
  3. ws://localhost:12345/server.php
    請問12345可以改成其他值嗎?

    回覆刪除

張貼留言

這個網誌中的熱門文章

HTML - CSS - footer floating toolbar bottom 瓢浮置底的工具列

自己設計讓網頁支援多國語系的架構

javascript - 樂透 lottery - 號碼產生器