Python Tornado 實現WEB服務器Socket服務器共存並實現交互

1、背景

最近有個項目,需要搭建一個socket服務器,一個web服務器,然後實現兩個服務器之間的通訊交互。剛開始的方案是用Python中socket模塊實現一個多線程的socket服務器,然後用Flask實現一個web服務器,他們之前通過線程交互實現通訊。
但是在我看來這個方案有例外一個更好的解決方法,就是用Torndao框架。鑑於網上用Tornado實現一個程序同時實現web服務和socket服務器並且實現交互的文章幾乎沒有,所以記錄一下。覺得寫得好麻煩點個贊,寫得不好請指出,有疑問可以留言。

2、準備

2.1、環境部署

  • Python3.x
  • pip3 install Tornado

2.2、目錄結構

在這裏插入圖片描述
目錄結構如上圖,這個目錄結構包括文件命名只是我的個人習慣。其實目錄結構不固定,只要合理就行。另外,原本項目是前後分離的只需要實現API接口,所以我這裏就沒有涉及到HTML的東西。

3、服務器的實現

3.1、Socket服務器實現

socket服務器部分實現主要靠 Tornado中的TCPServer類

3.1.1、 導入類

socket_server.py:

from tornado.iostream import IOStream   # 這句可以沒有,只是作爲參數的代碼提示
from tornado.tcpserver import TCPServer

3.1.2、 構建一個Connecter類

socket_server.py:

class Connecter:

    clients = set()     # 存放連接的客戶端

    async def init(self, stream: IOStream, address: tuple):
        """
        注意這個不是構造方法,這裏不用構造方法是爲了方便後續的與web端相互通信
        """
        self.stream, self.address = stream, address
        self.clients.add(self)
        print("{address} 上線!".format(address=address))
        self.stream.set_close_callback(self.onClose)  # 客戶端離線的時候會被調用
        await self.receive()        # 接受消息

    async def receive(self):
        """
        接受消息
        """
        while True:
            try:    # 因爲異步的原因。可能設備離線後還在接收消息,所以try一下,不讓錯誤打印出來,其實打印了錯誤也不影響程序運行
                data = await self.stream.read_bytes(num_bytes=1024, partial=True)   # num_bytes:每次最多接收字節,partial:數據中斷後視爲接收完成
                print(data)
                # TODO:接收到數據的處理
            except: 
                pass

    def send(self, data):
        """
        發送消息
        :param data: 消息內容
        """
        self.stream.write(bytes(data.encode('utf8')))

    def onClose(self):
        """
        客戶端離線
        """
        print("{address} 離線!".format(address=self.address))
        self.clients.remove(self)  # 在clients內刪掉該客戶端

3.1.3、 構建一個SocketServer類

socket_server.py:

class SocketServer(TCPServer):      # 需要繼承TCPServer這個類
    async def handle_stream(self, stream: IOStream, address: tuple):   # 實現類裏面的handle_stream方法
        await Connecter().init(stream, address)      # 每次有客戶端連入都實例化一個Connecter類

3.2、Web服務器實現

3.2.1、 實現一個requestHandler

web_test.py:

from tornado.web import RequestHandler      # 導入RequestHandler類


class TestApiHandler(RequestHandler):       # 繼承RequestHandler類

    def get(self):      # 實現GET方法,GET請求會執行這個方法
        pass

    def post(self):     # 實現POST方法,POST請求會執行這個方法
        pass

3.2.2、 實現web app

web_server.py:

from tornado.web import Application         # 導入Tornado的Application類
from .src.web_test import TestApiHandler    # 導入我們自己寫的TestApiHandler類


def webServerApp():     # 構造出webApp
    return Application([
        (r'/api_test/', TestApiHandler),    # 把/api_test/路由到TestApiHandler
    ])

3.3、程序入口

3.3.1、 導入web_server和socket_server,還有導入tornado的ioloop

main.py:

from web_server.web_server import webServerApp
from socket_server.socket_server import SocketServer
from tornado import ioloop
from tornado.options import define, options

3.3.2、 定義默認端口

main.py

#這裏用define定義端口,可以方便使用命令行參數的形式修改端口
define("socketPort", 8888, type=int)    # socket默認使用8888端口
define("webPort", 8080, type=int)       # web默認使用8080端口

3.3.3、 啓動代碼

main.py

def main():
    socket_server = SocketServer()
    socket_server.listen(options.socketPort, '0.0.0.0')
    print("socket服務器啓動,端口:{port}".format(port=options.socketPort))
    app = webServerApp()
    app.listen(options.webPort, '0.0.0.0')
    print("web服務器啓動,端口:{port}".format(port=options.webPort))
    ioloop.IOLoop.current().start()


if __name__ == '__main__':
    main()

4、服務器運行效果

到此,一個混合型的socket+web服務器已經搭建好了。我們我們運行main.py文件可以看到打印的信息,socket和web都正常運行。
在這裏插入圖片描述
我在這裏簡單地寫了一個socket客戶端測試,代碼如下:

import socket
import datetime


class Client:

    def __init__(self):
        with socket.create_connection(("127.0.0.1", 8888)) as sock:
            while True:
                msg = sock.recv(1024)
                if len(msg) > 0:
                    print(msg)
                    sock.send(bytes(str(datetime.datetime.now).encode('utf8')))
                msg = []
                        
                

if __name__ == "__main__":
    Client()

在這裏插入圖片描述
可以看到tornado異步的形式實現了多客戶端同時接入socket。同時也可以測試web接口是正常的,如下圖:
在這裏插入圖片描述

5、Web服務器與Socket服務器交互

重點來了,web和socket怎樣實現交互呢?其實很簡單。

5.2、 web >> socket

web_test.py -> TestApiHandler -> post:

5.2.1、 導入Connecter類

from socket_server.socket_server import Connecter

5.2.2、 實現請求接口發送消息到socket客戶端

    def post(self):     # 實現POST方法,POST請求會執行這個方法
        msg = self.get_argument("msg")  # 得到post請求中的msg的值
        ip = self.get_argument('ip')    # 得到要發送的ip
        c = Connecter()     # 實例化Connecter類
        counter = 0     # 記錄發送到客戶端的個數
        for client in c.clients:    # type:Connecter
            if client.address[0] == ip:     # 根據ip發送
                client.send(msg)    # 發送消息
                counter += 1        # 計數加1
        self.write("{'send_counter':" + str(counter) + "}")

5.2.3、 效果

請求接口可以返回數據,已經成功發送一個客戶端
在這裏插入圖片描述
客戶端也能收到消息:
在這裏插入圖片描述

5.1、 socket >> web

其實socket發送的消息讓web馬上收到消息是不太現實的,但是我們可以把數據保存起來(可以是數據庫、全局變量、緩存……),然後通過api接口再把數據取出。另外還有一種方法是通過socket和websocket進行交互通訊,這種方法是推薦的方法,同樣的也可以用Tornado去實現,感興趣可以去研究一下也很簡單。如何有需要我提供socket、websocket、web三個端都互相交互的例子可以留言。
這裏爲了簡單一點,我使用一個類作爲全局變量來保存數據,然後用接口訪問,拿出這個類的值來演示一下效果。

5.1.1、 聲明類作爲全局變量

socket_data_processing.py

class SocketData:
    msg = ""

5.1.2、 接受到的消息保存到這個類裏面的msg

socket_server.py -> Connecter -> receive

    async def receive(self):
        """
        接受消息
        """
        while True:
            try:    # 因爲異步的原因。可能設備離線後還在接收消息,所以try一下,不讓錯誤打印出來,其實打印了錯誤也不影響程序運行
                data = await self.stream.read_bytes(num_bytes=1024, partial=True)   # num_bytes:每次最多接收字節,partial:數據中斷後視爲接收完成
                print(data)
                from .src.socket_data_processing import SocketData
                SocketData.msg = data.decode('utf8')
            except:
                pass

5.1.3、 用get方法顯示socket顯示回來的數據

web_test.py -> TestApiHandler -> get:

    def get(self):      # 實現GET方法,GET請求會執行這個方法
        from socket_server.src.socket_data_processing import SocketData
        self.write(SocketData.msg)

5.1.4、 效果

在這裏插入圖片描述
可以看到,從socket傳過來的字符串,被我通過Api讀取到了。

6、完整代碼

GitHub:https://github.com/JohnDoe1996/socket-web

喜歡的話麻煩在github也點個星,謝謝!

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