WebSocket

Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來講。javascript

簡單的舉個例子吧,用目前應用比較普遍的PHP生命週期來解釋。css

HTTP的生命週期經過 Request 來界定,也就是一個 Request 一個 Response,那麼在 HTTP1.0 中,此次HTTP請求就結束了。html

在HTTP1.1中進行了改進,使得有一個keep-alive,也就是說,在一個HTTP鏈接中,能夠發送多個Request,接收多個Response。可是請記住 Request = Response , 在HTTP中永遠是這樣,也就是說一個request只能有一個response。並且這個response也是被動的,不能主動發起java

首先Websocket是基於HTTP協議的,或者說借用了HTTP的協議來完成一部分握手。jquery

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

告訴服務器我發送的是WebSocket協議web

而後服務器會返回下列東西,表示已經接受到請求, 成功創建Websocket啦!瀏覽器

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

須要理解一點,在使用WebSocket協議前,須要先使用HTTP協議用於構建最初的握手。這依賴於一個機制——創建HTTP,請求協議升級(或叫協議轉換)。當服務器贊成後,它會響應HTTP狀態碼101,表示贊成切換協議。假設經過TCP套接字成功握手,HTTP協議升級請求經過,那麼客戶端和服務器端均可以彼此互發消息。緩存

其餘特色包括:服務器

(1)創建在 TCP 協議之上,服務器端的實現比較容易。websocket

(2)與 HTTP 協議有着良好的兼容性。默認端口也是80和443,而且握手階段採用 HTTP 協議,所以握手時不容易屏蔽,能經過各類 HTTP 代理服務器。

(3)數據格式比較輕量,性能開銷小,通訊高效。

(4)能夠發送文本,也能夠發送二進制數據。

(5)沒有同源限制,客戶端能夠與任意服務器通訊。

(6)協議標識符是ws(若是加密,則爲wss),服務器網址就是 URL。

廣播式

服務端有消息時,會將消息發送給全部鏈接了當前endpoint的瀏覽器

@Configuration
@EnableWebSocketMessageBroker 
// 開啓使用STOMP協議來傳輸基於(MessageBroker)的消息  controller可使用@MessageMapping(相似於RequestMapping)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override    //註冊STOMP協議的 節點映射制定的url,使用SockJS協議
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }
    
    @Override//配置消息代理
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
    }


}

控制器

@Controller
public class WSController {

    @MessageMapping("/welcome")
    @SendTo("/topic/getResponse") // 當服務段有消息時會對訂閱了@sendTo的瀏覽器發送消息
    public Response say(Message msg) throws InterruptedException {
        Thread.sleep(3000);
        return new Response("Welcome" + msg.getName()+ "!");
    }
}


@SendTo 等同於

@Autowired
private SimpMessagingTemplate messagingTemplate;

messagingTemplate.convertAndSend("",對象)

 

客戶端

<!DOCTYPE html>
<html xmlns:th="http//www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>Insertsssssssssssssss title here</title>
</head>
<body onload="disconnect()">
<noscript><h2 style="color:#ff0000">貌似不支持websocket</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect()">鏈接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect()">斷開鏈接</button>
    </div>
    <div id="conversationDiv">
        <label>輸入你的名s字ss</label>
        <input type="text" id="name"/>
        <button id="sendName" onclick="sendName()">發送</button>
        <p id="response"></p>
    </div>
</div>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="text/javascript">
    var stompClient = null;
    
    function setConnected(connected) {
        $("#connect").disabled = connected;
        $("#disconnect").disabled = connected;
        $("#conversationDiv")[0].style.visibility = connected?'visible':'hidden';
        $('#response').html();
    }
    
    function connect() {
        var socket = new SockJS('/endpoint');  //鏈接SockJS的endpoint
        stompClient = Stomp.over(socket);  //使用STOMP子協議的WebSocket客戶
        stompClient.connect({}, function(frame) {  //鏈接WebSocket服務端
            setConnected(true);
            console.log('Connected:'+ frame);
            stompClient.subscribe('/topic/getResponse', function(response) {
                showResponse(JSON.parse(response.body).responseMessage);
            });
        });
    }
    
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }
    
    function sendName() {
        var name = $('#name').val();
        stompClient.send("/welcome",{},JSON.stringify({'name':name}));
    }
    
    function showResponse(message) {
        var response = $("#response");
        response.html(message);
    }
</script>
</body>
</html>

預期的效果是:當一個瀏覽器發送一個消息到服務端時,其餘註冊的瀏覽器也能接受到從服務端發送來的這個消息

可是廣播不能解決由誰發送由誰接受的問題,下面就來解決這個問題

