TCP通信原理與Socket編程
進程之間如何實現網絡通信
進程通信的概念最初來源於單機系統,由於每個進程都在自己的地址範圍內運行,爲了保證兩個相互通信的進程之間既互不干擾又能協調一致地工作,操作系統爲進程通信提供了相應的措施,如:
Unix BSD有管道(pipe)、命名管道(named pipe)、軟中斷信號(signal),Unix system V有消息(message)、共享存儲區(shared memory)和信號量(semaphore)等。
但是這些僅限於本機進程之間的通信,進程之間的網絡通信要解決不同主機進程之間相互通信的問題,爲此,首先要解決的是網絡中進程標識的問題。同一主機上,不同進程可以用進程號(process ID)唯一標識,但在網絡環境下,各主機獨立分配的進程號並不能唯一標識該進程。例如,主機A賦予某進程ID爲2363,主機B中也可能存在2363號進程,因此單靠進程ID也就失去了標識的意義。其次,操作系統支持的網絡協議衆多,不同協議的工作方式不同,地址格式也不同,因此,進程之間的網絡通信還要解決多重協議的識別問題。
其實TCP/IP協議族已經幫我們解決了這個問題,網絡層的IP地址可以唯一標識網絡中的主機,而傳輸層的協議+端口可以唯一標識主機中的應用程序(進程),這樣利用三元組(協議,IP地址,端口)就可以標識網絡的進程了。使用TCP/IP協議的應用程序通常採用Unix BSD的Socket來實現進程之間的網絡通信。
Socket通信過程
Socket起源於Unix,通常翻譯爲“套接字”,用於描述IP地址和端口,是一個通信鏈的句柄。Socket是在應用層和傳輸層(TCP/IP協議族)之間的一個軟件抽象層,它把TCP/IP層複雜的操作抽象成一組簡單的接口,供應用層調用,從而實現進程在網絡中通信。
socket抽象層在網絡中的位置:
TCP通信的標準流程一般是,服務器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然後主動連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求並處理請求,然後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉連接,一次交互結束。
Socket通信過程:
Socket服務端
開發步驟
- 創建服務端端套接字對象
- 綁定端口號
- 設置監聽
- 等待接受客戶端的連接請求
- 接收數據
- 發送數據
- 關閉套接字
import socket
if __name__ == '__main__':
# 1.創建tcp服務端套接字對象
# socket()函數的參數說明:
# AF_INET:協議族參數,AF_INET決定了socket的地址類型爲ipv4地址(32位的)與端口號(16位的)的組合
# SOCK_STREAM: 套接字類型參數,SOCK_STREAM決定了socket接受數據格式爲stream(流)
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 設置端口號複用,讓服務端程序退出後,端口號立即釋放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 2.給程序綁定端口號
tcp_server_socket.bind(("", 8989))
# 3.設置監聽
# 128表示最大等待建立連接的個數
# listen後的這個套接字只負責接收客戶端連接請求,不能收發消息,收發消息使用返回的這個新套接字來完成
tcp_server_socket.listen(128)
# 4.等待客戶端建立連接的請求, 只有客戶端和服務端建立連接成功代碼纔會解阻塞,代碼才能繼續往下執行
# 專門和客戶端通信的套接字: service_client_socket
# 客戶端的ip地址和端口號: ip_port
service_client_socket, ip_port = tcp_server_socket.accept()
# 代碼執行到此說明連接建立成功
print("客戶端的ip地址和端口號:", ip_port)
# 5.接收客戶端發送的數據, 這次接收數據的最大字節數是1024
recv_data = service_client_socket.recv(1024)
# 獲取數據的長度
recv_data_length = len(recv_data)
print("接收數據的長度爲:", recv_data_length)
# 對二進制數據進行解碼
recv_content = recv_data.decode("gbk")
print("接收客戶端的數據爲:", recv_content)
# 準備發送的數據
send_data = "ok".encode("gbk")
# 6.發送數據給客戶端
service_client_socket.send(send_data)
# 關閉服務與客戶端的套接字, 終止和客戶端通信的服務
service_client_socket.close()
# 7.關閉服務端的套接字, 終止和客戶端提供建立連接請求的服務
tcp_server_socket.close()
執行結果
客戶端的ip地址和端口號: ('172.16.47.209', 52472)
接收數據的長度爲: 5
接收客戶端的數據爲: hello
Socket客戶端
開發步驟
- 創建客戶端套接字對象
- 和服務端套接字建立連接
- 發送數據
- 接收數據
- 關閉客戶端套接字
import socket
if __name__ == '__main__':
# 1.創建tcp客戶端套接字對象
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2.和服務端應用程序建立連接
tcp_client_socket.connect(("192.168.131.62", 8080))
# 代碼執行到此,說明連接建立成功
# 準備發送的數據
send_data = "hello".encode("gbk")
# 3.發送數據
tcp_client_socket.send(send_data)
# 4.接收數據, 這次接收的數據最大字節數是1024
recv_data = tcp_client_socket.recv(1024)
# 返回的直接是服務端程序發送的二進制數據
print(recv_data)
# 對數據進行解碼
recv_content = recv_data.decode("gbk")
print("接收服務端的數據爲:", recv_content)
# 5.關閉套接字
tcp_client_socket.close()
執行結果
b'ok'
接收服務端的數據爲: ok
send和recv原理
當創建一個socket對象的時候會有一個發送緩衝區和一個接收緩衝區,這個發送和接收緩衝區指的就是內存分配的一片空間。
不管是send還是recv都不是直接發送數據到對方和接收到對方的數據,發送數據會寫入到發送緩衝區,接收數據是從接收緩衝區來讀取,發送數據和接收數據最終是由操作系統控制網卡來完成。
send和recv原理剖析圖:
TCP三次握手和四次揮手的Socket過程
TCP三次握手
- 服務端調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待;
- 客戶端Socket對象調用connect()向服務端發送了一個SYN報文並阻塞;
- 服務端完成了第一次握手,即發送SYN+ACK報文應答;
- 客戶端收到服務端發送的應答報文,從connect()返回,再發送一個ACK報文給服務器,完成第二次握手,該應答報文可攜帶客戶端到服務端的數據;
- 服務端Socket對象接收客戶端第三次握手ACK確認,此時服務端從accept()返回,建立連接。
TCP四次揮手
- 某個應用進程調用close()主動關閉,發送一個FIN;
- 另一端接收到FIN後被動執行關閉,併發送ACK確認;
- 之後被動執行關閉的應用進程調用close()關閉Socket,並也發送了一個FIN;
- 接收到這個FIN的一端向另一端ACK確認。