websocket介紹

来源:http://www.cnblogs.com/zhang-can/archive/2017/12/06/7994913.html
-Advertisement-
Play Games

websocket應用 手動實現的websocket 你所見過的websocket 你一定見過在網站中,有一個游客聊天的聊天框,比如人人影視。這個聊天框是如何實現即時通訊的呢,就是用到了websocket 你可以打開瀏覽器的network,會看到有個ws://xxxxx,這就代表了是websocke ...


websocket應用

手動實現的websocket

你所見過的websocket

你一定見過在網站中,有一個游客聊天的聊天框,比如人人影視。這個聊天框是如何實現即時通訊的呢,就是用到了websocket

你可以打開瀏覽器的network,會看到有個ws://xxxxx,這就代表了是websocket做的

那麼什麼是websocket?

websocket就是一套協議。

看名字,雖然有個websocket,但他和http協議一樣,也要走socket。

不同的是:http是短連接,處理完一個請求就斷開;

​ websocket是連上就不斷開,一直不斷開,屬於雙工通道,服務端可以主動給客戶端推送消息,客戶端也可以主動給服務端推送消息

當某一個客戶端發送一條消息,服務端接收以後,再推送給所有的客戶端,所以才會呈現出所有人都在即時通訊的效果

服務端當然就是我們寫的程式了,那客戶端是瀏覽器,所以還需要瀏覽器支持才行。不要以為瀏覽器是都支持的,如果所有人都用chrome,前端開發工程師估計就沒什麼工作了。還有,如果所有的瀏覽器都支持,騰訊的webQQ,web微信,也不會使用長輪詢來做這個事了。

來看一下具體的代碼實現

import socket
import base64
import hashlib


def get_headers(data):
    """
    將請求頭格式化成字典
    :param data:
    :return:
    """
    header_dict = {}
    data = str(data, encoding='utf-8')

    for i in data.split('\r\n'):
        print(i)
    header, body = data.split('\r\n\r\n', 1)
    header_list = header.split('\r\n')
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[i].split(' ')) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ')
        else:
            k, v = header_list[i].split(':', 1)
            header_dict[k] = v.strip()
    return header_dict


sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8002))
sock.listen(5)

conn, address = sock.accept()
data = conn.recv(1024)

headers = get_headers(data)  # 提取請求頭信息


# 對請求頭中的sec-websocket-key進行加密
response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
               "Upgrade:websocket\r\n" \
               "Connection: Upgrade\r\n" \
               "Sec-WebSocket-Accept: %s\r\n" \
               "WebSocket-Location: ws://%s%s\r\n\r\n"

magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'       #固定的,魔法字元串就是這個字元串
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) #把返回消息加密


response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
# 響應【握手】信息
conn.send(bytes(response_str, encoding='utf-8'))

info = conn.recv(8096)


#下麵是對瀏覽器發來的消息解密的過程
payload_len = info[1] & 127
if payload_len == 126:
    extend_payload_len = info[2:4]
    mask = info[4:8]
    decoded = info[8:] # 數據
elif payload_len == 127:
    extend_payload_len = info[2:10]
    mask = info[10:14]
    decoded = info[14:]
else:
    extend_payload_len = None
    mask = info[2:6]
    decoded = info[6:]

bytes_list = bytearray()
for i in range(len(decoded)):       #上面解密的最終結果,就是拿到這個decode,就是瀏覽器發來的真實的數據(加密的)
    chunk = decoded[i] ^ mask[i % 4]    #按位異或
    bytes_list.append(chunk)


body = str(bytes_list, encoding='utf-8')
print(body)

客戶端向服務端發送的請求里,有Sec-WebSocket-Key這樣一個key,服務端回消息的時候,就要拿到這個key,加密後再發給瀏覽器,瀏覽器會判斷自己加密後的值,與瀏覽器處理的是否一致,一致才能連接。加密的方式,用到一個magic_string,其實就是一段固定的字元串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,加密後打包發給瀏覽器,瀏覽器驗證通過後就可以通訊了,再來看看客戶端:

