1. 程式人生 > >Python 之 網絡編程——SOCKET開發

Python 之 網絡編程——SOCKET開發

top mage pan .so byte exc dto xxd 2.4.1

一、預備知識

技術分享圖片

對於我們,主要掌握5層協議就行。

物理層:
  轉成二進制數序列
數據鏈路層:

  形成統一的協議:Internet協議
  包括數據頭(18個字節,前6個字節原地址,中間6個字節為目標地址,後6個字節為數據的描述)和數據
網絡層:

  有IP協議,包括IP頭和數據
傳輸層:
  包括tcp、UDP兩個協議:基於端口(0-65535)的協議
應用層:
  包括http、ftp協議

TCP協議:流式協議,先把管道修好
      客戶端          服務端
       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=rC:\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=rC:\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開發