點對點式

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/","/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .defaultSuccessUrl("/chat")
            .permitAll()
            .and()
            .logout()
            .permitAll();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("aaa").password("123").roles("USER")
            .and()
            .withUser("bbb").password("123").roles("USER");
    }
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/static/**");
    }
}
Security配置
同上 在WebSocketConfig 註冊endpoint  和增長消息代理
@Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @MessageMapping("/chat")
    public void handleChat(Principal principal, Message msg) {
        if (principal.getName().equals("aaa")) {

            messagingTemplate.convertAndSendToUser("bbb",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg.getName());
        } else {
            messagingTemplate.convertAndSendToUser("aaa",
                    "/queue/notifications", principal.getName() + "-send:"
                            + msg.getName());
        }
    }
<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
    <title>Home</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<p>
    聊天室
</p>

<form id="wiselyForm">
    <textarea rows="4" cols="60" name="text"></textarea>
    <input type="submit"/>
</form>

<script th:inline="javascript">
    $('#wiselyForm').submit(function(e){
        e.preventDefault();
        var text = $('#wiselyForm').find('textarea[name="text"]').val();
        sendSpittle(text);
    });
    //連接endpoint名稱爲 "/endpointChat" 的endpoint。
    var sock = new SockJS("/endpointChat");
    var stomp = Stomp.over(sock);
    stomp.connect('guest', 'guest', function(frame) {

        /**
 訂閱了/user/queue/notifications 發送的消息,這裏雨在控制器的 convertAndSendToUser 定義的地址保持一致,

         *  這裏多用了一個/user,而且這個user 是必須的,使用user 纔會發送消息到指定的用戶。

         *  */
        stomp.subscribe("/user/queue/notifications", handleNotification);
    });

    function handleNotification(message) {
        $('#output').append("<b>Received: " + message.body + "</b><br/>")
    }

    function sendSpittle(text) {
        stomp.send("/chat", {}, JSON.stringify({ 'name': text }));//3
    }
    $('#stop').click(function() {sock.close()});
</script>

<div id="output"></div>
</body>
</html>

 對於羣聊, 能夠加上監聽器,監聽STOMP註冊的用戶

 *STOMP 監聽類   用於session的註冊
 */
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent>{

    @Autowired
    SocketSessionRegistry webAgentSessionRegistry;
    
    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
        
        String agentId = sha.getNativeHeader("login").get(0);
        String sessionId = sha.getSessionId();
        webAgentSessionRegistry.registerSessionId(agentId, sessionId);
    }

}
遍歷發送
@GetMapping(value = "/msg/sendcommuser") public @ResponseBody OutMessage SendToCommUserMessage(HttpServletRequest request){ List<String> keys=webAgentSessionRegistry.getAllSessionIds().entrySet() .stream().map(Map.Entry::getKey) .collect(Collectors.toList()); Date date=new Date(); keys.forEach(x->{ String sessionId=webAgentSessionRegistry.getSessionIds(x).stream().findFirst().get().toString(); template.convertAndSendToUser(sessionId,"/topic/greetings",new OutMessage("commmsg:allsend, " + "send comm" +date.getTime()+ "!"),createHeaders(sessionId)); }); return new OutMessage("sendcommuser, " + new Date() + "!"); }

 

 

STOMP協議分析

STOMP協議與HTTP協議很類似,它基於TCP協議,使用瞭如下命令:

CONNECT
SEND
SUBSCRIBE
UNSUBSCRIBE
BEGIN
COMMIT
ABORT
ACK
NACK
DISCONNECT

STOMP的客戶端和服務器之間的通訊是經過「幀」(Frame)實現的,每一個幀由多「行」(Line)組成。
第一行包含了命令,而後緊跟鍵值對形式的Header內容。
第二行必須是空行。
第三行開始就是Body內容,末尾都以空字符結尾。
STOMP的客戶端和服務器之間的通訊是經過MESSAGE幀、RECEIPT幀或ERROR幀實現的,它們的格式類似。

應用場景

在Web應用中,客戶端和服務器端須要以較高頻率和較低延遲來交換事件時,適合用WebSocket。所以WebSocket適合財經、遊戲、協做等應用場景。

而只有在低延遲和高頻消息通訊的場景下,選用WebSocket協議纔是很是適合的。即便是這樣的應用場景,仍然存在是選擇WebSocket通訊呢?又或者是選擇REST HTTP通訊呢?
答案是會根據應用程序的需求而定。可是,也可能同時使用這兩種技術,把須要頻繁交換的數據放到WebSocket中實現,而把REST API做爲過程性的業務的實現技術。另外,當REST API的調用中須要把某個信息廣播給多個客戶端是,也能夠經過WebSocket鏈接來實現。

 

 

對於一些其餘的訪問方式能夠看下

長輪詢適合瀏覽器的Chat聊天、股票行情顯示、股票狀態更新、體育直播的結果顯示等。固然,不是全部的例子都是對延遲很敏感的,但它們的需求都比較類似。
在標準的HTTP請求響應語義中,瀏覽器發起請求,服務器發送一個響應,這意味着在瀏覽器發起新請求前,服務器不能發送新信息給客戶端瀏覽器。有幾種解決方法,包括:傳統的輪詢、長輪詢、HTTP流、WebSocket協議等。

一、傳統的輪詢

