socket.io client + socketio-netty server簡析

風谷來客-商清逸發表於2019-03-26

一、 背景:

現在實時web訊息推送一般會用到websocket,但是由於此技術並沒有推廣開來,所以各瀏覽器對其支援也不同,例如下圖顯示了各類瀏覽器的支援情況。

粉紅色區域表示不支援Websocket。

至於IE瀏覽器,以及部分陳舊的桌面瀏覽器,可以選擇Flashsocket作為替代品。

客戶端如何把Websocket和Flashsocket結合在一起使用,可借鑑開源專案:web-socket-js (客戶端的Websocket實現方案)

 

其思路和socket.io大致一致,僅僅提供對websocket的客戶端的簡單包裝,若是Android 上原生瀏覽器,沒有安裝Flash Lite情況下,就無能為力了。

因此,僅僅憑藉Websocket + Flashsocket,是不能夠完成跨瀏覽器、統一客戶端API的重任。

在這種情況下,socket.io就應運而生了。

 

 

二、socket.io的介紹以及優點

socket.io 支援以下通訊通道傳輸協議:

  • WebSocket
  • Adobe® Flash® Socket
  • AJAX long polling
  • AJAX multipart streaming
  • Forever Iframe
  • JSONP Polling

socket.io並不是簡單的封裝了websocket,可以說websocket只是socket.io的其中一部分。使用socket.io客戶端和伺服器端雙方約定適合當前瀏覽器的最佳通訊通道,然後正常通訊。並且還可以手動指定使用某種方式進行通訊。只要我們指定socket.io transport的引數,就可以做到心裡有數。

在socketio-netty伺服器端配置:

transports = websocket,flashsocket,htmlfile,xhr-polling,jsonp-polling

在客戶端,簡單定義地址:

var socket = io.connect('http://localhost:9000');

在不遠的將來,桌面版瀏覽器可能升級了最新版本的websocket草案,導致客戶端原生的websocket協議無法被識別時,可使用Flashsocket作為替代品。但總會有一種通訊協議墊底,可以保證正常的運轉。

socket.io即提供了node.js伺服器端又提供了客戶端的整體解決方案,而socketio-netty則是基於JAVA伺服器端,支援最新socket.io client最新版規範。對JAVA程式設計人員來講,可以不用學習node.js,從而多了一個選擇。

注:附1中簡單的介紹了集中不同通訊協議的優缺點。有興趣的可以檢視。

 

 

三、socket.io事件和方法簡單介紹

Socket.IO內建了一些預設事件,我們在設計事件的時候應該避開預設的事件名稱,並靈活運用這些預設事件。

伺服器端事件:

server.addEventListener("con", Object.class, new DataListener<Object>() ;

新增監聽的事件以及該時間中傳輸訊息的實體物件,服務端可以通過此方法來捕捉到client傳輸過來的Message請求;

server.addConnectListener(new ConnectListener();

新增連線監聽時間,當client連線時會首先觸發此事件,server可以在這裡進行一些初始化操作。

server.addDisconnectListener( new DisconnectListener();

新增斷開連線監聽時間,當client斷開連線時會首先觸發此事件,server可以在這裡進行一些斷線操作。(包括關閉瀏覽器,主動斷開,掉線等任何斷開連線的情況)。客戶端事件:

connect:連線成功disconnect:斷開連線message:同伺服器端message事件 在這裡要提下客戶端socket發起連線時的順序。當第一次連線時,事件觸發順序為:connecting->connect;當失去連線時,事件觸發順序為:disconnect->reconnecting(可能進行多次)->connecting->reconnect->connect。

注意:重新整理瀏覽器時,相當與客戶端首先disconnect然後重新建立一次connect。並且此時socket.io會預設重連之前斷開的連線。

 

客戶端常用方法:

socket.emit()和socket.on();這兩種都可以用來傳送訊息,只是在寫法上有稍微不同。

socket.emit('action');表示傳送了一個action命令,命令是字串的,也可以這麼寫: socket.on('action',function(){...});

socket.emit('action',data);表示傳送了一個action命令,還有data資料,也可以這麼寫: socket.on('action',function(data){...});

socket.emit(action,arg1,arg2); 表示傳送了一個action命令,還有兩個資料,也可以這麼寫: socket.on('action',function(arg1,arg2){...});

在emit方法中包含回撥函式,例如:

socket.emit('action',data, function(arg1,arg2){...} );那麼這裡面有一個回撥函式可以在另一端呼叫,也可以這麼寫:socket.on('action',function(data,fn){   fn('a','b') ;  });

上面的data資料可以有0個或者多個,相應的在另一端改變function中引數的個數即可,function中的引數個數和順序應該和傳送時一致

上面的fn表示另一個端傳遞過來的引數,是個函式,寫fn('a','b') ;會回撥函式執行。一次傳送不應該寫多個回撥,否則只有最後一個起效,回撥應作為最後一個引數。

 

四、簡單的程式碼應用(聊天室)

服務端:

package com.corundumstudio.socketio.demo;

import java.util.HashMap;

import java.util.Map;

import com.corundumstudio.socketio.listener.*;

import com.corundumstudio.socketio.*;

 

public class ChatLauncher {

private static Map<String,String> clients;

public static void main(String[] args) throws InterruptedException {

 

Configuration config = new Configuration();

//設定主機名稱UR了

config.setHostname("localhost");

//設定埠,此處必須設定,不設定啟動時會報錯

config.setPort(80);

 

final SocketIOServer server = new SocketIOServer(config);

server.addEventListener("con", ChatObject.class, new DataListener<ChatObject>() {

@Override

public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {

// broadcast messages to all clients

data.setMessage("使用者處於線上狀態");

System.out.println("uuid"+client.getSessionId());

server.getBroadcastOperations().sendEvent("chatevent", data);

//回撥當前clicent的函式

//client.sendEvent("chatevent", data);

}

});

server.addEventListener("discon", ChatObject.class, new DataListener<ChatObject>() {

@Override

public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {

// broadcast messages to all clients

data.setMessage("使用者離線了");

server.getBroadcastOperations().sendEvent("chatevent", data);

System.out.println("離線了!!!");

client.disconnect();

//回撥當前clicent的函式

//client.sendEvent("chatevent", data);

}

});

//當client連線時觸發此事件

server.addConnectListener(new ConnectListener(){

@Override

public void onConnect(SocketIOClient client) {

System.out.println(client.getSessionId()+"線上了!!!");

}

});

//當client離線時觸發此事件

server.addDisconnectListener( new DisconnectListener(){

 

@Override

public void onDisconnect(SocketIOClient client) {

System.out.println(client.getSessionId()+"離線了!!!");

}

});

//監聽埠上的chatevent事件

server.addEventListener("chatevent", ChatObject.class, new DataListener<ChatObject>() {

@Override

public void onData(SocketIOClient client, ChatObject data, AckRequest ackRequest) {

// broadcast messages to all clients

server.getBroadcastOperations().sendEvent("chatevent", data);

//回撥當前clicent的函式

//client.sendEvent("chatevent", data);

}

});

 

server.start();

 

Thread.sleep(Integer.MAX_VALUE);

 

server.stop();

}

}

 

客戶端:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<title>Demo Chat</title>

<link href="bootstrap.css" rel="stylesheet">

<style>

body {

padding:20px;

}

#console {

height: 400px;

overflow: auto;

}

.username-msg {color:orange;}

.connect-msg {color:green;}

.disconnect-msg {color:red;}

.send-msg {color:#888}

</style>

<script src="js/socket.io/socket.io.js"></script>

<script src="js/moment.min.js"></script>

<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>

<script>

 

var userName = 'user' + Math.floor((Math.random()*1000)+1);

//頁面開啟時,進行連線,url後為埠資訊,若不設定預設為連線80埠

var socket = io.connect('http://localhost:80');

//監聽連線事件

socket.on('connect', function() {

//var jsonObject = {userName: userName,

// message: '1'};

//socket.emit("con",jsonObject);

output('<span class="connect-msg">Client has connected to the server!</span>');

});

//監聽chatevent事件

socket.on('chatevent', function(data) {

output('<span class="username-msg">' + data.userName + ':</span> ' + data.message);

});

//監聽離線事件

socket.on('disconnect', function() {

output('<span class="disconnect-msg">The client has disconnected!</span>');

});

 

function sendDisconnect() {

socket.disconnect();

// var jsonObject = {userName: userName,

// message: '2'};

//socket.emit("discon",jsonObject);

}

function sendReconnect(){

socket.connect();

}

 

function sendMessage() {

var message = $('#msg').val();

$('#msg').val('');

 

var jsonObject = {userName: userName,

message: message};

//1、通過send方法傳送資料

//chatSocket.json.send(jsonObject);

//2、原生emit方法

socket.emit('chatevent', jsonObject);

}

 

function output(message) {

var currentTime = "<span class='time'>" + moment().format('HH:mm:ss.SSS') + "</span>";

var element = $("<div>" + currentTime + " " + message + "</div>");

$('#console').prepend(element);

}

//回車事件

$(document).keydown(function(e){

if(e.keyCode == 13) {

$('#send').click();

}

});

</script>

</head>

 

<body>

<h1>Netty-socketio Demo Chat</h1>

<br/>

<div id="console" class="well">

</div>

<form class="well form-inline" οnsubmit="return false;">

<input id="msg" class="input-xlarge" type="text" placeholder="Type something..."/>

<button type="button" onClick="sendMessage()" class="btn" id="send">Send</button>

<button type="button" onClick="sendDisconnect()" class="btn">Disconnect</button>

<button type="button" onClick="sendReconnect()" class="btn">REconnect</button>

</form>

</body>

</html>

 

執行服務端.java檔案後,開啟客戶端頁面即可進行聊天。如下圖:

圖 1

 

 

圖 2

 

注意:完整程式碼在方的參考區域,有興趣的可以下載下來一起學習。

 

五、學習心得

在html5之前,因為http協議是無狀態的,要實現 瀏覽器與伺服器的實時通訊,如果不使用 flash、applet 等瀏覽器外掛的話,就需要定期輪詢伺服器來獲取資訊。這造成了一定的延遲和大量的網路通訊。隨著HTML5 的出現,這一情況有望徹底改觀,這與需要實現與伺服器實時通訊的應用來說,是一種極大的進步,而且隨和使用者對網路實時通訊的要求越來越高,學習sockey.io這門技術也是很有發揮空間的。

 

附1

不斷地輪詢(俗稱“拉”,polling)是獲取實時訊息的一個手段:Ajax 隔一段時間(通常使用 JavaScript 的 setTimeout 函式)就去伺服器查詢是否有改變,從而進行增量式的更新。但是間隔多長時間去查詢成了問題,因為效能和即時性造成了嚴重的反比關係。間隔太短,連續不斷的請求會沖垮伺服器,間隔太長,務器上的新資料就需要越多的時間才能到達客戶機。

    • 優點:服務端邏輯簡單;
    • 缺點:其中大多數請求可能是無效請求,在大量使用者輪詢很頻繁的情況下對伺服器的壓力很大;
    • 應用:併發使用者量少,而且要求訊息的實時性不高,一般很少採用;
  • 長輪詢技術(long-polling):客戶端向伺服器傳送Ajax請求,伺服器接到請求後hold住連線,直到有新訊息或超時(設定)才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求。
    • 優點:實時性高,無訊息的情況下不會進行頻繁的請求;
    • 缺點:伺服器維持著連線期間會消耗資源;
  • 基於Iframe及htmlfile的流(streaming)方式:iframe流方式是在頁面中插入一個隱藏的iframe,利用其src屬性在伺服器和客戶端之間建立一條長連結,伺服器向iframe傳輸資料(通常是HTML,內有負責插入資訊的javascript),來實時更新頁面。
    • 優點:訊息能夠實時到達;
    • 缺點:伺服器維持著長連線期會消耗資源;
  • 外掛提供socket方式:比如利用Flash XMLSocket,Java Applet套介面,Activex包裝的socket。
    • 優點:原生socket的支援,和PC端和移動端的實現方式相似;
    • 缺點:瀏覽器端需要裝相應的外掛;
  • WebSocket:是HTML5開始提供的一種瀏覽器與伺服器間進行全雙工通訊的網路技術。
    • 優點:更好的節省伺服器資源和頻寬並達到實時通訊;
    • 缺點:目前還未普及,瀏覽器支援不好;

綜上,考慮到瀏覽器相容性和效能問題,採用長輪詢(long-polling)是一種比較好的方式。

 

參考程式碼:

https://github.com/mrniko/netty-socketio

https://github.com/mrniko/netty-socketio-demo

 

參考文章:

http://tech.qq.com/a/20120521/000296.htm

http://www.cnblogs.com/luxiaoxun/p/4279997.html

http://blog.csdn.net/mengxianhua/article/details/44778733

http://blog.csdn.net/kelong_xhu/article/details/50846483 (socketio分散式)

 

 

 

 

相關文章