WebSocket

不羁的心 提交于 2019-12-06 05:42:43

一 . WebSocket原理

  1.1.背景

WebSocket 是基于Http 协议的改进,Http 为无状态协议,基于短连接,需要频繁的发起请求,第二 Http 只能客户端发起请求,服务端无法主动请求。

  1.2.相同点

1.都是基于TCP的应用层协议。
2.都使用Request/Response模型进行连接的建立。
3.在连接的建立过程中对错误的处理方式相同,在这个阶段WS可能返回和HTTP相同的返回码。
4.都可以在网络中传输数据。

  1.3.不同点

1.WS使用HTTP来建立连接,但是定义了一系列新的header域,这些域在HTTP中并不会使用。
2.WS的连接不能通过中间人来转发,它必须是一个直接连接。
3.WS连接建立之后,通信双方都可以在任何时刻向另一方发送数据。
4.WS连接建立之后,数据的传输使用帧来传递,不再需要Request消息。
5.WS的数据帧有序。
6.WebSocket 分为握手和数据传输

  1.4.WebSocket的握手

客户端的握手如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务端的握手如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

客户端和服务端都发送了握手,并且成功,数据传输即可开始。

  1.5.建立握手的时候需要遵守的规则

1.请求【握手】信息中提取 Sec-WebSocket-Key
2.利用magic_string 和 Sec-WebSocket-Key 进行sha1加密,再进行base64加密
3.将加密结果响应给客户端
# 注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

  1.6.数据传输

通过Http握手之后,如果是http 协议的话,tcp 连接会断开,这里在http 头部指明了升级为 websocket, 所以tcp 连接不断开。   WebSocket在握手后发送数据并象下层TCP协议那样由用户自定义,还是需要遵循对应的应用协议规范。 WebSocket 数据传输以数据帧的形式传输。

  1.7.payload length:传输数据长度

# hashstr = b'\x81\x83\xceH\xb6\x85\xffz\x85'
# b'\x81    \x83    \xceH\xb6\x85\xffz\x85'   
# print(hashstr[1]) #也就是打印\x83得到一个数字,这个数字与127做位运算,得到一个值:
如果这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;
如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;
如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。

二 . 轮询

# 轮询
    客户端向服务器不断发起类似Http请求
    服务器不断的响应客户端

    带上你的身份牌 - 服务器校验身份
    大爷去找你的消息 - 服务器获取你应该拿到数据
    if:拿到数据
    else:拿不到数据 - 再次发起请求询问服务器消息
    
    劣势:
        1.双端资源浪费
    2.带宽资源占用
    3.不能保证数据实时性
    
上个世纪90年代 - 本世纪初:
    24bps == 4-6KB
    CPU == 800MHZ
    内存 == 256MB
    QQ -- ICQ 

三 . 长轮询

# 长轮询
    1.客户端向服务器发起一个请求 
    2.服务器保持这个请求 不返回不响应
    3.一定时间之后,服务器抛弃 or 返回
    4.客户端收到请求 立即再次发起保持
    
    你去传达室,大爷款待你喝茶,
    喝茶等消息(保持)
    上厕所或者大爷撵你走(断开)
    再次回去喝茶等消息(保持)
    
    劣势:
    1.服务器资源浪费
    2.不能保证数据实时性(可能在上厕所的时候来消息)
    优势:
    1.节省客户端资源(不用总去)
    2.保证数据有效
        
当时的环境:本世纪初 - 目前
    128bps == 20-30KB
    Cpu == 1.4GHZ 奔腾4
    内存 == 512MB

四 . 长连接

# 长连接    
    永久保持连接
    
    1.你和大爷之间装了一台电话分机
    2.你派人告诉大爷你的分机号码
    3.大爷拨通分机
    4.你告诉大爷,有消息说句话,我派人去拿
    5.你和大爷同时开启了闭音(我平常说话你听不见)
    
    劣势:
        1.服务器CPU要求较高
        
    优势:
        1.节省大量资源
        2.数据实时有效性
        3.带宽几乎不占用
    
现在的环境:
    100mps == 百兆光纤 == 5MB/s || 2KB
    CPU == 16核32线程 i9 3.2GHZ
    内存 == 16GB

基于 geventwebsocket 模块来实现websocket

