我的Python成長之路--Day32-網路程式設計part1Socket程式設計介紹
引言: 我們之前編寫的程式都是隻能在自己計算機上面執行和實現,不能和其他的計算機進行交流和互動,比如簡單的單機小遊戲,掃雷啊,蜘蛛紙牌啊什麼的,現在但是又的人又會說我玩的單機遊戲也可以聯網啊,是的,你說的沒錯,現在做的好的單機遊戲都要聯網進行資料交換了,由此可見我們學程式設計寫程式最終的目的還是要讓使用者能夠使用我們編寫出來的程式,能夠相互之間進行資料的交換從而來滿足客戶相應的需求,所以我們編寫好的軟體要可以通過網路來實現相互之間的資料交換.
我們學習網路程式設計今天主要介紹的就是socket(套接字程式設計)程式設計,為什麼要學socket程式設計呢,在我們弄懂網路通訊協議之後,直接按照通訊協議標準進行通訊不好麼,吶,如果你非要這麼做也沒有人攔著你,但是我們需要想一下,在我們每一次編寫一個應用程式的時候都要花大量的時間和大量的程式碼去把網路底層的通訊協議去建立好,這樣不僅我們的開發效率會大大降低,每次的程式設計也會給我們帶來一定的難度,如果對網路底層的協議不是非常瞭解的話,是建立不好通訊的,這個時候socket就站出來給我們提供了很大的便利,它將網路底層的通訊協議的複雜操作封裝成了一個個介面來供我們使用,也就是說我們在程式設計的時候,只需要按照socket的原理就編寫程式,程式編寫完畢後自然也是遵循網路底層的通訊協議的.在詳細瞭解socket程式設計之前,我們需要了解網路底層的協議是怎麼樣進行工作的,這也是我們以後進行網路程式設計的基礎技能
一、客戶端/服務端架構
在正式開始介紹網路程式設計之前,我們需要了解基礎的網路通訊架構,平時我們使用的軟體基本上都是客戶端,然後通過伺服器與另一個客戶端進行通訊,比如我們使用的微信、QQ等 1.硬體C(Client)/S(Serve)架構(印表機,投影儀等)
2.軟體C/S架構 網際網路中處處都是C/S架構 1.如果百度是服務端為你提供搜尋服務,你的瀏覽器是客戶端(B(brower)/S(serve)架構也是C/S架構的一種) 2.YouTube作為服務端為你提供視訊,你必須的下載YouTube的客戶端才可看它的視訊
3.C/S架構和socket的關係: 我們學習socket就是為了完成C/S架構的開發
二、osi七層協議
在前面我們已經瞭解過了一個完整的計算機系統需要包含,計算機硬體、計算機系統、應用程式,這樣一個計算機就可以自己跟自己玩了,比如玩個掃雷和其他一些單機就可以執行的軟體,如果想要和其他人一起玩,那就需要上網了,那麼問題來了,我們平時說的上網,這個網到底是個什麼東西?
網際網路的核心就是由一堆協議組成,協議就是標準,是由人們規定好的標準,比如全世界人通訊和交流的標準是英語 如果把計算機比作人,網際網路協議就是計算機屆的英語.所有的計算機都學會了網際網路協議的話,那所有的計算機都可以按照統一的標準去收發資訊從而完成通訊了. 人們按照分工的不同吧網際網路協議從邏輯上劃分了等級,詳細的網路通訊原理,我會再寫一篇部落格來專門介紹,這裡就不詳細介紹了
為什麼學socket一定要先學習網際網路協議: 1.首先: 我們的目標就是如何基於socket,來開發一款自己的C/S架構軟體
2.其次: C/S架構的軟體(軟體屬於應用層)是基於網路進行通訊的
3.然後: 網路的核心就是一堆協議,協議即標準,想要開發一款基於網路通訊的軟體,就必須遵循這些標準.
4.最後: 讓我們從這些標磚開始研究,開啟我們的socket程式設計之旅
下面我們簡單介紹一下上圖表示的流程都是什麼意思: 首先應用層: 應用層就是我們電腦上的一個個應用軟體,開啟一個應用軟體在作業系統中就會多一個使用者程序,如果該應用程式需要用到網路,就會往下走到運輸層:
運輸層:運輸層其實也叫埠層,我們可以看到運輸層裡面有TCP UDP兩個方框,這兩個方框分別表示兩種基於不同協議的埠協議,後邊我們會詳細介紹,運輸層的作用就是分配給需要用到網路的計算機上的引用程式一個埠,讓每個軟體都能有序的進行網路請求.然後接著往下走
網路層:從運輸層過來的資料,到達網路層之後,會基於IP協議再進行一層包裝,做成一個IP資料包,這個IP資料包會包含本機的IP地址和目標伺服器的IP地址還有資料內容.然後再和鏈路層進行配合,將計算機的mac地址也標識上,然後通過物理連線層,將這些資料轉換成電訊號傳送出去,
資料鏈路層:鏈路層遵循的是乙太網協議(Ethernet協議),這裡涉及到計算機最基礎的通訊方式就是廣播,在同一個區域網內,如果想建立通訊,計算機就會進行廣播,傳送電訊號,但是如果只是單純的發一長串高低電平(虛擬為二進位制的0和1),沒有哪一個計算機能夠讀懂是什麼意思,也就是需要一個標準來規定一下,計算機進行廣播的時候需要按照什麼方式進行廣播,乙太網協議就是做這個事情的,它規定計算機進行廣播的時候必須按照一定的格式進行廣播,它規定的這個格式成為資料報或者資料幀,並且規定資料報的格式有兩段head和data.head的長度是固定的(18個位元組),裡面要包含傳送者/源地址(6個位元組)、接受者/目標地址(6個位元組)、資料型別(6個位元組),這裡的地址都是mac地址,然後後邊是data資料內容,詳細的內容在網路通訊原理中進行介紹.然後說一下mac地址:每塊網絡卡出廠時都被燒製上一個世界唯一的mac地址,長度為48位2進位制,通常由12位16進位制數表示(前六位是廠商編號,後六位是流水線號),這樣在計算機進行廣播的時候,就可以根據資料報來找到一個唯一的計算機從而完成通訊.
物理連線層:物理連線層是通過把以上幾層打包好的資料轉換成高低電平(虛擬成0和1)然後再通過連線好的通路(網線(一般都是雙絞線)、無線電波、光纜等)傳輸到另一個區域網,然後目標位置再遵循網路協議按照順序進行源資料的解包操作,最終解讀出來傳輸的資料內容,做出相應的響應
三、socket層
在上圖中我們沒有看到socket在應用程式通過網路傳輸資料的時候具體在那一層參與進來並且做了什麼操作,那麼他到底在哪裡呢,看下圖就一目瞭然了.
從圖中我們可以看到socket處在應用層和運輸層中間的,這也是我們為什麼說socket可以將網路傳輸中的各種協議封裝成一個個的介面來供我們呼叫的原因,在編寫應用程式的時候,直接按照socket的標準來進行程式設計,這樣編寫出來的程式自然就是遵循網路協議了.
四、socket是什麼
Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。
所以,我們無需深入理解tcp/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去程式設計,寫出的程式自然就是遵循tcp/udp標準的
也有人將socket說成ip+port,ip是用來標識網際網路中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程式,ip地址是配置到網絡卡上的,而port是應用程式開啟的,ip與port的繫結就標識了網際網路中獨一無二的一個應用程式
而程式的pid是同一臺機器上不同程序或者執行緒的標識
下面來簡單瞭解一下socket的發展史及其分類: 套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一臺主機上多個應用程式之間的通訊。這也被稱程序間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於檔案型的和基於網路型的。
基於檔案型別的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆檔案,基於檔案的套接字呼叫的就是底層的檔案系統來取資料,兩個套接字程序執行在同一機器,可以通過訪問同一個檔案系統間接完成通訊
基於網路型別的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支援很多種地址家族,但是由於我們只關心網路程式設計,所以大部分時候我們只使用AF_INET)
五、套接字的工作流程
我們通過一個生活中的場景來介紹套接字的工作流程,想象一下,你要打電話給一個朋友,首先你得有一個電話對吧,然後讓你的電話連上網也就是插上手機卡找到服務商,然後電話就處於了待機狀態等待電話進來,或者主動撥出去,撥電話的時候,先進行撥號,朋友聽到電話鈴聲之後提起電話,這時你和你的朋友就建立起了連線,就可以講話進行交流了,等交流結束,電話結束通話就結束了此次交談,相應的此次建立的連線也會斷開.然後雙方的電話又都會處於待機狀態,等待另一個電話或者你主動撥號出去和其他人建立連線.生活中的場景就解釋了套接字的這種工作原理,具體的原理圖見下圖:(是一個基於TCP的C/S架構)
先從伺服器端說起,伺服器端先初始化出socket物件, 然後與ip和埠進行繫結(bind),目的是確定伺服器在哪個位置,好讓客戶端能夠找到伺服器, 接著對埠進行監聽(listen),看有沒有連線請求傳送過來 然後呼叫accept阻塞,等待客戶端連線,在這時如果有個客戶端初始化出一個socket,然後連線伺服器(connect),如果連線成功,這時客戶端與服務端的連線就建立了.客戶端傳送資料請求,伺服器端接受請求並處理請求,然後把迴應資料傳送給客戶端,客戶端再讀取資料,然後關閉連線,這樣一次資料互動就完成了.
接下來我們總結一下使用socket的程式設計思路(以TCP為例):
TCP服務端:
1 建立套接字,繫結套接字到本地IP與埠
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.bind('IP地址',埠),注意這裡的埠可以是0-65535,但是0-1024一般都是系統埠,我們不應該去呼叫這些埠,以免和系統發生衝突,我們一般呼叫1025-65535之間的埠
2 開始監聽連線 s.listen() listen的括號中可以指定一個引數,這個引數是指最大半連線數,半連線指的是客戶端傳送了連線請求,服務端也做出了相應的響應,但是客戶端沒有做出進一步的響應,所以三次握手沒有完成,不算真正的連線成功,做出這樣的限制的原因是避免計算機在同一時間接收到很多請求的時候將記憶體撐爆(因為這些連線請求都是作業系統來維持的),這個缺點也會給不法分子造成可乘之機,利用多個虛擬的ip來發送請求(這種請求過多時,就稱為洪水攻擊),傳送完請求之後卻不響應伺服器的請求,白白浪費作業系統的資源,我們這樣做限制之後,就可以一定程度上避免這種現象的發生
3 進入迴圈,不斷接受客戶端的連線請求 s.accept() 這裡也是一個''阻塞''操作,說它阻塞是因為伺服器會一直在這等待客戶端的連線請求,知道它監聽到一個連線請求之後,就會和客戶端進行連線,伺服器的這一個步驟和客戶端的connect步驟就相當於TCP協議中的三次握手建立連線的操作 注意:這裡的accept會返回兩個值,第一個是連線成功的客戶端物件(Client),第二個是客戶端的地址,需要兩個變數進行接收,然後用客戶端物件進行資料收發的操作,完成之後再進行當前客戶端連線的close操作
4 然後接收傳來的資料,併發送給對方資料 ,傳輸完畢時關閉與當前客戶端的連線 client.recv() recv可以設定一個引數,這個引數表示一次接收的最大位元組數 client.send() 這裡要注意傳送的資料必須要是位元組形式的,英文和數字可以直接在字串前面加b來轉換,如果是中文或者其他的字元需要 使用,encode來指定編碼方式,要注意收發資料編碼方式要保持一致才能夠正常讀取資訊內容 client.close()
5 傳輸完畢後,關閉套接字 s.close()
TCP客戶端:
1 建立套接字,連線遠端地址
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect('伺服器的IP',伺服器的埠) 這一步和伺服器端的accept操作相當於TCP中的三次握手建立連線,這裡輸入的伺服器資訊一定要和伺服器端保持高度一致,不然是連線不到伺服器端的
2 連線後傳送資料和接收資料 s.send() s.recv()
3 傳輸完畢後,關閉套接字 s.close()
Python 提供了兩個基本的 socket 模組。
第一個是 Socket,它提供了標準的 BSD Sockets API。
第二個是 SocketServer, 它提供了伺服器中心類,可以簡化網路伺服器的開發。 接下來介紹一下socket模組的用法:
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,預設值為 0。
獲取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
獲取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
由於 socket 模組中有太多的屬性。我們在這裡破例使用了'from module import *'語句。使用 'from socket import *',我們就把 socket 模組裡的所有屬性都帶到我們的名稱空間裡了,這樣能 大幅減短我們的程式碼。
例如tcpSock = socket(AF_INET, SOCK_STREAM)
套接字格式:
socket(family,type[,protocal]) 使用給定的地址家族、套接字型別、協議編號(預設為0)來建立套接字。
socket型別 | 描述 |
socket.AF_UNIX | 只能夠用於單一的Unix系統程序間通訊 |
socket.AF_INET | 伺服器之間網路通訊 |
socket.AF_INET6 | IPv6 |
socket.SOCK_STREAM | 流式socket , for TCP |
socket.SOCK_DGRAM | 資料報式socket , for UDP |
socket.SOCK_RAW | 原始套接字,普通的套接字無法處理ICMP、IGMP等網路報文,而SOCK_RAW可以;其次,SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由使用者構造IP頭。 |
socket.SOCK_SEQPACKET | 可靠的連續資料包服務 |
建立UDP Socket: | s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) |
Socket函式 注意點: 1.TCP傳送資料時,已建立好TCP連線,所以不需要指定地址。UDP是面向無連線的,每次傳送要指定是發給誰。2.服務端與客戶端不能直接傳送列表,元組,字典。需要字串化repr(data)。
函式 | 描述 |
s.bind(address) | 將套接字繫結到地址, 在AF_INET下,以元組(host,port)的形式表示地址.元組中分別是主機地址和主機埠 |
s.listen(backlog) | 開始監聽TCP傳入連線。backlog指定在拒絕連線之前,作業系統可以掛起的最大連線數量。該值至少為1,大部分應用程式設為5就可以了 |
s.accept() | 接受TCP連線並返回(conn,address),其中conn是新的套接字物件,可以用來接收和傳送資料。address是連線客戶端的地址。(阻塞式)等待連線的到來 |
函式 | 描述 |
s.connect(address) | 主動初始化TCP伺服器連線.連線到address處的套接字。一般address的格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤。 |
s.connect_ex(adddress) |
connect()函式的擴充套件版本,出錯時返回出錯碼,而不是丟擲異常 功能與connect(address)相同,但是成功返回0,失敗返回errno的值。 |
函式 | 描述 | |
s.recv(bufsize[,flag]) |
接受TCP套接字的資料。資料以字串形式返回,bufsize指定要接收的最大資料量(位元組數)。flag提供有關訊息的其他資訊,通常可以忽略。 |
|
s.send(string[,flag]) |
傳送TCP資料。將string中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。 send在待發送資料量大於己端快取區剩餘空間時,資料丟失,不會發完 |
|
s.sendall(string[,flag]) |
完整發送TCP資料。將string中的資料傳送到連線的套接字,但在返回之前會嘗試傳送所有資料。成功返回None,失敗則丟擲異常。 |
|
s.recvfrom(bufsize[.flag]) |
接受UDP套接字的資料。與recv()類似,但返回值是(data,address)。其中data是包含接收資料的字串,address是傳送資料的套接字地址。 |
|
s.sendto(string[,flag],address) |
傳送UDP資料。將資料傳送到套接字,address是形式為(ipaddr,port)的元組,指定遠端地址。返回值是傳送的位元組數。 |
|
s.close() |
關閉套接字。 |
|
s.getpeername() |
返回連線套接字的遠端地址。返回值通常是元組(ipaddr,port)。 |
|
s.getsockname() |
返回套接字自己的地址。通常是一個元組(ipaddr,port) |
|
s.setsockopt(level,optname,value) |
設定給定套接字選項的值(設定引數)。 |
|
s.getsockopt(level,optname[.buflen]) |
返回套接字選項的值。 |
|
s.settimeout(timeout) |
設定套接字操作的超時期,timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛建立套接字時設定,因為它們可能用於連線的操作(如connect()) |
|
s.gettimeout() |
返回當前超時期的值,單位是秒,如果沒有設定超時期,則返回None。 |
|
s.fileno() |
返回套接字的檔案描述符。 |
|
s.setblocking(flag) |
如果flag為0,則將套接字設為非阻塞模式,否則將套接字設為阻塞模式(預設值)。非阻塞模式下,如果呼叫recv()沒有發現任何資料,或send()呼叫無法立即傳送資料,那麼將引起socket.error異常。 |
|
s.makefile() |
建立一個與該套接字相關連的檔案 |
函式 | 描述 |
s.setblocking() | 設定套接字的阻塞與非阻塞模式 |
s.settimeout() | 設定阻塞套接字操作的超時時間 |
s.gettimeout() | 得到阻塞套接字操作的超時時間 |
函式 | 描述 |
s.fileno() | 套接字的檔案描述符 |
s.makefile() | 建立一個與該套接字相關的檔案 |
一個基於TCP的S/C架構示例: TCP服務端: 這個例子中是沒有加通訊迴圈和連線迴圈的,只是簡單演示一下怎麼連線
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話
phone.bind(('127.0.0.1',8081)) #插手機卡,補充:0-65535 0-1024給系統用的
phone.listen(5) # 開機
print('start...')
conn, client_addr=phone.accept() # 等電話連線
print('連線來了:', conn, client_addr)
# 收發訊息
msg=conn.recvfrom(1024) #收訊息,1024是一個最大的限制
print('客戶端的訊息: ', msg)
conn.send(msg+b'SB')
# 掛電話
conn.close()
# 關機
phone.close()
TCP客戶端
import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 買電話
phone.connect(('127.0.0.1',8081)) # 撥電話,地址為服務端的ip和埠
phone.send('你好'.encode('utf-8')) # 發訊息b'hello'
data=phone.recv(1024) #收訊息
print(data.decode('utf-8'))
phone.close()