1. 程式人生 > >網絡編程(三)——通信循環、鏈接循環、粘包問題

網絡編程(三)——通信循環、鏈接循環、粘包問題

res font 優化算法 exce erro recv data name 機制

通信循環、鏈接循環、粘包問題

一、通信循環

服務端和客戶端可以進行連續的信息交流

技術分享圖片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)

conn, addr = ser_socket.accept()

while True:
    try:               # 拋出異常,若不拋出處理,一旦客戶端強行退出,服務端就會報錯
        data = conn.recv(1024)
        
print(data.decode(utf-8)) conn.send(data.upper()) except ConnectionResetError: break conn.close() ser_socket.close()
通信循環服務端 技術分享圖片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

#通信循環,可以多次輸入
while True:
    msg = input(>>>>:
).strip() if len(msg) == 0: # 如果輸入為空,給服務端發送信息之後,服務端什麽都沒接受,一直處於阻塞狀態 continue cli_socket.send(msg.encode(utf-8)) data = cli_socket.recv(1024) print(data.decode(utf-8)) cli_socket.close()
通信循環客戶端

tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制

二、鏈接循環

可以啟動多個客戶端,但是只有一個客戶端是處於連接狀態,其余部分在半連接池等待連接,等待的數量不能超過半連接池的最大監聽數量

技術分享圖片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)


#鏈接循環,可以同時啟動最多6個客戶端,但是只有一個處於連接狀態,其余最多5個在半連接池等待。只有當連接狀態的客戶端斷開連接,下一個客戶端才進入連接
while True:
    conn, addr = ser_socket.accept()

    # 通信循環
    while True:
        try:
            data = conn.recv(1024)
            print(data.decode(utf-8))

            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

ser_socket.close()
鏈接循環服務端 技術分享圖片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

while True:
    msg = input(>>>>:).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))

    data = cli_socket.recv(1024)
    print(data.decode(utf-8))

cli_socket.close()
鏈接循環客戶端

三、粘包問題

1、模擬ssh遠程執行命令

技術分享圖片
from socket import socket, AF_INET, SOCK_STREAM
import subprocess

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8882))

ser_socket.listen(5)
while True:
    conn, addr = ser_socket.accept()
    while True:
        try:
            data = conn.recv(1024)
            obj = subprocess.Popen(data.decode(utf-8), 
                                   shell=True, 
                                   stdout=subprocess.PIPE, 
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            conn.send(stdout + stderr)
        except ConnectionResetError:
            break
    conn.close()

ser_socket.close()
服務端 技術分享圖片
from socket import socket, AF_INET, SOCK_STREAM

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8882))

while True:
    msg = input(>>>).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))
    data = cli_socket.recv(1024)
    print(data.decode(gbk))      #  Windows系統,默認編碼gbk,所以用gbk解碼

cli_socket.close()
客戶端

2、產生粘包原因

(1)所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

(2)此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。

在上面的例子中,如果執行命令tasklist,那麽就會存在粘包問題。由於TCP協議是流式協議,所以數據都以流的形式傳輸。假如數據大小是123456,可是已經設定了接收的大小 是1024,所以只接受了數據中的一小部分,但是,剩余部分數據並不會消失,會一直存在於操作系統中,所以下一次接收數據的時候是優先從剩余數據中接收。這樣所有數據就亂套了,這就是粘包問題。

3、發生粘包的兩種情況

(1)發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)

技術分享圖片
from socket import *

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8886))

ser_socket.listen(5)

conn, addr = ser_socket.accept()

data = conn.recv(1024)
print(第一次接收:, data.decode(utf-8))
data1 = conn.recv(5)
print(第二次接收:, data1.decode(utf-8))
data2 = conn.recv(1024)
print(第三次接收:, data2.decode(utf-8))

conn.send(data.upper())

conn.close()

ser_socket.close()
服務端 技術分享圖片
from socket import *

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8886))

cli_socket.send(hello.encode(utf-8))
cli_socket.send(world.encode(utf-8))
cli_socket.send(object.encode(utf-8))

# data = cli_socket.recv(1024)
# print(data.decode(‘utf-8‘))

cli_socket.close()
客戶端

(2)接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)

  例如:模擬ssh遠程執行命令,若執行tasklist命令,在客戶端,無法幾次性全部接受執行結果,所以剩余結果會在下一次執行命令式優先接收

技術分享圖片
from socket import socket, AF_INET, SOCK_STREAM
import subprocess

ser_socket = socket(AF_INET, SOCK_STREAM)

