前言
本系列博客是筆者學習Python Socket的過程筆記,目的在於記錄。其中的解釋都爲自己的見解,僅供參考,如有錯誤,還望指出。本篇博客是對Python Socket的初步瞭解和使用,大佛請移駕。
網絡編程
網絡編程本質就是實現兩個設備之間的數據交換(通信),通常這個設備指的是計算機,實際上任何能連接網絡的硬件設備都能實現通信。也就是說我們的任何能連接網絡的設備都是可通信的,比如我有一個 LED顯示屏,我現在需要服務器控制這個 LED屏幕顯示一段文字,一些輪播圖片或者一個視頻,並且控制設備什麼時間播放什麼內容,播放多長時間等命令。那麼LED和我的服務器之間就需要通信,這個時候就不是計算機與計算機之間通信了。
在網絡編程中,發起連接的一方被稱作客戶端(Client),等待連接的一方被稱作服務器(Server)。服務端一般都需要一直啓動,等待客戶端來連接。連接一旦建立,雙方就可以互相發送數據了。
網絡通信和生活中的打電話類似,我要給某個人打電話我就得知道他的電話號碼,在網絡中也是如此,我要和服務器建立通信就需要知道服務器的在網絡中的位置。計算機在網絡中的位置由IP地址(具體概念查看IP百度百科)來區分標識。建議再看看私有IP和共有IP的區別。
一臺計算機上(設備)可以運行多個程序(多個進程),爲了區分這些程序就設計了端口(Port)的概念。設備最多有216=65536個端口,一個端口可以對應一個唯一程序,無論是服務端還是客戶端,每個程序都對應一個或多個端口。其中0-1024之間的大多端口已經被操作系統佔用,我們的程序一般就使用之後的端口號(僅僅針對計算機設備)。也就是說通過IP和端口號就可以實現兩個設備的某個程序之間進行通信了。
對於建立了連接的兩個設備以何種方式進行數據的傳輸呢,傳輸數據的方式無論是有線傳輸還是無線傳輸,一般就兩種傳輸協議方式:
1. TCP(Transfer Control Protocol)
傳輸控制協議方式,該傳輸方式是一種穩定可靠的傳送方式,類似於顯示中的打電話。只需要建立一次連接,就可以多次傳輸數據。就像電話只需要撥一次號,就可以實現一直通話一樣,如果你說的話不清楚,對方會要求你重複,保證傳輸的數據可靠。使用該種方式的優點是穩定可靠,缺點是建立連接和維持連接的代價高,傳輸速度不快。
2. UDP(User Datagram Protocol)
用戶數據報協議方式,該傳輸方式不建立穩定的連接,類似於發短信息。每次發送數據都直接發送。發送多條短信,就需要多次輸入對方的號碼。該傳輸方式不可靠,數據有可能收不到,系統只保證盡力發送。使用該種方式的優點是開銷小,傳輸速度快,缺點是數據有可能會丟失。
協議(Protocol)在網絡裏是一個重要的概念,平時所說的協議實際就是指網絡協議,網絡協議就是設備實現通信的規定,雙方必須遵守這個規定才能獲取到正確的通信信息。比如在建立連接的時候應該何種方式連接,怎麼互相判別,傳輸的數據格式(也就是要按照一定的格式來發送和接收數據)是什麼。只有遵守這個規定,設備之間才能相互通信交流。它的三要素是:語法、語義、時序。
爲了使數據在網絡上從源到達目的,網絡通信的參與方必須遵循相同的規則,這套規則稱爲協議(protocol),它最終體現爲在網絡上傳輸的數據包的格式。
Socket
Socket即是套接字,Python將低級別的網絡服務封裝成了一個模塊,通過socket就可以將網絡中的兩個設備的某一進程(應用程序)以TCP或者UDP協議方式建立連接,在建立連接過後就可以實現設備進程與設備進程之間的通訊了。
我們導入socket(使用import socket
)模塊,使用s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
來創建返回一個基於IPv4地址流式TCP傳輸協議的套接字s,我們需要創建套接字最主要考慮前兩個參數,第一個是IP地址簇,第二個是傳輸協議類型,下面列了常用的幾個可選值,想要了解全部請前往Python3 socket。
- family:IP地址簇參數
可選值 | 描述 |
---|---|
socket.AF_INET | IPv4(默認) |
socket.AF_INET6 | IPv6 |
socket.AF_UNIX | 用於Unix系統進程間通信 |
socket.AF_… | … |
- type:類型參數
可選值 | 描述 |
---|---|
socket.SOCK_STREAM | 流式socket,TCP(默認) |
socket.SOCK_DGRAM | 數據報式socket,UDP |
socket.SOCK_… | … |
函數 | 類型 | 描述 |
---|---|---|
s.bind() | 服務端用 | 綁定地址和端口到套接字,參數address爲元組形式的(host, port) |
s.listen() | 服務端用 | 開始監聽綁定的地址端口程序(進程),參數backlog指定在拒絕連接之前,可以掛起的最大連接數量。 |
s.accept() | 服務端用 | 等待客戶端的連接(阻塞式)。 |
s.connect() | 客戶端用 | 主動去連接服務器,參數address爲元組形式的(host, port),如果連接出錯,返回socket.error錯誤。 |
s.connect_ex() | 客戶端用 | 主動去連接服務器,參數address爲元組形式的(host, port),出錯時返回出錯碼,而不是拋出異常。 |
s.recv() | 公共使用 | 接收數據,返回字節型字符串數據,參數bufsize指定接收數據的最大長度。 |
s.send() | 公共使用 | 發送數據,返回發送成功的字節數,參數data是要發送的字節型字符串數據。 |
s.sendall() | 公共使用 | 完整發送數據。內部循環調用send直至數據發送完整,參數data是要發送的字節型字符串數據。 |
s.recvfrom() | 公共使用 | 接收數據,返回值是(data, address)。data是包含接收數據的字符串,address是發送數據的套接字地址。 |
s.sendto() | 公共使用 | 發送數據,將數據發送到套接字,address形式爲(host, port)的元組,指定遠程地址。返回值是發送字節數。 |
s.close() | 公共使用 | 關閉套接字 |
s.getpeername() | 公共使用 | 返回連接套接字的遠程地址。返回值通常是元組(host, port)。 |
s.getsockname() | 公共使用 | 返回套接字自己的地址。通常是一個元組(host, port)。 |
s.setsockopt() | 公共使用 | 設置給定套接字選項的值。 |
s.getsockopt() | 公共使用 | 返回套接字選項的值。 |
s.settimeout() | 公共使用 | 設置套接字操作的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。 |
s.gettimeout() | 公共使用 | 返回當前超時期的值,單位是秒,如果沒有設置超時期,則返回None。 |
s.fileno() | 公共使用 | 接收數據,返回字節型字符串數據,參數bufsize指定接收數據的最大長度。 |
s.setblocking() | 公共使用 | flag爲0非阻塞,否則爲阻塞模式(默認)。非阻塞模式調recv()或send()沒有數據,將引起socket.error異常。 |
s.makefile() | 公共使用 | 創建一個與該套接字相關連的文件。 |
初步使用
由於要實現的是兩個進程間的通信(同一個設備),那麼需要兩個程序,我們分別叫客戶端程序和服務端程序。
在這個初級使用實例中,實現的是一臺計算機中兩個進程之間的通信(一個進程爲等待客戶端連接的服務端程序,一個進程爲主動去連接服務端的客戶端程序)。也就是說客戶端和服務端程序都運行在本地的一臺計算機上,服務端建立socket,監聽本機IP的6688端口等待客戶端來連接,一旦有一個客戶端來連接了就打印連接信息,然後等待連接的客戶端發送數據,接收到客戶端傳來的數據後,就反饋給客戶端收到信息了;客戶端程序先建立socket套接字,然後去連接本機的IP和6688端口進程,連接成功後等待控制檯輸入數據,接受到輸入的數據後發送到服務端,然後結束連接。
先新建一個server.py文件,這個文件裏寫服務端功能程序,如下:
import socket
# 創建一個socket套接字,該套接字還沒有建立連接
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定監聽端口
server.bind(('localhost', 6688))
# 開始監聽,並設置最大連接數
server.listen(5)
# 獲取未建立連接的服務端的IP和端口信息
print(server.getsockname())
# 下面註釋掉的是獲取未建立連接的服務端套接字的遠程IP和端口信息,執行下面語句會報錯,原因就是現在還沒有遠程客戶端程序連接
# print(server.getpeername())
print(u'waiting for connect...')
# 等待連接,一旦有客戶端連接後,返回一個建立了連接後的套接字和連接的客戶端的IP和端口元組
connect, (host, port) = server.accept()
# 現在建立連接就可以獲取這兩個信息了,注意server和connect套接字的區別,一個是未建立連接的套接字,一個是已經和客戶端建立了連接的套接字
peer_name = connect.getpeername()
sock_name = connect.getsockname()
print(u'the client %s:%s has connected.' % (host, port))
print('The peer name is %s and sock name is %s' % (peer_name, sock_name))
# 接受客戶端的數據
data = connect.recv(1024)
# 發送數據給客戶端告訴他接收到了
connect.sendall(b'your words has received.')
print(b'the client say:' + data)
# 結束socket
server.close()
然後建立一個client.py文件,實現客戶端程序,內容如下:
import socket
# 創建一個socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 主動去連接本機IP和端口號爲6688的進程,localhost等效於127.0.0.1,也就是去連接本機端口爲6688的進程
client.connect(('localhost', 6688))
# 接受控制檯的輸入
data = input()
# 對數據進行編碼格式轉換,不然報錯
data = data.encode('utf-8')
# 發送數據
client.sendall(data)
# 接收服務端的反饋數據
rec_data = client.recv(1024)
print(b'form server receive:' + rec_data)
client.close()
其中左邊爲服務端運行結果,右邊爲客戶端運行結果,右邊的hello是手動輸入然後回車。
結語
到此初步的使用已經差不多了,不過上述代碼只能是一對一的通信,一對多不是本篇博客的內容,因爲客戶端一般都不會只有一個,所以這個一對一不能滿足我們的常用需求,後面的博客將會介紹多線程實現多客戶端連接服務器,與服務器通信。在下一篇博客會講解和實現局域網內兩臺設備,廣域網之間,局域網與廣域網之間設備的通信。