需要导入模块

from geventwebsocket.handler import WebSocketHandler
from geventwebsocket.websocket import WebSocket
from gevent.pywsgi import WSGIServer

以下为Flask的版本

简陋版

后端

from flask import Flask,render_template,request
from geventwebsocket.handler import WebSocketHandler
from geventwebsocket.websocket import WebSocket
from gevent.pywsgi import WSGIServer
import json

app=Flask(__name__)

user_socket_list = []

user_socket_dict = {

}

# 这边的type 为标识当前类, 或定义类, 解释来源于百度..
@app.route("/ws/<username>")
def ws(username):
    # 获取连接当前websocket服务的socket对象
    user_socket = request.environ.get("wsgi.websocket") #type:WebSocket
    print(user_socket)
    if user_socket:
        # 将当前存在的用户对象放入 用户scoket字典中
        user_socket_dict[username] = user_socket
    print(len(user_socket_dict),user_socket_dict)
    while 1:
        # 接受传入的数据
        msg = user_socket.receive() # 收件人 消息 发件人
        # json反序列化获取对应的消息后, 拼接前端需要的数据, 这边获取socket的原始用户,用于返回消息
        msg_dict = json.loads(msg)
        msg_dict["from_user"] = username
        to_user = msg_dict.get("to_user")
        # chat = msg_dict.get("msg")
        u_socket = user_socket_dict.get(to_user) # type:WebSocket
        # 返回当前的用户信息
        u_socket.send(json.dumps(msg_dict))

        # for u_socket in user_socket_list:
        #     if u_socket == user_socket:
        #         continue
        #     try:
        #         u_socket.send(msg)
        #     except:
        #         continue

@app.route("/")
def index():
    print(123)
    return render_template("ws.html")

if __name__ == '__main__':
    # app.run("0.0.0.0",9527,debug=True)
    http_serv = WSGIServer(("0.0.0.0",9527),app,handler_class=WebSocketHandler)
    http_serv.serve_forever()

ps: main函数体重启动wsgiserver服务, 这边0.0.0.0:9527 开放所有的ip访问, 执行类为geventscoket里的 WebSocketHandler

前端

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  
  <title>Title</title>
  
</head>
<body>
<input type="text" id="username"> <button onclick="login()">登录聊天室</button>
给<input type="text" id="to_user">发送:<input type="text" id="msg"> <button onclick="send_msg()">发送</button>
<div id="chat_list" style="width: 500px;height: 500px;"></div>

</body>
<script type="application/javascript">
  var ws = null;

  function login() {
    var username = document.getElementById("username").value;
    // 创建WebSocket连接对象, 这边加username 为了匹配后端的有名分组, 来对不同的websocket进行区分
    ws = new WebSocket("ws://192.168.12.87:9527/ws/"+username);
    ws.onmessage = function (data) {
      console.log(data.data);
      var recv_msg = JSON.parse(data.data);
      var ptag = document.createElement("p");
      ptag.innerText= recv_msg.from_user + ":" + recv_msg.msg;
      document.getElementById("chat_list").appendChild(ptag);
    }
  }

  function send_msg() {
    var to_user = document.getElementById("to_user").value;
    var msg = document.getElementById("msg").value;
    var send_str = {
      "to_user" :to_user,
      "msg":msg
    };
    // 使用在login()函数中创建的ws对象 执行send方法来发送自己拼接的消息
    ws.send(JSON.stringify(send_str));
  }
</script>
</html>

WebSocket握手原理

magic_string 唯一不可改变

import socket, base64, hashlib
from pprint import pprint

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 9527))
sock.listen(5)
# 获取客户端socket对象
conn, address = sock.accept()
# 获取客户端的【握手】信息
data = conn.recv(1024)

print(data)
print("====== pprint ======================>")
pprint(data)
"""
GET /ws HTTP/1.1\r\n
Host: 127.0.0.1:9527\r\n
Connection: Upgrade\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36\r\n
Upgrade: websocket\r\n
Origin: http://localhost:63342\r\n
Sec-WebSocket-Version: 13\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: session=a6f96c20-c59e-4f33-84d9-c664a2f29dfc\r\n
Sec-WebSocket-Key: MAZZU5DPIxWmhk/UWL2+BA==\r\n
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n
"""
# 以下动作是有websockethandler完成的
# magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11


