服務器架構 操作小記

代碼位置

https://github.com/duganlx/fopnp/tree/m/py3

支持《Python之禪》示例協議的數據與規則

文件位置:fopnp/py3/chapter07/zen_utils.py

import argparse, socket, time

aphorisms = {b'Beautiful is better than?': b'Ugly.',
             b'Explicit is better than?': b'Implicit.',
             b'Simple is better than?': b'Complex.'}


def get_answer(aphorism):
    """Return the string response to a particular Zen-of-Python aphorism."""
    time.sleep(0.0)  # increase to simulate an expensive operation
    return aphorisms.get(aphorism, b'Error: unknown aphorism.')


def parse_command_line(description):
    """Parse command line and return a socket address."""
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('host', help='IP or hostname')
    parser.add_argument('-p', metavar='port', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    address = (args.host, args.p)
    return address


def create_srv_socket(address):
    """Build and return a listening server socket."""
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listener.bind(address)
    listener.listen(64)
    print('Listening at {}'.format(address))
    return listener


def accept_connections_forever(listener):
    """Forever answer incoming connections on a listening socket."""
    while True:
        sock, address = listener.accept()
        print('Accepted connection from {}'.format(address))
        handle_conversation(sock, address)


def handle_conversation(sock, address):
    """Converse with a client over `sock` until they are done talking."""
    try:
        while True:
            handle_request(sock)
    except EOFError:
        print('Client socket to {} has closed'.format(address))
    except Exception as e:
        print('Client {} error: {}'.format(address, e))
    finally:
        sock.close()


def handle_request(sock):
    """Receive a single client request on `sock` and send the answer."""
    aphorism = recv_until(sock, b'?')
    answer = get_answer(aphorism)
    sock.sendall(answer)


def recv_until(sock, suffix):
    """Receive bytes over socket `sock` until we receive the `suffix`."""
    message = sock.recv(4096)
    if not message:
        raise EOFError('socket closed')
    while not message.endswith(suffix):
        data = sock.recv(4096)
        if not data:
            raise IOError('received {!r} then socket closed'.format(message))
        message += data
    return message

代碼解析:

aphorisms

aphorisms = {b'Beautiful is better than?': b'Ugly.',
             b'Explicit is better than?': b'Implicit.',
             b'Simple is better than?': b'Complex.'}
  • 客戶端希望服務器理解的3個間題作爲aphorisms字典的鍵列出,對應的回答則以字典值的形式存儲

get_answer(aphorism)

def get_answer(aphorism):
    """Return the string response to a particular Zen-of-Python aphorism."""
    time.sleep(0.0)  # increase to simulate an expensive operation
    return aphorisms.get(aphorism, b'Error: unknown aphorism.')
  • 爲了在字典中查找回答而編寫的一個簡單的快速函數
  • 如果傳入的問題無法被識別的話,該函數會返回一個簡短的錯誤信息
  • 客戶端的請求始終以問號結尾
  • 回答則始終以句點結尾
  • 這兩個標點符號爲這個協議提供了封幀的功能

parse_command_line(description)

def parse_command_line(description):
    """Parse command line and return a socket address."""
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument('host', help='IP or hostname')
    parser.add_argument('-p', metavar='port', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    address = (args.host, args.p)
    return address
  • 提供了用於讀取命令行參數的通用機制

create_srv_socket(address)

def create_srv_socket(address):
    """Build and return a listening server socket."""
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listener.bind(address)
    listener.listen(64)
    print('Listening at {}'.format(address))
    return listener
  • 則用於構造TCP的監聽套接字,服務器通過監聽套接字來接受連接請求

accept_connections_forever(listener)

def accept_connections_forever(listener):
    """Forever answer incoming connections on a listening socket."""
    while True:
        sock, address = listener.accept()
        print('Accepted connection from {}'.format(address))
        handle_conversation(sock, address)
  • 只包含一個簡單的循環
  • 循環中不斷通過監聽套接字接受連接請求
  • 並且使用print()把每個連接的客戶端打印出來
  • 然後將連接套接字作爲參數傳遞給handle_conversation()

handle_conversation(sock, address)

def handle_conversation(sock, address):
    """Converse with a client over `sock` until they are done talking."""
    try:
        while True:
            handle_request(sock)
    except EOFError:
        print('Client socket to {} has closed'.format(address))
    except Exception as e:
        print('Client {} error: {}'.format(address, e))
    finally:
        sock.close()
  • 包含一個無限循環,來不斷地處理請求
  • 該程序會捕捉可能發生的錯誤,這樣的設計使得客戶端套接字的任何問題都不會引起程序的崩潰
  • 如果客戶端完成了所有的請求並且已經掛起,那麼最內層的數據接收循環會拋出EOFError異常作爲信號傳遞的方式
  • 程序專門在一個單獨的except從句中捕捉了E0FError異常,而將所有其他異常都視爲錯誤,這些錯誤被捕捉後會通過print()函數進行輸出
  • finally從句能夠確保無論該函數通過哪一條代碼路徑退出,始終都會將客戶端套接字關閉

handle_request(sock)

def handle_request(sock):
    """Receive a single client request on `sock` and send the answer."""
    aphorism = recv_until(sock, b'?')
    answer = get_answer(aphorism)
    sock.sendall(answer)
  • 簡單地讀取客戶端的問題,然後做出應答
  • 因爲send()調用本身無法保證數據發送的完整性,所以要使用sendall()

recv_until(sock, suffix)

def recv_until(sock, suffix):
    """Receive bytes over socket `sock` until we receive the `suffix`."""
    message = sock.recv(4096)
    if not message:
        raise EOFError('socket closed')
    while not message.endswith(suffix):
        data = sock.recv(4096)
        if not data:
            raise IOError('received {!r} then socket closed'.format(message))
        message += data
    return message
  • 使用第5章中概述的方法進行封幀
  • 不斷累加的字節字符串沒有形成一個完整的問題,就會不斷重複調用套接字的recv()方法

用於《Python之禪》示例協議的客戶端程序

文件位置:fopnp/py3/chapter07/client.py

import argparse, random, socket, zen_utils


def client(address, cause_error=False):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(address)
    aphorisms = list(zen_utils.aphorisms)
    if cause_error:
        sock.sendall(aphorisms[0][:-1])
        return
    for aphorism in random.sample(aphorisms, 3):
        sock.sendall(aphorism)
        print(aphorism, zen_utils.recv_until(sock, b'.'))
    sock.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Example client')
    parser.add_argument('host', help='IP or hostname')
    parser.add_argument('-e', action='store_true', help='cause an error')
    parser.add_argument('-p', metavar='port', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    address = (args.host, args.p)
    client(address, args.e)

說明:

  • 在正常情況下,cause_errorFalse
  • 此時客戶端將創建一個TCP套接字,然後發送3句格言作爲請求,每發送一個就等待服務器返回相應的答案
  • 不過有時會想知道本章的服務器會如何處理輸入有誤的情況,因此該客戶端提供了-e選項,用來發送不完整的問題,然後使服務器突然掛起
  • 如果沒有提供-e選項,那麼只要服務器已經啓動並且正確運行,就能在客戶端看到這3個問題以及相應的答案

運行效果

# 需要運行一個服務器
python client.py 127.0.0.1
b'Explicit is better than?' b'Implicit.'
b'Beautiful is better than?' b'Ugly.'
b'Simple is better than?' b'Complex.'

最簡單的可用服務器是單線程的

文件位置:fopnp/py3/chapter07/srv_single.py

import zen_utils

if __name__ == '__main__':
    address = zen_utils.parse_command_line('simple single-threaded server')
    listener = zen_utils.create_srv_socket(address)
    zen_utils.accept_connections_forever(listener)

說明

  • 上面的這個服務器要求提供一個命令行參數,供服務器用來監聽接請求的接口
  • 如果要防止LAN或網絡中的其他用戶訪問該服務器的話,應指定標準本地主機IP地址127.0.0.1作爲監聽接口
  • 提供空字符串作爲參數(這在Python中表示當前機器上的任意接口),這樣就能夠通過本機的所有接口來提供服務
  • 上一個連接一關閉,這個服務器就可以準備好進行下一個連接

運行效果

python srv_single.py 127.0.0.1
Listening at ('127.0.0.1', 1060)
Accepted connection from ('127.0.0.1', 11954)
Client socket to ('127.0.0.1', 11954) has closed

多線程服務器

文件位置:fopnp/py3/chapter07/srv_threaded.py

import zen_utils
from threading import Thread


def start_threads(listener, workers=4):
    t = (listener,)
    for i in range(workers):
        Thread(target=zen_utils.accept_connections_forever, args=t).start()


if __name__ == '__main__':
    address = zen_utils.parse_command_line('multi-threaded server')
    listener = zen_utils.create_srv_socket(address)
    start_threads(listener)

使用標準庫服務器模式構建的多線程服務器

文件位置:fopnp/py3/chapter07/srv_legacy1.py

from socketserver import BaseRequestHandler, TCPServer, ThreadingMixIn
import zen_utils


class ZenHandler(BaseRequestHandler):
    def handle(self):
        zen_utils.handle_conversation(self.request, self.client_address)


class ZenServer(ThreadingMixIn, TCPServer):
    allow_reuse_address = 1
    # address_family = socket.AF_INET6  # uncomment if you need IPv6


if __name__ == '__main__':
    address = zen_utils.parse_command_line('legacy "SocketServer" server')
    server = ZenServer(address, ZenHandler)
    server.serve_forever()
發佈了116 篇原創文章 · 獲贊 58 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章