python網絡通信 --- socket


socket

socket 通常被翻譯為“套接字”,它是計算機之間進行通信的一種約定或一種方式。通過socket這種約定,一台計算機可以接收其他計算機的數據,也可以向其他計算機發送數據。

Python標准庫提供了socket模塊來實現這種網絡通信。實例化一個socket類便能得到一個socket對象sock = socket.socket(),使用這個socket對象就可以進行通信了。常用的socket有兩種。

SOCK_STREAM 面向連接的流式socket,基於TCP協議
SOCK_DGRAM 無連接的數據報式socket,基於UDP協議

相同類型的socket才能正常的通信,因為他們都有各自發送和接收消息的協議。

socket對象

import socket
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None)

實例化時指定對應的參數可以得到不同類型的socket,默認使用IPV4和TCP協議的類型

參數 可選值 說明
family socket.AF_UNIX 只能夠用於單一的Unix系統進程間通信
  socket.AF_INET 默認使用IPv4協議
  socket.AF_INET6 使用IPv6協議
type socket.SOCK_STREAM 面向連接的流式socket,基於TCP協議
  socket.SOCK_DGRAM 無連接的數據報式socket,基於UDP協議

實踐

通過寫一個聊天的服務器和客戶端體驗這種通信

TCP服務端

使用socket構建一個最簡單TCP服務器可接收客戶端的連接。我們需要一個socket用於網絡通信,並監聽一個地址和端口,等待其他的網絡連接訪問該端口,代碼如下。

server = socket.socket()  # 創建

server.bind(('127.0.0.1', 8000))   # 綁定本機地址和端口

server.listen() # 開始監聽端口

# 阻塞等待客戶端的連接,連接后返回一個新的可與客戶端通信的socket和客戶端的(ip,port)
s, raddr = server.accept()

當執行上面的python程序后,操作系統將會啟動一個進程,該服務進程正在監聽8000端口,在Windows命令行中使用netstat -anp tcp | findstr 8000查詢監聽狀態。在Linux上可以使用ss -tanl | grep 8000命令查看。

C:\Users\user>netstat -anp tcp | findstr 8000
TCP    127.0.0.1:8000         0.0.0.0:0              LISTENING

下面構建一個完整的TCP服務器。這是基本的服務器和客戶端通信結構圖。根據結構圖構建聊天服務器

 

簡單步驟和思路

  • 創建socket
  • 綁定一個ip地址和端口
  • 開始監聽(listen)
  • 阻塞等待連接(accept)
  • 客戶端連接到來后,開啟新線程與該客戶端交互,發送和接收消息。(recv和send)
  • 同時我們使用主線程操作服務端退出。

通過以上分析,我們需要使用多線程,分別與服務器交互,等待客戶端連接,與一個連接后的客戶端交互;每當成功的連接一個客戶端,都需要新啟動一個線程進行交互。

import socket
import threading

class Server:
    def __init__(self, ip='127.0.0.1', port=8000):  # 設置默認值
        self.addr = ip, port
        self.lock = threading.Lock()
        self.sock = socket.socket()
        self.sock.bind(self.addr)
        self.socks = {"accept": self.sock}  # 將所有創建的socket都放字典,方便釋放

    def start(self):  # 啟動接口
        self.sock.listen()
        threading.Thread(target=self.accept, name="accept", daemon=True).start()

    def accept(self):  # 該線程等待連接並創建處理線程
        while True:
            s, raddr = self.sock.accept()
            with self.lock:
                self.socks[raddr] = s
            threading.Thread(target=self.recv, args=(s, raddr), name="recv", daemon=True).start()

    def recv(self, s, raddr):  # 每個客戶端開啟一個線程與其交互
        while True:
            data = s.recv(1024).decode()
            if data.strip() == "" or data.strip() == "quit":  # 客戶端結束條件
                with self.lock:
                    self.socks.pop(raddr)
                    s.close()
                    break
            print(data)
            s.send("server:{}\n".format(data).encode())

    def stop(self):
        with self.lock:
            for s in self.socks.values():
                s.close()
s = Server()
s.start()

while True:
    cmd = input("server commond:>>>")
    if cmd == "quit":  # 服務器退出條件
        s.stop()
        break
    print(threading.enumerate())

我們需要注意的問題:

  1. 服務端需要與多個不同客戶端進行交互,所以我們需要開啟不同線程去處理各自的業務,
  1. 為了服務端在啟動后可以獲得控制權,我們使用主線程來與服務器管理者交互,使用命令行輸入指令就能在服務器啟動后與服務器做一些交互,例如代碼中的強制關閉服務器,並在強制關閉服務前提前關閉掉這些socket對象。
  1. 在遍歷字典來關閉socket對象時,我們使用了鎖,要求在這個遍歷操作完成前,其他線程無法進行增加或者刪除操作,保證了字典遍歷時的線程安全。

socket常用的方法

  方法 含義
服務端 s.bind(address) 將套接字綁定到地址,以元組(host,port)的形式表示地址
  s.listen(backlog) 開始監聽TCP傳入連接。backlog:操作系統可以掛起的最大連接數量。該值至少為1,大部分應用程序設為5就可以了
  s.accept() 接受TCP連接並返回(conn,address),其中conn是新的套接字對象,可以用來接收和發送數據。address是連接客戶端的地址,為一個元組
