websocket協議

使用http協議的問題

場景:客戶端的展現隨着服務端維護的狀態的改變而實時改變。javascript

可能會採起的方式:html

1 輪詢:client按設置好的時間間隔主動訪問server,有可能server返回有用數據,有可能server無有用數據返回,但都是一個創建鏈接-request-response-關閉鏈接的過程。java

2 長輪詢:client訪問server,若server有數據返回,則返回,關閉鏈接,client繼續發請求;若server沒有數據返回,鏈接保持,等待直到server有數據返回,關閉鏈接,client繼續發請求。創建鏈接-request-(wait)-response-關閉鏈接。web

3 推:client和server之間維護長鏈接,當server有返回的時候主動推給client。編程

問題:bootstrap

http協議是一種無狀態的,基於請求響應模式的協議。瀏覽器

半雙工協議:能夠在客戶端和服務端2個方向上傳輸,可是不能同時傳輸。同一時刻,只能在一個方向上傳輸。服務器

響應數據不實時,空輪詢對資源的浪費。 HTTP消息冗長(輪詢中每次http請求攜帶了大量無用的頭信息)。websocket

HTTP1.0 每一個請求會打開一個新鏈接,通常打開和關閉鏈接花費的時間遠大於數據傳輸的時間,對於HTTPS更是。session

HTTP1.1 服務器不會在發送響應後當即關閉鏈接,能夠在同一個socket上等待客戶端的新請求

Websocket 協議

WebSocket是一種規範,是Html5規範的一部分。WebSocket通訊協議於2011年被IETF定爲標準RFC 6455,並被RFC7936所補充規範。WebSocket API也被W3C定爲標準。

單個 TCP 鏈接上進行全雙工通信的協議。

瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。websocket是全雙工,沒有嚴格的clientserver概念。

opening handshake

request:

GET /chat HTTP/1.1 
Host: server.example.com
Upgrade: websocket   //請求升級到WebSocket 協議
Connection: Upgrade //通道類型,keep-alive:通道長連,close:請求完畢後通道斷開,Upgrade:升級協議
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==  //客戶端隨機生成的Key,校驗服務器合法性,生成方式:隨機16字節再被base64編碼
Sec-WebSocket-Protocol: chat, superchat //子協議,特定於具體應用的消息格式或編排
Sec-WebSocket-Version: 13 
Origin: http://example.com

response:

HTTP/1.1 101 Switching Protocols  //非101仍然是http
Upgrade: websocket    //服務端協議已切換到WebSocket
Connection: Upgrade 
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
//indicates whether  the server is willing to accept the connection.
//用於校驗WebSocket服務端是否合法,生成方式:客戶端請求參數中的 Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11(GUID),
//SHA-1 hash  再進行base64
//GUID:which is unlikely to be used by network endpoints that do not understand the WebSocket protocol.
Sec-WebSocket-Protocol: chat
close handshake

image.png

fin:0,後續還有幀;1 本條消息的最後一幀

rsv1,rsv2,rsv3:不實用擴展協議,爲0

Opcode:

image.png

websocket協議:https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 https://tools.ietf.org/html/rfc6455#section-5

生命週期

image.png

4個生命週期事件:

打開事件 OnOpen;消息事件 OnMessage;錯誤事件 OnError;關閉事件 OnClose。

websocket簡單實現

Java Websocket示例
//註解式
package echo;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value =  "/echo")
public class EchoServer {
    @OnMessage
    public String echo(String message){
        return "I get this ("+message +") so I am sending it back";
    }
}
//編程式
package echo;

import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;
import java.io.IOException;

/**
 * 編程式websocket
 */
public class ProgrammaticEchoServer extends Endpoint {

    @Override
    public void onOpen(Session session, EndpointConfig endpointConfig) {
        final Session mySession = session;
        mySession.addMessageHandler(
                new MessageHandler.Whole<String>() {

                    @Override
                    public void onMessage(String s) {
                        try {
                            mySession.getBasicRemote().sendText("I got this by prommmatic method ("+
                            s+") so I am sending it back ");
                        }catch (IOException io){
                            io.printStackTrace();
                        }
                    }
                });
    }
}
package echo;

import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;
import java.util.HashSet;
import java.util.Set;

