引言
sockets的歷史悠久,它們最早在 1971 年的 APPANET 中使用,后來成為1983年發(fā)布的Berkeley Software Distribution(BSD)操作系統(tǒng)中的API,稱為Berkeley sockets。
Web服務(wù)器和瀏覽器并不是使用sockets的唯一程序,各種規(guī)模和類型的客戶端 - 服務(wù)器(client - server)應(yīng)用程序也得到了廣泛使用。
今天,盡管socket API使用的底層協(xié)議已經(jīng)發(fā)展多年,而且已經(jīng)有新協(xié)議出現(xiàn),但是底層 API 仍然保持不變。
最常見的套接字應(yīng)用程序類型是客戶端 - 服務(wù)器(client - server)應(yīng)用程序,其中一方充當(dāng)服務(wù)器并等待來自客戶端的連接。
Socket API介紹
Python中的socket模塊提供了一個(gè)到Berkeley sockets API的接口,其中的主要接口函數(shù)如下:
socket()
bind()
listen()
accept()
connect()
connect_ex()
send()
recv()
close()
這些方便使用的接口函數(shù)和系統(tǒng)底層的功能調(diào)用相一致。
TCP Sockets
我們準(zhǔn)備構(gòu)建一個(gè)基于 TCP 協(xié)議的socket對(duì)象,為什么使用 TCP 呢,因?yàn)椋?/p>
可靠性:如果在傳輸過程中因?yàn)榫W(wǎng)絡(luò)原因?qū)е聰?shù)據(jù)包丟失,會(huì)有相關(guān)機(jī)制檢測(cè)到并且進(jìn)行重新傳輸
按序到達(dá):一方發(fā)送到另一方的數(shù)據(jù)包是按發(fā)送順序被接收的。
對(duì)比之下,UDP 協(xié)議是不提供這些保證的,但是它的響應(yīng)效率更高,資源消耗更少。
TCP 協(xié)議并不需要我們自己去實(shí)現(xiàn),在底層都已經(jīng)實(shí)現(xiàn)好了,我們只需要使用Python的socket模塊,進(jìn)行協(xié)議指定就可以了。socket.SOCK_STREAM表示使用 TCP 協(xié)議,socket.SOCK_DGRAM表示使用 UDP 協(xié)議
我們來看看基于 TCP 協(xié)議socket的 API 調(diào)用和數(shù)據(jù)傳送流程圖,右邊的一列是服務(wù)器端(server),左邊的一列是客戶端(client)。
要實(shí)現(xiàn)左邊的處于監(jiān)聽狀態(tài)的server,我們需要按照順序調(diào)用這樣幾個(gè)函數(shù):
socket(): 創(chuàng)建一個(gè)socket對(duì)象
bind(): 關(guān)聯(lián)對(duì)應(yīng) ip 地址和端口號(hào)
listen(): 允許對(duì)象接收其他socket的連接
accept(): 接收其他socket的連接,返回一個(gè)元組(conn, addr),conn 是一個(gè)新的socket對(duì)象,代表這個(gè)連接,addr 是連接端的地址信息。
client調(diào)用connect()時(shí),會(huì)通過 TCP 的三次握手,建立連接。當(dāng)client連接到server時(shí),server會(huì)調(diào)用accept()完成這次連接。
雙方通過send()和recv()來接收和發(fā)送數(shù)據(jù),最后通過close()來關(guān)閉這次連接,釋放資源。一般server端是不關(guān)閉的,會(huì)繼續(xù)等待其他的連接。
Echo Client and Server
剛才我們弄清楚了server和client使用socket進(jìn)行通信的過程,我們現(xiàn)在要自己進(jìn)行一個(gè)簡(jiǎn)單的也是經(jīng)典的實(shí)現(xiàn):server復(fù)述從client接收的信息。
Echo Server
import socket
HOST = '127.0.0.1' # Standard loopback interface address (localhost)
PORT = 65431 # Port to listen on (non-privileged ports are > 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
socket.socket()創(chuàng)建了一個(gè)socket對(duì)象,它實(shí)現(xiàn)了上下文管理器協(xié)議,我們直接用?with?語句進(jìn)行創(chuàng)建即可,而且最后不需要調(diào)用close()函數(shù)。
socket()中的兩個(gè)參數(shù)指明了連接需要的?ip地址類型和傳輸協(xié)議類型,socket.AF_INET 表示使用 IPv4的地址進(jìn)行連接,socket.SOCK_STREAM 表示使用 TCP 協(xié)議進(jìn)行數(shù)據(jù)的傳輸。
bind()用來將socket對(duì)象和特定的網(wǎng)絡(luò)對(duì)象和端口號(hào)進(jìn)行關(guān)聯(lián),函數(shù)中的兩個(gè)參數(shù)是由創(chuàng)建socket對(duì)象時(shí)指定的 ip地址類型 決定的,這里使用的是socket.AF_INET(IPv4),因此,bind()函數(shù)接收一個(gè)元組對(duì)象作為參數(shù)(HOST, PORT)
host可以是一個(gè)主機(jī)名,IP地址,或者空字符串。如果使用的是 IP地址,host必須是 IPv4格式的地址字符串。127.0.0.1是本地環(huán)路的標(biāo)準(zhǔn)寫法,因此只有在主機(jī)上的進(jìn)程才能夠連接到server,如果設(shè)置為空字符串,它可以接受所有合法 IPv4地址的連接。
port應(yīng)該是從1 - 65535的一個(gè)整數(shù)(0被保留了),它相當(dāng)于是一個(gè)窗口和其他的客戶端建立連接,如果想使用1 - 1024的端口,一些系統(tǒng)可能會(huì)要求要有管理員權(quán)限。
listen()使得server可以接受連接,它可以接受一個(gè)參數(shù):backlog,用來指明系統(tǒng)可以接受的連接數(shù)量,雖然同一時(shí)刻只能與一端建立連接,但是其他的連接請(qǐng)求可以被放入等待隊(duì)列中,當(dāng)前面的連接斷開,后面的請(qǐng)求會(huì)依次被處理,超過這個(gè)數(shù)量的連接請(qǐng)求再次發(fā)起后,會(huì)被server直接拒絕。
從Python 3.5開始,這個(gè)參數(shù)是可選的,如果我們不明確指明,它就采用系統(tǒng)默認(rèn)值。如果server端在同一時(shí)刻會(huì)收到大量的連接請(qǐng)求,通常要把這個(gè)值調(diào)大一些,在Linux中,可以在/proc/sys/net/core/somaxconn看到值的情況,詳細(xì)請(qǐng)參閱:
Will increasing net.core.somaxconn make a difference?
How TCP backlog works in Linux
accept()監(jiān)聽連接的建立,是一個(gè)阻塞式調(diào)用,當(dāng)有client連接之后,它會(huì)返回一個(gè)代表這個(gè)連接的新的socket對(duì)象和代表client地址信息的元組。對(duì)于 IPv4 的地址連接,地址信息是?(host, port),對(duì)于 IPv6 ,(host, port, flowinfo, scopeid)
有一件事情需要特別注意,accept()之后,我們獲得了一個(gè)新的socket對(duì)象,它和server以及client都不同,我們用它來進(jìn)行和client的通信。
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
conn是我們新獲得的socket對(duì)象,conn.recv()也是一個(gè)阻塞式調(diào)用,它會(huì)等待底層的 I/O 響應(yīng),直到獲得數(shù)據(jù)才繼續(xù)向下執(zhí)行。外面的while循環(huán)保證server端一直監(jiān)聽,通過conn.sendall將數(shù)據(jù)再發(fā)送回去。
Echo Client
import socket
HOST = '127.0.0.1' # The server's hostname or IP address
PORT = 65431 # The port used by the server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall("Hello, world".encode("utf8"))
data = s.recv(1024)
print('Received', data.decode("utf8"))
和server相比,client更加簡(jiǎn)單,先是創(chuàng)建了一個(gè)socket對(duì)象,然后將它和server連接,通過s.sendall()將信息發(fā)送給server,通過s.recv()獲得來自server的數(shù)據(jù),然后將其打印輸出。
在發(fā)送數(shù)據(jù)時(shí),只支持發(fā)送字節(jié)型數(shù)據(jù),所以我們要將需要發(fā)送的數(shù)據(jù)進(jìn)行編碼,在收到server端的回應(yīng)后,將得到的數(shù)據(jù)進(jìn)行解碼,就能還原出我們能夠識(shí)別的字符串了。
啟動(dòng)程序
我們要先啟動(dòng)server端,做好監(jiān)聽準(zhǔn)備,然后再啟動(dòng)client端,進(jìn)行連接。
這個(gè)信息是在client連接后打印出來的。
可以使用netstat這個(gè)命令查看socket的狀態(tài),更詳細(xì)使用可以查閱幫助文檔。
查看系統(tǒng)中處于監(jiān)聽狀態(tài)的socket,過濾出了使用 TCP協(xié)議 和 IPv4 地址的對(duì)象:
如果先啟動(dòng)了client,會(huì)有下面這個(gè)經(jīng)典的錯(cuò)誤:
造成的原因可能是端口號(hào)寫錯(cuò)了,或者server根本就沒運(yùn)行,也可能是在server端存在防火墻阻值了連接建立,下面是一些常見的錯(cuò)誤異常:
Exceptionerrno ConstantDescriptionBlockingIOErrorEWOULDBLOCKResource temporarily unavailable. For example, in non-blocking mode, when calling send() and the peer is busy and not reading, the send queue (network buffer) is full. Or there are issues with the network. Hopefully this is a temporary condition.OSErrorADDRINUSEAddress already in use. Make sure there’s not another process running that’s using the same port number and your server is setting the socket option SO_REUSEADDR: socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1).ConnectionResetErrorECONNRESETConnection reset by peer. The remote process crashed or did not close its socket properly (unclean shutdown). Or there’s a firewall or other device in the network path that’s missing rules or misbehaving.TimeoutErrorETIMEDOUTOperation timed out. No response from peer.ConnectionRefusedErrorECONNREFUSEDConnection refused. No application listening on specified port.
連接的建立
現(xiàn)在我們仔細(xì)看一下server和client是怎樣建通信的:
當(dāng)使用環(huán)路網(wǎng)絡(luò)((IPv4 address 127.0.0.1 or IPv6 address ::1))的時(shí)候,數(shù)據(jù)沒有離開過主機(jī)跑到外部的網(wǎng)絡(luò)。如圖所示,環(huán)路網(wǎng)絡(luò)是在主機(jī)內(nèi)部建立的,數(shù)據(jù)就經(jīng)過它來發(fā)送,從主機(jī)上運(yùn)行的一個(gè)程序發(fā)送到另一個(gè)程序,從主機(jī)發(fā)到主機(jī)。這就是為什么我們喜歡說環(huán)路網(wǎng)絡(luò)和 IP地址 127.0.0.1(IPv4) 或 ::1(IPv6) 都表示主機(jī)
如果server使用的時(shí)其他的合法IP地址,它就會(huì)通過以太網(wǎng)接口與外部網(wǎng)絡(luò)建立聯(lián)系:
如何處理多端連接
echo server最大的缺點(diǎn)就是它同一時(shí)間只能服務(wù)一個(gè)client,直到連接的斷開,echo client同樣也有不足,當(dāng)client進(jìn)行如下操作時(shí),有可能s.recv()只返回了一個(gè)字節(jié)的數(shù)據(jù),數(shù)據(jù)并不完整。
data = s.recv(1024)
這里所設(shè)定的參數(shù) 1024 表示單次接收的最大數(shù)據(jù)量,并不是說會(huì)返回 1024 字節(jié)的數(shù)據(jù)。在server中使用的send()與之類似,調(diào)用后它有一個(gè)返回值,標(biāo)示已經(jīng)發(fā)送出去的數(shù)據(jù)量,可能是小于我們實(shí)際要發(fā)送的數(shù)據(jù)量,比如說有 6666 字節(jié)的數(shù)據(jù)要發(fā)送,用上面的發(fā)送方式要發(fā)送很多此才行,也就是說一次調(diào)用send()數(shù)據(jù)并沒有被完整發(fā)送,我們需要自己做這個(gè)檢查來確保數(shù)據(jù)完整發(fā)送了。
因此,這里使用了sendall(),它會(huì)不斷地幫我們發(fā)送數(shù)據(jù)直到數(shù)據(jù)全部發(fā)送或者出現(xiàn)錯(cuò)誤。
所以,目前有兩個(gè)問題:
怎樣同時(shí)處理多個(gè)連接?
怎樣調(diào)用send()和recv()直到數(shù)據(jù)全部發(fā)送或接收。
要實(shí)現(xiàn)并發(fā),傳統(tǒng)方法是使用多線程,最近比較流行的方法是使用在Python3.4中引入的異步IO模塊asyncio。
這里準(zhǔn)備用更加傳統(tǒng),但是更容易理解的方式來實(shí)現(xiàn),基于系統(tǒng)底層的一個(gè)調(diào)用:select(),Python中也提供了
對(duì)應(yīng)的模塊:selectors
select()通過了一種機(jī)制,它來監(jiān)聽操作發(fā)生情況,一旦某個(gè)操作準(zhǔn)備就緒(一般是讀就緒或者是寫就緒),然后將需要進(jìn)行這些操作的應(yīng)用程序select出來,進(jìn)行相應(yīng)的讀和寫操作。到這里,你可能會(huì)發(fā)現(xiàn)這并沒有實(shí)現(xiàn)并發(fā),但是它的響應(yīng)速度非常快,通過異步操作,足夠模擬并發(fā)的效果了。
Muti-Connection Client and Server
Multi-Connection Server
import selectors
sel = selectors.DefaultSelector()
# ...
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)
和echo server最大的不同就在于,通過lsock.setblocking(False),將這個(gè)socket對(duì)象設(shè)置成了非阻塞形式,與sel.select()一起使用,就可以在一個(gè)或多個(gè)socket對(duì)象上等待事件,然后在數(shù)據(jù)準(zhǔn)備就緒時(shí)進(jìn)行數(shù)據(jù)的讀寫操作。
sel.register()給server注冊(cè)了我們需要的事件,對(duì)server來說,我們需要 I/O 可讀,從而進(jìn)行client發(fā)送數(shù)據(jù)的讀入,因此,通過selector.EVENT_READ來指明。
data用來存儲(chǔ)和socket有關(guān)的任何數(shù)據(jù),當(dāng)sel.select()返回結(jié)果時(shí),它也被返回,我們用它作為一個(gè)標(biāo)志,來追蹤擁有讀入和寫入操作的socket對(duì)象。
接下來是事件循環(huán):
import selectors
sel = selectors.DefaultSelector()
# ...
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None:
accept_wrapper(key.fileobj)
else:
service_connection(key, mask)
sel.select(timeout=None)是一個(gè)阻塞式調(diào)用,直到有socket對(duì)象準(zhǔn)備好了 I/O 操作,或者等待時(shí)間超過設(shè)定的timeout。它將返回(key, events)這類元組構(gòu)成的一個(gè)列表,每一個(gè)對(duì)應(yīng)一個(gè)就緒的socket對(duì)象。
key是一個(gè)SeletorKey類型的實(shí)例,它有一個(gè)fileobj的屬性,這個(gè)屬性就是sokect對(duì)象。
mask是就緒操作的狀態(tài)掩碼。
如果key.data is None,我們就知道,這是一個(gè)server對(duì)象,于是要調(diào)用accept()方法,用來等待client的連接。不過我們要調(diào)用我們自己的accept_wrapper()函數(shù),里面還會(huì)包含其他的邏輯。
如果key.data is not None,我們就知道,這是一個(gè)client對(duì)象,它帶著數(shù)據(jù)來建立連接啦!然后我們要為它提供服務(wù),于是就調(diào)用service_connection(key, mask),完成所有的服務(wù)邏輯。
def accept_wrapper(sock):
conn, addr = sock.accept() # Should be ready to read
print('accepted connection from', addr)
conn.setblocking(False)
data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data)
這個(gè)函數(shù)用來處理與client的連接,使用conn.setblocking(False)將該對(duì)象設(shè)置為非阻塞狀態(tài),這正是我們?cè)谶@個(gè)版本的程序中所需要的,否則,整個(gè)server會(huì)停止,直到它返回,這意味著其他socket對(duì)象進(jìn)入等待狀態(tài)。
然后,使用types.SimplleNamespace()構(gòu)建了一個(gè)data對(duì)象,存儲(chǔ)我們想保存的數(shù)據(jù)和socket對(duì)象。
因?yàn)閿?shù)據(jù)的讀寫都是通過conn,所以使用selectors.EVENT_READ | selectors.EVENT_WRITE,然后用sel.register(conn, events, data=data)進(jìn)行注冊(cè)。
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print('echoing', repr(data.outb), 'to', data.addr)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]
這就時(shí)服務(wù)邏輯的核心,key中包含了socket對(duì)象和data對(duì)象,mask是已經(jīng)就緒操作的掩碼。根據(jù)sock可以讀,將數(shù)據(jù)保存在data.outb中,這也將成為寫出的數(shù)據(jù)。
if recv_data:
data.outb += recv_data
else:
print('closing connection to', data.addr)
sel.unregister(sock)
sock.close()
如果沒有接收到數(shù)據(jù),說明client數(shù)據(jù)發(fā)完了,sock的狀態(tài)不再被追蹤,然后關(guān)閉這次連接。
Multi-Connection Client
messages = [b'Message 1 from client.', b'Message 2 from client.']
def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print('starting connection', connid, 'to', server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b'')
sel.register(sock, events, data=data)
使用connect_ex()而不是connect(),因?yàn)閏onnect()會(huì)立即引發(fā)BlockingIOError異常。connect_ex()只返回錯(cuò)誤碼 errno.EINPROGRESS,而不是在連接正在進(jìn)行時(shí)引發(fā)異常。連接完成后,socket對(duì)象就可以進(jìn)行讀寫,并通過select()返回。
連接建立完成后,我們使用了types.SimpleNamespace構(gòu)建出和socket對(duì)象一同保存的數(shù)據(jù),里面的messages對(duì)我們要發(fā)送的數(shù)據(jù)做了一個(gè)拷貝,因?yàn)樵诤罄m(xù)的發(fā)送過程中,它會(huì)被修改。client需要發(fā)送什么,已經(jīng)發(fā)送了什么以及已經(jīng)接收了什么都要進(jìn)行追蹤,總共要發(fā)送的數(shù)據(jù)字節(jié)數(shù)也保存在了data對(duì)象中。
def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # Should be ready to read
if recv_data:
print('received', repr(recv_data), 'from connection', data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print('closing connection', data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0)
if data.outb:
print('sending', repr(data.outb), 'to connection', data.connid)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]
client要追蹤來自server的數(shù)據(jù)字節(jié)數(shù),如果收到的數(shù)據(jù)字節(jié)數(shù)和發(fā)送的相等,或者有一次沒有收到數(shù)據(jù),說明數(shù)據(jù)接收完成,本次服務(wù)目的已經(jīng)達(dá)成,就可以關(guān)閉這次連接了。
data.outb用來維護(hù)發(fā)送的數(shù)據(jù),前面提到過,一次發(fā)送不一定能將數(shù)據(jù)全部送出,使用data.outb = data.outb[sent:]來更新數(shù)據(jù)的發(fā)送。發(fā)送完畢后,再messages中取出數(shù)據(jù)準(zhǔn)備再次發(fā)送。
可以在這里看到最后的完整代碼:
server.py
client.py
最后的運(yùn)行效果如下:
還是要先啟動(dòng)server,進(jìn)入監(jiān)聽狀態(tài),然后client啟動(dòng),與server建立兩條連接,要發(fā)送的信息有兩條,這里分開發(fā)送,先將fist message分別發(fā)送到server,然后再發(fā)送second message。server端收到信息后進(jìn)行暫時(shí)保存,當(dāng)兩條信息都收到了才開始進(jìn)行echo,client端收到完整信息后表示服務(wù)結(jié)束,斷開連接。
注:喜歡python + qun:839383765 可以獲取Python各類免費(fèi)最新入門學(xué)習(xí)資料!