客戶端socket函數 s.connect(address) 連接到address處的套接字,格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤
  s.connect_ex(adddress) 功能與connect(address)相同,但是成功返回0,失敗返回errno的值
公共socket函數 s.recv(bufsize[,flag]) 從s接受bytes類型的數據,有數據就接受返回,bufsize指定要接收的最大數據量
  s.send(bytes[,flag]) TCP發送數據。將bytes中的數據發送到連接的套接字。返回值是要發送的字節數量,該數量可能小於bytes的字節大小
  s.sendall(bytes[,flag]) 發送全部TCP數據。將bytes中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None,失敗則拋出異常
  sendfile() 使用os.sendfile()高效的發送文件的方法,必須使用SOCK_STREAM類型的套接字才能使用
  s.recvfrom(bufsize[.flag]) 接受UDP套接字的數據。與recv()類似,但返回值是(data,address)。其中data是包含接收數據的bytes,address是發送方地址
  s.sendto(string[,flag],address) 發送UDP數據。address是形式為(ipaddr,port)的元組。返回值是發送的字節數
     
  s.getpeername() 返回連接套接字的遠程地址(ipaddr,port)
  s.getsockname() 返回套接字自己的地址(ipaddr,port)
  s.setsockopt(level,optname,value) 設置給定套接字選項的值
  s.getsockopt(level,optname[.buflen]) 返回套接字選項的值
  s.settimeout(timeout) 設置套接字操作的超時間,值為None表示沒有超時期。一般超時期在創建時設置
  s.gettimeout() 返回當前超時期的值,單位是秒,如果沒有設置超時期,則返回None
  s.fileno() 返回套接字的文件描述符
  s.setblocking(flag) 設置阻塞模式,非阻塞模式下,如果調用recv()沒有發現任何數據,或send()調用無法立即發送數據,那么將引起socket.error異常
  s.makefile() 創建一個與該套接字相關連的文件,返回一個類文件對象,可是使用文件操作發送和接收數據

sendfile是一個高效的傳送方式,文件數據始終處於內核態,在操作系統緩沖區直接發送,不會到應用層緩沖區。

使用makefile方法將返回該socket對應的文件對象(io.TextIOWrapper),該對象的write()等價於send()方法, read方法等價於recv(),還可以使用readline等方法。這樣我們可以使用文件的接口去收發信息,客戶端將使用這種方式與服務器交互。

sock = socket.socket()
file = sock.makefile("rw")  # mode="rw" 可讀可寫

data = file.read()   # 等價於socket.recv()

data = file.read(10) # 指定讀取字符大小長度,滿10個字符才會返回。
data = file.readlin()   # 每次讀取一行,遇到換行符才返回。
# 寫入數據
msg = "hello world"
file.write(msg)
file.flush()      # 手動flush,否則在緩沖區滿或者退出時自動才寫入socket。同文件寫入操作

TCP客戶端

相比於服務端,客戶端只需要連接服務器后發送和接受消息即可,相對更容易實現。

客戶端需要同時接受和發送消息,而這兩個操作均會阻塞,所以兩個功能需要在不同的線程。下面代碼使用了socket的makefile()方法,使用文件對象進行收發數據。

import socket
import threading
import datetime

class Client:
    def __init__(self, rip, rport):  # 服務器ip 和 端口
        self._raddr = rip, rport
        self._sock = socket.socket()
        self._connect()

    def _connect(self):
        self._sock.connect(self._raddr)   # 嘗試連接指定的地址
        self.f = self._sock.makefile("rw")
        self.f.write("i am client at {}\n".format(self._sock.getsockname()))
        self.f.flush()
        threading.Thread(target=self.recv, name="recv", daemon=True).start()  # 一個進程接收消息
        self.send()   # 主進程發送消息

    def send(self):
        while True:
            msg = input(">>>").strip()
            self.f.write(msg)
            self.f.flush()
            if msg == "quit":
                self.stop()
                break

    def recv(self):
        while True:
            msg = self.f.readline()
            print("server:{}{:%Y/%m/%d %H:%M:%S}\n\t{}".format(self._sock.getpeername(), datetime.datetime.now(), msg))

    def stop(self):
        self.f.close()
        self._sock.close()

c = Client("127.0.0.1", 8000)

客戶端使用connect()方法將會嘗試連接服務器(這個服務必須存在,否則無法連接),由於服務基於TCP協議,所以在connect()連接時候,實際上會進行TCP三次握手的連接,但是我們在應用層面無法感知到這個下層行為。同樣的在進行close關閉socket時,在斷開連接前將會進行四次揮手操作。

 

使用makefile后會得到該socket的文件對象,在進行read和write時會先將數據放入緩沖區暫存,write方法對應一個發送緩沖區,將需要發送到對方的數據暫存到該緩沖區,在調用flush時才會將數據發送,當寫入緩沖區滿了而沒有及時發送數據,發送數據沒有緩存空間可用,將會發生阻塞等待。同樣read方法對應一個讀取緩沖區,每次從讀取緩沖區中讀取數據,緩沖區沒有數據可讀取將會發生阻塞等待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM