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

异步通信WebSocket

什么是WebSocket?WebSocketAPI是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。Web
什么是WebSocket?

WebSocket API是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。WebSocket目前由W3C进行标准化。WebSocket已经受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等浏览器的支持。

WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允许跨域通信。

Ajax技术很聪明的一点是没有设计要使用的方式。WebSocket为指定目标创建,用于双向推送消息。

WebSocket通信原理
        - 服务端(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. 客户端接收到加密的值

  基于代码实现:

1. 启动服务端

import socket
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()
...
...
...

  启动Socket服务器后,等待用户【连接】,然后进行收发数据。

2. 客户端连接


  当客户端向服务端发送连接请求时,不仅连接还会发送【握手】信息,并等待服务端响应,至此连接才创建成功!

3. 建立连接【握手】

import socket
 
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)
# 获取客户端socket对象
conn, address = sock.accept()
# 获取客户端的【握手】信息
data = conn.recv(1024)
...
...
...
conn.send(‘响应【握手】信息‘)  

请求和响应的【握手】信息需要遵循规则:

  • 从请求【握手】信息中提取 Sec-WebSocket-Key  #这个是API随机生成的
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端

  注:magic string为(亘古不变):258EAFA5-E914-47DA-95CA-C5AB0DC85B11

请求【握手】信息为:

GET /chatsocket HTTP/1.1
Host: 127.0.0.1:8002
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
...
...

提取Sec-WebSocket-Key值并加密:

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‘))
...
...
...

4.客户端和服务端收发数据

客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的Javascript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。

第一步:获取客户端发送的数据【解包】

技术分享技术分享
 1 info = conn.recv(8096)
 2 
 3     payload_len = info[1] & 127
 4     if payload_len == 126:
 5         extend_payload_len = info[2:4]
 6         mask = info[4:8]
 7         decoded = info[8:]
 8     elif payload_len == 127:
 9         extend_payload_len = info[2:10]
10         mask = info[10:14]
11         decoded = info[14:]
12     else:
13         extend_payload_len = None
14         mask = info[2:6]
15         decoded = info[6:]
16 
17     bytes_list = bytearray()
18     for i in range(len(decoded)):
19         chunk = decoded[i] ^ mask[i % 4]
20         bytes_list.append(chunk)
21     body = str(bytes_list, encoding=utf-8)
22     print(body)
基于Python实现解包过程(未实现长内容)

数据交互协议:

0                   1                   2                   3
 0 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    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

 协议解读:

第一个字节

最高位用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;后面3位是用于扩展定义的,如果没有扩展约定的情况则必须为0.可以通过以下c#代码方式得到相应值


mDataPackage.IsEof = (data[start] >> 7) > 0;
最低4位用于描述消息类型,消息类型暂定有15种,其中有几种是预留设置.c#代码可以这样得到消息类型:

int type = data[start] & 0xF;
mDataPackage.Type = (PackageType)type;
第二个字节

消息的第二个字节主要用一描述掩码和消息长度,最高位用0或1来描述是否有掩码处理,可以通过以下c#代码方式得到相应值


bool hasMask = (data[start] >>7) > 0;
剩下的后面7位用来描述消息长度,由于7位最多只能描述127所以这个值会代表三种情况,一种是消息内容少于126存储消息长度,如果消息长度少于UINT16的情况此值为126,当消息长度大于UINT16的情况下此值为127;这两种情况的消息长度存储到紧随后面的byte[],分别是UINT16(2位byte)和UINT64(4位byte).可以通过以下c#代码方式得到相应值


mPackageLength = (uint)(data[start] & 0x7F);
start++;
if (mPackageLength == 126)
{
    mPackageLength = BitConverter.ToUInt16(data, start);
    start = start + 2;
}
else if (mPackageLength == 127)
{
    mPackageLength = BitConverter.ToUInt64(data, start);
    start = start + 8;
}
如果存在掩码的情况下获取4位掩码值:


if (hasMask)
{
    mDataPackage.Masking_key = new byte[4];
    Buffer.BlockCopy(data, start, mDataPackage.Masking_key, 0, 4);
           
    start = start + 4;
    count = count - 4;
}
获取消息体

