day 27

day 27

1. 粘包

1.1 什麼是粘包

socket原理

發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。python

發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。算法

  • TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。

1.2 爲何會粘包

主要是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。shell

  • 發送方緣由編程

    TCP默認使用Nagle算法(主要做用:減小網絡中報文段的數量),而Nagle算法主要作兩件事:json

    1. 只有上一個分組獲得確認,纔會發送下一個分組
    2. 收集多個小分組,在一個確認到來時一塊兒發送

    Nagle算法形成了發送方可能會出現粘包問題。緩存

  • 接收方緣由服務器

    TCP接收到數據包時,並不會立刻交到應用層進行處理,或者說應用層並不會當即處理。實際上,TCP將接收到的數據包保存在接收緩存裏,而後應用程序主動從緩存讀取收到的分組。這樣一來,若是TCP接收數據包到緩存的速度大於應用程序從緩存中讀取數據包的速度,多個包就會被緩存,應用程序就有可能讀取到多個首尾相接粘到一塊兒的包。網絡

1.3 發生粘包的兩種狀況

  1. 發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)併發

    # server端
    import socket
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr=tcp_socket_server.accept()
    
    data1=conn.recv(10)
    data2=conn.recv(10)
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    # client端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    
    s.send('hello'.encode('utf-8'))
    s.send('egg'.encode('utf-8'))
  2. 接收方的緩存機制 接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)socket

    # server端
    from socket import *
    ip_port=('127.0.0.1',8080)
    
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(5)
    
    conn,addr=tcp_socket_server.accept()
    
    data1=conn.recv(2) #一次沒有收完整
    data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的
    
    print('----->',data1.decode('utf-8'))
    print('----->',data2.decode('utf-8'))
    
    conn.close()
    # client端
    import socket
    BUFSIZE=1024
    ip_port=('127.0.0.1',8080)
    
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    res=s.connect_ex(ip_port)
    
    s.send('hello egg'.encode('utf-8'))

2 struct 模塊

2.1 struct 模塊介紹

struct 模塊: 是用來將整型的數字轉成固定長度的bytes。

  1. 按照指定格式將Python數據轉換爲字符串,該字符串爲字節流,如網絡傳輸時,不能傳輸int,此時先將int轉化爲字節流,而後再發送;

  2. 按照指定格式將字節流轉換爲Python指定的數據類型

    # 按照給定的格式(fmt),把數據封裝成字符串(其實是相似於c結構體的字節流)
    pack(fmt, v1, v2, ...) 
    
    # 按照給定的格式(fmt)解析字節流string,返回解析出來的tuple
    unpack(fmt,string)

    fmt 支持的格式包括:

2.2 解決粘包問題

# 服務端
from socket import *
 
import subprocess
import struct
import json
 
server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
    conn,client_addr=server.accept()
    print(conn,client_addr)#(鏈接對象,客戶端的ip和端口)
    while True:
        try:
            cmd=conn.recv(1024)
            obj=subprocess.Popen(
                cmd.decode('utf-8'),
                shell=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
                                 )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            #一、製做報頭
            header_dic={
                'total_size':len(stdout)+len(stderr),
                'md5':'dgdsfsdfdsdfsfewrewge',
                'file_name':'a.txt'
            }
            header_json=json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')
            #二、先發送報頭的長度
            header_size=len(header_bytes)
            conn.send(struct.pack('i',header_size))
            #三、發送報頭
            conn.send(header_bytes)
            #四、發送真實的數據
            conn.send(stdout)
            conn.send(stderr)
 
        except ConnectionResetError:
            break
    conn.close()
server.close()
# 客戶端
from socket import *
import json
import struct
 
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
 
while True:
    cmd=input(">>:").strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
 
    #一、接收報文頭的長度
    header_size=struct.unpack('i',client.recv(4))[0]
    #二、接收報文
    header_bytes=client.recv(header_size)
 
    #三、解析報文
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)
 
    #四、獲取真實數據的長度
    totol_size=header_dic['total_size']
 
    #五、獲取數據
    recv_size=0
    res=b''
    while recv_size<totol_size:
        recv_date=client.recv(1024)
        res+=recv_date
        recv_size+=len(recv_date)
 
    print(res.decode('gbk'))
client.close()

2.3 補充

import struct
#struct是用來將整型的數字轉成固定長度的bytes.
import json
 
header_dic={
    'total_size':32322,
    'md5':'gdssfsfsdfsf',
    'filename':'a.txt'
 
}
#一、將報頭字典序列化。
header_json=json.dumps(header_dic)
#二、將序列後的字典轉成字節
header_bytes=header_json.encode('utf-8')
#三、獲取序列的字字典轉成字節的個數
header_size=len(header_bytes)
print(header_size)
#四、將這個個數轉成固字長度的字節表示
obj=struct.pack('i',header_size)
print(obj,len(obj))
#、這個固定長度的字節通過反轉後是一個元組。
res=struct.unpack('i',obj)
#、經過按索取值就可等到報頭字典長度。
header_size=res[0]

