python中基於tcp協議與udp的通信(數據傳輸)


一、TCP協議介紹

流式協議(以數據流的形式通信傳輸)

安全協議(收發信息都需收到確認信息才能完成收發,是一種雙向通道的通信)

tcp協議在OSI七層協議中屬於傳輸層,它上承用戶層的數據收發,下啟網絡層、數據鏈路層、物理層。可以說很多安全數據的傳輸通信都是基於tcp協議進行的。

為了讓tcp通信更加方便需要引入一個socket模塊(將網絡層、數據鏈路層、物理層封裝的模塊),我們只要調用模塊中的相關接口就能實現傳輸層下面的繁瑣操作。

簡單的tcp協議通信模板(需要一個服務端和一個客戶端)

服務端:

from socket import *
# 確定服務端傳輸協議↓↓↓↓↓↓↓
server = socket(AF_INET, SOCK_STREAM) # 這里的SOCK_STREAM代表的就是流式協議TCP,如果是SOCK_DGRAM就代表UDP協議
# 固定服務端IP和PORT,讓客戶端能夠通過IP和端口訪問服務端↓↓↓↓↓↓↓
server.bind(('127.0.0.1', 8080))    # ('127.0.0.1', 8080)這里必須用元組形式傳入IP和PORT,本地訪問本地IP默認為'127.0.0.1'
# 設置半連接池數量(一般為5)
server.listen(5)  # 半連接池:客戶端連接請求個數的容器,當前已連接的客戶端信息收發未完成前,會有最大5個客戶端連接請求進入排隊狀態,
                  # 等待上一個通信完畢后,就可以連接進入開始通信。                                                                                      

# 雙向通道建立成功,可以進行下一步數據的通信了↓↓↓↓↓↓↓
conn, client_addr = server.accept()
# 進行一次信息的收與發
data = conn.recv(1024)    # 每次最大接收1024字節,收到的數據為二進制Bytes類型

conn.send(data.upper())   # 將收到的數據進行處理,返回新的數據,反饋給客戶端(給客戶端發數據),發的數據類型也必須是Bytes類型

# 一輪信息收發完畢,關閉已經建立的雙向通道
conn.close()


客戶端:
from socket import *
# 確定客戶端傳輸協議↓↓↓↓↓↓↓(服務端和客戶端服務協議一樣才能進行有效的通信)
client = socket(AF_INET, SOCK_STREAM)  # 這里的SOCK_STREAM代表的就是流式協議TCP,如果是SOCK_DGRAM就代表UDP協議
# 開始連接服務端IP和PORT,建立雙向鏈接
client.connect(('127.0.0.1', 8080))  # 通過服務端IP和PORT進行連接

# 走到這一步就已經建立連接完畢,接下來開始數據通信:
client.send('hello,server'.encode('utf-8'))    # 將發送的信息轉碼成Bytes類型數據

data = client.recv(1024)  # 每次最大收數據大小為1024字節(1kb)

print(data.decode('utf-8'))  # 將b類型數據轉換成字符串格式

# 一次傳輸完畢
client.close()    # 關閉客戶端連接


啟動服務端(服務端開始監聽客戶端的連接請求)
啟動客戶端(客戶端給服務端發送連接請求)
建立雙向鏈接完成
客戶端給服務端發送信息 hello,server
服務端收到hello,server,將其轉換成大寫,返回給客戶端(此時服務端一輪通信完畢)
客戶端收到服務端的反饋信息,打印出HELLO,SERVER(此時客戶端一輪通信完畢)

以上是最基本的一次基於tcp協議通信的過程客戶端發,服務端收,服務端處理數據然后發,客戶端收到服務端發了的反饋數據。

TCP協議的通信粘包問題

但是由於tcp協議是一種流式協議,流式協議就會有一個特點:數據的傳輸像一涓涓水流的形式傳輸,我們在收數據的時候默認最大收數據大小為1024字節,當發送的數據小於1024字節時候當然不會有問題,一次性全部收完,但是但是但是當發送的數據大於1024字節的時候,我們這邊又不知道發送的數據大小是多少,只能默認的1024字節的時候,數據一次性就不可能收完,只能在這次收1024字節,那1024字節以外的數據呢?由於數據的傳輸是流式協議,所以沒有收完的數據會依次排隊在門外等着,等待你下次收數據時候再次收取,這樣如果每次傳的數據大小不確認,收的時候數據也不知道該收多少的時候,就會導致每次收數據的時候收不完,收不完的數據就會在緩存中排隊,等待下次收,收不完的數據就好像粘粘在一起(zhan nian)。這就叫tcp的流式協議的通信粘包問題。

這個問題的更形象過程可以見下圖:

 

知道這粘包的大致過程,就能夠找到方法對症下葯了:

粘包問題的解決分析:

粘包問題歸根到底是數據接收不徹底導致,那么要解決這個問題最直接的方法就是每次都徹底地收完數據。

要想達到這個目的就需要每次在收數據之前事先知道我要收數據的文件大小,知道了文件大小我們就能有的放矢,准確的把數據收完不遺留。

 解決方法:先發個包含待發送文件大小長度的報頭文件>>>>再發送原始文件

引入模塊struct 

具體看代碼:

服務端:
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('已連接到服務端')
        while True:
            try:
                msg = 'abcdefghijklmnopqrstuvwxyz1234567890'.encode('utf-8')
                head = struct.pack('i', len(msg))
                client.send(head)
                client.send(msg)

            except ConnectionResetError:
                print('服務端已中斷連接')
                client.close()
                break

    except ConnectionRefusedError:
        print('無法連接到服務器')