def get_headers(data):
    header_dict = {}
    header_str = data.decode("utf8")
    for i in header_str.split("\r\n"):
        if str(i).startswith("Sec-WebSocket-Key"):
            header_dict["Sec-WebSocket-Key"] = i.split(":")[1].strip()

    return header_dict


headers = get_headers(data)  # 提取请求头信息

#魔法字符串, WebSocket协议中 这个magic_string是不改变的, 一旦变化握手必然失败
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
#Sec-WebSocket-Key: MAZZU5DPIxWmhk/UWL2+BA==
value = headers['Sec-WebSocket-Key'] + magic_string
print(value)

ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())

# 对请求头中的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://127.0.0.1:9527\r\n\r\n"
print("ac decode",ac.decode('utf-8'))
response_str = response_tpl % (ac.decode('utf-8'))
# 响应【握手】信息
conn.send(response_str.encode("utf8"))

while True:
    msg = conn.recv(8096)
    print(msg)

ps: 参key就是客户端发上来的Sec-WebSocket-Ke。 然后服务器进行sha1计算并且拼上一个GUID RFC6455中可以找到这个字符串。然后进行base64encode返回给客户端。客户端拿到后拿自己的key做同样的加密,如果对得上握手完成。到此为止就可以开始愉快的使用

WebSocket加密

import struct
msg_bytes = "the emperor has not been half-baked in the early days of the collapse of the road, today down three points, yizhou weakness, this serious crisis autumn".encode("utf8")
token = b"\x81" # + 数据长度/运算位 + mask/数据长度 + mask/数据 + 数据
length = len(msg_bytes)

if length < 126:
    token += struct.pack("B", length)
elif length == 126:
    token += struct.pack("!BH", 126, length)
else:
    token += struct.pack("!BQ", 127, length)

msg = token + msg_bytes

print(msg)

输出结果(b=>二进制形式的)

b'\x81\x7f\x00\x00\x00\x00\x00\x00\x00\x97the emperor has not been half-baked in the early days of the collapse of the road, today down three points, yizhou weakness, this serious crisis autumn'

X81开头, 为协议要求

信息长度决定了发送的消息的方式, 最终的结果在解密解读对应不同的类型进行位运算解开

WebSocket解密

#b'\x81\x89\xf3\x99\x81-\x15\x05\x01\xcbO\x1be\x97]'
#b'\x81\x85s\x92a\x10\x1b\xf7\r|\x1c'
#b'\x81\x83H\xc0x\xa6y\xf2K'

