【網絡接口API】Python Socket與Linux Socket


Python Socket與Linux Socket

socket: Python的底層網絡接口,一般情況程序員不需要接觸到這個模塊。有更多的高級模塊,比如requests可以直接使用。本文章試圖從Python的socket模塊和linux socket api的角度來對Python實現網絡通訊方式進行分析,提高對TCP,UDP通訊方式的理解。最后用Python實現一個hello/hi的簡單的網絡聊天程序。

1. socket

在Python中如果想要使用一個自己的網絡應用層協議,或者說想使用純原生TCP,UDP來實現通訊,就需要使用Python的socket模塊。

import socket

socket模塊提供了訪問BSD套接字的接口。在所有現代Unix系統、Windows、macOS和其他一些平台上可用。

1.1 socket()方法

# 使用socket()方法返回一個socket對象
s = socket.socket([family[, type, proto, fileno]])

重要參數:

  • family: 套接字家族,如ipv4,ipv6,unix系統進程間通信
  • type: 套接字類型,如tcp,upd
參數 描述
family
socket.AF_INET(默認) IPv4
socket.AF_INET6 IPv6
socket.AF_UNIX Unix系統進程間通信
type
socket.SOCK_STREAM 流式套接字,TCP
socket.SOCK_DGRAM 數據報套接字,UDP
socket.SOCK_RAW 原始套接字

socket方法與Linux Socket的socket函數對應

// socket(協議域,套接字類型,協議)
int socket(int domain, int type, int protocol);

通過s = socket.socket()方法,得到了一個socket對象。

Python中的socket對象的成員方法,是對套接字系統調用的高級實現,往往比C語言更高級。


2. TCP

2.1 bind()方法

通常如果是服務器,需要綁定一個總所周知的地址用於提供服務,所以需要綁定一個(IP:PORT),客戶端可以通過連接這個地址來獲得服務。而客戶端則直接通過連接,由系統隨機分配一個端口號。

python中bind()方法傳入一個地址和端口的元組

s.bind((host: str, port: int))

linux socket將套接字作為對象,傳入一個套接字和地址結構體

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2.2 listen()方法

開始監聽,backlog指定在拒絕連接之前,操作系統可以掛起的最大連接數,默認為1

s.listen(backlog: int)

linux socket同樣需要額外傳入套接字參數

int listen(int sockfd, int backlog);

2.3 connect()方法

connect方法是客戶端用發起某個連接的,接受一個目標主機名和端口號的元組參數

# address -> (hostname, port)
s.connect(address)
# connect_ex是connect的擴展方法,不同在於返回錯誤代碼,而不是拋出錯誤
s.connect_ex(address)

linux socket中,參數分別為客戶端套接字socket描述符,服務器socket地址,socket地址長度

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2.4 accpet()方法

服務器依次調用socket(), bind(), listen()后就會監聽指定地址,客戶端通過connect()向服務器發起連接請求。服務器監聽到請求后會調用accept()函數接受請求。這樣端與端的連接就建立好了

python中,accept()方法阻塞進程,等待連接,返回一個新的套接字對象和連接請求者地址信息。

# accept() -> (socket object, address info)
s.accept()

linux socket中,第一個參數是服務器套接字描述符,第二個為一個地址指針,用於返回客戶端協議地址,第三個參數是協議地址長度。如果連接成功,函數返回值為內核自動生成的一個全新描述符

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

服務器的監聽套接字一般只創建一個,而accept函數會返回一個連接套接字,服務器與客戶端之間的通信是在連接套接字上進行。每來一個服務請求新建一個連接套接字與請求者通信,而監聽套接字只有一個,完成服務后對應的連接套接字就會被關閉。(可以理解成,監聽套接字是專門的接線員,只負責將電話轉接給別的部門)

2.5 recv()與send()

send(data[, flags]) ->count

發送數據到socket,發送前要將數據轉換為utf-8的二進制格式。返回發送數據長度,因為網絡可能繁忙,導致數據沒有全部發送完畢,所以要再次對剩下的數據進行發送。

python還有一個sendall()

sendall(data[, flags])

