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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...