ser_socket.bind((127.0.0.1, 8882))

ser_socket.listen(5)
while True:
    conn, addr = ser_socket.accept()
    while True:
        try:
            data = conn.recv(1024)
            obj = subprocess.Popen(data.decode(utf-8),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            conn.send(stdout + stderr)
        except ConnectionResetError:
            break
    conn.close()

ser_socket.close()
服務端 技術分享圖片
from socket import socket, AF_INET, SOCK_STREAM

cli_socket = socket(AF_INET, SOCK_STREAM)

cli_socket.connect((127.0.0.1, 8882))

while True:
    msg = input(>>>).strip()
    if len(msg) == 0:
        continue
    cli_socket.send(msg.encode(utf-8))
    data = cli_socket.recv(1024)
    print(data.decode(gbk))

cli_socket.close()
客戶端

4、解決粘包問題的方法

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然後再取真實數據。

(1)簡單版本

技術分享圖片
# 服務端必須滿足至少三點:
# 1. 綁定一個固定的ip和port
# 2. 一直對外提供服務,穩定運行
# 3. 能夠支持並發
from socket import *
import subprocess
import struct

server = socket(AF_INET, SOCK_STREAM)
server.bind((127.0.0.1, 8081))
server.listen(5)

# 鏈接循環
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    # 通信循環
    while True:
        try:
            cmd = conn.recv(1024) #cmd=b‘dir‘
            # if len(cmd) == 0: break  # 針對linux系統
            obj=subprocess.Popen(cmd.decode(utf-8),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE
                             )
            stdout=obj.stdout.read()
            stderr=obj.stderr.read()
            # 1. 先制作固定長度的報頭
            header=struct.pack(i,len(stdout) + len(stderr))
            # 2. 再發送報頭
            conn.send(header)
            # 3. 最後發送真實的數據
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break

    conn.close()

server.close()
服務端 技術分享圖片
from socket import *
import struct

client = socket(AF_INET, SOCK_STREAM)
client.connect((127.0.0.1, 8081))

# 通信循環
while True:
    cmd=input(>>: ).strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode(utf-8))
    #1. 先收報頭,從報頭裏解出數據的長度
    header=client.recv(4)
    total_size=struct.unpack(i,header)[0]
    #2. 接收真正的數據
    cmd_res=b‘‘
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data

    print(cmd_res.decode(gbk))

client.close()
客戶端

(2)終極版本

技術分享圖片
# 服務端必須滿足至少三點:
# 1. 綁定一個固定的ip和port
# 2. 一直對外提供服務,穩定運行
# 3. 能夠支持並發
from socket import *
import subprocess
import struct
import json

server = socket(AF_INET, SOCK_STREAM)
server.bind((127.0.0.1, 8081))
server.listen(5)

# 鏈接循環
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    # 通信循環
    while True:
        try:
            cmd = conn.recv(1024)  # cmd=b‘dir‘
            if len(cmd) == 0: break  # 針對linux系統
            obj = subprocess.Popen(cmd.decode(utf-8),
                                   shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE
                                   )
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            # 1. 先制作報頭
            header_dic = {
                filename: a.txt,
                md5: asdfasdf123123x1,
                total_size: len(stdout) + len(stderr)
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode(utf-8)

            # 2. 先發送4個bytes(包含報頭的長度)
            conn.send(struct.pack(i, len(header_bytes)))
            # 3  再發送報頭
            conn.send(header_bytes)

            # 4. 最後發送真實的數據
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break

    conn.close()

server.close()
服務端 技術分享圖片
from socket import *
import struct
import json

client = socket(AF_INET, SOCK_STREAM)
client.connect((127.0.0.1, 8081))

# 通信循環
while True:
    cmd=input(>>: ).strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode(utf-8))
    #1. 先收4bytes,解出報頭的長度
    header_size=struct.unpack(i,client.recv(4))[0]

    #2. 再接收報頭,拿到header_dic
    header_bytes=client.recv(header_size)
    header_json=header_bytes.decode(utf-8)
    header_dic=json.loads(header_json)
    print(header_dic)
    total_size=header_dic[total_size]

    #3. 接收真正的數據
    cmd_res=b‘‘
    recv_size=0
    while recv_size < total_size:
        data=client.recv(1024)
        recv_size+=len(data)
        cmd_res+=data

    print(cmd_res.decode(gbk))

client.close()
客戶端

網絡編程(三)——通信循環、鏈接循環、粘包問題