客戶端就直接用瀏覽器運行這個html文件就行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="dist/css/bootstrap.css">
</head>
<body>

    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="關閉連接" onclick="closeConn();"/>
    </div>
    <div id="content"></div>



    <script type="text/javascript">
          var socket = new WebSocket("ws://127.0.0.1:8002");
            socket.onopen = function () {
                /* 與伺服器端連接成功後,自動執行 */
                var newTag = document.createElement('div');
                newTag.innerHTML = "【連接成功】";
                document.getElementById('content').appendChild(newTag);
            };
            socket.onmessage = function (event) {
                /* 伺服器端向客戶端發送數據時,自動執行 */
                var response = event.data;
                var newTag = document.createElement('div');
                newTag.innerHTML = response;
                document.getElementById('content').appendChild(newTag);
            };
            socket.onclose = function (event) {
                /* 伺服器端主動斷開連接時,自動執行 */
                var newTag = document.createElement('div');
                newTag.innerHTML = "【關閉連接】";
                document.getElementById('content').appendChild(newTag);
            };
            function sendMsg() {
                var txt = document.getElementById('txt');
                socket.send(txt.value);
                txt.value = "";
            }
            function closeConn() {
                socket.close();
                var newTag = document.createElement('div');
                newTag.innerHTML = "【關閉連接】";
                document.getElementById('content').appendChild(newTag);
            }
    </script>

<script></script>
</body>
</html>

這裡面有三個方法:

  1. 連接上後,onopen會自動執行
  2. 發消息時,onmessage自動執行
  3. 斷開連接,onclose自動執行

客戶端發送給服務端的數據,還有一層加密,必須通過解密才能拿到正確的消息

payload_len = info[1] & 127
if payload_len == 126:
    extend_payload_len = info[2:4]
    mask = info[4:8]
    decoded = info[8:] # 數據
elif payload_len == 127:
    extend_payload_len = info[2:10]
    mask = info[10:14]
    decoded = info[14:]
else:
    extend_payload_len = None
    mask = info[2:6]
    decoded = info[6:]

bytes_list = bytearray()
for i in range(len(decoded)):       #上面解密的最終結果,就是拿到這個decode,就是瀏覽器發來的真實的數據(加密的)
    chunk = decoded[i] ^ mask[i % 4]    #按位異或
    bytes_list.append(chunk)


body = str(bytes_list, encoding='utf-8')

這段就是解密的過程,用到位運算

Django預設是不支持websocket的,雖然有個第三方的channels插件

但是tornado預設就支持

tornado實現websocket

如果用tornado,客戶端不能直接用瀏覽器運行了,而應該是運行tornado的一個模板文件

服務端代碼:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import json
import tornado.ioloop
import tornado.web
import tornado.websocket


class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('index.html')


class ChatHandler(tornado.websocket.WebSocketHandler):
    # 用戶存儲當前聊天室用戶
    waiters = set()
    # 用於存儲歷時消息
    messages = []

    def open(self):
        """
        客戶端連接成功時,自動執行
        :return:
        """
        ChatHandler.waiters.add(self)
        uid = str(uuid.uuid4())
        self.write_message(uid)

        # 下麵這段代碼是給新加入的用戶,顯示歷史信息的
        for msg in ChatHandler.messages:
            # {'uid':'xxx','message':asdfasd}
            content = self.render_string('message.html', **msg)
            self.write_message(content)

    def on_message(self, message):
        """
        客戶端連發送消息時,自動執行
        :param message:
        :return:
        """
        msg = json.loads(message)
        ChatHandler.messages.append(msg)

        for client in ChatHandler.waiters:
            content = client.render_string('message.html', **msg)
            client.write_message(content)

    def on_close(self):
        """
        客戶端關閉連接時,,自動執行
        :return:
        """
        ChatHandler.waiters.remove(self)


def run():
    settings = {
        'template_path': 'templates',       # 配置模板文件
        'static_path': 'static',            # 配置靜態文件路徑
    }
    application = tornado.web.Application([         # 配置路由
        (r"/", IndexHandler),
        (r"/chat", ChatHandler),
    ], **settings)
    application.listen(8009)
    tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
    run()

模板文件(客戶端代碼):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Python聊天室</title>
</head>
<body>
    <div>
        <input type="text" id="txt"/>
        <input type="button" id="btn" value="提交" onclick="sendMsg();"/>
        <input type="button" id="close" value="關閉連接" onclick="closeConn();"/>
    </div>
    <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;">

    </div>

    <script src="/static/jquery-3.2.1.js"></script>
    <script type="text/javascript">
        $(function () {
            wsUpdater.start();
        });
        var wsUpdater = {
            socket: null,
            uid: null,
            start: function() {
                var url = "ws://192.168.16.200:8009/chat";
                wsUpdater.socket = new WebSocket(url);
                wsUpdater.socket.onmessage = function(event) {
                    if(wsUpdater.uid){
                        wsUpdater.showMessage(event.data);
                    }else{
                        wsUpdater.uid = event.data;
                    }
                }
            },
            showMessage: function(content) {
                $('#container').append(content);
            }
        };
        function sendMsg() {
            var msg = {
                uid: wsUpdater.uid,
                message: $("#txt").val()
            };
            wsUpdater.socket.send(JSON.stringify(msg));
        }
</script>

</body>
</html>

原理都一樣,但是用tornado實現起來,就清爽多了。

ps:再說一下騰訊的長輪詢,如果你登錄webQQ,或者web微信,你可以在network裡面找到 pending的字樣,這就是表示是使用的長輪詢。

長輪詢與輪詢的區別就是:

​ 輪詢是過來以後看到沒消息就立馬去走了,但是長輪詢不會立馬走,而是在這等30秒(約定的時間)之後,如果一直沒有消息,才返回,下一次來在等30秒,直到有消息了,這樣有個缺點就是,拿到的消息並不是即時的。那騰訊這麼大的公司,為什麼不用性能更好的websocket呢?原因就是他是個大公司,必須要考慮相容性,必須要保證所有的瀏覽器都能使用才行。

你可以從這裡拿到完整 的示例代碼

https://github.com/zEllis/websocket_demo


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 21342134123 ...
  • 在上文中《Java IO(1)基礎知識——位元組與字元》瞭解到了什麼是位元組和字元,主要是為了對Java IO中有關位元組流和字元流有一個更好的瞭解。 本文所述的輸出輸出指的是Java中傳統的IO,也就是阻塞式輸入輸出(Blocking I/O, BIO),在JDK1.4之後出現了新的輸入輸出API——N ...
  • 貪心演算法(又稱貪婪演算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的局部最優解。貪心演算法不是對所有問題都能得到整體最優解,但對範圍相當廣泛的許多問題他能產生整體最優解或者是整體最優解的近似解。 ...
  • Spring註解 Spring的對象訪問 Spring面向切麵編程 Spring MVC框架1.spring的優點輕量級:基礎版本的spring框架大約2mb控制反轉(IOC):把生成對象的權利反轉給spring框架面向切麵(AOP):把可重用的功能提取出來,然後再將這些通用的功能在合適的時候織入到 ...
  • 異常一 只開啟一個客戶端,輸入信息後關閉,客戶端出現如下異常 根據異常說明 ChatClientFrame客戶端117行 提示原因是Socket關閉 分析原因 客戶端代碼 while (connected) { String str = dis.readUTF(); 當視窗關閉後,Socket已經關 ...
  • 裝飾器: 定義:本質就是函數,(裝飾其它函數),就是為其它函數添加附加功能。 原則:1.不能修改被裝飾的函數的源代碼。 2.不能修改被裝飾的函數的調用方式。(被修飾函數感知不到) 實現裝飾器知識儲備: 函數即"變數" 高階函數 A:把一個函數名,當做形參傳給另外一個函數。 B:返回值中包含函數名 D... ...
  • Item 1. 考慮用靜態工廠方法替代構造器 獲得一個類的實例時我們都會採取一個公有的構造器。Foo x = new Foo(); 同時我們應該掌握另一種方法就是靜態工廠方法(static factory method)。 一句話總結,靜態工廠方法其實就是一個返回類的實例的靜態方法。 書中給出的例子 ...
  • 1. 輸入校驗章節目錄 輸入校驗概述 客戶端校驗 伺服器端校驗 手動編程校驗 重寫validate方法 重寫validateXxx()方法 輸入校驗流程 校驗框架校驗 Struts2 內置的校驗器 常用的內置校驗器的配置 客戶端校驗 伺服器端校驗 重寫validate方法 重寫validateXxx ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...