實時請求數據-輪詢、長輪詢和WebSocket


在web開發時經常會遇到實時請求數據的需求,比如聊天功能、投票功能、搶購功能等,通過哪些技術可以實現這些功能呢,這里給出三種常用的技術,分別是輪詢,長輪詢和基於WebSock協議來實現,本文以基於Flask框架的開發的一個簡單的投票功能來演示這三種技術是如何實現投票和實時更新投票信息的。

輪詢

輪詢其實就是客戶端定時去請求服務端,  是客戶端主動請求來促使數據更新,如何實現的,例子如下;

服務端:

from flask import Flask,render_template,request,jsonify

app = Flask(__name__)

USERS = {
    '1':{'name':'劉能','count':1},
    '2':{'name':'趙四','count':0},
    '3':{'name':'廣坤','count':0},
}

@app.route('/user/list')
def user_list():
    import time
    return render_template('user_list.html',users=USERS)

@app.route('/vote',methods=['POST'])
def vote():
    uid = request.form.get('uid')
    USERS[uid]['count'] += 1
    return "投票成功"

@app.route('/get/vote',methods=['GET'])
def get_vote():

    return jsonify(USERS)

if __name__ == '__main__':
    # app.run(host='192.168.13.253',threaded=True)
    app.run(threaded=True)

客戶端:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        li{
            cursor: pointer;
        }
    </style>
</head>
<body>
    <ul id="userList">
        {% for key,val in users.items() %}
            <li uid="{{key}}">{{val.name}} ({{val.count}})</li>
        {% endfor %}
    </ul>

    <script src="https://cdn.bootcss.com/jquery/3.3.0/jquery.min.js"></script>
    <script>

        $(function () {
            $('#userList').on('dblclick','li',function () {
                var uid = $(this).attr('uid');
                $.ajax({
                    url:'/vote',
                    type:'POST',
                    data:{uid:uid},
                    success:function (arg) {
                        console.log(arg);
                    }
                })
            });

        });

/* 獲取投票信息 */ function get_vote() { $.ajax({ url:'/get/vote', type:"GET", dataType:'JSON', success:function (arg) { $('#userList').empty(); $.each(arg,function (k,v) { var li = document.createElement('li'); li.setAttribute('uid',k); li.innerText = v.name + "(" + v.count + ')' ; $('#userList').append(li); }) } }) } setInterval(get_vote,3000); </script> </body> </html>

從上面可以看出來輪詢主要有兩個缺點:(1)大量耗費服務器內存和寬帶資源,因為不停的請求服務器,很多時候 並沒有新的數據更新,因此絕大部分請求都是無效請求;(2)數據不一定是實時更新,要看設定的請求間隔,基本會有延遲,如何解決這兩個問題呢,長輪詢就能夠解決這兩個問題。

長輪詢

長輪詢其實 也是客戶端請求服務端,但是服務端並不是即時返回,而是當有內容更新的時候才返回內容給客戶端,從流程上講,可以理解為服務器向客戶端推送內容;

服務端:

from flask import Flask,render_template,request,jsonify,session
import uuid
import queue

app = Flask(__name__)
app.secret_key = 'asdfasdfasd'

USERS = {
    '1':{'name':'劉能','count':1},
    '2':{'name':'趙四','count':0},
    '3':{'name':'廣坤','count':0},
}

QUEQUE_DICT = {
    # 'asdfasdfasdfasdf':Queue()
}

@app.route('/user/list')
def user_list():
    user_uuid = str(uuid.uuid4())
    QUEQUE_DICT[user_uuid] = queue.Queue()

    session['current_user_uuid'] = user_uuid
    return render_template('user_list.html',users=USERS)

@app.route('/vote',methods=['POST'])
def vote():
    uid = request.form.get('uid')
    USERS[uid]['count'] += 1
    for q in QUEQUE_DICT.values():
        q.put(USERS)
    return "投票成功"


@app.route('/get/vote',methods=['GET'])
def get_vote():
    user_uuid = session['current_user_uuid']
    q = QUEQUE_DICT[user_uuid]

    ret = {'status':True,'data':None}
    try:
        users = q.get(timeout=5)  # 沒有新的數據進來時,最多夯住請求5秒時間,時間可以自己定
        ret['data'] = users
    except queue.Empty:
        ret['status'] = False

    return jsonify(ret)


if __name__ == '__main__':
    app.run(host='192.168.13.253',threaded=True)
    # app.run(threaded=True)

客戶端:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        li{
            cursor: pointer;
        }
    </style>