hashstr = b'\x81\x85s\x92a\x10\x1b\xf7\r|\x1c'
# b'\x81 \x85s \x92a\x10\x1b\xf7  \r|\x1c' <126
# \x85s = 5
hashstr = b'\x81\xfe\x02\xdc\x8d\xe8-\xb2hm\xa5W5u\xc8:\x16\x0c\x95(kt\x87W\x00b\xc52\x01\x0c\x95\x1fdi\xbeW9A\xcb\x1c\x0f\x07\x91>iS\xa7W)A\xc9\n\x06\x0c\x95;h`\xab]1d\xca)\x07\r\x9a,j~\x9fW1b\xc2\x0e\x01\x0e\x80\x16eG\xb7W\x00Y\xcb2(\r\x80*iR\x8cV4c\xca\x15\x06\x0c\x94-nh\xafU\t^\xc9\x0c\x00\r\xa0\x19iQ\xa6Z\nK\xc9\n\x00\x0e\xaa:iR\xa3W\x0bm\xc2\x0e\x01\r\x92\x12hW\xbaV4c\xc8\x11&\r\x92*eR\x86V7f\xc8\x16\x1b\x00\xad7bT\xa1U\x16~\xc5\r0\r\xa8:hP\xb0V4c\xcb\x1c\x07\x01\xac5bT\xa1T!Z\xcb8(\x0c\x949iR\xa3[\x14s\xc9\n\x06\x0c\x94-nh\xafZ"r\xc8\x1c\x11\r\x912hT\x8dW\x11K\xc8"!\x07\x91>iS\x88W\x08a\xc87\x05\r\x95/di\xbaW3_\xc2\x0e\x01\x0e\xac\x10hT\xb5W2\x7f\xc8\x11&\x0c\x949kX\xb9]1d\xc9\n\x00\r\x83.hN\xa9Z\nB\xc5=?\x00\xbb6bT\xa1W1}\xc8$6\r\x89\x03iQ\xa4]1d\xc9\t(\r\x8c,hW\x8dZ=g\xc9\x0b\x06\x00\x9a\x1diQ\xb2Q\rj\xc8\x1c&\x0c\x95\x1fhR\xb1V5E\xc2\x0e\x01\x0c\x92\x03iP\x97V5h\xc9\x0f\x1e\x07\x91>dq\xb2U0r\xc55*\r\xbd\x14bT\xa1V5e\xc8\x1c\x11\r\x910hx\xa1Q\rj\xc59(\x0e\xb1;iU\xb1W(P\xca8"\x0f\x8a#hg\xa7V5R\xc8\r-\r\xbb6eh\xa8]1d\xc8\x1c\x11\x0c\x96*kt\xa4W\x02P\xc5\x1c7\r\xa8\x04h`\xbcZ8g\xc2\x0e\x01\x0c\x96\x17kp\x80[\x14s\xc9\n\x06\r\x94\x01kp\xa3V4c\xca"\x0b\x07\x91>iP\xa0W#t\xc83\x02\x0f\x8a3bT\xa1V0W\xc84\x08\r\x89$hT\xafT>}\xc9\x0b\x12\x0b\xad0iV\xa0V5E\xce2\x0c\x0c\x93?dk\xa3[\x0eE\xcb&5\x0c\x949nh\xacZ9Q\xca\x17\x03\x0b\xad3ey\x8eW\x08i\xca\x1f\x04\x07\x91>kE\x89U\x17n\xc5;"\r\x83,bT\xa1W2\x7f\xc5+\x1c\r\x92\x12jR\x82]1d\xcb*"\x0c\x96\x17hm\xa5W5u\xca\x1c\r\x0e\xa6&iS\x88[\x0c\x7f\xc4+\x16\x0c\x959nh\xafT\tr\xc9\t(\x0c\x95\x08hF\x86V5E\xc9\x0b\x06\x0c\x979bT\xa1V7c\xcb%-\r\x89\x15hX\xa2]1d\xcb0\x04\x0c\x96\x17hz\x85V4c\xc2\x0e\x01\x0f\xa9\x04hx\xa3T\x1bU\xc5\x13\x01\x07\x91>hW\xa8Z\x0eU\xc5\x11%\x00\x8c\x17dp\xb4T1g\xc2\x0e\x01\x0e\xb1;ka\xadW4W\xca)\x07\x0b\xad0'
# print(chushibiao[1],chushibiao[1]&127)
# print(chushibiao[2:4],chushibiao[4:8])
# 将第二个字节也就是 \x83 第9-16位 进行与127进行位运算
payload = hashstr[1] & 127
print(payload)
if payload == 127:
    extend_payload_len = hashstr[2:10]
    mask = hashstr[10:14]
    decoded = hashstr[14:]
# 当位运算结果等于127时,则第3-10个字节为数据长度
# 第11-14字节为mask 解密所需字符串
# 则数据为第15字节至结尾

if payload == 126:
    extend_payload_len = hashstr[2:4]
    mask = hashstr[4:8]
    decoded = hashstr[8:]
# 当位运算结果等于126时,则第3-4个字节为数据长度
# 第5-8字节为mask 解密所需字符串
# 则数据为第9字节至结尾


if payload <= 125:
    extend_payload_len = None
    mask = hashstr[2:6]
    decoded = hashstr[6:]

# 当位运算结果小于等于125时,则这个数字就是数据的长度
# 第3-6字节为mask 解密所需字符串
# 则数据为第7字节至结尾

str_byte = bytearray()
# b'\x81 \x85s \x92a\x10\x1b \xf7\r|\x1c' <126
for i in range(len(decoded)): # 0  \xf7 ^ \x92a 1 \r ^ \x10 \x1c ^ \x1b
    byte = decoded[i] ^ mask[i % 4]
    str_byte.append(byte)

print(str_byte.decode("utf8"))

输出结果

先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。

获取mask, 解密所需字符串获得数据

总结:

扩展:

  http协议:\r\n分割、请求头和请求体\r\n分割、无状态、短连接。

  websocket协议:\r\n分割,创建连接后不断开、验证+数据加密;

websocket本质:

  就是一个创建连接后不断开的socket,当连接成功之后:

  客户端(浏览器)会自动向服务端发送消息,包含: Sec-WebSocket-Key: iyRe1KMHi4S4QXzcoboMmw==

  服务端接收之后,会对于该数据进行加密:

  base64(sha1(swk+magic_string))

构造响应头:

  HTTP/1.1 101 Switching Protocols\s\n

  Upgrade:websocket\r\n Connection: Upgrade\r\n

  Sec-WebSocket-Accept: 加密后的值\r\n

  WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n

发送客户端(浏览器)

  -建立:双工通道,接下来就可以进行收发数据  

  -发送的数据是加密,解密,根据payload_len的值进行处理:

  -payload_len <= 125

  -payload_len == 126

  -payload_len == 127

  获取内容:

  -mask_key

  数据

  根据mask_key和数据进行位运算,就可以把值解析出来。

Websocket再解释

WebSocket是一种在单个TCP连接上进行全双工通信的协议

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯

Django实现Websocket

django实现websocket大致上有两种方式,一种channels,一种是dwebsocket。channels依赖于redis,twisted等,相比之下使用dwebsocket要更为方便一些

回到目录

dwebsocket安装

pip3 install dwebsocket

dwebsocket配置

INSTALLED_APPS = [
    .....
    .....
    'dwebsocket',
]
 
MIDDLEWARE_CLASSES = [
    ......
    ......
    'dwebsocket.middleware.WebSocketMiddleware'  # 为所有的URL提供websocket,如果只是单独的视图需要可以不选
 
]
WEBSOCKET_ACCEPT_ALL=True   # 可以允许每一个单独的视图实用websockets

使用

html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    
    <title>Title</title>
</head>
<body>


<button onclick="WebSocketTest()">test</button>
</body>


<script>


    function WebSocketTest() {
        alert(1)
        if ("WebSocket" in window) {
            alert("您的浏览器支持 WebSocket!");

            // 打开一个 web socket
            ws = new WebSocket("ws://127.0.0.1:8000/path/");

            ws.onopen = function () {
                // Web Socket 已连接上,使用 send() 方法发送数据
                ws.send("发送数据");
                alert("数据发送中...");
            };

            ws.onmessage = function (evt) {
                var received_msg = evt.data;
                alert("数据已接收...");
                alert("数据:" + received_msg)
            };

            ws.onclose = function () {
                // 关闭 websocket
                alert("连接已关闭...");
            };
        }

        else {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
    }


</script>
</html>

views视图层:

from django.shortcuts import render,HttpResponse

# Create your views here.
def login(request):
    return render(request,'login.html')

from dwebsocket.decorators import accept_websocket
@accept_websocket
def path(request):
    if request.is_websocket():
        print(1)
        request.websocket.send('下载完成'.encode('utf-8'))

[

路由层:

from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^login/', views.login),
    url(r'^path/', views.path),
]

六 详解

dwebsocket有两种装饰器:require_websocket和accept_websocekt,使用require_websocket装饰器会导致视图函数无法接收导致正常的http请求,

一般情况使用accept_websocket方式就可以了,
dwebsocket的一些内置方法:
request.is_websocket():判断请求是否是websocket方式,是返回true,否则返回falserequest.websocket: 
当请求为websocket的时候,会在request中增加一个websocket属性,WebSocket.wait() 返回客户端发送的一条消息,没有收到消息则会导致阻塞WebSocket.read() 和wait一样可以接受返回的消息,只是这种是非阻塞的,没有消息返回NoneWebSocket.count_messages()返回消息的数量WebSocket.has_messages()返回是否有新的消息过来WebSocket.send(message)像客户端发送消息,message为byte类型

全理解后, 部分复制之

关于通讯方式的参考博客==>

https://www.cnblogs.com/huchong/p/8595644.html#_label1

关于django实现的websocket的参考博客==>

https://www.cnblogs.com/liuqingzheng/p/10151572.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!