python---websocket的使用

轉載:https://www.cnblogs.com/ssyfj/p/9245150.html

一:簡介

推文:WebSocket 是什麼原理?爲什麼可以實現持久連接?

推文:WebSocket:5分鐘從入門到精通(很好)

WebSocket協議是基於TCP的一種新的協議。WebSocket最初在HTML5規範中被引用爲TCP連接,作爲基於TCP的套接字API的佔位符。它實現了瀏覽器與服務器全雙工(full-duplex)通信。其本質是保持TCP連接,在瀏覽器和服務端通過Socket進行通信。

二:對比:

Http:

  socket實現,單工通道(瀏覽器只發起,服務端只做響應),短連接,請求響應

WebSocket:

  socket實現,雙工通道,請求響應,推送。socket創建連接,不斷開

三:socket實現步驟

服務端:

1、服務端開啓socket,監聽IP和端口

3、允許連接

5、服務端接收到特殊值【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】

6、加密後的值發送給客戶端

客戶端:

2、客戶端發起連接請求(IP和端口)

4、客戶端生成一個xxx,【加密sha1,特殊值,migic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服務端發送一段特殊值

7、 客戶端接收到加密的值

注意:這個魔數是固定的 258EAFA5-E914-47DA-95CA-C5AB0DC85B11

四:簡單實現,實現連接

服務端:

# coding:utf8
# __author:  Administrator
# date:      2018/6/29 0029
# /usr/bin/env python
import socket,base64,hashlib

def get_headers(data):
    '''將請求頭轉換爲字典'''
    header_dict = {}
    data = str(data,encoding="utf-8")

    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[0].split(" ")) == 3:
                header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
        else:
            k,v=header_list[i].split(":",1)
            header_dict[k]=v.strip()
    return header_dict

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)

#等待用戶連接
conn,addr = sock.accept()
print("conn from ",conn,addr)
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)
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'))

 

 

瀏覽器:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
<script>
    ws =new WebSocket("ws://127.0.0.1:8080");
    ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
        console.log(22222)
    }
</script>

五:數據接收規則

1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |  #Payload len(第二個字節的前七位,最大127)決定頭部的長度
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |  #若是小於126:Extended payload length擴展頭部長度爲0字節,後面全部爲主體數據
|N|V|V|V|       |S|             |   (if payload len==126/127)   |  #若是等於126:Extended payload length擴展頭部長度爲2字節,後面全部爲主體數據
| |1|2|3|       |K|             |                               |  #若是等於127:Extended payload length擴展頭部長度爲8字節,後面全部爲主體數據
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |  #注意:主體數據中的前四位爲mask掩碼,用於後面的消息的解碼,解碼方式爲循環異或操作
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |  #數據過長,需要分部發送,這時需要FIN和opcode
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

 

數據幀格式:

FIN:1個比特。

如果是1,表示這是消息(message)的最後一個分片(fragment),如果是0,表示不是是消息(message)的最後一個分片(fragment)。

RSV1, RSV2, RSV3:各佔1個比特。

一般情況下全爲0。當客戶端、服務端協商採用WebSocket擴展時,這三個標誌位可以非0,且值的含義由擴展進行定義。如果出現非零的值,且並沒有採用WebSocket擴展,連接出錯。

Opcode: 4個比特。

操作代碼,Opcode的值決定了應該如何解析後續的數據載荷(data payload)。如果操作代碼是不認識的,那麼接收端應該斷開連接(fail the connection)。可選的操作代碼如下:

%x0:表示一個延續幀。當Opcode爲0時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片。
%x1:表示這是一個文本幀(frame)
%x2:表示這是一個二進制幀(frame)
%x3-7:保留的操作代碼,用於後續定義的非控制幀。
%x8:表示連接斷開。
%x9:表示這是一個ping操作。
%xA:表示這是一個pong操作。
%xB-F:保留的操作代碼,用於後續定義的控制幀。

Mask: 1個比特。

操作代碼,Opcode的值決定了應該如何解析後續的數據載荷(data payload)。如果操作代碼是不認識的,那麼接收端應該斷開連接(fail the connection)。可選的操作代碼如下:

%x0:表示一個延續幀。當Opcode爲0時,表示本次數據傳輸採用了數據分片,當前收到的數據幀爲其中一個數據分片。
%x1:表示這是一個文本幀(frame)
%x2:表示這是一個二進制幀(frame)
%x3-7:保留的操作代碼,用於後續定義的非控制幀。
%x8:表示連接斷開。
%x9:表示這是一個ping操作。
%xA:表示這是一個pong操作。
%xB-F:保留的操作代碼,用於後續定義的控制幀。

Payload length:數據載荷的長度,單位是字節。爲7位,或7+16位,或1+64位。

假設數Payload length === x,如果

x爲0~126:數據的長度爲x字節。
x爲126:後續2個字節代表一個16位的無符號整數,該無符號整數的值爲數據的長度。
x爲127:後續8個字節代表一個64位的無符號整數(最高位爲0),該無符號整數的值爲數據的長度。
此外,如果payload length佔用了多個字節的話,payload length的二進制表達採用網絡序(big endian,重要的位在前)。

Masking-key:0或4字節(32位)

所有從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操作,Mask爲1,且攜帶了4字節的Masking-key。如果Mask爲0,則沒有Masking-key。

備註:載荷數據的長度,不包括mask key的長度。

Payload data:(x+y) 字節

載荷數據:包括了擴展數據、應用數據。其中,擴展數據x字節,應用數據y字節。

擴展數據:如果沒有協商使用擴展的話,擴展數據數據爲0字節。所有的擴展都必須聲明擴展數據的長度,或者可以如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。如果擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。

應用數據:任意的應用數據,在擴展數據之後(如果存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就得到應用數據的長度。

實現規則解碼:

def get_data(info):    #info是我們連接後,接受的數據
    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)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    return body

 

 實現循環獲取數據

服務端代碼

import socket,base64,hashlib

def get_headers(data):
    '''將請求頭轉換爲字典'''
    header_dict = {}
    data = str(data,encoding="utf-8")

    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[0].split(" ")) == 3:
                header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
        else:
            k,v=header_list[i].split(":",1)
            header_dict[k]=v.strip()
    return header_dict

def get_data(info):
    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)):
        chunk = decoded[i] ^ mask[i % 4]    #解碼方式
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    return body

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)

#等待用戶連接
conn,addr = sock.accept()
print("conn from ",conn,addr)
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)

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'))

#可以進行通信
while True:
    data = conn.recv(8096)
    data = get_data(data)
    print(data)

服務端代碼

客戶端代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
<script>
    ws =new WebSocket("ws://127.0.0.1:8080");
    ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
        console.log(22222);
        ws.send("你好");
    }
</script>

客戶端代碼

 

 

注意:使用控制檯完成發送,而不是刷新頁面,會報錯,因爲我們關閉了連接,試圖將關閉信號字節編碼出錯。這裏我們需要利用mask(第二字節中,1表示連接,0斷開)

六:數據發送規則(需要發送二進制包struct模塊)

 

def send_msg(conn, msg_bytes):  
    """
    WebSocket服務端向客戶端發送消息
    :param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept()
    :param msg_bytes: 向客戶端發送的字節
    :return: 
    """
    import struct

    token = b"\x81"  #接收的第一字節,一般都是x81不變
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True

實現發送數據

服務端

# coding:utf8
# __author:  Administrator
# date:      2018/6/29 0029
# /usr/bin/env python
import socket,base64,hashlib

def get_headers(data):
    '''將請求頭轉換爲字典'''
    header_dict = {}
    data = str(data,encoding="utf-8")

    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[0].split(" ")) == 3:
                header_dict['method'],header_dict['url'],header_dict['protocol'] = header_list[0].split(" ")
        else:
            k,v=header_list[i].split(":",1)
            header_dict[k]=v.strip()
    return header_dict

