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 運行截圖
- 服務器
- 客戶端
作者:SA19225176,萬有引力丶
參考資料來源:USTC Socket網絡編程