Web Sockets กับการ Impliment เว็บแชทขั้นพื้นฐาน
ตอนที่ 1 : http://aobkung.blogspot.com/2015/03/websockets-1.htmlตอนที่ 3 : http://aobkung.blogspot.com/2015/05/websockets-3.html
ก่อนอื่นเราจะต้องบอกก่อนว่า จะทำการ implement ในฟั่ง Process ของ server โดยการใช้ Class ของคนที่เขียน WebSocket มาอยู่แล้ว ซึ้งเป็นภาษา PHP และทำออกมาได้ดีพอสมควร (ในแง่การใช้ มันง่ายดี)
สามารถเข้าไป Download ได้ที่
https://github.com/ghedipunk/PHP-Websockets
โดยที่เราจะสนใจ file ที่ชื่อ websockets.php และ users.php
และที่สำคัญจะต้องติดตั้ง PHP Command line มาด้วย ซึ้งผมใช้ Mac OS X ดังนั้นจึงทำการติดตั้งผ่านโปรแกรมที่ชื่อว่า MAMP ซึ้งจะมีเวอร์ชั่นฟรีให้โหลด (ค้นหาเอาเลยใน Google) เอาไปใช้ได้
ขอบเขตของ web chat ที่เราจะเขียน
- เป็นห้อง Chat รวม เมื่อเข้าไปแล้วทุกคนสามารถคุยกันได้ (ทุกคนเห็นข้อความที่เราพิมพ์ออกไป)
- เป็นเมื่อมีคน connect เข้ามา หรือปิดการเชื่อมต่อ (ปิดโปรแกรม) ก็จะแจ้งเตือนให้กับผู้อื่นได้รับทราบด้วย
- บอกหมายเลข ID ของแต่ละคนเพื่อแยกตัวตนซึ้ง Class WebSocketServer จะทำหน้าที่สร้างขึ้นมา
ขั้นตอนที่ 1
เริ่มจากการ Impliment ทางฟั่ง Server กันก่อนเลย (บางแหล่งเริ่มอธิบายจาก Client ก่อน แต่ผมเข้าใจมันมาจากทาง Server ก่อน ดังนั้นขอแบบนี้ละกันนะครับ ฮาๆ)เอาละ เริ่มจากเข้าไปดู code ที่ไฟล์ websockets.php กันก่อน ใน class จะเห็น code ดังนี้
// Called immediately when the data is recieved.จะมี abstract function อยู่ 3 function ให้เราเอาไป implement เอาเอง ซึ้งก็คือ 3 function หลักที่เป็นหัวใจของการทำ Web Socket ก็คือ connected closed และ Process(การ get และ send ข้อมูล) ดังนั้นเราจะสร้าง Class ของเราขึ้นมาเอาไว้ใช้งาน โดนสือทอด Class WebSocketServer ที่คุณ ghedipunk ได้เขียนและนำเอามาลงเอาไว้ใน Github นั้นเอง
abstract protected function process($user,$message);
// Called after the handshake response is sent to the client.
abstract protected function connected($user);
// Called after the connection is closed.
abstract protected function closed($user);
สร้าง testWebSocketChat.php
<?php
require './websockets.php';
class testWebSocketChat extends WebSocketServer {
protected function process ($user, $message) {
}
protected function connected ($user) {
}
protected function closed ($user) {
}
}
$testWebSocketChat = new testWebSocketChat("127.0.0.1","9000");
try {
$testWebSocketChat->run();
}
catch (Exception $e) {
$testWebSocketChat->stdout($e->getMessage());
}
จะอธิบายทีละขั้น ดังนี้
- require ไฟล์ของ Class WebSocketServer เข้ามา
- สร้าง function ที่เป็น abstract ทั้งหมดเอาไว้ และจะอธิบายว่าแต่ละกันทำอะไรบ้างในขั้นตอนที่ 2
- สร้าง Object ของ testWebSocketChat ขึ้นมา พร้อมระบุ ip ของ Server เราในที่นี้คือเครื่องของเราเองนั่นคือ 127.0.0.1 พร้อม Port ที่ต้องการจะใช้เชื่อมต่อ Socket กันซึ้งผมกำหนดไปเป็น 9000 (ตัวอย่างไฟล์การทำงานของต้นฉบับก็ใช้ port นี้เลยใช้ตามไม่มีเหตุผลอื่น ฮาๆๆๆ) ซึ้งการกำหนดนี้จะเข้าไปทำงานที่ __construct ในไฟล์ websockets.php ซึ้งมันทำงานยังไงก็ตามเข้าไปดูได้เลยจ้า
- และใช้คำสั่ง run() เพื่อเริ่มกระบวนการทำงาน
ขั้นตอนที่ 2
เราจะ Implement function connected กันก่อนซึ้งจะเป็นไปตามนี้คือ
- เมื่อมี user connect เข้ามานั้นจะทำการแจ้งเตือนให้กับทุกคนที่อยู่ในห้อง Chat (ยกเว้นตัวเราเอง)
- function connect มีการ pass parameter ที่ชื่อ $user คือรายละเอียดของ user คนนั้นที่ connect เข้ามา
- $this->users คือ ตัวแปรที่เก็บรายละเอียดของ User ทุกๆคน ที่ Connect เข้ามายัง Process Socket ของเรา ซึ้งจะเป็น Array
- $this->send( <user> , <data> ) คือคำสั้งที่ใช้ส่งข้อมูลไปยัง user คนนั้นๆ ซึ้นมันทำงานยังไงนั้น ....... ยาวเลยบ่องตง ตามเข้าไปดูเองใน websockets.php เลยจ้า
ดังนั้นเราจะเขียนโปรแกรมอกมาได้ดังนี้
protected function connected ($user) {ภายในส่วนของ data จะส่งข้อควาที่เป็น id และ เวลา ไปด้วยตามขอบเขตที่เขียนเอาไว้ด้วย
foreach ($this->users as $value) {
if ($value != $user)
$this->send($value , "id ".$user->id." (".date("Y-m-d H:i:s").") : Connected");
}
}
ขั้นตอนที่ 3
Implement function closed ก็จะมีรายละเอียดเหมือนๆกับ connected นั่นละครับ เพียงแต่เปลี่ยนคำพูดนิดหน่อยเท่านั้นเอง
protected function closed ($user) {
foreach ($this->users as $value) {
if ($value != $user)
$this->send($value , "id ".$user->id." (".date("Y-m-d H:i:s").") : Disconnect");
}
}
ขั้นตอนที่ 4
Implement function process
- จะทีการ pass parameter เข้า user และ message ที่ถูกส่งมาจาก Client
- เพิ่ม id เข้าไปในข้อความ เพื่อระบุตัวตนได้ตามขอบเขต
- ส่งข้อความไปยังทุกคนยกเว้นตนเอง
protected function process ($user, $message) {
$message = "id ".$user->id." : ".$message;
foreach ($this->users as $key => $value) {
if ($value != $user) {
$this->send($value,$message);
}
}
}
ขั้นตอนที่ 5
จากที่ได้ Implement code ทั้งหมดไปแล้ว ซึ้งง่ายมากๆ ก็จะถึงตอนที่เราจะทำการรัน Daemon Process ของโปรแกรมที่เราเขียนกันแล้ว ซึ้งง่ายมากๆ โดยไปยัง path ที่เขียนไฟล์ testWebSocketChat.php และใช้คำสั่ง.....
php testWebSocketChat.php
เท่านี้ก็เรียบร้อย รอให้บริการกับทาง Client ที่จะเข้ามายัง Process ได้เลย และสำหรับการ Implement ทางฟั่ง Server ก็มีเท่านี้ ส่วนทางด้าน Client นั้นจะขอยกเอาไปเล่ากันต่อบทความต่อไปนะครับ แต่จะทิ้ง code ทาง client เอาไว้ให้แล้วเอาไปดูกัน เพราะมันเข้าใจได้โดยที่ไม่ต้องอธิบาย แต่สำหรับคนที่ไม่เข้าใจก็รอในบทความต่อไปเลยครับ
ไฟล์ client.html
<!doctype html>
<html>
<head>
<meta charset='UTF-8' />
<title>WebSocket4Chat</title>
<style>
input, textarea {border:1px solid #CCC;margin:5px;padding:5px}
.main {max-width:800px;margin:auto}
#chatbox {width:100%;height:400px;font-size: 15px;}
#message {width:100%;line-height:30px;font-size: 15px;}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script type="text/javascript">
function getDate () {
var objToday = new Date(),
weekday = new Array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'),
dayOfWeek = weekday[objToday.getDay()],
domEnder = new Array( 'th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th' ),
dayOfMonth = today + (objToday.getDate() < 10) ? '0' + objToday.getDate() + domEnder[objToday.getDate()] : objToday.getDate() + domEnder[parseFloat(("" + objToday.getDate()).substr(("" + objToday.getDate()).length - 1))],
months = new Array('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'),
curMonth = months[objToday.getMonth()],
curYear = objToday.getFullYear(),
curHour = objToday.getHours() > 12 ? objToday.getHours() - 12 : (objToday.getHours() < 10 ? "0" + objToday.getHours() : objToday.getHours()),
curMinute = objToday.getMinutes() < 10 ? "0" + objToday.getMinutes() : objToday.getMinutes(),
curSeconds = objToday.getSeconds() < 10 ? "0" + objToday.getSeconds() : objToday.getSeconds(),
curMeridiem = objToday.getHours() > 12 ? "PM" : "AM";
var today = curHour + ":" + curMinute + ":" + curSeconds + curMeridiem + "," + dayOfMonth + " " + curMonth + ", " + curYear;
return today;
}
function writeChatbox( text ) {
$chatbox = $('#chatbox');
//Add text to chatbox
$chatbox.append(($chatbox.val()?"\n":'')+text);
//Autoscroll
$chatbox[0].scrollTop = $chatbox[0].scrollHeight - $chatbox[0].clientHeight;
}
function checkStatusConnectSocket(state) {
if (state == 1) {
return 'You : Connected ('+getDate()+')';
}
else if (state == 3) {
return 'You : Disconnected ('+getDate()+')';
}
else if (state == 0) {
return 'You : Init Socket ('+getDate()+')';
}
}
function init() {
var host = "ws://127.0.0.1:9000/"; // SET THIS TO YOUR SERVER
try {
socket = new WebSocket(host);
socket.onopen = function(data) {
writeChatbox (checkStatusConnectSocket(socket.readyState));
};
socket.onmessage = function(data) {
// console.log(data);
writeChatbox (data.data);
};
socket.onclose = function(data) {
writeChatbox (checkStatusConnectSocket(socket.readyState));
};
}
catch (ex) {
writeChatbox (ex);
}
}
function send(data) {
if(!data) {
alert("Message can not be empty");
return;
}
try {
socket.send(data);
} catch(ex) {
writeChatbox ('Can not send Data to server ('+ex+')');
}
}
$(document).ready(function() {
$('#message').keypress(function(e) {
if ( e.keyCode == 13 && this.value ) {
writeChatbox( 'You: ' + this.value );
send( this.value );
$(this).val('');
$(this).focus();
}
});
init();
});
</script>
</head>
<body>
<div class="main">
<textarea id='chatbox' name='chatbox' readonly='readonly'></textarea>
<input type='text' id='message' name='message' />
</div>
</body>
</html>