代碼位置
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_error
爲False
- 此時客戶端將創建一個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()