python基礎-----socket編程
一,客戶端、服務器架構
1,硬件C\S架構(打印機)
2,軟件C\S架構(web服務)
常用的軟件服務器是web服務器,一臺機器裏放一些網頁或web應用程序,然後啟動服務,這樣的服務器的任務就是接受客戶的請求,把網頁發給客戶(如計算機上的瀏覽器),之後等待下一個用戶請求。這些服務器啟動的目標就是“永遠的運行下去”。顯然他們不可能實現這樣的目標,但只要沒有關機或硬件出錯等外力幹擾,他們就能運行非常長的一段時間。
生活中的C\S架構:
老男孩是S端,學生是C端
飯店是S端,吃飯的人是C端
C\S架構與socket的關系:
我們學習socket就是為了完成C\S架構的開發
二,osi七層
一個完整的計算機系統是由硬件、操作系統、應用軟件三者組成,具備了這三個條件,一臺計算機系統就可以自己跟自己玩了(打個單機遊戲,玩個掃雷啥的)
如果你要跟別人一起玩,那你就需要上網了(訪問個黃色網站,發個黃色微博啥的),互聯網的核心就是由一堆協議組成,協議就是標準,全世界人通信的標準是英語,如果把計算機比作人,互聯網協議就是計算機界的英語。所有的計算機都學會了互聯網協議,那所有的計算機都就可以按照統一的標準去收發信息從而完成通信了。人們按照分工不同把互聯網協議從邏輯上劃分了層級
1.首先:本節課程的目標就是教會你如何基於socket編程,來開發一款自己的C/S架構軟件
2.其次:C/S架構的軟件(軟件屬於應用層)是基於網絡進行通信的
3.然後:網絡的核心即一堆協議,協議即標準,你想開發一款基於網絡通信的軟件,就必須遵循這些標準。
4.最後:就讓我們從這些標準開始研究,開啟我們的socket編程之旅
TCP\IP協議包括運輸層,網絡層,鏈路層。
三,socket層
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
所以,我們無需深入理解tcp/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標準的。
socket也是套接字
先從服務器端說起。服務器端先初始化Socket,然後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然後連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求並處理請求,然後把回應數據發送給客戶端,客戶端讀取數據,最後關閉連接,一次交互結束
1,用socket模擬打電話:
服務器端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#買手機
3 phone.bind(("127.0.0.1",8000)) #綁定手機卡
4 phone.listen(5) #開機 最多鏈接5個客戶端
5
6 print("starting......")
7 while True: #鏈接循環
8 conn,addr=phone.accept() #等待電話鏈接
9 print("電話線路是",conn)
10 print("客戶端手機號是",addr)
11
12 while True: #通訊循環
13 try:
14 data = conn.recv(1024) #收消息 最大接收1024字節
15 print("客戶發來的消息是",data)
16
17 conn.send(data.upper()) #回客戶端的消息
18 except Exception:
19 break
20
21 conn.close() #掛斷電話
22 phone.close() #關機
客戶端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
3 phone.connect(("127.0.0.1",8000))
4 while True:
5 msg = input(">>>>>>>>:").strip()
6 if not msg:continue
7 phone.send(msg.encode("utf-8")) #給服務端發的消息
8 data = phone.recv(1024) #接收服務端傳回來的消息
9 print(data)
10
11
12 phone.close() #關閉手機
服務端套接字函數:
1 s.bind() #綁定(主機,端口號)到套接字
2 s.listen() #開始TCP監聽
3 s.accept() #被動接受TCP客戶的鏈接,(阻礙式)等待連接的到來
客戶端套接字函數:
1 s.connect() #主動初始化TCP服務器連接
2 s.connect_ex() #函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
套接字家族的名字:AF_INET
AF_INET是使用最廣泛的一個,我們網絡編程基本用這個
四,基於TCP的套接字
1,模擬windons命令:
server端:
1 #基於tcp的套接字
2 import socket,subprocess
3 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#買手機
4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
5 phone.bind(("127.0.0.1",8000)) #綁定手機卡
6 phone.listen(5) #開機 最多鏈接5個客戶端
7
8 print("正在鏈接服務器.....")
9 while True: #鏈接循環
10 conn,addr=phone.accept() #等待電話鏈接
11 print("連接成功......")
12
13 while True: #通訊循環
14 try:
15 cmd = conn.recv(1024)#收消息 最大接收1024字節
16
17 msg = subprocess.Popen(cmd.decode("utf-8"),
18 shell=True,
19 stdout=subprocess.PIPE,
20 stderr=subprocess.PIPE)
21 new_res = msg.stdout.read()
22 res = msg.stderr.read()
23
24 if not res:
25 conn.send(new_res) #回客戶端的消息
26 conn.send(res)
27 except Exception:
28 break
29
30 conn.close() #掛斷電話
31 phone.close() #關機
client端:
1 import socket
2 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
3 phone.connect(("127.0.0.1",8000))
4 while True:
5 msg = input(">>>>>>>>:").strip()
6 if not msg:continue
7 phone.send(msg.encode("utf-8")) #給服務端發的消息
8 data = phone.recv(1024) #接收服務端傳回來的消息
9 print(data.decode("gbk"))
10
11
12 phone.close() #關閉手機
五,基於UDP的套接字:
server端:
1 import socket
2
3 ip_port = ("127.0.0.1",8081)
4 server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
5 server.bind(ip_port)
6 bufsize = 1024
7
8 while True:
9 conn, client_addr = server.recvfrom(bufsize)
10 print("來自[%s]的一條消息:%s"%(client_addr,conn.decode("utf-8")))
11 inp = input("回復消息:")
12 if not inp:continue
13 server.sendto(inp.encode("utf-8"),client_addr)
client端:
import socket
ip_port = ("127.0.0.1",8081)
bufsize = 1024
client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
inp = input("請輸入發送內容:").strip()
if not inp:continue
client.sendto(inp.encode("utf-8"),ip_port)
data,server_addr = client.recvfrom(bufsize)
print("來自[%s]的一條消息:%s"%(server_addr,data.decode("utf-8")))
client.close()
六,recv與recvfrom的區別
發消息:都是將數據發送到已端的緩存中。
收消息:都是從已端的緩存中收。
----------- part1 -------------
1,tcp:send發消息,recv收消息
2,udp:sendto發消息,recvfrom收消息
----------- part2:send與sendto -------------
tcp 是基於數據流的,udp是基於數據報的,最後都是以bytes類型發送到對方
1,send(bytes_data):發送數據流,數據流bytes_data若為空,自己這段的緩沖區也為空,操作系統不會控制tcp協議發空包
2,sendto(bytes_data,ip_port):發送數據報,bytes_data若為空,還有ip_port,所以即便是發現空的bytes_data,數據其實也不是空的,自己這端的緩沖區收到內容,操作系統就會控制udp協議發包
----------- part3:recv與revfrom -------------
1,tcp協議:
1.1 如果收消息緩沖區裏的數據為空,那麽recv就會阻塞(就是一直等著收)
1.2 只不過tcp協議的客服端send一個空數據就是真的空數據,客戶端即使有無窮個send空,也跟沒有是一樣的
1.3 tcp基於鏈接通信
基於鏈接,則需要listen(backlog),指定連接池的大小
基於鏈接,必須先運行服務端,然後客戶端發起鏈接請求
對於windows和linux系統:如果一端斷開了鏈接,另一端的鏈接也會斷開,recv不會阻塞,收到的是空(解決方法:服務器端通信循環內加異常處理,捕捉到異常後就break 跳出通信循環)
2,udp協議:
2.1 如果收消息緩沖區裏的數據為空,recvfrom也會阻塞,udp協議的客服端sendto一個空數據並不是真的空數據(空數據+地址信息,得到的數據報仍然不會為空),所以客戶端只要有一個sendto(不管是否發送空數據,都不是真的空數據),服務端就可以recvfrm到數據。
2.2 udp無連接
無連接,因而無需listen(backlog),就沒有什麽連接之說了
無連接,udp的sendto不管是否有一個正在運行的服務端,可以在客服端一直發消息,但是,發出去的消息服務端沒有接受到,所以就丟失了
recvfrom收的數據小於sendto發送的數據時,在max和linux系統上數據直接丟失,在windows系統上發送的比接收的數據大,直接報錯
只有sendto數據,而沒有recvfrom收數據,結果就是,數據丟失。
註意:
單獨運行udp客服端,沒有事,因為udp協議只負責把包發出去,服務端收不收,客戶端根本不管。而要是單獨運行tcp客戶端,會報錯,因為tcp是基於鏈接的,必須有一個服務端先運行著,客戶端去跟服務端建立鏈接,然後依托於鏈接才能傳遞消息,任何一方把鏈接關閉,都會導致對方的程序崩潰。
七,粘包
只有tcp有粘包現象,而udp永遠不會發生粘包
因為tcp是面向數據流的,就如同一條水流,一條水流不知道在哪裏開始,在哪裏結束。比如客戶端發送一個20字節的數據,服務端一次只接受1字節的數據,剩下的數據還會存在服務端的緩存裏,這時,客戶端又發來了一條20字節的數據,服務端這次改為接收1024字節(0-1024),結果,第一次剩下的數據跟這次的數據粘到一塊了,一並接收了過來,這種情況就是 粘包。
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取出多少字節的數據所造成的。
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
兩種情況下會發生粘包。
1,發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包),
2,接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
八,解決粘包的方法:
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然後一次send到服務端,服務端在接收時,先從緩存中取出自定義的報頭長度,再取報頭,最後再取真是的數據
struct模塊
該模塊可以吧一個類型,如數字,轉成固定長度的bytes
通常還有json模塊
我們可以把報頭做成字典,字典裏包含將要發送的真實數據的詳細信息,然後json序列化,然後用struck將序列化後的數據長度打包成4個字節(4個自己足夠用了)
發送時:
先發報頭長度
再編碼報頭內容然後發送
最後發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然後解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,然後去取真實的數據內容
這裏模擬windows的命令
服務端:
1 #server端,解決粘包問題
2 import socket
3 import struct
4 import json
5 import subprocess
6 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
7 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
8
9 phone.bind((‘127.0.0.1‘,8080))
10
11 phone.listen(5)
12 #開機 等待電話鏈接
13 #listen()TCP的緩存鏈接池,可以將別的等待鏈接掛起,方便之後鏈接。測試的時候可以寫5,但在實際應用中,應該在配置文件中設置這個值
14 while True:#鏈接循環
15 print(‘start and waiting‘)
16 conn,addr = phone.accept()
17 while True: #通訊循環
18 try:
19 data = conn.recv(1024)
20 # 收消息,1024:單次接收的量;設置的時候考慮到獲取的消息來自自己的緩存池,所以不能很大,通常為1024或8192
21 # linux上客戶強制斷開後,服務端會接收空信息,空會使服務端進入無限接收發送的循環,所以需要判斷是否為空,若為空就直接判斷斷開連接
22 if not data:break
23 print(‘>>>:‘,data.decode(‘utf-8‘))
24 res = subprocess.Popen(str(data.decode(‘utf-8‘)), shell=True,
25 stdout=subprocess.PIPE,
26 stderr=subprocess.PIPE)
27 out_res = res.stdout.read()
28 err_res = res.stdout.read()
29 data_size = len(out_res) + len(err_res)
30 head_dic = {"data_size":data_size} #自定義報頭 字典的格式
31 head_json = json.dumps(head_dic) # #序列化json字符串
32 head_bytes = head_json.encode("utf-8") # 編碼成bytes
33
34 #part1:先發報頭的長度
35 head_len = len(head_bytes)
36 conn.send(struct.pack("i",head_len)) #把報頭的長度統一打包為4個字節,發出去
37
38 #part2:再發報頭
39 conn.send(head_bytes)
40
41 #part3:最後發送數據部分
42 conn.send(out_res)
43 conn.send(err_res)
44
45
46 except Exception:
47 break
48 conn.close()
49
50 phone.close()
客戶端:
1 #client端,解決粘包問題
2 import socket
3 import struct
4 import json
5 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
6 phone.connect((‘127.0.0.1‘,8080))
7
8 while True:
9 cmd = input(‘>>:‘).strip()
10 if not cmd:continue
11 phone.send(cmd.encode(‘utf-8‘))#編碼成bytes 格式才能轉發出去
12
13 #part1:先接收報頭的長度
14 head_struct = phone.recv(4)
15 head_len = struct.unpack("i",head_struct)[0] #解開報頭的長度,返回的是一個元組,取第一個
16
17 # part2:在接收報頭
18 head_bytes = phone.recv(head_len) # 接收報頭的長度(字節)
19 head_json = head_bytes.decode("utf-8") #解碼成betys
20 head_dic = json.loads(head_json) #反序列化json
21 data_size = head_dic["data_size"] #取出報頭長度
22
23 # part3:最後接收數據
24 recv_size = 0
25 recv_data = b‘‘
26 while recv_size < data_size:
27 data = phone.recv(1024) # 單次獲取上限設置為1024,如果沒有1024長度的數據,也沒有關系
28 recv_size += len(data) # recv_size用於累加計算收集的字符數據長度
29 recv_data += data # 字符串拼接
30
31 print(recv_data.decode(‘gbk‘))#因為是windows系統解碼要gbk
32
33
34
35 phone.close()
FTP上傳下載文件:
只做了上傳,下載反過來就行,多並發,同時可以最大5個客服端上傳文件
服務端:
1 import socketserver
2 import struct
3 import json
4 import os
5 import sys
6 import time
7
8 class Server(socketserver.BaseRequestHandler):
9 max_packet_size = 1024
10 coding = "utf-8"
11 requset_queue_size = 5
12 server_dir = r"F:\\file_upload"
13
14 def handle(self):
15 print(self.request)
16 while True:
17 try:
18 #print("已成功連接")
19 head_struct = self.request.recv(4)
20 if not head_struct:break
21 head_len = struct.unpack("i",head_struct)[0]
22 head_json = self.request.recv(head_len).decode(self.coding)
23 head_dic = json.loads(head_json)
24
25 print(head_dic)
26 cmd = head_dic["cmd"]
27 if hasattr(self,cmd):
28 func = getattr(self,cmd)
29 func(head_dic)
30 except Exception:
31 break
32
33 def put(self,args):
34 file_path = os.path.normpath(os.path.join(self.server_dir,args["filename"]))
35 filesize = args["filesize"]
36 recv_size = 0
37 print("------->",file_path)
38 with open(file_path,"wb") as f:
39 while recv_size < filesize:
40 recv_data = self.request.recv(self.max_packet_size)
41 f.write(recv_data)
42 recv_size+=len(recv_data)
43 print("recvsize : %s filesize : %s"%(recv_size,filesize))
44
45
46 if __name__ == ‘__main__‘:
47 obj = socketserver.ThreadingTCPServer(("127.0.0.1",8080),Server)
48 obj.serve_forever()
客戶端:
1 import socket
2 import struct
3 import json
4 import os
5
6
7
8 class Client:
9 address_family = socket.AF_INET
10
11 socket_type = socket.SOCK_STREAM
12
13 allow_reuse_address = False
14
15 max_packet_size = 8192
16
17 coding=‘utf-8‘
18
19 request_queue_size = 5
20
21 def __init__(self, server_address, connect=True):
22 self.server_address=server_address
23 self.socket = socket.socket(self.address_family,
24 self.socket_type)
25 if connect:
26 try:
27 self.client_connect()
28 except:
29 self.client_close()
30 raise
31
32 def client_connect(self):
33 self.socket.connect(self.server_address)
34
35 def client_close(self):
36 self.socket.close()
37
38 def run(self):
39 while True:
40 inp=input(">>: ").strip()
41 if not inp:continue
42 l=inp.split()
43 cmd=l[0]
44 if hasattr(self,cmd):
45 func=getattr(self,cmd)
46 func(l)
47
48
49 def put(self,args):
50 cmd=args[0]
51 filename=args[1]
52 if not os.path.isfile(filename):
53 print(‘file:%s is not exists‘ %filename)
54 return
55 else:
56 filesize=os.path.getsize(filename)
57
58 head_dic={‘cmd‘:cmd,‘filename‘:os.path.basename(filename),‘filesize‘:filesize}
59 print(head_dic)
60 head_json=json.dumps(head_dic)
61 head_json_bytes=bytes(head_json,encoding=self.coding)
62
63 head_struct=struct.pack(‘i‘,len(head_json_bytes))
64 self.socket.send(head_struct)
65 self.socket.send(head_json_bytes)
66 send_size=0
67 with open(filename,‘rb‘) as f:
68 for line in f:
69 self.socket.send(line)
70 send_size+=len(line)
71 print(send_size)
72 else:
73 print(‘upload successful‘)
74
75
76
77
78 client=Client((‘127.0.0.1‘,8080))
79
80 client.run()
python基礎-----socket編程