</head>
<body>
    <ul id="userList">
        {% for key,val in users.items() %}
            <li uid="{{key}}">{{val.name}} ({{val.count}})</li>
        {% endfor %}
    </ul>

    <script src="https://cdn.bootcss.com/jquery/3.3.0/jquery.min.js"></script>
    <script>

        $(function () {
            $('#userList').on('click','li',function () {
                var uid = $(this).attr('uid');
                $.ajax({
                    url:'/vote',
                    type:'POST',
                    data:{uid:uid},
                    success:function (arg) {
                        console.log(arg);
                    }
                })
            });
            get_vote();
        });

        /*
        獲取投票信息
         */
        function get_vote() {
            $.ajax({
                url:'/get/vote',
                type:"GET",
                dataType:'JSON',
                success:function (arg) {   
                    if(arg.status){   // 獲取到了新的數據,改變網頁展示的數據
                        $('#userList').empty();
                            $.each(arg.data,function (k,v) {
                                var li = document.createElement('li');
                                li.setAttribute('uid',k);
                                li.innerText = v.name + "(" + v.count + ')' ;
                                $('#userList').append(li);
                            })
                    }
                    get_vote();  // 不管獲取到了還是沒有獲取到新的數據,都會再次發一個請求

                }
            })
        }

    </script>
</body>
</html>

這里的長輪詢解決了輪詢中的兩個問題,使實時請求數據的技術前進了一大步;但是本身在hold住請求的時候也會消耗一些服務端的資源,並且不能做到真正的前后端連接不斷開,只是斷開的時間非常短,肉眼無法看到而已,那么如何做到比較完美一點的連接呢,WebSocket就能實現不斷開連接。

WebSocket 

WebSocket協議是基於TCP的一種新的協議,同時也是在http請求的基礎上發展的。WebSocket最初在HTML5規范中被引用為TCP連接,作為基於TCP的套接字API的占位符。它實現了瀏覽器與服務器全雙工(full-duplex)通信。其本質是保持TCP連接,在瀏覽器和服務端通過Socket進行通信。

下面將使用Python編寫Socket服務端,一步一步分析請求過程!!!

websocket 其實就是 web socket,它也是一種協議

http 的問題:
    1. http 是一個協議 - 數據格式 - 一次請求和響應之后斷開連接(無狀態、短連接) 2. 服務端不可以向客戶端主動推送消息(因為不知道客戶端的IP端口) 3. 服務端只能做出響應 4. 為了偽造服務端向客戶端主動推送消息的效果,我們使用:輪詢和長輪詢 websocket 是一種新的協議: 1. 連接時需要握手;2.發送數據進行加密;3.連接之后不斷開; websocket 解決了 服務端能夠真正向客戶端推送消息;缺點:兼容性 - 數據格式: - 連接請求: http協議 - 收發請求: websocket協議 - 不斷開連接 基於 flask的websocket示例: # 安裝: pip install gevent-websocket

支持 websocket 的框架:
  所有框架都支持,但是,
  flask : gevent-websocket
  django:channel
  tornado 框架自帶 websocket

應用場景:實時響應

服務端:

from flask import Flask,render_template,request
import json # 使用 websocket 時 需要導入下面的兩個模塊 from geventwebsocket.handler import WebSocketHandler from gevent.pywsgi import WSGIServer # 發送 websocket 請求得通過 js 來實現  app = Flask(__name__) USERS = { '1':{'name':'劉能','count':0}, '2':{'name':'趙四','count':0}, '3':{'name':'廣坤','count':100}, } # http://127.0.0.1:5000/index # 瀏覽器在渲染 index.html 時,在執行 var ws = new WebSocket('ws://192.168.13.253:5000/message') 這句js代碼時,客戶端就會和服務端建立一個 websocket 的連接 @app.route('/index') def index(): return render_template('index.html',users=USERS) # http://127.0.0.1:5000/message WEBSOCKET_LIST = [] # 用於保存所有投票人員的 websocket 連接;可理解為所有的客戶端  @app.route('/message') def message(): ws = request.environ.get('wsgi.websocket') # 如果不是 websocket 協議的請求,request.environ.get('wsgi.websocket') 的值是 None;如果是 websocket 請求, ws 將是一個 websocket 對象 if not ws: print('http') return '您使用的是Http協議' WEBSOCKET_LIST.append(ws) # 把當前的 ws 連接添加到 WEBSOCKET_LIST 中 while True: cid = ws.receive() # 接收投票的 cid; ws.receive() :接收客戶端發過來的 websocket協議的 數據(有數據就往下走,沒有請求數據時就在這里夯住) if not cid: # 如果客戶端關閉連接 ws.receive() 接收到的將會是 None  WEBSOCKET_LIST.remove(ws) ws.close() # 后台的 ws 也關閉 break old = USERS[cid]['count'] new = old + 1 USERS[cid]['count'] = new for client in WEBSOCKET_LIST: client.send(json.dumps({'cid':cid,'count':new})) # 給前端返回一個字典(數據會發給還在列表中的每一個投票人員) # 服務端通過 websocket 協議就可以向客戶端主動推送消息;這是由於 客戶端與服務端的連接沒有斷開; 需要事先在客戶端的瀏覽器上 new 一個 WebSocket 的對象 (WebSocket 支持H5等,但低版本的瀏覽器不支持) if __name__ == '__main__': http_server = WSGIServer(('0.0.0.0', 5000), app, handler_class=WebSocketHandler) # 如果是 http 協議的請求,就交給 app 去處理;如果是 websocket 協議的請求,就交給 WebSocketHandler 去處理。 # app 的 werkzeug 只能處理 http 請求 http_server.serve_forever()

 客戶端:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1>丑男投票系統</h1>
    <ul>
        {% for k,v in users.items() %}
            <li onclick="vote({{k}})" id="id_{{k}}">{{v.name}}<span>{{v.count}}</span></li> {% endfor %} </ul> <script src="https://cdn.bootcss.com/jquery/3.3.0/jquery.min.js"></script>
 <script> var ws = new WebSocket('ws://192.168.13.253:5000/message') // 實例化一個WebSocket 對象: 用於向 192.168.13.253:5000/message 這個url 發送 websocket 協議的請求;該連接不會斷開 // 前端通過 ws.send("你好") 來發送 websocket協議的數據  ws.onmessage = function (event) { // 服務端返回 ws 數據的時候, ws.onmessage() 自動觸發 /* 服務器端向客戶端發送數據時,自動執行 */ // {'cid':cid,'count':new} var response = JSON.parse(event.data); // event.data 就是 服務端返回回來的數據  $('#id_'+response.cid).find('span').text(response.count); }; function vote(cid) { ws.send(cid) // 發送 websocket 數據  } </script> </body> </html>

 可以看出WebSocket可以真正的實現不斷開連接,實時的請求數據,使用起來也比較簡單,僅限於使用的話參考上面示例足夠使用了。