瀏覽器保持發送請求,檢查服務器是否有新信息返回,服務器對於每次請求均應當即響應。這適合的場景下,輪詢能夠設定爲合理的時間間隔。例如,郵件客戶端能夠每隔10分鐘檢查服務器是否有新郵件。傳統的輪詢的優勢是簡單且工做可靠。然而,其缺點是效率不高。若是須要儘快得到新信息,那麼輪詢頻率就必須很是高。

二、長輪詢

瀏覽器不斷髮送請求,可是服務器不予以響應,一直到服務器有了新信息才響應客戶端。從客戶端的角度看它和傳統的輪詢相同。但從服務器端的角度來看它與傳統的輪詢相比,減小了服務器端的開銷。
那麼響應應該保持Open多久呢?瀏覽器一般對此時間的設置是5分鐘,而網絡中介(好比代理)對此時間設置的更短。所以,即便服務器端沒有新消息,客戶端也應該按期發起一個新長輪詢請求。IEFT文件建議這個時間間隔在30秒~120秒之間,而實際使用取決於你的網絡狀況。
IEFT文件: http://tools.ietf.org/html/rfc6202

長輪詢能夠極大地減小須要低延遲的接收信息更新請求的數量,特別是新信息在無規律的時間間隔變得可用時。可是,若是信息更新的越頻繁,那麼整個方案就越像傳統的輪詢。

三、HTTP流

瀏覽器向服務器發出請求,服務器要發送信息時就會響應。可是它與長輪詢不一樣,服務器需保持響應是Open的,有更新時就會響應客戶端。該方法去除了輪詢的須要,並且偏離了典型的HTTP請求/響應的語義。例如,客戶端和服務器須要協商如何解釋響應流,這樣客戶端會知道哪個更新信息結束了,哪個更新信息開始了。可是,網絡中介能夠緩存響應流,阻撓此方法的意圖。這就是爲何長輪詢更爲經常使用。

四、WebSocket協議

瀏覽器發送一個HTTP請求到服務器,請求切換到WebSocket協議,服務器響應,確認升級協議到WebSocket。此後,瀏覽器和服務器能夠在TCP套接字上雙向發送數據幀。

WebSocket協議被設計用於取代須要輪詢,特別是適用於須要在服務器和瀏覽器之間頻繁交換數據的場景。在HTTP協議上完成初始握手,以確保WebSocket請求能夠穿透防火牆。

WebSockets雙向交換的數據有兩種類型,文本信息或二進制信息。這使得它與RESTful HTTP方法有顯著不一樣。事實上,還有一些其它協議,好比XMPP,AMQP,STOMP等,目前仍在普遍使用。

WebSocket協議已經被IETF組織進行了標準化,而WebSocket API規範也由W3C標準完成了制訂。在Java領域也制訂了JSR-356規範以支持WebSocket協議。像Jetty、Tomcat這樣的Servlet容器也實現了對WebSocket協議的支持。

五、長鏈接(Persistent Connection)

HTTP Persistent Connection,即HTTP長鏈接,也叫HTTP Keep-alive或HTTP Connection Reuse。其思想是使用單個的TCP鏈接來發送和接收多個HTTP請求/響應,而不是爲每一個請求/響應都創建一個新鏈接。新發布的HTTP /2協議就使用了這種思想,並進一步容許在單個鏈接上多路複用多個併發的請求/響應。
而早期的長鏈接技術只是要求在客戶端與服務器之間建立和保持穩定可靠的鏈接。早期因爲瀏覽器技術發展較緩慢,沒有爲這種機制的實現提供很好的支持。早期一般的作法是在頁面裏嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設爲對一個長鏈接的請求或是採用xhr請求,服務器端就能源源不斷地往客戶端輸入數據。

六、Pushlet

在這種技術中,服務器端利用了HTTP長鏈接的優勢,使得響應老是Open的,即服務器不會終止響應,有效地讓瀏覽器能夠在初始頁面加載後繼續加載其它內容。隨後服務器端能夠週期性的發送JavaScript代碼片斷來更新頁面的內容,從而達到推進能力。經過使用這種技術,客戶端不須要Java Applet或其它插件才能保持與服務器的鏈接Open;客戶端會對服務器推送的新事件自動通知。其缺點是服務器端缺乏對瀏覽器端的超時控制,若是瀏覽器發生超時,必須使用頁面刷新。
Pushlets的官方站點: http://www.pushlets.com/ 
Pushlet從2000年發展到2010年,逐漸淡出市場。

七、Comet

Comet是一個Web應用模型,它使用一個HTTP長鏈接,容許服務器推送數據到瀏覽器,無需瀏覽器顯式的發起請求。Comet技術是這種技術方式的統稱,實際上有多種具體的實現技術,下面以具體的時間軸介紹Comet技術有哪些。1)早期的Java Applet2)2000年興起的Pushlets框架3)Hidden iframe4)XMLHttpRequest5)XMLHttpRequest的長輪詢6)腳本標籤長輪詢

相關文章
相關標籤/搜索