11-03 socket通信


Socket概念

Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。

image-20190916193420725

基於TCP協議的socket

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址綁定到套接字
sk.listen()          #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024)  #接收客戶端信息
print(ret)       #打印客戶端信息
conn.send(b'hi')        #向客戶端發送信息
conn.close()       #關閉客戶端套接字
sk.close()        #關閉服務器套接字(可選)

client端

import socket
sk = socket.socket()           # 創建客戶套接字
sk.connect(('127.0.0.1',8898))    # 嘗試連接服務器
sk.send(b'hello!')
ret = sk.recv(1024)         # 對話(發送/接收)
print(ret)
sk.close()            # 關閉客戶套接字
問題:有的同學在重啟服務端時可能會遇到報錯
#加入一條socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898))  #把地址綁定到套接字
sk.listen()          #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024)   #接收客戶端信息
print(ret)              #打印客戶端信息
conn.send(b'hi')        #向客戶端發送信息
conn.close()       #關閉客戶端套接字
sk.close()        #關閉服務器套接字(可選)

tcp是基於鏈接的,必須先啟動服務端,然后再啟動客戶端去鏈接服務端

![img](../配圖/Mon Oct 21 2019 13:29:15 GMT+0800 (CST).png)

基於UDP協議的socket

server端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #創建一個服務器的套接字
udp_sk.bind(('127.0.0.1',9000))        #綁定服務器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 對話(接收與發送)
udp_sk.close()                         # 關閉服務器套接字

client端

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

udp是無鏈接的,啟動服務之后可以直接接受消息,不需要提前建立鏈接

img

黏包現象

res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)

輸出的結果的編碼是以當前所在的系統為准的,如果是windows,那么res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼,且只能從管道里讀一次結果

同時執行多條命令之后,得到的結果很可能只有一部分,在執行其他命令的時候又接收到之前執行的另外一部分結果,這種顯現就是黏包

img

基於tcp協議實現的黏包

Server端

#_*_coding:utf-8_*_
from socket import *
import subprocess

ip_port=('127.0.0.1',8888)
BUFSIZE=1024

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

while True:
    conn,addr=tcp_socket_server.accept()
    print('客戶端',addr)

    while True:
        cmd=conn.recv(BUFSIZE)
        if len(cmd) == 0:break

        res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)

        stderr=res.stderr.read()
        stdout=res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)

Client端

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8888)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    act_res=s.recv(BUFSIZE)

    print(act_res.decode('utf-8'),end='')

img

黏包成因

TCP面向流的通信特點和Nagle算法

TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。 
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。 
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。

UDP不會發生黏包

UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。 
不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。 
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。 
不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y;x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠。

555

會發生黏包的兩種情況

情況一 發送方的緩存機制

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

情況二 接收方的緩存機制

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

總結

黏包現象只發生在tcp協議中:

1.從表面上看,黏包問題主要是因為發送方和接收方的緩存機制、tcp協議面向流通信的特點。

2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的

666

黏包解決方案

解決方案一

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據。

image-20190916195808091

存在的問題:
程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗

解決方案進階

剛剛的方法,問題在於我們我們在發送

我們可以借助一個模塊,這個模塊可以把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息之前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那么最終接受的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了。

struct模塊

該模塊可以把一個類型,如數字,轉成固定長度的bytes

import struct
obj = struct.pack('i',123456)
print(len(obj))  # 4
obj = struct.pack('i',898898789)
print(len(obj))  # 4
# 無論數字多大,打包后長度恆為4

image-20190916200515652

使用struct解決黏包

借助struct模塊,我們知道長度數字可以被轉換成一個標准大小的4字節數字。因此可以利用這個特點來預先發送數據長度。

發送時 接收時
先發送struct轉換好的數據長度4字節 先接受4個字節使用struct轉換成數字來獲取要接收的數據長度
再發送數據 再按照長度接收數據
# server端
		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)
# client端
		#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'))

我們還可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)

發送時 接收時
先發報頭長度 先收報頭長度,用struct取出來
再編碼報頭內容然后發送 根據取出的長度收取報頭內容,然后解碼,反序列化
最后發真實內容 從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容
# server端
		 # 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)
# client端
		#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'))

總結:先發字典報頭,再發字典數據,最后發真實數據

777

SocketServer模塊介紹

# TCP socketserver使用
import socketserver
class MyTcpServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            try:
                data = self.request.recv(1024)  # 對於tcp,self.request相當於conn對象
                if len(data) == 0:break
                print(data)
                self.request.send(data.upper())
            except ConnectionResetError:
                break
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTcpServer)
    server.serve_forever()

# UDP socketserver使用
import socketserver


class MyUdpServer(socketserver.BaseRequestHandler):
    def handle(self):
        while True:
            data, sock = self.request
            print(data)
            sock.sendto(data.upper(), self.client_address)


if __name__ == '__main__':
    server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyUdpServer)
    server.serve_forever()

img


免責聲明!

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



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