def get_data(info):
    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)):
        chunk = decoded[i] ^ mask[i % 4]    #解碼方式
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    return body

def send_msg(conn, msg_bytes):
    """
    WebSocket服務端向客戶端發送消息
    :param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept()
    :param msg_bytes: 向客戶端發送的字節
    :return:
    """
    import struct

    token = b"\x81" #接收的第一字節,一般都是x81不變
    length = len(msg_bytes)
    if length < 126:
        token += struct.pack("B", length)
    elif length <= 0xFFFF:
        token += struct.pack("!BH", 126, length)
    else:
        token += struct.pack("!BQ", 127, length)

    msg = token + msg_bytes
    conn.send(msg)
    return True

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sock.bind(("127.0.0.1",8080))
sock.listen(5)

#等待用戶連接
conn,addr = sock.accept()
#獲取握手消息,magic string ,sha1加密
#發送給客戶端
#握手消息
data = conn.recv(8096)

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'))

#可以進行通信
while True:
    data = conn.recv(8096)
    data = get_data(data)
    print(data)
    send_msg(conn,bytes(data+"geah",encoding="utf-8"))

服務端

前端onmessage 當數據接收會觸發

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
<script>
    ws =new WebSocket("ws://127.0.0.1:8080");
    ws.onopen = function (ev) { //若是連接成功,onopen函數會執行
        console.log(22222);
        ws.send("你好");
    }
    ws.onmessage = function (ev) {
        console.log(ev);
    }
</script>

前端onmessage 當數據接收會觸發

 

 

七:tornado實現websocket聊天室

 tornado服務端

import tornado.ioloop
import tornado.web
import tornado.websocket
import datetime

class MainHandler(tornado.web.RequestHandler):
    def get(self):

        self.render("s1.html")

    def post(self, *args, **kwargs):
        pass

users = set()
class ChatHandler(tornado.websocket.WebSocketHandler):
    def open(self, *args, **kwargs):
        '''客戶端連接'''
        print("connect....")
        print(self.request)
        users.add(self)

    def on_message(self, message):
        '''有消息到達'''
        now = datetime.datetime.now()
        content = self.render_string("recv_msg.html",date=now.strftime("%Y-%m-%d %H:%M:%S"),msg=message)
        for client in users:
            if client == self:
                continue
            client.write_message(content)

    def on_close(self):
        '''客戶端主動關閉連接'''
        users.remove(self)


st ={
    "template_path": "template",#模板路徑配置
    "static_path":'static',
}

#路由映射   匹配執行,否則404
application = tornado.web.Application([
    ("/index",MainHandler),
    ("/wschat",ChatHandler),
],**st)

if __name__=="__main__":
    application.listen(8080)

    #io多路複用
    tornado.ioloop.IOLoop.instance().start()

前端模板

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="/static/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/css/nifty.min.css" rel="stylesheet">
    <link href="/static/css/demo/nifty-demo-icons.min.css" rel="stylesheet">
    <link href="/static/css/demo/nifty-demo.min.css" rel="stylesheet">
    <link href="/static/plugins/pace/pace.min.css" rel="stylesheet">
    <script src="/static/js/jquery-2.2.4.min.js"></script>
    <script src="/static/plugins/pace/pace.min.js"></script>
    <script src="/static/js/bootstrap.min.js"></script>
    <script src="/static/js/nifty.min.js"></script>
    <script src="/static/js/demo/nifty-demo.min.js"></script>
    <script src="/static/plugins/flot-charts/jquery.flot.min.js"></script>
    <script src="/static/plugins/flot-charts/jquery.flot.resize.min.js"></script>
    <script src="/static/plugins/gauge-js/gauge.min.js"></script>
    <script src="/static/plugins/skycons/skycons.min.js"></script>
    <script src="/static/plugins/easy-pie-chart/jquery.easypiechart.min.js"></script>
    <script src="/static/js/demo/widgets.js"></script>
</head>
<body>
<div id="container" class="effect  aside-bright mainnav-sm aside-right aside-in">
    <div class="boxed">
        <div id="content-container">
            <div class="row">
                <div class="col-md-8 col-lg-8 col-sm-8">

            <!--Chat widget-->
            <!--===================================================-->
            <div class="panel" style="height: 640px">
                <!--Heading-->
                <div class="panel-heading">
                    <h3 class="panel-title">Chat</h3>
                </div>

                <!--Widget body-->
                <div style="height:510px;padding-top:0px;" class="widget-body">
                    <div class="nano">
                        <div class="nano-content pad-all">
                            <ul class="list-unstyled media-block">

                            </ul>
                        </div>
                    </div>

                    <!--Widget footer-->
                    <div class="panel-footer" style="height: 90px;">
                        <div class="row">
                            <div class="col-xs-9">
                                <input type="text" placeholder="Enter your text" class="form-control chat-input">
                            </div>
                            <div class="col-xs-3">
                                <button class="btn btn-primary btn-block" οnclick="sendMsg(this);" type="submit">Send</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!--===================================================-->
            <!--Chat widget-->

        </div>
                <div class="col-md-4 col-lg-4 col-sm-4">
                    <aside id="aside-container">
                <div id="aside">
                    <div class="nano has-scrollbar">
                        <div class="nano-content" tabindex="0" style="right: -17px;">

                            <!--Nav tabs-->
                            <!--================================-->
                            <ul class="nav nav-tabs nav-justified">
                                <li class="active">
                                    <a href="#demo-asd-tab-1" data-toggle="tab">
                                        <i class="demo-pli-speech-bubble-7"></i>
                                    </a>
                                </li>
                            </ul>
                            <!--================================-->
                            <!--End nav tabs-->

                            <!-- Tabs Content -->
                            <!--================================-->
                            <div class="tab-content">
                                <div class="tab-pane fade in active" id="demo-asd-tab-1">
                                    <p class="pad-hor text-semibold text-main">
                                        <span class="pull-right badge badge-success">0</span> Friends
                                    </p>

                                </div>
                            </div>
                        </div>
                    <div class="nano-pane" style="display: none;"><div class="nano-slider" style="height: 4059px; transform: translate(0px, 0px);"></div></div></div>
                </div>
            </aside>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

<script>
    ws = new WebSocket("ws://127.0.0.1:8080/wschat");

    function sendMsg(ths) {
        var dt = new Date()
        var now_time = dt.toLocaleString();

        var msg = $(ths).parents(".row").find(".chat-input").val();
        $(ths).parents(".row").find(".chat-input").empty();
        var li = '<li class="mar-btm"><div class="media-right"><img src="" class="img-circle img-sm" alt="Profile Picture"></div>';
        li += '<div class="media-body pad-hor speech-right"><div class="speech"><a href="#" class="media-heading">遊客</a>';
        li += '<p>'+msg+'</p>';
        li += '<p class="speech-time">';
        li += '<i class="demo-pli-clock icon-fw"></i>'+now_time;
        li += '</p></div></div></li>';
        $(ths).parents(".widget-body").find(".list-unstyled").append(li);
        $(ths).parents(".panel-footer").find(".chat-input").val("");
        ws.send(msg);
    }

    ws.onmessage=function (ev) {
        $(".list-unstyled").append(ev.data);
    }
</script>

s1.html

消息插件

 

<li class="mar-btm">
                                    <div class="media-left">
                                        <img src="img/profile-photos/1.png" class="img-circle img-sm" alt="Profile Picture">
                                    </div>
                                    <div class="media-body pad-hor">
                                        <div class="speech">
                                            <a href="#" class="media-heading">遊客</a>
                                            <p>{{msg}}</p>
                                            <p class="speech-time">
                                            <i class="demo-pli-clock icon-fw"></i>{{date}}
                                            </p>
                                        </div>
                                    </div>
                                </li>

recv_msg.html

 

 

 

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