วันพุธที่ 18 มีนาคม พ.ศ. 2558

WebSockets กับเว็บแชทขั้นพื้นฐาน ตอนที่ 2


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 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);           
จะมี abstract function อยู่ 3 function ให้เราเอาไป implement เอาเอง ซึ้งก็คือ 3 function หลักที่เป็นหัวใจของการทำ Web Socket ก็คือ connected closed และ Process(การ get และ send ข้อมูล) ดังนั้นเราจะสร้าง Class ของเราขึ้นมาเอาไว้ใช้งาน โดนสือทอด Class WebSocketServer ที่คุณ ghedipunk ได้เขียนและนำเอามาลงเอาไว้ใน Github นั้นเอง

สร้าง 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) {
    foreach ($this->users as $value) {
         if ($value != $user)
              $this->send($value , "id ".$user->id." (".date("Y-m-d H:i:s").") : Connected");
    }
}
 ภายในส่วนของ data จะส่งข้อควาที่เป็น id และ เวลา ไปด้วยตามขอบเขตที่เขียนเอาไว้ด้วย

           ขั้นตอนที่ 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>

วันอังคารที่ 17 มีนาคม พ.ศ. 2558

WebSockets กับเว็บแชทขั้นพื้นฐาน ตอนที่ 1


WebSockets 

ตอนที่ 2 http://aobkung.blogspot.com/2015/03/websockets-2.html
ตอนที่ 3 http://aobkung.blogspot.com/2015/05/websockets-3.html

           ถ้าจะให้พูดง่ายๆ คำพูดไม่สวยหรูก็คือ เป็นสิ่งหนึ่งที่ทำให้ Client สามารถรับอะไรต่างๆนาๆที่ server อยากจะให้โดยที่ไม่ต้องส่ง requests ไปยัง server ว่ามีอะไรจะให้ไหม ถ้ามีส่งมาน๊ะ อะไรประมานนั้น จึงประหยัดทั้งเวลาที่ใช้รอในการ requests ลดการทำงานของ server ลง เพราะไม่ต้องรับ requests ถี่ๆ(บางคนใช้วิธีการตั้งเวลาเพื่อขอรีเควสเรื่อยๆอาจจะทุกๆ 2 วินาที) เป็นต้น 
            แต่ถ้าให้พูดถึงการทำโปรแกรมแชท , ทำ notification หรืออะไรก็แล้วแต่ที่เป็นไปในลักษณะ real time ก็มีวิธีที่จะนำไปใช้ได้อยู่คร่าวๆดังนี้

  1. Long Polling
  2. Server-Sent Event
  3. Websockets

            Ajax Long Polling

       


           
            client จะทำการส่ง requests ผ่าน http ไปยัง server จากนั้นเมื่อ server มี state ใหม่ๆหรือมี new information ที่จะต้องการส่งไปยัง Client ก็จะส่งข้อมูลมาให้ (โดยที่ไม่ต้องร้องขอข้อมูลไปบ่อยๆ แต่ร้องขอไปครั้งเดียว เมื่อมีอะไรเกิดขึ้นก็จะบอก) จึงต่างจาก Ajax Ploling ธรรมดา

            Server-Sent Event

           

                เมื่อ Client ส่ง request ไปยัง server ผ่าน HTTP ทาง sever จะสร้าง Process ขึ้นมาพร้อมกับการลืบค้น State ใหม่ๆกับ Database และส่งกลับมายัง Client รับ State ใหม่ๆไป ซึ้งจะเห็นได้จะเป็น Real-time traffic จาก server ไปสู่ client แต่วิธีการเช่นนี้จะมี Default delay อยู่ที่ 3 วินาที

                Websockets

             

               จะเป็นการที่ Client กับ Server เปิดการเชื่มต่อกัน ถ้าพูดให้เห็นภาพ เหมือนกับการที่เรา(Server)เอาเชือกมาผูกกับคนอื่นๆ(Client) และเชื่อมีแรงตึง ถ้าเชือกหย่อนเมื่อไหร่แปลว่าคนๆนั้นไม่ได้เชื่อมต่อ และการส่งของให้กันก็ร้อยผ่านเชือกไป เมื่อของไปกระทบตัวคนๆนั้นก็จะรู้ตัวและหยิบของไป ประมานนั้น ซึ้งไอ้เจ้า Web Sockets นี้จะสร้าง Process 1 Process ขึ้นมาด้วยการใช้คำสั่งของ Server เพื่อเป็น Process ที่ใช้ในการรองรับการเชื่มต่อ รอรับข้อมูล ส่งข้อมูล (Process เดียวบริการทุก Client) มันจึงเป็นเหตุผลให้มีสรรพคุณที่ได้โม้เอาไว้ในย่อหน้าแรก และนี่ละคือพระเอกของเรา



        Conclusion

..Long-pollingServer-Sent EventsWebSockets
Browser supportSupported by the most of currently used browsersSupported by Chrome 9+, Firefox 6+, Opera 11+, Safari 5+The latest hybi-10 protocol supported by Chrome 14, Firefox 7 betas, hybi-07 supported by Firefox 6
Server-loadingTakes little of CPU resources, but creates idle processes per user expending server memoryWorks in many ways as long-polling, unless SSE doesn’t need to close connection every time when response is sentThe best possible solution. Server has the only process serving any requests. No loops, memory/CPU expense per client, but per client action
Client-loadingDepends on implementation, though it always an asynchronous process.Natively implemented in the browser, takes minimum resourcesNatively implemented in the browser, takes minimum resources
TimelinessNear real-time, though black out period between sending response and making a new request adds some delayDefault delay 3 sec., but can be changedTrue real-time
Complexity of implementationVery easyEven easierRequires an EventMachine server and a custom port being open

จากตารางเปรียบเทียบจะเห็นได้ว่า WebSockets นั้น ด้าน server-loading นั้นเป็น The best possible solution เลยทีเดียวและ only process serving any requests ส่วนทางด้าน Timeliness นั้นก็คงพูดได้แค่ว่า True real-time ปัจจุบันเลยละจร้าาาาา

ข้อมูลอ้างอิง 



งั้นเรามาเริ่มการ Impliment กันเลยดีกว่า ซึ้งเราจะทำด้วย ภาษา php ในฟั่งของ Server (ที่ใช้ Run เพื่อรองรับการเชื่อมต่อ ) และ javascript ใช้สำหรับ Client เพื่อเชื่อมต่อ ส่งข้อมูล และรับข้อมูล
(ผู้อ่านท่านไหนไม่คล่องเรื่อง javascript ajax jquery ก็ศึกษาเพิ่มเติมตามเว็บทั่วๆไป เพราะจะอธิบายทุกบรรทัดคงไม่ไหว )
.
.
.
.
.
.
.
แต่สำหรับตอนที่ 1 ขอพักเอาไว้เท่านี้ก่อนครับ บทความต่อไปในตอนที่ 2 จะเป็นการ impliment ครับ