協議與接口
對于程式開發者而言,幾乎每天都需要和協議和接口打交道,盡管有時候很少直接使用他們。開發一個應用程序,為了提高開發效率,往往會選擇一個框架/庫來進行開發,于是乎不可避免的就需要了解框架的API接口。協議往往是一些底層的通信規則,框架的最大職能就是實現和解析這些協議,并指定一套簡潔的接口提供給開發者使用。
對于web編程,TCP,HTTP協議最熟悉不過了。TCP握手與揮手中,我們了解了TCP連接相關的具體狀態細節。這些算是協議上的規定,對于程式而言,需要如何實現這些協議的從而提供接口呢?所幸,socket的為網絡通信協議提供了編程接口。下面就來討論tcp連接中的socket編程。
socket編程
socket就是不同計算機之間進行通信的一個抽象。socket是兩個節點之間的數據傳輸,端點可能處于同一臺主機,也可能位于不同的主機中,通常屬于C/S架構,一個連接發起者(initiator)另外一個連接偵聽者(listener),通常將從事偵聽的socket稱作“服務器”,將發起連接的套接字稱作“客戶端”。一圖勝千言,下圖幾位常見的clinet-server通信模型:
上圖中的左邊是clinet,右邊是server。server的生命周期大致如下:
- 創建socket。
- 綁定(bind)地址端口
- 監聽網絡連接
- 接受連接
- 關閉連接
clinet的生命周期則稍微簡單點:
- 創建socket
- 綁定地址
- 發起連接
- 關閉連接
上述的過程使用python代碼也非常簡單:
clinet
import socket
address = ('127.0.0.1', 8000)
# 創建 tcp socket
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 發起連接
client.connect(address)
# 發送數據
client.send('hello-world')
# 接受數據
data = client.recv(1024)
print data
# 關閉連接
client.close()
服務端也很簡單
import socket
address = ('127.0.0.1', 8000)
# 創建 tcp socket
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定地址
server.bind(address)
# 監聽
server.listen(5)
while True:
# 獲取連接
conn, addr = server.accept()
while True:
# 讀取數據
data = conn.recv(1024)
# 發送數據給客戶端
conn.sendall(data)
if not data:
# 關閉連接
conn.close()
break
# 關閉socket
server.close()
運行服務端server.py 服務端將會開啟一個無限循環的監聽,與客戶端創建連接之后,進行數據交互,然后關閉連接,同時進入下一個生命周期的監聽。
對于clinet的代碼,發現我們并沒有顯示的綁定地址。對于服務器,綁定固定的端口,用于監聽,客戶端如果沒有綁定,操作系統會自動分配綁定一個端口
socket與握手細節
從最上面的網絡模型圖看出,握手的連接發生在客戶端connect,服務端的accept返回連接。握手的細節其實是客戶端connet和服務端的listen過程,而accept只是返回隊列里的連接。下面通過nc和wireshaker抓包來分析tcp三次握手和socket的通信細節。
clinet創建socket,server尚未bind地址
server如果尚未綁定地址,clinet就發起連接,將會收到一個RST拒絕連接的包。修改server.py運行
import socket
import time
address = ('127.0.0.1', 8000)
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
while True:
print 'sleep'
time.sleep(1)
clinet運行之后,connect調用之后觸發了一個異常,程序退出。查看連接情況如下:
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
可見并沒有創建連接通道,使用wireshaker的抓包如下:
這個過程對于客戶端是
- 客戶端發送一個握手包(SYN Seq)
- 端口直接返回(RST, ACK)拒絕連接
server綁定地址
接下來把server的地址進行綁定,但是尚未監聽。運行服務端
import socket
import time
address = ('127.0.0.1', 8000)
# 創建 tcp socket
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定地址
server.bind(address)
while True:
print 'sleep'
time.sleep(1)
使用nc查看連接情況
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 *.* CLOSED
然后運行clinet.py代碼,這一次并不像之前connect調用就出現異常,而是客戶端阻塞了一段時間之后,最后超時退出。查看wireshaker如下:
- 客戶端發送一個握手包(SYN Seq)
- 端口沒有響應,導致客戶端一直重發,此時connect阻塞,直到超時退出(退出進程)。
此時再使用netstat查看會得到如下結果:
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.55808 127.0.0.1.8000 SYN_SENT
tcp4 0 0 127.0.0.1.8000 *.* CLOSED
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.55808 127.0.0.1.8000 SYN_SENT
tcp4 0 0 127.0.0.1.8000 *.* CLOSED
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.55808 127.0.0.1.8000 SYN_SENT
tcp4 0 0 127.0.0.1.8000 *.* CLOSED
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 *.* CLOSED
通過wireshaker和netstat的觀察,client發送SYN之后,自身狀態編程SYS_SENT,server則一直處于CLOSED。這其實是第一次握手,clinet正等待server的ACK,可惜server并沒有呼應,最后clinet超時退出。
server進行listen監聽
將server.py加上監聽調用,listen接口有一個參數,即連接隊列中的連接數,通常設置為5,表示如果創建了連接,就存儲再連接隊列中,這個隊列最大的容量是5。在mac上最大的則是128。
server.py
import socket
import time
address = ('127.0.0.1', 8000)
# 創建 tcp socket
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定地址
server.bind(address)
# 監聽
server.listen(5)
while True:
print 'sleep'
time.sleep(1)
查看連接狀況:
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 *.* LISTEN
可見server處于LISTEM狀態。
這次我們使用nc工具來進行交互實驗:
? ~ nc 127.0.0.1 8000
上述命令等同于connect函數的調用,可見運行nc命令之后,客戶端一直被阻塞。一段時間之后,再手動command+c 結束 nc 命令。此時再查看連接狀態和抓包:
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 127.0.0.1.55832 ESTABLISHED
tcp4 0 0 127.0.0.1.55832 127.0.0.1.8000 ESTABLISHED
tcp4 0 0 127.0.0.1.8000 *.* LISTEN
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 127.0.0.1.55832 ESTABLISHED
tcp4 0 0 127.0.0.1.55832 127.0.0.1.8000 ESTABLISHED
tcp4 0 0 127.0.0.1.8000 *.* LISTEN
? ~ netstat -nat | grep -i "8000" | grep 127.0.0.1
tcp4 0 0 127.0.0.1.8000 127.0.0.1.55832 CLOSE_WAIT
tcp4 0 0 127.0.0.1.55832 127.0.0.1.8000 FIN_WAIT_2
tcp4 0 0 127.0.0.1.8000 *.*
從netstat和抓包的情況來看,此時client和server進行了三次握手:
- 客戶端發送一個握手包(SYN Seq),第一次握手,客戶端連接狀態為 SYS_SEND
- 服務端端有響應,并發送一個(SYN, ACK)的應答包, 第二次握手 客戶端的狀態都變成ESTABLISHED表示可以發送數據了
- 客戶端再向服務端發送(ACK)應答包,第三次握手,建立TCP連接,服務端也可以發送數據。
- 客戶端發送一個[FIN, ACK]的包,表示關閉連接。
- 服務端發送一個[ACK]包表示確定關閉客戶端到服務端的連接。客戶端到服務端的連接斷了。服務端到客戶端的連接還保持。
listen&accept函數
通過上述的實驗,可見tcp三次握手發生在client的connect調用之后,server端的listen函數調用之后,但是并不是由listen函數完成的,listen()的作用僅僅告訴內核一些信息,即連接隊列的大小,三次握手是以后內核完成的。一旦完成了三次握手,內核就把連接放入連接隊列之中。
當socket對象調用accpet函數的時候,就從連接隊列中將tcp連接取出并返回。如果連接隊列中沒有連接,那么accept函數調用將會阻塞(如果socket是非阻塞模式,accept此時會返回,并觸發一個異常)。
上述的代碼listen只設置了5,如果連接同事超過了5個會怎么樣呢?對于unix,server不會對再對建立新連接的握手進行應答,clinet的 connect 就會返回 ETIMEDOUT。可是在linux中server并不會拒絕連接,只是會延時連接,然后accept調用的時候卻未必能把已經建立好的連接全部取出來。
總結
tcp握手的系統調用有 connect bind listen函數。python封裝的api和C的很像。客戶端connect發起握手,接受到服務器的ack應答返回。server在listen調用之后創建連接隊列,同時監聽端口,客戶端connect發起的syn送達,在服務器的內核完成握手。只有當server端調用accept,才從連接隊列中取tcp連接,如果連接隊列中還沒有握手后的連接,那么aceept調用在socket阻塞模式中就會阻塞,在非阻塞模式中則會返回異常錯誤。