Netty系列8-服務器推送

一般客戶端和服務端交互是由客戶端發起一個請求,服務端回答響應。但有時候服務端需要主動的推送數據,比如視頻、彈幕、新聞實時刷新等,這時候就用到了服務器推送技術。

1.Ajax短輪詢

Ajax短輪詢就是前端通過ajax不斷向服務端發送請求,這種方式最簡單但是性能最低,尤其在服務端未使用netty等高性能框架下。
客戶端代碼樣例:
function showTime(){
…//發送請求

setInterval(showTime, 1000);

2.Ajax長輪詢

長輪詢是短輪詢的變種,服務器使用我在之前的文章裏面介紹的DeferredResult,服務器實現異步處理請求,增大服務器響應併發。有興趣的可以看下之前的文章。

3.SSE

SSE(Server-Sent Events)全稱是服務器發送事件。我們知道HTTP協議無法做到服務器主動推送信息,但是SSE協議中由服務器向客戶端聲明接下來要發送的是流信息,即數據會不斷地發送過來。這時客戶端不會關閉連接,會一直等着服務器發過來的新的數據流。
SSE基於HTTP協議,目前瀏覽器基本都支持(除了IE/Edge)。
SSE協議規範
服務器向瀏覽器發送的數據,必須是UTF-8編碼並且具有如下的HTTP頭信息:

  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive

每次響應的信息由若干個message組成,每個message之間用\n\n分隔。每個message內部由若干行組成,每一行都是如下格式:
[field]: value\n。
field有四種類型的值:

  • data:數據內容
  • event:信息類型
  • id:數據標識符,瀏覽器用lastEventId屬性讀取這個值。一旦連接斷線,瀏覽器會發送一個 HTTP 頭,裏面包含一個Last-Event-ID頭信息,將這個值發送回來,用來幫助服務器端重建連接。
  • retry:最大間隔時間

此外,有冒號開頭的行表示註釋。通常服務器每隔一段時間就會向瀏覽器發送一個註釋,保持連接不中斷。
一個JSON數據的示例:

data: {\n
data: "context": "aaa",\n
data: }\n\n

SSE相對WebSocket優勢:

  • SSE基於HTTP協議,現有的服務器軟件都支持。WebSocket是一個獨立協議。
  • SSE使用簡單,WebSocket協議相對複雜。
  • SSE默認支持斷線重連,WebSocket需要自己實現。
  • SSE支持自定義發送的消息類型。

缺點:
WebSocket更強大和靈活。因爲它是全雙工通道,可以雙向通信;SSE是單向通道,只能服務器向瀏覽器發送,因爲流信息本質上就是下載。
前端核心代碼:


    if(!!window.EventSource){//判斷瀏覽器支持度
        //拿到sse的對象
        var source = new EventSource('needPrice');
        //接收到服務器的消息
        source.onmessage=function (e) {
            var dataObj=e.data;
           ....//業務處理
        };

        source.onopen=function (e) {
        };

        source.onerror=function () {
        };

    }else{
        $("#hint").html("您的瀏覽器不支持SSE!");
    }

服務端核心代碼:

 public void push(HttpServletResponse response){
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("utf-8");
        Random r = new Random();
        int sendCount = 0;
        try {
            PrintWriter pw = response.getWriter();
            while(true){
                if(pw.checkError()){
                    return;
                }
                Thread.sleep(1000);
                //字符串拼接
                StringBuilder sb = new StringBuilder("");
                sb//.append("retry:2000\n")
                        .append("data:")
                        .append((r.nextInt(1000)+50)+",")
                        .append((r.nextInt(800)+100)+",")
                        .append((r.nextInt(2000)+150)+",")
                        .append((r.nextInt(1500)+100)+",")
                        .append("\n\n");

                pw.write(sb.toString());
                pw.flush();
                sendCount++;
                if(sendCount>=100){
                    return;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4.WebSocket

WebSocket可以讓客戶端和服務器進行雙向通信。WebSocket只需要建立一次連接,就可以一直保持連接狀態。相比於輪詢方式的不停建立連接顯然效率要大大提高。使用WebSockets需要客戶端和服務器都支持WebSockets協議,但不是所有瀏覽器都支持。

4.1 通信規範:

客戶端的請求

  • Connection必須設置Upgrade,表示連接升級。
  • Upgrade字段必須設置Websocket,表示升級到Websocket協議。
  • Sec-WebSocket-Key是隨機的字符串,服務器端會用這些數據來構造出一個SHA-1的信息摘要。 Sec-WebSocket-Key加上一個特殊字符串計算SHA-1摘要,再進行BASE-64編碼,將結果做爲Sec-WebSocket-Accept 頭的值返回給客戶端。如此操作,可以儘量避免普通 HTTP 請求被誤認爲 Websocket 協議。
  • Sec-WebSocket-Version表示支持的Websocket版本。

服務器端
Upgrade:websocket
Connection:Upgrade
告訴客戶端即將升級的是Websocket協議。Sec-WebSocket-Accept表示是經過服務器確認,並且加密過後的Sec-WebSocket-Key。
Sec-WebSocket-Protocol表示最終使用的協議。

4.2 代碼示例:

(1)STOMP
WebSocket是個規範,在實際的實現中有HTML5規範中的WebSocket API、WebSocket的子協議STOMP。
STOMP(Simple Text Oriented Messaging Protocol):

  • 簡單(流)文本定向消息協議
  • STOMP協議專爲消息中間件設計,屬於消息隊列的一種協議, 和AMQP, JMS平級。STOMP協議很多MQ都已支持, 比如RabbitMq, ActiveMq。
  • 生產者(發送消息)、消息代理、消費者(訂閱然後收到消息)
  • STOMP是基於幀的協議

客戶端代碼:

var stompClient = null;

$(function(){
    //連接SockJS的endpoint名稱爲"endpointMark"
    var socket = new SockJS('/endpointMark');
    stompClient = Stomp.over(socket);//使用STMOP子協議的WebSocket客戶端
    stompClient.connect({},function(frame){//連接WebSocket服務端

        console.log('Connected:' + frame);
        //廣播接收信息
        stompTopic();

    });
})

//關閉瀏覽器時關閉連接
window.onunload = function() {
   if(stompClient != null) {
        stompClient.disconnect();
    }
}

//一對多,發起訂閱
function stompTopic(){
    //通過stompClient.subscribe訂閱目標(destination)發送的消息(廣播接收信息)
    stompClient.subscribe('/mass/getResponse',function(response){
        var message=JSON.parse(response.body);
		...//業務處理
    });
}

//羣發消息
function sendMassMessage(){
    var postValue={};
    var chatValue=$("#sendChatValue");
    var userName=$("#selectName").val();
    postValue.name=userName;
    postValue.chatValue=chatValue.val();
    stompClient.send("/massRequest",{},JSON.stringify(postValue));
    chatValue.val("");
}

//單獨發消息
function sendAloneMessage(){
    var postValue={};
    var chatValue=$("#sendChatValue2");
    var userName=$("#selectName").val();
    var sendToId=$("#selectName2").val();
    var response = $("#alone_div");
    postValue.name=userName;//發送者姓名
    postValue.chatValue=chatValue.val();//聊天內容
    postValue.userId=sendToId;//發送給誰

    stompClient.send("/aloneRequest",{},JSON.stringify(postValue));
    response.append("<div class='user-group'>" +
        "          <div class='user-msg'>" +
        "                <span class='user-reply'>"+chatValue.val()+"</span>" +
        "                <i class='triangle-user'></i>" +
        "          </div>" +userName+
        "     </div>");
    chatValue.val("");
}

//一對一,發起訂閱
function stompQueue(){

    var userId=$("#selectName").val();
    alert("監聽:"+userId)
    //通過stompClient.subscribe訂閱目標(destination)發送的消息(隊列接收信息)
    stompClient.subscribe('/queue/' + userId + '/alone',
        function(response){
        var message=JSON.parse(response.body);
       ...//業務處理
    });
}


前端頁面核心代碼:

<body>
<div>
    <div style="float:left;width:47%">
        <p>請選擇你是誰:
        <select id="selectName" onchange="stompQueue();">
            <option value="1">請選擇</option>
            <option value="Mark">Mark</option>
            <option value="James">James</option>
            <option value="Lison">Lison</option>
            <option value="Peter">Peter</option>
            <option value="King">King</option>
        </select>
        </p>
        <div class="chatWindow">
            <p style="color:darkgrey">羣聊:</p>
            <section id="chatRecord1" class="chatRecord">
                <div id="mass_div" class="mobile-page">

                </div>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>
                <input type="button" name="sendMessage" id="sendMassMessage" class="sendMessage" onclick="sendMassMessage()" value="發送">
            </section>
        </div>
    </div>


    <div style="float:right; width:47%">
        <p>請選擇你要發給誰:
        <select id="selectName2">
            <option value="1">請選擇</option>
            <option value="A">A</option>
            <option value="B">B</option>
        </select>
        </p>
        <div class="chatWindow">

            <p style="color:darkgrey">單聊:</p>
            <section id="chatRecord2" class="chatRecord">
                <div id="alone_div"  class="mobile-page">

                </div>
            </section>
            <section class="sendWindow">
                <textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
                <input type="button" name="sendMessage" id="sendAloneMessage" class="sendMessage" onclick="sendAloneMessage()" value="發送">
            </section>
        </div>
    </div>
</div>

<!-- 獨立JS -->
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
<script th:src="@{wechat_room.js}"></script>
</body>

服務端核心代碼:

@Configuration
/*開啓使用Stomp協議來傳輸基於消息broker的消息
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /*註冊STOMP協議的節點(endpoint),並映射指定的url,
        * 添加一個訪問端點“/endpointMark”,客戶端打開雙通道時需要的url,
        * 允許所有的域名跨域訪問,指定使用SockJS協議。*/
        registry.addEndpoint("/endpointMark")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        /*配置一個消息代理
        * mass 負責羣聊
        * queue 單聊*/
        registry.enableSimpleBroker(
                "/mass","/queue");

        //一對一的用戶,請求發到/queue
        registry.setUserDestinationPrefix("/queue");
    }

}

@Controller
public class StompController {

    @Autowired
    private SimpMessagingTemplate template;/*Spring實現的一個發送模板類*/

    /*消息羣發,接受發送至自massRequest的請求*/
    @MessageMapping("/massRequest")
    @SendTo("/mass/getResponse")
    //SendTo 發送至 Broker 下的指定訂閱路徑mass ,
    // Broker再根據getResponse發送消息到訂閱了/mass/getResponse的用戶處
    public ChatRoomResponse mass(ChatRoomRequest chatRoomRequest){
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        return response;
    }

    /*單獨聊天,接受發送至自aloneRequest的請求*/
    @MessageMapping("/aloneRequest")
    //@SendToUser
    public ChatRoomResponse alone(ChatRoomRequest chatRoomRequest){
        ChatRoomResponse response=new ChatRoomResponse();
        response.setName(chatRoomRequest.getName());
        response.setChatValue(chatRoomRequest.getChatValue());
        //會發送到訂閱了 /user/{用戶的id}/alone 的用戶處
        this.template.convertAndSendToUser(chatRoomRequest.getUserId()
                +"","/alone",response);
        return response;
    }
}

(2)websocket
前端核心代碼:

 var socket;
    if (typeof (WebSocket) == "undefined") {
        console.log("遺憾:您的瀏覽器不支持WebSocket");
    } else {
        console.log("恭喜:您的瀏覽器支持WebSocket");

        //實現化WebSocket對象
        //指定要連接的服務器地址與端口建立連接
        //ws對應http、wss對應https。
        socket = new WebSocket("ws://localhost:8080/ws/asset");
        //連接打開事件
        socket.onopen = function() {
            socket.send("消息內容");
        };
        //收到消息事件
        socket.onmessage = function(msg) {
         
        };
        //連接關閉事件
        socket.onclose = function() {
        };
        //發生了錯誤事件
        socket.onerror = function() {
        }

        //窗口關閉時,關閉連接
        window.unload=function() {
            socket.close();
        };
    }

後端核心代碼:

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

@ServerEndpoint(value = "/ws/asset")
@Component
public class WebSocketServer {

    private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    // concurrent包的線程安全Set,用來存放每個客戶端對應的Session對象。
    private static CopyOnWriteArraySet<Session> SessionSet
            = new CopyOnWriteArraySet<Session>();


    /**
     * 連接建立成功調用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        SessionSet.add(session);
        int cnt = OnlineCount.incrementAndGet(); // 在線數加1
        SendMessage(session, "連接成功");
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose(Session session) {
        SessionSet.remove(session);
        int cnt = OnlineCount.decrementAndGet();
    }

    /**
     * 收到客戶端消息後調用的方法
     *
     * @param message
     *            客戶端發送過來的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        SendMessage(session, "收到消息,消息內容:"+message);

    }

    /**
     * 出現錯誤
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
    }

    /**
     * 發送消息,實踐表明,每次瀏覽器刷新,session會發生變化。
     * @param session
     * @param message
     */
    public static void SendMessage(Session session, String message) {
        try {
            session.getBasicRemote()
                    .sendText(String.format("%s (From Server,Session ID=%s)",
                            message,session.getId()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 羣發消息
     * @param message
     * @throws IOException
     */
    public static void BroadCastInfo(String message) throws IOException {
        for (Session session : SessionSet) {
            if(session.isOpen()){
                SendMessage(session, message);
            }
        }
    }

    /**
     * 指定Session發送消息
     * @param sessionId
     * @param message
     * @throws IOException
     */
    public static void SendMessage(String sessionId,String message)
            throws IOException {
        Session session = null;
        for (Session s : SessionSet) {
            if(s.getId().equals(sessionId)){
                session = s;
                break;
            }
        }
        if(session!=null){
            SendMessage(session, message);
        }
        else{
        }
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章