public class ProgrammaticEchoServerAppConfig implements ServerApplicationConfig {
    @Override
    public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {
        Set configs = new HashSet<ServerEndpointConfig>();
        ServerEndpointConfig curSec = ServerEndpointConfig.Builder.create(ProgrammaticEchoServer.class,"/programmaticecho").build();
        configs.add(curSec);
        return configs;
    }

    /**
     * 獲取全部經過註解註冊的endpoint
     * @param set
     * @return
     */
    @Override
    public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> set) {
        return null;
    }
}

//客戶端
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title> new document </title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Web Socket Echo CLient</title>
    <script language="javascript" type="text/javascript">
        var echo_websocket;
        function  init() {
            output = document.getElementById("output");
        }

        function  send_echo() {
            var wsUri = "ws://localhost:8080/programmaticecho";//註解式爲ws://localhost:8080/echo
            writeToScreen("Connecting to "+wsUri);
            echo_websocket = new WebSocket(wsUri);
            echo_websocket.onopen = function (evt) {
                writeToScreen("Connected!");
                doSend(textID.value);
            };

            echo_websocket.onmessage = function (evt) {
                writeToScreen("Received message: "+evt.data);
                echo_websocket.close();
            };

            echo_websocket.onerror = function (evt) {
                writeToScreen('<span style= "color"red;">ERROR:<span> '+evt.data);
                echo_websocket.close();
            };

        }

        function doSend(message) {
            echo_websocket.send(message);
            writeToScreen("Sent message: "+message);

        }


        function writeToScreen(message) {
            var pre = document.createElement("p");
            pre.style.wordWrap = "break-word";
            pre.innerHTML = message;
            output.appendChild(pre);

        }

        window.addEventListener("load",init,false);


    </script>
</head>

<body>

<h1>Echo Server</h1>

    <div style="text-align: left;">
        <form action=""\>
            <input onclick="send_echo()" value="Press to send" type = "button"/>
            <input id="textID" name = "message" value="Hello Web Sockets" type = "text"/>
        </form>
    </div>
<br/>
<div id="output"></div>

</body>
</html>
netty對websocket的支持
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoop;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import java.net.InetSocketAddress;
public class WebsocketNettyServer {
    public static void main(String[] args) throws Exception{
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup wokerGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>(){
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();

                            //websocket協議自己是基於http協議的,因此這邊也要使用http解編碼器
                            pipeline.addLast(new HttpServerCodec());
                            //以塊的方式來寫的處理器
                            pipeline.addLast(new ChunkedWriteHandler());
                            //netty是基於分段請求的,HttpObjectAggregator的做用是將請求分段再聚合,參數是聚合字節的最大長度
                            pipeline.addLast(new HttpObjectAggregator(8192));

                            //ws://server:port/context_path
                            //參數指的是contex_path
                            pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
                            //websocket定義了傳遞數據的6中frame類型
                            pipeline.addLast(new TextWebSocketFrameHandler());
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(new InetSocketAddress(8899)).sync();
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }
    }
}



import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
        System.out.println("收到消息"+textWebSocketFrame.text());
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("有新的鏈接加入");
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("鏈接關閉");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("啊哦,出錯了");
        ctx.fireExceptionCaught(cause);
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket客戶端</title>
</head>
<body>
<script type="text/javascript">
    var socket;
    //若是瀏覽器支持WebSocket
    if(window.WebSocket){
        //參數就是與服務器鏈接的地址
        socket = new WebSocket("ws://localhost:8899/ws");
        //客戶端收到服務器消息的時候就會執行這個回調方法
        socket.onmessage = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n"+event.data;
        }
        //鏈接創建的回調函數
        socket.onopen = function(event){
            var ta = document.getElementById("responseText");
            ta.value = "鏈接開啓";
        }
        //鏈接斷掉的回調函數
        socket.onclose = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value +"\n"+"鏈接關閉";
        }
    }else{
        alert("瀏覽器不支持WebSocket!");
    }
    //發送數據
    function send(message){
        if(!window.WebSocket){
            return;
        }
        //當websocket狀態打開
        if(socket.readyState == WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("鏈接沒有開啓");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name = "message" style="width: 400px;height: 200px"></textarea>
    <input type ="button" value="發送數據" onclick="send(this.form.message.value);">
    <h3>服務器輸出:</h3>
    <textarea id ="responseText" style="width: 400px;height: 300px;"></textarea>
    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空數據">
</form>
</body>
</html>

image.png

netty-socketio實現,即基於netty的java版socket io Server
相關文章
相關標籤/搜索