對python中基于tcp協(xié)議的通信(數(shù)據(jù)傳輸)實(shí)例講解
閱讀目錄
tcp協(xié)議:流式協(xié)議(以數(shù)據(jù)流的形式通信傳輸)、安全協(xié)議(收發(fā)信息都需收到確認(rèn)信息才能完成收發(fā),是一種雙向通道的通信)
tcp協(xié)議在OSI七層協(xié)議中屬于傳輸層,它上承用戶層的數(shù)據(jù)收發(fā),下啟網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層、物理層??梢哉f很多安全數(shù)據(jù)的傳輸通信都是基于tcp協(xié)議進(jìn)行的。
為了讓tcp通信更加方便需要引入一個socket模塊(將網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層、物理層封裝的模塊),我們只要調(diào)用模塊中的相關(guān)接口就能實(shí)現(xiàn)傳輸層下面的繁瑣操作。
簡單的tcp協(xié)議通信模板:(需要一個服務(wù)端和一個客戶端)
服務(wù)端: from socket import * # 確定服務(wù)端傳輸協(xié)議↓↓↓↓↓↓↓ server = socket(AF_INET, SOCK_STREAM) # 這里的SOCK_STREAM代表的就是流式協(xié)議TCP,如果是SOCK_DGRAM就代表UDP協(xié)議 # 固定服務(wù)端IP和PORT,讓客戶端能夠通過IP和端口訪問服務(wù)端↓↓↓↓↓↓↓ server.bind(('127.0.0.1', 8080)) # ('127.0.0.1', 8080)這里必須用元組形式傳入IP和PORT,本地訪問本地IP默認(rèn)為'127.0.0.1' # 設(shè)置半連接池數(shù)量(一般為5) server.listen(5) # 半連接池:客戶端連接請求個數(shù)的容器,當(dāng)前已連接的客戶端信息收發(fā)未完成前,會有最大5個客戶端連接請求進(jìn)入排隊(duì)狀態(tài), # 等待上一個通信完畢后,就可以連接進(jìn)入開始通信。 # 雙向通道建立成功,可以進(jìn)行下一步數(shù)據(jù)的通信了↓↓↓↓↓↓↓ conn, client_addr = server.accept() # 進(jìn)行一次信息的收與發(fā) data = conn.recv(1024) # 每次最大接收1024字節(jié),收到的數(shù)據(jù)為二進(jìn)制Bytes類型 conn.send(data.upper()) # 將收到的數(shù)據(jù)進(jìn)行處理,返回新的數(shù)據(jù),反饋給客戶端(給客戶端發(fā)數(shù)據(jù)),發(fā)的數(shù)據(jù)類型也必須是Bytes類型 # 一輪信息收發(fā)完畢,關(guān)閉已經(jīng)建立的雙向通道 conn.close() 客戶端: from socket import * # 確定客戶端傳輸協(xié)議↓↓↓↓↓↓↓(服務(wù)端和客戶端服務(wù)協(xié)議一樣才能進(jìn)行有效的通信) client = socket(AF_INET, SOCK_STREAM) # 這里的SOCK_STREAM代表的就是流式協(xié)議TCP,如果是SOCK_DGRAM就代表UDP協(xié)議 # 開始連接服務(wù)端IP和PORT,建立雙向鏈接 client.connect(('127.0.0.1', 8080)) # 通過服務(wù)端IP和PORT進(jìn)行連接 # 走到這一步就已經(jīng)建立連接完畢,接下來開始數(shù)據(jù)通信: client.send('hello,server'.encode('utf-8')) # 將發(fā)送的信息轉(zhuǎn)碼成Bytes類型數(shù)據(jù) data = client.recv(1024) # 每次最大收數(shù)據(jù)大小為1024字節(jié)(1kb) print(data.decode('utf-8')) # 將b類型數(shù)據(jù)轉(zhuǎn)換成字符串格式 # 一次傳輸完畢 client.close() # 關(guān)閉客戶端連接 啟動服務(wù)端(服務(wù)端開始監(jiān)聽客戶端的連接請求) 啟動客戶端(客戶端給服務(wù)端發(fā)送連接請求) 建立雙向鏈接完成 客戶端給服務(wù)端發(fā)送信息 hello,server 服務(wù)端收到hello,server,將其轉(zhuǎn)換成大寫,返回給客戶端(此時服務(wù)端一輪通信完畢) 客戶端收到服務(wù)端的反饋信息,打印出HELLO,SERVER(此時客戶端一輪通信完畢)
以上是最基本的一次基于tcp協(xié)議通信的過程客戶端發(fā),服務(wù)端收,服務(wù)端處理數(shù)據(jù)然后發(fā),客戶端收到服務(wù)端發(fā)了的反饋數(shù)據(jù)。
TCP協(xié)議的通信粘包問題:
但是由于tcp協(xié)議是一種流式協(xié)議,流式協(xié)議就會有一個特點(diǎn):數(shù)據(jù)的傳輸像一涓涓水流的形式傳輸,我們在收數(shù)據(jù)的時候默認(rèn)最大收數(shù)據(jù)大小為1024字節(jié),當(dāng)發(fā)送的數(shù)據(jù)小于1024字節(jié)時候當(dāng)然不會有問題,一次性全部收完,但是但是但是當(dāng)發(fā)送的數(shù)據(jù)大于1024字節(jié)的時候,我們這邊又不知道發(fā)送的數(shù)據(jù)大小是多少,只能默認(rèn)的1024字節(jié)的時候,數(shù)據(jù)一次性就不可能收完,只能在這次收1024字節(jié),那1024字節(jié)以外的數(shù)據(jù)呢?由于數(shù)據(jù)的傳輸是流式協(xié)議,所以沒有收完的數(shù)據(jù)會依次排隊(duì)在門外等著,等待你下次收數(shù)據(jù)時候再次收取,這樣如果每次傳的數(shù)據(jù)大小不確認(rèn),收的時候數(shù)據(jù)也不知道該收多少的時候,就會導(dǎo)致每次收數(shù)據(jù)的時候收不完,收不完的數(shù)據(jù)就會在緩存中排隊(duì),等待下次收,收不完的數(shù)據(jù)就好像粘粘在一起(zhan nian)。這就叫tcp的流式協(xié)議的通信粘包問題。
這個問題的更形象過程可以見下圖:
知道這粘包的大致過程,就能夠找到方法對癥下藥了:
粘包問題的解決分析:
粘包問題歸根到底是數(shù)據(jù)接收不徹底導(dǎo)致,那么要解決這個問題最直接的方法就是每次都徹底地收完數(shù)據(jù)。
要想達(dá)到這個目的就需要每次在收數(shù)據(jù)之前事先知道我要收數(shù)據(jù)的文件大小,知道了文件大小我們就能有的放矢,準(zhǔn)確的把數(shù)據(jù)收完不遺留。
解決方法:先發(fā)個包含待發(fā)送文件大小長度的報頭文件>>>>再發(fā)送原始文件
引入模塊struct
具體看代碼:
服務(wù)端: import socket import struct server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: conn, client_addr = server.accept() print('客戶端已連接') while True: try: head = conn.recv(4) size = struct.unpack('i', head)[0] data = conn.recv(size) print('已收到客戶端信息:', data.decode('utf-8')) except ConnectionResetError: print('客戶端已中斷連接') conn.close() break 客戶端: import socket import struct while True: try: client = socket.socket() client.connect(('127.0.0.1', 8080)) print('已連接到服務(wù)端') while True: try: msg = 'abcdefghijklmnopqrstuvwxyz1234567890'.encode('utf-8') head = struct.pack('i', len(msg)) client.send(head) client.send(msg) except ConnectionResetError: print('服務(wù)端已中斷連接') client.close() break except ConnectionRefusedError: print('無法連接到服務(wù)器')
以上方法只是為了試驗(yàn)解決粘包問題,真正應(yīng)用場景可以是上傳或者下載一個大文件的時候,這時就必須要提前知道接收的文件實(shí)際大小,做到100%精確的接收每一個數(shù)據(jù),這時就需要收數(shù)據(jù)前獲取即將收到的文件大小,然后對癥下藥,做到精確接收,但實(shí)現(xiàn)方法不一定非要用struct模塊,struct模塊只是解決粘包問題中的一個官方正式的方法,自己還可以有自己的想法,比如先直接把要發(fā)送文件的大小已字符串的格式發(fā)送過去,然后再發(fā)送這個文件,目的只有一個,知道我接收的文件的大小,精準(zhǔn)接收文件。
下面寫一個客戶端從服務(wù)端下載文件的實(shí)例,供大家參考:(假設(shè)下載文件在服務(wù)端文件同一級)
下載服務(wù)端: import socket import time import struct import json # 計(jì)算當(dāng)前文件夾下文件的md5值、大小 import os, hashlib def get_info(file_name): file_info = {} base_dir = os.path.dirname(__file__) file_dir = os.path.join(base_dir, file_name) if os.path.exists(file_dir): # md5計(jì)算時文件數(shù)據(jù)是放在內(nèi)存中的,當(dāng)我們計(jì)算一個大文件時,可以用update方法進(jìn)行分步計(jì)算, # 每次添加部分文件數(shù)據(jù)進(jìn)行計(jì)算,減少內(nèi)存占用。 with open(file_dir, 'rb') as f: le = 0 d5 = hashlib.md5() for line in f: le += len(line) d5.update(line) file_info['lenth'] = le # 將文件長度加入報頭字典 file_md5 = d5.hexdigest() file_info['md5'] = file_md5 # 將文件md5加入報頭字典 file_size = os.path.getsize(file_dir) / float(1024 * 1024) file_info['size(MB)'] = round(file_size, 2) # 將文件大小加入報頭字典 return file_info else: return file_info server = socket.socket() server.bind(('127.0.0.1', 8080)) server.listen(5) while True: conn, client_addr = server.accept() print('%s >:客戶端(%s)已連接' % (time.strftime('%Y-%m-%d %H:%M:%S'), client_addr)) while True: try: download_filename = conn.recv(1024).decode('utf-8') download_file_info_dic = get_info(download_filename) j_head = json.dumps(download_file_info_dic) # 將文件信息字典轉(zhuǎn)成json字符串格式 head = struct.pack('i', len(j_head)) conn.send(head) conn.send(j_head.encode('utf-8')) if not download_file_info_dic: continue with open(download_filename, 'rb') as f: while True: data=f.read(1024) conn.send(data) # for line in f: # conn.send(line) except ConnectionResetError: print('%s >:客戶端(%s)已斷開' % (time.strftime('%Y-%m-%d %H:%M:%S'), client_addr)) conn.close() break
下載客戶端: import socket import time import struct import json # 進(jìn)度條顯示 def progress(percent,width=30): text=('\r[%%-%ds]'%width)%('x'*int(percent*width)) text=text+'%3s%%' text=text%(round(percent*100)) print(text,end='') while True: try: client = socket.socket() client.connect(('127.0.0.1', 8080)) print('%s >:已連接到服務(wù)端' % time.strftime('%Y-%m-%d %H:%M:%S')) while True: try: file_name = input('請輸入下載文件名稱:') client.send(file_name.encode('utf-8')) head = client.recv(4) # 收報頭 j_dic_lenth = struct.unpack('i', head)[0] # 解壓報頭,獲取json格式的文件信息字典的長度 j_head = client.recv(j_dic_lenth) # 收json格式的信息字典 file_info_dic = json.loads(j_head) # 反序列化json字典,得到文件信息字典 if not file_info_dic: print('文件不存在') continue file_lenth = file_info_dic.get('lenth') file_size = file_info_dic.get('size(MB)') file_md5 = file_info_dic.get('md5') rec_len = 0 with open('cpoy_'+file_name, 'wb') as f: while rec_len < file_lenth: data = client.recv(1024) f.write(data) rec_len += len(data) per=rec_len/file_lenth progress(per) print() # print('下載比例:%6s %%'%) if not rec_len: print('文件不存在') else: print('文件[%s]下載成功: 大?。?s MB|md5值:[%s]' % (file_name, file_size, file_md5)) except ConnectionResetError: print('%s >:服務(wù)端已終止' % time.strftime('%Y-%m-%d %H:%M:%S')) client.close() break except ConnectionRefusedError: print('%s >:無法連接到服務(wù)器' % time.strftime('%Y-%m-%d %H:%M:%S'))
文件上傳同理,只是換成客戶端給服務(wù)端發(fā)送文件,服務(wù)端接收。
接下來我們來學(xué)習(xí)一下TCP協(xié)議下通信利用socketserver模塊實(shí)現(xiàn)多客戶端并發(fā)通信的效果:
服務(wù)端: import socketserver import time class MyTcpHandler(socketserver.BaseRequestHandler): # 到這里表示服務(wù)端已監(jiān)聽到一個客戶端的連接請求,將通信交給一個handle方法實(shí)現(xiàn),自己再去監(jiān)聽客戶連接請求 def handle(self): # 建立雙向通道,進(jìn)行通信 print('%s|客戶端%s已連接' % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address)) while True: try: data = self.request.recv(1024) msg = '我已收到您的請求[%s],感謝您的關(guān)注!' % data.decode('utf-8') self.request.send(msg.encode('utf-8')) except ConnectionResetError: print('%s|客戶端%s已斷開連接' % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address)) break if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyTcpHandler) # 綁定服務(wù)端IP和PORT,并產(chǎn)生并發(fā)方法對象 print('等待連接請求中...') server.serve_forever() # 服務(wù)端一直開啟
客戶端: from socket import * import time server_addr = ('127.0.0.1', 8080) count = 1 while True: if count > 10: time.sleep(1) print('%s|連接%s超時' % (time.strftime('%Y-%m-%d %H:%M:%S'), server_addr)) break try: client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 8080)) count = 1 print('%s|服務(wù)端%s連接成功' % (time.strftime('%Y-%m-%d %H:%M:%S'), server_addr)) while True: try: client.send('北鼻'.encode('utf-8')) data = client.recv(1024) print(data.decode('utf-8')) time.sleep(0.5) except ConnectionResetError: print('%s|服務(wù)端%s已中斷' % (time.strftime('%Y-%m-%d %H:%M:%S'), server_addr)) client.close() break except ConnectionRefusedError: print('無法連接到服務(wù)端') count += 1
同時再添加客戶端2、客戶端3,將發(fā)送數(shù)據(jù)稍微修改一下,實(shí)現(xiàn)多客戶端并發(fā)通信服務(wù)端。
通過subprocess模塊,實(shí)現(xiàn)遠(yuǎn)程shell命令行命令
服務(wù)端 import socketserver import struct import subprocess class MyTcpHandler(socketserver.BaseRequestHandler): def handle(self): while True: print('客戶端<%s,%s>已連接' % self.client_address) try: cmd = self.request.recv(1024).decode('utf-8') res = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = res.stdout.read() stderr = res.stderr.read() head = struct.pack('i', len(stdout + stderr)) self.request.send(head) self.request.send(stdout) self.request.send(stderr) except ConnectionResetError: print('客戶端<%s,%s>已中斷連接' % self.client_address) self.request.close() break if __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyTcpHandler) server.serve_forever()
客戶端 from socket import * import struct while True: try: client = socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: try: cmd = input('>>>>>>>:').strip().encode('utf-8') client.send(cmd) head = client.recv(4) size = struct.unpack('i', head)[0] cur_size = 0 result = b'' while cur_size < size: data = client.recv(1024) cur_size += len(data) result += data print(result.decode('gbk')) # windows系統(tǒng)默認(rèn)編碼是gbk,解碼肯定也要用gbk except ConnectionResetError: print('服務(wù)端已中斷') client.close() break except ConnectionRefusedError: print('無法連接服務(wù)端')
通過客戶端輸入命令,在服務(wù)端執(zhí)行shell命令,通過服務(wù)端執(zhí)行subprocess模塊達(dá)到遠(yuǎn)程shell命令操作,此過程主要需要考慮2個難點(diǎn),①解決命令產(chǎn)生結(jié)果數(shù)據(jù)的發(fā)送粘包問題,②注意返回結(jié)果的shell命令結(jié)果是gbk編碼,接收后需要用gbk解碼一下。
以上這篇對python中基于tcp協(xié)議的通信(數(shù)據(jù)傳輸)實(shí)例講解就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
- Python Scapy隨心所欲研究TCP協(xié)議棧
- Python實(shí)現(xiàn)基于TCP UDP協(xié)議的IPv4 IPv6模式客戶端和服務(wù)端功能示例
- Python網(wǎng)絡(luò)編程之TCP與UDP協(xié)議套接字用法示例
- Python+Socket實(shí)現(xiàn)基于TCP協(xié)議的客戶與服務(wù)端中文自動回復(fù)聊天功能示例
- Python實(shí)現(xiàn)TCP/IP協(xié)議下的端口轉(zhuǎn)發(fā)及重定向示例
- Python使用?TCP協(xié)議實(shí)現(xiàn)智能聊天機(jī)器人功能
- Python網(wǎng)絡(luò)編程之Python編寫TCP協(xié)議程序的步驟
相關(guān)文章
pycharm配置python環(huán)境的詳細(xì)圖文教程
PyCharm是一款功能強(qiáng)大的Python編輯器,具有跨平臺性,下面這篇文章主要給大家介紹了關(guān)于pycharm配置python環(huán)境的詳細(xì)圖文教程,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01Django使用HTTP協(xié)議向服務(wù)器傳參方式小結(jié)
本文主要介紹了Django使用HTTP協(xié)議向服務(wù)器傳參方式小結(jié),用戶發(fā)送請求時攜帶的參數(shù)后端需要使用,而不同的發(fā)送參數(shù)的方式對應(yīng)了不同的提取參數(shù)的方式,本文就詳細(xì)的介紹一下2021-08-08Python數(shù)據(jù)解析之BeautifulSoup4的用法詳解
Beautiful?Soup?是一個可以從?HTML?或?XML?文件中提取數(shù)據(jù)的?Python?庫,這篇文章主要來和大家介紹一下BeautifulSoup4的用法,需要的可以參考一下2023-06-06python 將對象設(shè)置為可迭代的兩種實(shí)現(xiàn)方法
今天小編就為大家分享一篇python 將對象設(shè)置為可迭代的兩種實(shí)現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-01-01淺析Python+OpenCV使用攝像頭追蹤人臉面部血液變化實(shí)現(xiàn)脈搏評估
這篇文章主要介紹了Python+OpenCV使用攝像頭追蹤人臉面部血液變化實(shí)現(xiàn)脈搏評估,本文通過一段代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10