热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

WebSocket协议学习

websocket协议规定了客户端和服务端socket连接和通信时的规则,一是连接握手时的认证,二是通信时的数据报文解析。其整个流程的简单分析如下:(websocket简介参见:h

  websocket协议规定了客户端和服务端socket连接和通信时的规则,一是连接握手时的认证,二是通信时的数据报文解析。其整个流程的简单分析如下:

     (websocket简介参见:https://www.zhihu.com/question/20215561/answer/40316953)

1.websocket服务器和客户端连接

    socket服务端

技术分享图片技术分享图片
#coding: utf-8

import socket


soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
soc.bind((127.0.0.1,8080))
soc.listen(5)

client,address = soc.accept()

msg = client.recv(8096)

print msg
View Code

  websocket客户端

技术分享图片技术分享图片
"zh-CN">

    "UTF-8">
    "X-UA-Compatible" cOntent="IE=edge">
    "viewport" cOntent=">
    




View Code

  执行后可以看到客户端发过来的请求信息如下,比普通的http请求头多了一个Sec-WebSocket-Key,用来进行握手认证

GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost:63342
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: lOfBaOFgUccUfIKUDD5Bxw==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

服务端接受websocket客户端的请求消息后,若要与客户端进行握手认证,要遵循的规则如下:

  • 从上述客户端请求信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密 (magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11  固定不变
  • 将加密结果响应给客户端

 返回的请求头如下:

HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection: Upgrade
Sec-WebSocket-Accept: Ip8Lp7v3m6xnPYlNIQ83SgGwrwA=
WebSocket-Location: ws://127.0.0.1:8080/
Sec-WebSocket-Accept为最重要的验证字段,其计算过程如下:
  1. 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;

  2. 通过 SHA1 计算出摘要,并转成 base64 字符串。

代码实现如下:

#coding: utf-8

import socket
import base64
import hashlib

#处理请求头消息
def get_header(data):
    data = str(data)
    header_dict={}
    if data:
        header,body = data.split(\r\n\r\n,1)
        header_list = header.split(\r\n)
        #print header_list
        for i in range(0,len(header_list)):
            if i==0:
                lenth = len(header_list[i].split( ))
                if lenth==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() # 此处注意要去除空格,否则后面的Sec-WebSocket-Key的加密验证会失败
    return header_dict

soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
soc.bind((127.0.0.1,8080))
soc.listen(5)

client,address = soc.accept()

data = client.recv(8096)
header = get_header(data)

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

msg = header[Sec-WebSocket-Key].strip()+magic_string  #注意header[‘Sec-WebSocket-Key‘]前后是否有多余的空格
print msg
encrypt_msg = base64.b64encode(hashlib.sha1(msg).digest())  #加密得到Sec-WebSocket-Accept
response_str=response_tpl%(encrypt_msg,header[Host],header[Url])
print response_str
client.send(response_str)

2.websocket服务端和客户端通信

   websocket客户端发送过来的数据报文格式如下,服务端需要对报文进行解析,然后再将回复内容进行封包,发送给客户端。

    (websocket protocol:   https://tools.ietf.org/html/rfc6455#section-5.1)

技术分享图片

相关含义如下:

The MASK bit simply tells whether the message is encoded. Messages from the client must be masked, so your server should expect this to be 1. (In fact, section 5.1 of the spec says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We‘ll explain masking later. Note: You have to mask messages even when using a secure socket.RSV1-3 can be ignored, they are for extensions.

The opcode field defines how to interpret the payload data: 0x0 for continuation, 0x1 for text (which is always encoded in UTF-8), 0x2 for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, 0x3 to 0x7 and 0xB to 0xF have no meaning.

The FIN bit tells whether this is the last message in a series. If it‘s 0, then the server will keep listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later.

Decoding Payload Length

To read the payload data, you must know when to stop reading. That‘s why the payload length is important to know. Unfortunately, this is somewhat complicated. To read it, follow these steps:

  1. Read bits 9-15 (inclusive) and interpret that as an unsigned integer. If it‘s 125 or less, then that‘s the length; you‘re done. If it‘s 126, go to step 2. If it‘s 127, go to step 3.
  2. Read the next 16 bits and interpret those as an unsigned integer. You‘re done.
  3. Read the next 64 bits and interpret those as an unsigned integer (The most significant bit MUST be 0). You‘re done.

Reading and Unmasking the Data

If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can go ahead and read that number of bytes from the socket. Let‘s call the data ENCODED, and the key MASK. To get DECODED, loop through the octets (bytes a.k.a. characters for text data) of ENCODED and XOR the octet with the (i modulo 4)th octet of MASK. In pseudo-code (that happens to be valid Javascript):

var DECODED = "";
for (var i = 0; i     DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

Now you can figure out what DECODED means depending on your application.

第一步:对客户端数据报文解析

  解包流程:

    1,根据payload len的值(字节序号1的后七位)来确定payload占几个字节

         2, 确定payload占的字节数后,其后四个字节即为Masking-key(MASK bit 设置为1时,Masking-key才存在),Masking-key后面的所有字节为payload data

    3,利用Masking-key对payload data进行异或运算进行解码,拿到客户端发送的数据

  代码实现解包流程如下:

  python 2.7

技术分享图片技术分享图片
def get_data(msg):
    length = ord(msg[1])&127    #127的二进制为01111111,和127进行与运算,能拿到msg[1]的后七位
    if length==126:             #不加ord时,msg[1]为字符窜,不支持与运算
        mask = msg[4:8]
        pay_data = msg[8:]
    elif length==127:
        mask = msg[10:14]
        pay_data = msg[14:]
    else:
        mask = msg[2:6]
        pay_data = msg[6:]
    decode=‘‘
    for i in range(len(pay_data)):
        decode+=chr(ord(pay_data[i]) ^ ord(mask[i%4]))
    return decode

#python3环境下代码
# def get_data(msg):
#     length = msg[1]&127
#     if length==126:
#         mask = msg[4:8]
#         pay_data = msg[8:]
#     elif length==127:
#         mask = msg[10:14]
#         pay_data = msg[14:]
#     else:
#         mask = msg[2:6]
#         pay_data = msg[6:]
#     bytes_list = bytearray()
#     for i in range(len(pay_data)):
#         chunk=pay_data[i] ^ mask[i%4]
#         decode=str(bytes_list.append(chunk),encoding=‘utf-8‘)
#     return decode
View Code

第二步:将数据封包,发送给客户端

  返回数据报文的MASK bit为0,因此没有Masking-key,数据报文组成:token(字节序号0)+payload lenth +payload data

  实现代码如下:

技术分享图片技术分享图片
def response_data(msg):                                  
    token = struct.pack(B,129) #写入第一个字节 10000001       
    payload_len = len(msg)                               
    if payload_len <=125:                                
        token += struct.pack(B,payload_len)            
    elif payload_len<=126:                               
        token += struct.pack(BH,126,payload_len)       
    else:                                                
        token += struct.pack(BH, 127, payload_len)     
    data = token+msg                                     
    return data                                          
View Code

3. 基于websocket的聊天简单测试

  客户端:以js中的websocket做为客户端

技术分享图片技术分享图片
"zh-CN">

    "UTF-8">
    "X-UA-Compatible" cOntent="IE=edge">
    "viewport" cOntent=">
    



"content" color: #800000">"border:solid gray 1px; width:400px; height:400px;margin:100px 0px 0px 100px">
"margin-left:100px"> "text" id="msg"/>
client

  服务器:基于上面的握手和通信过程,对于客户端发过来的消息,回复其消息

技术分享图片技术分享图片
#coding:utf-8

import socket
import base64
import hashlib
import struct

#处理请求头消息
def get_header(data):
    data = str(data)
    header_dict={}
    if data:
        header,body = data.split(\r\n\r\n,1)
        header_list = header.split(\r\n)
        #print header_list
        for i in range(0,len(header_list)):
            if i==0:
                lenth = len(header_list[i].split( ))
                if lenth==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() # 此处注意要去除空格,否则后面的Sec-WebSocket-Key的加密验证会失败
    return header_dict

def get_data(msg):
    length = ord(msg[1])&127    #127的二进制为01111111,和127进行与运算,能拿到msg[1]的后七位
    if length==126:             #不加ord时,msg[1]为字符窜,不支持与运算
        mask = msg[4:8]
        pay_data = msg[8:]
    elif length==127:
        mask = msg[10:14]
        pay_data = msg[14:]
    else:
        mask = msg[2:6]
        pay_data = msg[6:]
    decode=‘‘
    for i in range(len(pay_data)):
        decode+=chr(ord(pay_data[i]) ^ ord(mask[i%4]))
    return decode

def response_data(msg):
    token = struct.pack(B,129) #写入第一个字节 10000001
    payload_len = len(msg)
    if payload_len <=125:
        token += struct.pack(B,payload_len)
    elif payload_len<=126:
        token += struct.pack(BH,126,payload_len)
    else:
        token += struct.pack(BH, 127, payload_len)
    data = token+msg
    return data



def run():
    soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    soc.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    soc.bind((127.0.0.1,8080))
    soc.listen(5)

    client,address = soc.accept()

    data = client.recv(8096)
    header = get_header(data)

    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

    hand_str = header[Sec-WebSocket-Key].strip()+magic_string  #注意header[‘Sec-WebSocket-Key‘]前后是否有多余的空格

    encrypt_str = base64.b64encode(hashlib.sha1(hand_str).digest())
    response_str=response_tpl%(encrypt_str,header[Host],header[Url])
    print response_str
    client.send(response_str)

    while True:
        try:
            msg = client.recv(8096)
            decoded_msg = get_data(msg)
            print decoded_msg
            send_msg = response_data(回复:+decoded_msg)
            print send_msg
            client.send(send_msg)
            #client.send(‘%c%c%s‘ % (0x81, 4, ‘zack‘))
        except Exception as e:
            print e

if __name__ == __main__:
    run()
server

4.tonardo框架中websocket的使用

  https://www.tornadoweb.org/en/stable/websocket.html?highlight=websocket

  tornado.websocket.WebSocketHandler中封装的三个方法如下:

class EchoWebSocket(tornado.websocket.WebSocketHandler):
    def open(self):  #客户端连接时执行
        print("WebSocket opened")

    def on_message(self, message):  #接收到客户端消息时执行
        self.write_message(u"You said: " + message)

    def on_close(self): #断开连接时执行 
        print("WebSocket closed")

  简单在线聊天室实现:

技术分享图片

 app.py

技术分享图片技术分享图片
#coding:utf-8


import tornado.web
import tornado.websocket
import tornado.ioloop
import uuid

Users = set()
class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(index.html)
class ChatHandler(tornado.websocket.WebSocketHandler):

    def open(self):
        self.id = str(uuid.uuid4())
        Users.add(self)
    def on_message(self, message):
        for client in Users:
            content = client.render_string(message.html,id=self.id,msg=message)
            client.write_message(content)
    def on_close(self):
        delattr(self,id)
        Users.remove(self)


settings={
    template_path:templates,
    static_path:statics,
    static_url_prefix:/statics/,
}

app = tornado.web.Application([
    (r/,IndexHandler),
    (r/chat,ChatHandler),
],**settings)

if __name__ == __main__:
    app.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
app.py

index.html

技术分享图片技术分享图片
"zh-CN">

    "UTF-8">
    "X-UA-Compatible" cOntent="IE=edge">
    "viewport" cOntent=">
    
    


"width: 750px; margin: 0 auto">

websocket聊天室

"content" >
"margin-left:100px"> "text" id="msg"/>
index.html

message.html

技术分享图片技术分享图片
"margin: 20px; background-color: green">{{id}}:{{msg}}
message.html

参考文章:

http://www.cnblogs.com/wupeiqi/p/6558766.html

https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

https://www.cnblogs.com/aguncn/p/5059337.html

https://www.cnblogs.com/JetpropelledSnake/p/9033064.html

WebSocket协议学习


推荐阅读
  • Mac OS 升级到11.2.2 Eclipse打不开了,报错Failed to create the Java Virtual Machine
    本文介绍了在Mac OS升级到11.2.2版本后,使用Eclipse打开时出现报错Failed to create the Java Virtual Machine的问题,并提供了解决方法。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
author-avatar
几米小八_198
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有