3. 基於UDP協議的socket套接字

3.1 UDP套接字簡單實現

# 服務端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》UDP
server.bind(('127.0.0.1', 8080))

while True:
    data, client_addr = server.recvfrom(1024)
    print('===>', data, client_addr)
    server.sendto(data.upper(), client_addr)

server.close()
# 客戶端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》UDP

while True:
    msg = input('>>: ').strip()  # msg=''
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
  • UDP是無連接的,先啓動哪一端都不會報錯
  • UDP協議是數據報協議,發空的時候也會自帶報頭,所以客戶端輸入空,服務端也能收到

3.2 UDP套接字無粘包問題

# 服務端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》udp
server.bind(('127.0.0.1', 8080))

data, client_addr = server.recvfrom(1024)  # b'hello'==>b'h'
print('第一次:', client_addr, data)

data, client_addr = server.recvfrom(1024)  # b'world' =>b'world'
print('第二次:', client_addr, data)
#
# data,client_addr=server.recvfrom(1024)
# print('第三次:',client_addr,data)

server.close()
# 客戶端
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》udp

client.sendto('hello'.encode('utf-8'), ('127.0.0.1', 8080))
client.sendto('world'.encode('utf-8'), ('127.0.0.1', 8080))
# client.sendto(''.encode('utf-8'),('127.0.0.1',8080))

client.close()

3.3 基於UDP實現QQ聊天室

# 服務端
import socket

server = socket.socket(type=socket.SOCK_DGRAM)

server.bind(
    ('127.0.0.1', 9527)
)

while True:

    # 服務端接收客戶端傳過來的消息
    msg, addr = server.recvfrom(1024)  # (消息,客戶端地址)
    msg1, addr1 = server.recvfrom(1024)  # (消息,客戶端地址)
    msg2, addr2 = server.recvfrom(1024)  # (消息,客戶端地址)

    print(addr)
    print(addr1)
    print(addr2)
    print(msg.decode('utf-8'))
    print(msg1.decode('utf-8'))
    print(msg2.decode('utf-8'))

    # 服務端往客戶端發送消息
    send_msg = input('服務端發送消息:').encode('utf-8')
    server.sendto(send_msg, addr)
    server.sendto(send_msg, addr1)
    server.sendto(send_msg, addr2)
# 客戶端 1
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客戶端1: ').encode('utf-8')

    # 發送消息必需要加上對方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))
# 客戶端 2
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客戶端2: ').encode('utf-8')

    # 發送消息必需要加上對方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))
# 客戶端 3
import socket

client = socket.socket(type=socket.SOCK_DGRAM)

server_ip_port = ('127.0.0.1', 9527)

while True:
    send_msg = input('客戶端3: ').encode('utf-8')

    # 發送消息必需要加上對方地址
    client.sendto(send_msg, server_ip_port)

    # 能接收任何人的消息
    msg = client.recv(1024)

    print(msg.decode('utf-8'))

4 基於socketserver實現併發的socket編程

4.1 基於TCP協議

# 服務端
import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # 通訊循環
        while True:
            # print(self.client_address)
            # print(self.request) #self.request=conn

            try:
                data = self.request.recv(1024)
                if len(data) == 0: break
                self.request.send(data.upper())
            except ConnectionResetError:
                break


if __name__ == '__main__':
    s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyHandler, bind_and_activate=True)

    s.serve_forever()  # 表明鏈接循環
    # 循環創建鏈接,每創建一個鏈接就會啓動一個線程(服務員)+調用Myhanlder類產生一個對象,調用該對象下的handle方法,專門與剛剛創建好的鏈接作通訊循環
# 客戶端 1 
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))  # 指定服務端ip和端口

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client33333'  # msg=''
    if len(msg) == 0: continue
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)

phone.close()
# 客戶端 2 
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080))  # 指定服務端ip和端口

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client11111'  # msg=''
    if len(msg) == 0: continue
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)
    print(data)

phone.close()

4.2 基於UDP協議

# 服務端
import socketserver


class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # 通訊循環
        print(self.client_address)
        print(self.request)

        data = self.request[0]
        print('客戶消息', data)
        self.request[1].sendto(data.upper(), self.client_address)


if __name__ == '__main__':
    s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyHandler)
    s.serve_forever()
# 客戶端 1 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》udp

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client1111'
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
# 客戶端 2 
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 數據報協議-》udp

while True:
    # msg=input('>>: ').strip() #msg=''
    msg = 'client2222'
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data, server_addr = client.recvfrom(1024)
    print(data)

client.close()
相關文章
相關標籤/搜索