Python socket程式設計

鹹魚彬發表於2021-04-25

背景

關於Python Socket程式設計,首先需要了解幾個計算機網路的知識,通過以下的幾個問題,有助於更好的理解Socket程式設計的意義,以及整個框架方面的知識:

  • TCP和UDP協議本質上的區別?

TCP協議,面向連線,可靠,基於位元組流的傳輸層通訊協議;UDP協議無連線,不可靠,基於資料包的傳輸層協議。

TCP協議在建立連線的過程需要經歷三次握手,斷開連線則需要經歷四次揮手,而這建立連線的過程增加了傳輸過程中的安全性。
而建立連線的過程則會消耗系統的資源,消耗更多的時間,而相比較UDP協議傳輸過程則不會出現這種問題。

總結來講,基於TCP協議傳輸,需要不斷的確認對方是否收到資訊,從而建立連線(確認過程次數有限制,即三次握手),UDP協議傳輸則
不需要確認接收端是否收到資訊,只需要將資訊發給對方。

  • TCP/IP協議棧、HTTP協議、Socket之間的區別和聯絡?

TCP/IP協議棧就是一系列網路協議,可以分為四層模型來分析:應用層、傳輸層、網路層、鏈路層;

HTTP協議(超文字傳輸協議)就是在這一協議棧中的應用層協議;HTTP協議簡單來說,它的作用就是規範資料的格式,讓程式能夠方便的識別,並且收發雙方都需要遵循同樣的協議格式進行資料傳輸。(應用層的協議也和HTTP協議的作用類似,不一樣的是定義不同的資料格式。)

Socket可以理解為TCP/IP協議棧提供的對外的操作介面,即應用層通過網路協議進行通訊的介面。Socket可以使用不同的網路協議進行端對端的通訊;

  • TCP Socket伺服器的通訊過程?

Server端:

建立連線(socket()函式建立socket描述符、bind()函式繫結特定的監聽地址(ip+port)、listen()函式監聽socket、accept()阻塞等待客戶端連線)

資料互動(read()函式阻塞等待客戶端傳送資料、write()函式傳送給客戶端資料)

Client端:

建立連線(socket()函式建立socket描述符、connect()函式向指定的監聽地址傳送連線請求)

資料互動(wirte()函式傳送服務端資料、read()函式足阻塞等待接受服務端傳送的資料)

  • socket和websocket之間的聯絡?

webosocket是一種通訊協議,不同於HTTP請求,客戶端請求服務端資源,服務端響應的通訊過程;websocket允許服務端主動
向客戶端推送訊息,同時做到客戶端和服務端雙向通訊的協議。(具體底層原理有待後面實踐,暫時未接觸)

  • HTTP,WSGI協議的聯絡和區別?

HTTP協議(超文字傳輸協議),屬於TCP/IP協議棧中應用層的協議。用於規範傳輸資料的格式,是一種客戶端和服務端傳輸的規則。

WSGI協議則是Python定義的Web伺服器和框架程式通訊的介面規則。兩者聯絡不大,強行說的話,Python框架程式主要處理的是HTTP請求。

(後期可以實現一個WSGI協議的Python框架,用於處理HTTP請求的實驗。)

  • 主流Web框架,非同步Web框架?

主流Web框架:Django、Flask

非同步Web框架:Tornado(內建非同步模組)、Snaic(Python自帶asyncio)、FastAPI(基於Starlette庫) 、aiohttp(基於asyncio)

  • asyncio,aiohttp之間的聯絡?(非同步程式設計)

asyncio是一個非同步IO庫,aiohttp就是基於asyncio的非同步HTTP框架(支援客戶端/服務端)

程式碼設計

Python提供了基本的socket模組:

  1. socket模組;提供了標準的BSD Sockets API;
  2. socketserver模組:提供了伺服器中心類,簡化伺服器的開發;

TCP Socket服務端

socket模組:

# -*- coding: utf-8 -*-
from socket import socket, AF_INET, SOCK_STREAM

def echo_handler(sock ,address):
	print("Get Connection from address:", address)

	while True:
		response = sock.recv(8192)
		if not response:
			break
		print(f"Got {response}")
		sock.sendall(response)

def echo_server(address, back_log=5):
	sock = socket(AF_INET, SOCK_STREAM)
	sock.bind(address)
	sock.listen(back_log)

	while True:
		sock_client, address = sock.accept()
		echo_handler(sock_client, address)

if __name__ == "__main__":
	echo_server(('localhost', 5000))

程式碼詳解:

  • 建立一個基於IPV4和TCP協議的Socket,這裡AF_INET指的是使用IPV4協議,SOCK_STREAM指定使用面向流的TCP協議,繫結監聽埠,設定等待連線的最大數量
  • 建立一個永久迴圈,獲取客戶端請求的連線,accept()會等待並返回一個客戶端的連線;
  • 連線建立後,等待客戶端資料,接受完客戶端資料,然後返回資料給客戶端,最後關閉連線

存在的問題:當出現多個客戶端請求時,由於是單個執行緒會發生阻塞的情況,所以如果需要多執行緒處理多個客戶端請求,可以這樣改;

from threading import Thread

while True:
        client_sock, address = sock.accept()
        thread = Thread(target=echo_handler, args=(client_sock, address))
        thread.start()

這樣的話,就會在每個客戶端請求的時候,生成一個子執行緒然後處理請求;
(但是存在一個問題:當突然大量請求連線,消耗系統資源達到上限後,很可能造成程式無法處理後續請求。)

socketserver模組:

from socketserver import BaseRequestHandler, TCPServer

class EchoHandler(BaseRequestHandler):
    def handle(self):
        print("Got Connection From: %s" % str(self.client_address))
        while True:
            msg = self.request.recv(8192)
            if not msg:
                break
            self.request.send(msg)

if __name__ == "__main__":
    server = TCPServer(("", 5000), EchoHandler)
    server.serve_forever()
from socketserver import StreamRequestHandler, TCPServer, ThreadingTCPServer
import time

class EchoHandler(StreamRequestHandler):
    def handle(self):
        print("Got Connection Address: %s" % str(self.client_address))
        for line in self.rfile:
            print(line)
            self.wfile.write(bytes("hello {}".format(line.decode('utf-8')).encode('utf-8')))

if __name__ == "__main__":
    serv = ThreadingTCPServer(("", 5000), EchoHandler)
    serv.serve_forever()

程式碼詳解:

  • 處理多個客戶端,初始化一個ThreadingTCPServer例項;
  • 設定繫結的IP地址和埠,以及處理類;
  • 使用StreamRequestHandler(使用流的請求處理程式類,類似file-like物件,提供標準檔案介面簡化通訊過程),重寫裡面的handle方法,獲取請求資料,返回資料給客戶端;

TCP Socket客戶端

socket模組:

# -*- coding: utf-8 -*-
from socket import socket, AF_INET, SOCK_STREAM
import time

def request_handler():
	start_time = time.time()
	sock_client = socket(AF_INET, SOCK_STREAM)
	sock_client.connect(('localhost', 5000))
	
	book_content = ""
	with open("send_books.txt", "r") as f:
		book_content = f.read()
	
	content_list = book_content.split("\n")
	for content in content_list:
		if content:
			sock_client.send((content).encode())
			time.sleep(2)
			response = sock_client.recv(8192)
			print(response)

	end_time = time.time()
	print("總共耗時:", end_time-start_time)

		

if __name__ == "__main__":
	request_handler()

UDP Socket

Socket模組:

from socket import socket, AF_INET, SOCK_DGRAM
import time

def time_server(address):
    sock = socket(AF_INET, SOCK_DGRAM)
    sock.bind(address)

    while True:
        msg, addr = sock.recvfrom(8192)
        print('Get message from', addr)
        resp = time.ctime()
        sock.sendto(resp.encode('ascii'), addr)

if __name__ == "__main__":
    time_server(('', 5000))

程式碼不詳解,和之前的差不多,注意不同的協議就完事了

客戶端測試:

from socket import socket, AF_INET, SOCK_DGRAM

if __name__ == "__main__":
    s = socket(AF_INET, SOCK_DGRAM)
    s.sendto(b'hello', ('localhost', 5000))
    text = s.recvfrom(8192)
    print(text)

socketserver模組:

from socketserver import BaseRequestHandler, UDPServer
import time


class TimeHandler(BaseRequestHandler):
    def handle(self):
        print("Got Connection %s".format(str(self.client_address)))
        data = self.request[0]
        print(data)
        msg, sock = self.request
        print(msg)
        data = time.ctime()
        sock.sendto(data.encode('ascii'), self.client_address)

if __name__ == "__main__":
    u = UDPServer(("localhost", 9999), TimeHandler)
    u.serve_forever()

程式碼不在贅述,如果需要多執行緒處理併發操作可以使用ThreadingUDPServer

總結

關於本篇介紹Python Socket程式設計,大都是皮毛,只是談到了Python實際處理socket的幾個模組,
關於socket底層方面的知識並未提及,先了解個大概,從實際使用方面出發,在實際使用過程中結合
計算機網路知識,能夠對socket在整個TCP/IP協議棧中的作用。

socket和socketserver模組都可以用來編寫網路程式,不同的是socketserver省事很多,你可以專注
業務邏輯,不用去理會socket的各種細節,包括不限於多執行緒/多程式,接收資料,傳送資料,通訊過程。

相關文章