探索WebSocket實現原理(選看)

但是websocket的實現原理比較復雜,感興趣的話繼續往下探索大致步驟如下:首先建立連接時,前端通過js代碼創建websocket對象時,向后端發請求,請求頭中有段秘鑰,后端通過使用秘鑰,並通過一定規則加密的響應返回給前端,前端判斷正確后才真正的建立了連接;然后就是彼此收發數據了,所有發的數據也都是加密了的,具體的如何實現這些過程的請往下看:

1. 啟動服務端

?
1
2
3
4
5
6
7
8
9
10
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. 客戶端連接

?
1
2
3
4
<script type = "text/javascript" >
     var socket = new WebSocket( "ws://127.0.0.1:8002/xxoo" );
     ...
< / script>

當客戶端向服務端發送連接請求時,不僅連接還會發送【握手】信息,並等待服務端響應,至此連接才創建成功!

3. 建立連接【握手】

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
  • 利用magic_string 和 Sec-WebSocket-Key 進行hmac1加密,再進行base64加密
  • 將加密結果響應給客戶端

注:magic string為:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

請求【握手】信息為:

1
2
3
4
5
6
7
8
9
10
11
12
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值並加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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服務端需要手動實現。

第一步:獲取客戶端發送的數據【解包】

基於Python實現解包過程(未實現長內容)

解包詳細過程: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ...                |
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

 第二步:向客戶端發送數據【封包】

View Code

5. 基於Python實現WebSocket連接示例(匯總了上面4步)

a. 基於Python socket實現的WebSocket服務端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import base64
import hashlib
 
 
def get_headers(data):
     """
     將請求頭格式化成字典
     :param data:
     :return:
     """
     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[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
 
 
def send_msg(conn, msg_bytes):
     """
     WebSocket服務端向客戶端發送消息
     :param conn: 客戶端連接到服務器端的socket對象,即: conn,address = socket.accept()
     :param msg_bytes: 向客戶端發送的字節
     :return:
     """
     import struct
 
     token = b "\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
 
 
def run():
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
     sock.bind(( '127.0.0.1' , 8003 ))
     sock.listen( 5 )
 
     conn, address = sock.accept()
     data = conn.recv( 1024 )
     headers = get_headers(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"
 
     value = headers[ 'Sec-WebSocket-Key' ] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
     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 :
         try :
             info = conn.recv( 8096 )
         except Exception as e:
             info = None
         if not info:
             break
         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' )
         send_msg(conn,body.encode( 'utf-8' ))
 
     sock.close()
 
if __name__ = = '__main__' :
     run()

b. 利用JavaScript類庫實現客戶端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<! DOCTYPE html>
< html >
< head lang="en">
     < meta charset="UTF-8">
     < title ></ 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="content"></ div >
 
< script type="text/javascript">
     var socket = new WebSocket("ws://127.0.0.1:8003/chatsocket");
 
     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 >
</ body >
</ html >



作者: E-QUAL
出處: https://www.cnblogs.com/liujiajia_me/
本文版權歸作者和博客園共有,不得轉載,未經作者同意參考時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。
                                            本文內容參考如下網絡文獻得來,用於個人學習,如有侵權,請您告知刪除修改。
                                            參考鏈接: https://www.cnblogs.com/linhaifeng/
                                                              https://www.cnblogs.com/yuanchenqi/
                                                              https://www.cnblogs.com/Eva-J/
                                                              https://www.cnblogs.com/jin-xin/
                                                              https://www.cnblogs.com/liwenzhou/
                                                              https://www.cnblogs.com/wupeiqi/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM