Python網絡編程

Python 提供了兩個級別訪問的網絡服務。:html

  • 低級別的網絡服務支持基本的 socket,,能夠訪問底層操做系統Socket接口的方法。
  • 高級別的網絡服務模塊 socketserver, 能夠簡化網絡服務器的開發。

socket

查看socket類的幫助以下python

import socket  # 導入socket模塊
>>> help(socket.socket)

重點關注初始化函數:程序員

__init__(self, family=<AddressFamily.AF_INET: 2>, type=<SocketKind.SOCK_STREAM: 1>, proto=0, fileno=None)
  • family:網絡協議簇,默認值爲AF_INET
  • type:套接字的類型,根據是面向鏈接的仍是非鏈接分爲SOCK_STREAMSOCK_DGRAM
  • proto:套接字協議,通常默認爲0,表示
  • fileno:套接字的int型的文件描述符

下面實現一個TCP聊天室和一個UDP聊天室segmentfault

<!--more-->服務器

TCP聊天室

概要設計

獲取多個鏈接的處理網絡

開啓accept線程,執行accept操做開始阻塞,有客戶端鏈接時,再開啓一個線程recv進行數據接收的處理。而後accept線程繼續阻塞,等待後續客戶端的鏈接。socket

阻塞的處理tcp

服務端處理客戶端的鏈接時,有兩處存在阻塞,分別是:ide

  • 獲取鏈接時,socket.accept()會阻塞
  • 每個創建成功的鏈接在獲取數據時,socket.recv(1024)

所以這兩處都須要開啓線程單獨處理,不然會阻塞主線程。函數

客戶端主動斷開的處理

客戶端主動斷開時,若是不通知服務端,那麼服務端上保存的客戶端鏈接不會被清理,這是不合理的。所以客戶端主動斷開時,咱們在應用層約定,客戶端推出前須要發送/quit指令到服務端上,而後有服務端關閉socket。

TCP聊天室-server

聊天室的server端主要是監聽端口,處理來自client端的鏈接,而且分發數據到全部的client端

代碼

import socket
import threading


class TcpChatServer:
    def __init__(self, ip='192.168.110.13', port=9001):
        self.ip = ip
        self.port = port
        self.clients = {}
        self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
        self.event = threading.Event()

    def recv(self, so, ip ,port):
        while not self.event.is_set():
            data = so.recv(1024).decode()  # 將接受到的字節數據bytes轉化爲utf-8格式的字符串
            if data.strip() == '/quit':  # 客戶端主動斷開時的處理
                so.close()
                self.clients.pop((ip, port))
                return 
            for s in self.clients.values():  # 廣播發送
                s.send('{}:{}\n{}'.format(ip, port, data).encode())

    def accept(self):
        while not self.event.is_set():
            so, (ip, port) = self.sock.accept()
            self.clients[(ip, port)] = so
            # 由於so.recv會產生阻塞,所以單獨開一個線程處理數據的接受部分。這樣accept能夠繼續接受來自其餘客戶端的連接
            threading.Thread(target=self.recv, args=(so, ip, port), name='client-{}:{}'.format(ip, port)).start()

    def start(self):
        self.sock.bind((self.ip, self.port))
        self.sock.listen()
        t = threading.Thread(target=self.accept, daemon=True)  # 爲了避免阻塞主線程,單獨開啓一個線程處理accept(accept會阻塞線程)
        try:
            t.start()
            t.join()  # 阻塞直到獲取到KeyboardInterrupt
        except KeyboardInterrupt:
            self.stop()

    def stop(self):
        for s in self.clients.values():
            s.close()
        self.sock.close()
        self.event.set()  # 中止全部的循環


if __name__ == '__main__':
    tcp_chat_server = TcpChatServer()
    tcp_chat_server.start()

TCP聊天室-client

聊天室的client端主要是發起鏈接,鏈接到server端,而且要接受來自服務端廣播分發的消息。

代碼

import socket
import threading


class TcpChatClient:
    def __init__(self):
        self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
        self.event = threading.Event()

    def recv(self):  # 客戶端須要一直接收服務端廣播分發的消息
        while not self.event.is_set():
            data = self.sock.recv(1024).decode()
            data = data.strip()
            print(data)

    def send(self):  # 輸入消息就發送
        while not self.event.is_set():
            data = input()
            self.sock.send(data.encode())
            if data.strip() == '/quit':  # 發送/quit的時候自身關閉
                self.stop()

    def start(self, ip, port):
        self.sock.connect((ip, port))
        s = threading.Thread(target=self.send, daemon=False)
        r = threading.Thread(target=self.recv, daemon=False)
        s.start()
        r.start()

    def stop(self):
        self.sock.close()
        self.event.set()


if __name__ == '__main__':
    tcp_chat_client = TcpChatClient()
    tcp_chat_client.start('192.168.110.13', 9001)

UDP聊天室

概要設計

阻塞的處理

在UDP服務端接收客戶端的消息時,採用socket.recvfrom(1024)這個方法以便保存客戶端的地址信息,這個方法會阻塞當前線程,所以須要開啓線程單獨處理。

客戶端主動斷開的處理

UDP客戶端主動關閉以後,服務端是沒法檢測到客戶端已經關閉的。咱們能夠採用如下兩種方法:

  1. 若是相似於TCP採用約定退出指令的方法,那麼客戶端發送退出指令後就調用close方法,而後服務端根據獲得的指令剔除客戶端字典中對應的客戶端。
  2. 還能夠經過客戶端定時發送心跳給服務端,服務端經過心跳來判斷客戶端進程是否存活。

UDP聊天室-server

UDP服務端程序開啓線程等待接收客戶端的數據,而後廣播給其餘的客戶端,而且檢查全部鏈接的心跳是否超時。

代碼

import socket
import datetime
import threading


class UdpChatServer:
    def __init__(self, ip='192.168.110.13', port=9001):
        self.addr = (ip, port)
        self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
        self.clients = {}
        self.event = threading.Event()
    
    def recv(self):
        while not self.event.is_set():
            data, addr = self.sock.recvfrom(1024)
            data = data.decode().strip()
            now = datetime.datetime.now()
            if data == '#ping#':  # 判斷是否收到心跳
                self.clients[addr] = now  # 收到心跳則保存客戶端地址,而且更新時間戳
                continue
                
            disconnected = set()  # 沒收到一次數據就判斷全部的失效連接
            for addr, timestamp in self.clients.items():
                if (now - timestamp).total_seconds() > 10:  # 失效條件:2次(即10s)沒收到心跳就判斷客戶端關閉
                    disconnected.add(addr)
                else:
                    self.sock.sendto('{}:{}\n{}'.format(addr[0], addr[1], data).encode(), addr)
                    
            for addr in disconnected:
                self.clients.pop(addr)
            
    def start(self):
        self.sock.bind(self.addr)  # 綁定端口以後就開啓線程一直接受客戶端的數據
        t = threading.Thread(target=self.recv(), daemon=True)
        try:
            t.start()
            t.join()
        except KeyboardInterrupt:
            self.stop()
        
    def stop(self):
        self.event.set()
        self.sock.close()

if __name__ == '__main__':
    udp_chat_server = UdpChatServer()
    udp_chat_server.start()

UDP聊天室-client

UDP的客戶端的主線程一直在等待用戶輸入數據而後將數據發送到服務端,同時開啓了一個心跳進程和一個接受服務端廣播數據的線程。

代碼

import socket
import threading
import time


class UdpChatClient:
    def __init__(self, ip, port):
        self.addr = (ip, port)
        self.sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
        self.event = threading.Event()
    
    def heartbeat(self):  # 心跳線程函數:每5s發一次心跳
        while not self.event.wait(5):
            self.sock.sendto(b'#ping#', self.addr)
            
    def recv(self):  # 一直等待接受udp服務器廣播的數據
        while not self.event.is_set():
            data = self.sock.recv(1024)
            print(data.decode())
    
    def start(self):
        threading.Thread(target=self.heartbeat, name='heartbeat', daemon=True).start()
        threading.Thread(target=self.recv, name='recv', daemon=True).start()
        print('請在5s後發言')
        time.sleep(5)  # 由於服務端必須收到一個心跳以後纔會保存次客戶端,所以須要等待5s
        print('請開始發言')
        while not self.event.is_set():
            data = input('')
            data = data.strip()
            if data == '/quit':
                self.event.set()
                self.sock.close()
                return
            self.sock.sendto(data.encode(), self.addr)

if __name__ == '__main__':
    udp_chat_client = UdpChatClient('192.168.110.13', 9001)
    udp_chat_client.start()

SocketServer

TODO(Flowsnow):改寫聊天室程序的TcpChatServer和UdpChatServer

附一:TCP和UDP的本質區別

  • udp:全部的客戶端發來的數據報都堆積在隊列上,而後服務端一個一個的處理
  • tcp:每個客戶端和服務端都有一個鏈接通道,只處理對應客戶端的數據流

附二:參考資料

  1. socketserver — A framework for network servers

記得幫我點贊哦!

精心整理了計算機各個方向的從入門、進階、實戰的視頻課程和電子書,按照目錄合理分類,總能找到你須要的學習資料,還在等什麼?快去關注下載吧!!!

resource-introduce

念念不忘,必有迴響,小夥伴們幫我點個贊吧,很是感謝。

我是職場亮哥,YY高級軟件工程師、四年工做經驗,拒絕鹹魚爭當龍頭的斜槓程序員。

聽我說,進步多,程序人生一把梭

若是有幸能幫到你,請幫我點個【贊】,給個關注,若是能順帶評論給個鼓勵,將不勝感激。

職場亮哥文章列表:更多文章

wechat-platform-guide-attention

本人全部文章、回答都與版權保護平臺有合做,著做權歸職場亮哥全部,未經受權,轉載必究!

相關文章
相關標籤/搜索