Python 之 網絡編程——SOCKET開發
一、預備知識
對於我們,主要掌握5層協議就行。
物理層:
轉成二進制數序列
數據鏈路層:
形成統一的協議:Internet協議
包括數據頭(18個字節,前6個字節原地址,中間6個字節為目標地址,後6個字節為數據的描述)和數據
網絡層:
有IP協議,包括IP頭和數據
傳輸層:
包括tcp、UDP兩個協議:基於端口(0-65535)的協議
應用層:
包括http、ftp協議
客戶端 服務端
C-------------------------------->S
<--------------------------------
發包:
C請求,S同意後並我也要挖隧道,C才可以挖隧道到S。(三次握手)
結束發包:
C請求,S確認,S請求,C確認(四次揮手)
UDP協議:傳輸不可靠,但不需要建管道,直接按IP發過去
總結:①TCP傳輸可靠,但效率低
②UDP傳輸不可靠,但效率高
二、網絡編程SOCKET
1 socket.socket(socket.AF_INET,socket.SOCK_STREAM)
其中:
socket.AF_UNIX:用於本機進程間通訊,為了實現兩個進程間的通訊,可以通過創建一個本地的socket來完成(一個機器兩個不同的軟件)。
socket.AF_INET:我們只關心網絡編程,因此大多使用這個(還有socket.AF_INET6被用於ipv6。)
socket.SOCK_STREAM:制動使用面向流的TCP協議。
socket.SOCK_DGRAM:指向UDP協議。
2.1 socket套接字
- s.recv(1024)接受數據
- s.send(1024)發送數據
- s.recvfrom()接收所有數據
- s.sendall()發送所有數據(本質是循環調用send)
- s.sendto(信息,(IP地址,端口號)),將發給服務端的消息、(IP地址,端口號)發給服務端。
- s.close()關閉套接字
一個sendto對應一個recvfrom
2.2 TCP
2.2.1 服務端
由上圖可知,服務端需要先建立SOCKET鏈接,首先需要導入socket模塊,並鏈接。
1 import socket 2 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
之後就需要綁定(主機,端口號)到套接字,開始監聽。其中綁定時,IP號和端口號是元組,並且端口號是0-65535,但其中0-1024是給操作系統的,使用需要管理員權限。監聽,其中5代表最大鏈接數量。
s.bind((‘127.0.0.1‘,8080))#0-65535:0-1024給操作系統使用 s.listen(5)
緊接著,服務器通過一個永久循環來接收來自客戶端的連接,accept()會一直等待,知道客戶端發來信息(暫只考慮單線程情況)。
1 while True:#鏈接循環 2 conn,client_addr=s.accept()
接下來就是收發消息了,並需要進行通信循環。
1 #收發消息 2 while True:#通信循環 3 try: 4 data=conn.recv(1024) #1024表示接收數據的最大數,單位是bytes 5 print(‘客戶端的數據‘,data) 6 conn.send(data.upper()) 7 except ConnectionResetError: 8 break 9 conn.close()
接下來就是關閉套接字。
1 s.close()
2.2.2 客戶端
首先和服務端一樣,需要先建立SOCKET鏈接,首先需要導入socket模塊,並鏈接。
1 import socket 2 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
之後通過(主機IP號,端口號)到套接字連接。
1 s.connect((‘127.0.0.1‘,8080))
之後發收消息,同樣有著通信循環,和服務端相比,由於沒有等待連接,因此少個鏈接循環。
1 #發收消息 2 while True:#通信循環 3 msg=input(‘>>‘).strip() 4 phone.send(msg.encode(‘utf-8‘)) 5 data=phone.recv(1024) 6 print(data.decode(‘utf-8‘))
接下來就是關閉套接字。
1 s.close()
2.3 UDP協議
相比TCP協議,UDP是面向無連接的協議,因此使用UDP協議時,不需要建立連接,只需要知道對方的IP地址和端口號,就可以發送數據包,其不管是否發送到達。
和TCP協議類似,也是服務端和客戶端。
2.3.1 服務端
服務端需要先建立SOCKET鏈接,首先需要導入socket模塊,並綁定端口。
1 import socket 2 server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 3 server.bind((‘127.0.0.1‘,8080))
其不需要監聽和連接,即不需要listen()和accept(),而是直接接收來自客戶端的數據。
1 while True: 2 data,cliend_addr=server.recvfrom(1024) 3 print(data) 4 server.sendto(data.upper(),cliend_addr)
最後關閉套接字。
1 server.close()
2.3.2 客戶端
同樣,也需要先建立SOCKET鏈接,首先需要導入socket模塊。
1 import socket 2 client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
但不需要調用connect(),直接通過sendto()給服務端發數據。
1 while True: 2 msg=input(‘>>:‘).strip() 3 data=client.sendto(msg.encode(‘utf-8‘),(‘127.0.0.1‘,8080)) 4 data,server_addr=client.recvfrom(1024) 5 print(data,server_addr)
最後關閉套接字。
1 server.close()
2.4 粘包現象及解決方案
2.4.1 粘包現象
何為粘包,在上文中,我們一直使用s.recv(1024)來接收數據,但如果需要接收的數據比1024長,那麽剩余的數據會在發送端的IO緩沖區暫存下來,等下次接收端來接收數據時,先將緩沖區的數據發送出去,再接收下次的數據。當然,我們可以將1024改為8192,但數據比這個還大呢,我們接收的額定值就不能變大了,還是會發生這樣的事件。因此,這樣的事件我們稱之為粘包現象。當然,粘包現象僅存在於TCP協議中,UDP協議中不存在。
2.4.2 解決方案
粘包問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。此處,我們就需要借助於第三方模塊struct。用法為:
1 import json,struct 2 #為避免粘包,必須制作固定長度的報頭 3 header_dic={‘file_size‘:1073741824,‘file_name‘:‘a.txt‘,‘md5‘:‘8f6fbf8347faa4924a76856701edb0f3‘} #1G文件大小,文件名和md5值 4 5 #為了該報頭能傳送,需要序列化並且轉為bytes,用於傳輸 6 header_json = json.dumps(header_dic) # 轉成字符串類型 7 header_bytes = header_json.encode(‘utf-8‘) 8 9 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節 10 head_len_bytes=struct.pack(‘i‘,len(head_bytes)) #這4個字節裏只包含了一個數字,該數字是報頭的長度 11 12 #客戶端開始發送報文長度 13 conn.send(head_len_bytes) #先發報頭的長度,4個bytes 14 #再發報頭的字節格式 15 conn.send(head_bytes) 16 #然後發真實內容的字節格式 17 conn.sendall(文件內容) 18 19 #服務端開始接收 20 head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的字節格式 21 x=struct.unpack(‘i‘,head_len_bytes)[0] #提取報頭的長度 22 23 header_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式 24 header_str=header_bytes.decode(‘utf-8‘) 25 header_dic=json.loads(header_str) #提取報頭 26 27 #最後根據報頭的內容提取真實的數據,比如數據的長度 28 real_data_len=s.recv(header_dic[‘file_size‘]) 29 s.recv(real_data_len)
因此對於一個文件傳輸:
服務端:
1 import socket 2 import os 3 import struct 4 import json 5 share_dir=r‘C:\Users\。。。\Desktop\python\oldboypython\day6\10文件傳輸\服務端\share‘ 6 7 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 8 # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 9 phone.bind((‘127.0.0.1‘,9901)) #0-65535:0-1024給操作系統使用 10 phone.listen(5) 11 print(‘starting...‘) 12 while True: # 鏈接循環 13 conn,client_addr=phone.accept() 14 print(client_addr) 15 while True: #通信循環 16 try: 17 #1、收命令 18 res=conn.recv(8096)#b‘get a.txt‘ 19 if not res:break #適用於linux操作系統 20 #2、解析命令,提取相應的命令參數 21 cmds=res.decode(‘utf-8‘).split()#[‘get‘,‘a.txt‘] 22 filename=cmds[1] 23 24 #3、以讀的方式打開文件,讀取文件內容發送給客戶端 25 #3.1 制作固定長度的報頭 26 header_dic={ 27 ‘filename‘:filename, 28 ‘md5‘:‘xxdxxx‘, 29 ‘file_size‘:os.path.getsize(‘%s/%s‘%(share_dir,filename)) 30 } 31 header_json=json.dumps(header_dic)#轉成字符串類型 32 header_bytes=header_json.encode(‘utf-8‘) 33 34 #3.2 先發送報頭的長度 35 conn.send(struct.pack(‘i‘,len(header_bytes))) 36 37 #3.3 再發報頭 38 conn.send(header_bytes) 39 40 #3.4 發真實的數據 41 # conn.send(stdout+stderr) #+是一個可以優化的點 42 with open(‘%s/%s‘%(share_dir,filename),‘rb‘) as f: 43 # conn.send(f.read()) 44 for line in f: 45 conn.send(line) 46 except ConnectionResetError: #適用於windows操作系統 47 break 48 conn.close() 49 50 phone.close()文件傳輸服務端
客戶端:
1 import socket 2 import struct 3 import json 4 5 download_dir=r‘C:\Users\。。。\Desktop\python\oldboypython\day6\10文件傳輸\客戶端\download‘ 6 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 7 phone.connect((‘127.0.0.1‘,9901)) 8 while True: 9 #1、發命令 10 cmd=input(‘>>: ‘).strip() #get a.txt 11 if not cmd:continue 12 phone.send(cmd.encode(‘utf-8‘)) 13 #2、接收文件的內容,以寫的方式打開新文件,接收服務端發來的文件的內容寫入客戶端的新文件 14 #2.1 先收報頭的長度 15 obj=phone.recv(4) 16 header_size=struct.unpack(‘i‘,obj)[0] 17 #2.2 在收報頭 18 header_bytes=phone.recv(header_size) 19 #2.3 從包頭中解析出對真實數據的描述的信息 20 header_json=header_bytes.decode(‘utf-8‘) 21 header_dic=json.loads(header_json) 22 print(header_dic) 23 total_size=header_dic[‘file_size‘] 24 file_name=header_dic[‘filename‘] 25 #2.4 接收數據 26 with open(‘%s/%s‘%(download_dir,file_name),‘wb‘) as f: 27 recv_size=0 28 while recv_size<total_size: 29 line=phone.recv(1024) 30 f.write(line) 31 recv_size+=len(line) 32 print(‘總大小:%s,已下載大小:%s‘%(total_size,recv_size)) 33 phone.close()文件傳輸客戶端
Python 之 網絡編程——SOCKET開發