作用是不停調用send()函數,直到所有數據發送完畢

linux socket中的send()

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • send()函數先檢查協議是否正在發送緩沖區數據,等待協議發送完畢或則緩沖區已沒有數據,那么send比較sockfd緩沖區剩余空間大小和發送數據的len。
  • 如果len大於剩余空間大小,則等待協議發送緩沖中數據
  • 若len小於剩余空間,則將buf中數據拷貝到剩余空間
  • 若發送數據長度大於套接字發送緩沖區長度,則返回-1

python中,從已連接套接字讀取數據的函數為recv()

s.recv(bufsize: int)

從套接字接受數據,如果沒有數據到達套接字,將會阻塞直到來數據或則遠程連接關閉。

如果遠程連接關閉且數據已全部讀取,則拋出一個錯誤。

linux socket也有讀取數據函數recv()

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • recv()等待s發送緩沖區發送完畢
  • 檢查套接字s的接受緩沖區,若協議正在接收數據,則等待接受完畢。
  • 將接收緩沖區的數據拷到buf中,接受數據可能大於buf長度,所以需要多次調用recv()

3. UDP

在無連接的情況下,端到端需要使用另外的數據發送和接受方式

3.1 sendto()

python中發送UDP數據,將數據data發送到套接字,address是形式為(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。

s.sendto(data,address)

linux socket: 由於本地socket並沒有與遠端機器建立連接,所以在發送數據時應指明目的地址

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

該函數比send()函數多了兩個參數,dest_addr表示目地機的IP地址和端口號信息,而addrlen是地址長度。

3.2 recvfrom()

s.recvfrom() -> (data, address)

接收UDP數據,與recv()類似,但返回值是(data,address)。其中data是包含接收的數據,address是發送數據的套接字地址。

linux socket: recvfrom()的情況與sendto()類似,需要指針來存放發送數據的套接字地址

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

4. close()

python和linux socket都需要對套接字關閉

python:

s.close()

linux socket:

int close(int socketfd)

5. Python實現hello/hi的簡單的網絡聊天程序

5.1 server.py

#! /usr/bin/env python3

import socket
from threading import Thread
import traceback

HOST = "127.0.0.1"
PORT = 65432

def recv_from_client(conn):
    try:
        content = conn.recv(1024)
        return content
    except Exception:
        return None

class ServiceThread(Thread):
    def __init__(self, conn, addr):
        super().__init__()
        self.conn = conn
        self.addr = addr

    def run(self):
        try:
            while True:
                content = recv_from_client(self.conn)
                if not content:
                    break
                print(f"{self.addr}: {content.decode('utf-8')}")
                self.conn.sendall(content)
            self.conn.close()
            print(f"{self.addr[0]}:{self.addr[1]} leave.")
        except Exception:
            traceback.print_exc()

if __name__ == "__main__":
    s = None
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind((HOST, PORT))
        s.listen()
        print("Repeater server started successfully.")
        while True:
            conn, addr = s.accept()
            print(f"Connected from {addr}")
            service_thread = ServiceThread(conn, addr)
            service_thread.daemon = True
            service_thread.start()
    except Exception:
        traceback.print_exc()
    s.close()

5.2 client.py

#! /usr/bin/env python3

import socket
from threading import Thread

HOST = "127.0.0.1"
PORT = 65432

class ReadFromConnThread(Thread):
    def __init__(self, conn):
        super().__init__()
        self.conn = conn

    def run(self):
        try:
            while True:
                content = self.conn.recv(1024)
                print(f"\n({HOST}:{PORT}): {content.decode('utf-8')}\nYOUR:", end="")
        except Exception:
            pass

if __name__ == "__main__":
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    read_thread = ReadFromConnThread(s)
    read_thread.daemon = True
    read_thread.start()
    while True:
        content = input("YOUR:")
        if content == "quit":
            break
        s.sendall(content.encode("utf-8"))
    s.close()

5.3 運行截圖

  • 服務器

server

  • 客戶端

client

作者:SA19225176,萬有引力丶

參考資料來源:USTC Socket網絡編程


免責聲明!

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



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