当得到消息体长度后就可以获取对应长度的byte[],有些消息类型是没有长度的如%x8 denotes a connection close.对于Text类型的消息对应的byte[]是相应字符的UTF8编码.获取消息体还有一个需要注意的地方就是掩码,如果存在掩码的情况下接收的byte[]要做如下转换处理:


if (mDataPackage.Masking_key != null)
    {
        int length = mDataPackage.Data.Count;
        for (var i = 0; i 

第二步:向客户端发送数据【封包】

技术分享技术分享
 1 def send_msg(conn, msg_bytes):
 2     """
 3     WebSocket服务端向客户端发送消息
 4     :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
 5     :param msg_bytes: 向客户端发送的字节
 6     :return: 
 7     """
 8     import struct
 9 
10     token = b"\x81"   #用于描述数据交互协议中数据传输是否完成
11     length = len(msg_bytes)
12     if length <126:
13         token += struct.pack("B", length)
14     elif length <= 0xFFFF:
15         token += struct.pack("!BH", 126, length)
16     else:
17         token += struct.pack("!BQ", 127, length)
18 
19     msg = token + msg_bytes
20     conn.send(msg)
21     return True
View Code
基于Python实现简单示例

a. 基于Python socket实现的WebSocket服务端:

技术分享技术分享
  1 import socket
  2 import base64
  3 import hashlib
  4  
  5  
  6 def get_headers(data):
  7     """
  8     将请求头格式化成字典
  9     :param data:
 10     :return:
 11     """
 12     header_dict = {}
 13     data = str(data, encoding=utf-8)
 14  
 15     header, body = data.split(\r\n\r\n, 1)
 16     header_list = header.split(\r\n)
 17     for i in range(0, len(header_list)):
 18         if i == 0:
 19             if len(header_list[i].split( )) == 3:
 20                 header_dict[method], header_dict[url], header_dict[protocol] = header_list[i].split( )
 21         else:
 22             k, v = header_list[i].split(:, 1)
 23             header_dict[k] = v.strip()
 24     return header_dict
 25  
 26  
 27 def send_msg(conn, msg_bytes):
 28     """
 29     WebSocket服务端向客户端发送消息
 30     :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
 31     :param msg_bytes: 向客户端发送的字节
 32     :return:
 33     """
 34     import struct
 35  
 36     token = b"\x81"
 37     length = len(msg_bytes)
 38     if length <126:
 39         token += struct.pack("B", length)
 40     elif length <= 0xFFFF:
 41         token += struct.pack("!BH", 126, length)
 42     else:
 43         token += struct.pack("!BQ", 127, length)
 44  
 45     msg = token + msg_bytes
 46     conn.send(msg)
 47     return True
 48  
 49  
 50 def run():
 51     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 52     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 53     sock.bind((127.0.0.1, 8003))
 54     sock.listen(5)
 55  
 56     conn, address = sock.accept()
 57     data = conn.recv(1024)
 58     headers = get_headers(data)
 59     response_tpl = "HTTP/1.1 101 Switching Protocols\r\n"  60                    "Upgrade:websocket\r\n"  61                    "Connection:Upgrade\r\n"  62                    "Sec-WebSocket-Accept:%s\r\n"  63                    "WebSocket-Location:ws://%s%s\r\n\r\n"
 64  
 65     value = headers[Sec-WebSocket-Key] + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
 66     ac = base64.b64encode(hashlib.sha1(value.encode(utf-8)).digest())
 67     response_str = response_tpl % (ac.decode(utf-8), headers[Host], headers[url])
 68     conn.send(bytes(response_str, encoding=utf-8))
 69  
 70     while True:
 71         try:
 72             info = conn.recv(8096)
 73         except Exception as e:
 74             info = None
 75         if not info:
 76             break
 77         payload_len = info[1] & 127
 78         if payload_len == 126:
 79             extend_payload_len = info[2:4]
 80             mask = info[4:8]
 81             decoded = info[8:]
 82         elif payload_len == 127:
 83             extend_payload_len = info[2:10]
 84             mask = info[10:14]
 85             decoded = info[14:]
 86         else:
 87             extend_payload_len = None
 88             mask = info[2:6]
 89             decoded = info[6:]
 90  
 91         bytes_list = bytearray()
 92         for i in range(len(decoded)):
 93             chunk = decoded[i] ^ mask[i % 4]
 94             bytes_list.append(chunk)
 95         body = str(bytes_list, encoding=utf-8)
 96         send_msg(conn,body.encode(utf-8))
 97  
 98     sock.close()
 99  
100 if __name__ == __main__:
101     run()
View Code

b. 利用Javascript类库实现客户端

技术分享技术分享
 1 
 2 
 3 "en">
 4     "UTF-8">
 5     
 6 
 7 
 8     
9 "text" id="txt"/> 10 "button" id="btn" value="提交" Onclick="sendMsg();"/> 11 "button" id="close" value="关闭连接" Onclick="closeConn();"/> 12
13
"content">
14 15 54 55
View Code
基于Tornado框架实现Web聊天室

Tornado是一个支持WebSocket的优秀框架,其内部原理正如1~5步骤描述,当然Tornado内部封装功能更加完整。

源码见链接:点我下载

异步通信----WebSocket


推荐阅读
  • 前言:关于跨域CORS1.没有跨域时,ajax默认是带cookie的2.跨域时,两种解决方案:1)服务器端在filter中配置详情:http:blog.csdn.netwzl002 ... [详细]
  • ECMA262规定typeof操作符的返回值和instanceof的使用方法
    本文介绍了ECMA262规定的typeof操作符对不同类型的变量的返回值,以及instanceof操作符的使用方法。同时还提到了在不同浏览器中对正则表达式应用typeof操作符的返回值的差异。 ... [详细]
  • 本文介绍了使用AJAX的POST请求实现数据修改功能的方法。通过ajax-post技术,可以实现在输入某个id后,通过ajax技术调用post.jsp修改具有该id记录的姓名的值。文章还提到了AJAX的概念和作用,以及使用async参数和open()方法的注意事项。同时强调了不推荐使用async=false的情况,并解释了JavaScript等待服务器响应的机制。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • Python瓦片图下载、合并、绘图、标记的代码示例
    本文提供了Python瓦片图下载、合并、绘图、标记的代码示例,包括下载代码、多线程下载、图像处理等功能。通过参考geoserver,使用PIL、cv2、numpy、gdal、osr等库实现了瓦片图的下载、合并、绘图和标记功能。代码示例详细介绍了各个功能的实现方法,供读者参考使用。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • 深入理解CSS中的margin属性及其应用场景
    本文主要介绍了CSS中的margin属性及其应用场景,包括垂直外边距合并、padding的使用时机、行内替换元素与费替换元素的区别、margin的基线、盒子的物理大小、显示大小、逻辑大小等知识点。通过深入理解这些概念,读者可以更好地掌握margin的用法和原理。同时,文中提供了一些相关的文档和规范供读者参考。 ... [详细]
  • 使用chrome编辑器实现网页截图功能的方法
    本文介绍了在chrome浏览器中使用编辑器实现网页截图功能的方法。通过在地址栏中输入特定命令,打开控制台并调用命令面板,用户可以方便地进行网页截图操作。 ... [详细]
  • 工作经验谈之-让百度地图API调用数据库内容 及详解
    这段时间,所在项目中要用到的一个模块,就是让数据库中的内容在百度地图上展现出来,如经纬度。主要实现以下几点功能:1.读取数据库中的经纬度值在百度上标注出来。2.点击标注弹出对应信息。3 ... [详细]
  • 一、Struts2是一个基于MVC设计模式的Web应用框架在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。Struts2优点1、实现 ... [详细]
  • pyecharts 介绍
    一、pyecharts介绍ECharts,一个使用JavaScript实现的开源可视化库,可以流畅的运行在PC和移动设备上,兼容当前绝大部 ... [详细]
  • 文章目录简介HTTP请求过程HTTP状态码含义HTTP头部信息Cookie状态管理HTTP请求方式简介HTTP协议(超文本传输协议)是用于从WWW服务 ... [详细]
  • 这个问题发生在重新安装系统后,丢失了之前的privatekey等。所以解决方法就是提示的revokeandrequest。到developercenter中找到certificat ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
author-avatar
mobiledu2502902725
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有