以上方法只是為了試驗解決粘包問題,真正應用場景可以是上傳或者下載一個大文件的時候,這時就必須要提前知道接收的文件實際大小,做到100%精確的接收每一個數據,這時就需要收數據前獲取即將收到的文件大小,然后對症下葯,做到精確接收,但實現方法不一定非要用struct模塊,struct模塊只是解決粘包問題中的一個官方正式的方法,自己還可以有自己的想法,比如先直接把要發送文件的大小已字符串的格式發送過去,然后再發送這個文件,目的只有一個,知道我接收的文件的大小,精准接收文件。

下面寫一個客戶端從服務端下載文件的實例,供大家參考:(假設下載文件在服務端文件同一級)

下載服務端:

import socket
import time
import struct
import json

# 計算當前文件夾下文件的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計算時文件數據是放在內存中的,當我們計算一個大文件時,可以用update方法進行分步計算,
        # 每次添加部分文件數據進行計算,減少內存占用。
        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)  # 將文件信息字典轉成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)
            if not data:
              break 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

# 進度條顯示
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 >:已連接到服務端' % 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 >:服務端已終止' % time.strftime('%Y-%m-%d %H:%M:%S'))
                client.close()
                break

    except ConnectionRefusedError:
        print('%s >:無法連接到服務器' % time.strftime('%Y-%m-%d %H:%M:%S'))

 文件上傳同理,只是換成客戶端給服務端發送文件,服務端接收。

TCP協議下通信利用socketserver模塊實現多客戶端並發通信

服務端:
import socketserver
import time

class MyTcpHandler(socketserver.BaseRequestHandler):
    # 到這里表示服務端已監聽到一個客戶端的連接請求,將通信交給一個handle方法實現,自己再去監聽客戶連接請求
    def handle(self):
        # 建立雙向通道,進行通信
        print('%s|客戶端%s已連接' % (time.strftime('%Y-%m-%d %H:%M:%S'), self.client_address))
        while True:
            try:
                data = self.request.recv(1024)
                msg = '我已收到您的請求[%s],感謝您的關注!' % 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)   # 綁定服務端IP和PORT,並產生並發方法對象
    print('等待連接請求中...')
    server.serve_forever()  # 服務端一直開啟
客戶端:
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|服務端%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|服務端%s已中斷' % (time.strftime('%Y-%m-%d %H:%M:%S'), server_addr))
                client.close()
                break
    except ConnectionRefusedError:
        print('無法連接到服務端')
        count += 1

同時再添加客戶端2、客戶端3,將發送數據稍微修改一下,實現多客戶端並發通信服務端。

通過subprocess模塊,實現遠程shell命令行命令

服務端
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系統默認編碼是gbk,解碼肯定也要用gbk
            except ConnectionResetError:
                print('服務端已中斷')
                client.close()
                break

    except ConnectionRefusedError:
        print('無法連接服務端')

通過客戶端輸入命令,在服務端執行shell命令,通過服務端執行subprocess模塊達到遠程shell命令操作,此過程主要需要考慮2個難點,①解決命令產生結果數據的發送粘包問題,②注意返回結果的shell命令結果是gbk編碼,接收后需要用gbk解碼一下。

二、UDP協議介紹

UDP叫數據報協議,意味着發消息都帶有數據報頭
udp的server不需要就行監聽也不需要建立連接
在啟動服務之后只能被動的等待客戶端發送消息過來,客戶端發送消息的時候,要帶上服務端的地址
服務端在回復消息的時候,也需要帶上客戶端的地址

1.udp協議客戶端允許發空
2.udp協議不會粘包
3.udp協議服務端不存在的情況下,客戶端照樣不會報錯
4.udp協議支持並發

簡單的UDP協議數據傳輸

# 服務端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))

msg, addr = server.recvfrom(1024)
print(msg.decode('utf-8'))
server.sendto(b'hello', addr)

server.close()

#客戶端
import socket

client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8080)

client.sendto(b'hello server baby!', server_addr)
msg, addr = client.recvfrom(1024)
print(msg, addr)

tips:

udp特點 >>> 無鏈接,類似於發短信,發了就行對方愛回不回,沒有任何關系
將服務端關了,客戶端起起來照樣能夠發數據。因為不需要考慮服務端能不能收到

驗證udp協議有無粘包問題

import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1',8080))
print(server.recvfrom(1024))
print(server.recvfrom(1024))
print(server.recvfrom(1024))

import socket
client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1',8080)
client.sendto(b'hello',server_addr)
client.sendto(b'hello',server_addr)
client.sendto(b'hello',server_addr)

小知識點補充:

windows電腦和max電腦的時間同步功能,其實就是基於udp朝windows,max服務器發送請求獲取標准時間

UDP協議下通信利用socketserver模塊實現多客戶端並發通信

# 服務端
import socketserver

class MyUdpHandler(socketserver.BaseRequestHandler):
    def handle(self):
        data, sock = self.request
        sock.sendto(data.upper(), self.client_address)

if __name__ == '__main__':
    server = socketserver.ThreadingUDPServer(('127.0.0.1', 8081), MyUdpHandler)
    server.serve_forever()


# 客戶端1、2、3、4...
from _socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
    client.sendto(b'ddddddd', ('127.0.0.1', 8081))
    data, addr = client.recvfrom(1024)
    print(data.decode('utf-8'))

 

 

 

                                                                      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM