【Python3之socket編程】


一、socket的定義

  Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。所以,我們無需深入理解tcp/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標准的。

補充:也有人將socket說成ip+port,ip是用來標識互聯網中的一台主機的位置,而port是用來標識這台機器上的一個應用程序,ip地址是配置到網卡上的,而port是應用程序開啟的,ip與port的綁定就標識了互聯網中獨一無二的一個應用程序,而程序的pid是同一台機器上不同進程或者線程的標識

 

二、套接字發展史及分類

  套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一台主機上多個應用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於文件型的和基於網絡型的。 

  • 基於文件類型的套接字家族

套接字家族的名字:AF_UNIX

unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信

  • 基於網絡類型的套接字家族

套接字家族的名字:AF_INET

(還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要么是只用於某個平台,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由於我們只關心網絡編程,所以大部分時候我么只使用AF_INET)

 

三、套接字的工作流程

    一個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲后提起電話,這時你和你的朋友就建立起了連接,就可以講話了。等交流結束,掛斷電話結束此次交談。

生活中的場景就解釋了套接字的工作原理

先從服務器端說起。服務器端先初始化Socket,然后與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求並處理請求,然后把回應數據發送給客戶端,客戶端讀取數據,最后關閉連接,一次交互結束。

 

四、socket函數使用

  • socket函數用法
import socket
socket.socket(socket_family,socket_type,protocal=0)
#socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默認值為 0。

#獲取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#獲取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

#由於 socket 模塊中有太多的屬性。我們在這里破例使用了'from module import *'語句。使用 'from socket import *',我們就把 socket 模塊里的所有屬性都帶到我們的命名空間里了,這樣能 大幅減短我們的代碼。
#例如tcpSock = socket(AF_INET, SOCK_STREAM)


  • 服務端套接字函數
s.bind()    #綁定(主機,端口號)到套接字
s.listen()  #開始TCP監聽
s.accept()  #被動接受TCP客戶的連接,(阻塞式)等待連接的到來

 

  • 客戶端套接字函數
s.connect()     #主動初始化TCP服務器連接
s.connect_ex()  #connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常

 

  • 公共用途的套接字函數
s.recv()            #接收TCP數據
s.send()            #發送TCP數據(send在待發送數據量大於己端緩存區剩余空間時,數據丟失,不會發完)
s.sendall()         #發送完整的TCP數據(本質就是循環調用send,sendall在待發送數據量大於己端緩存區剩余空間時,數據不丟失,循環調用send直到發完)
s.recvfrom()        #接收UDP數據
s.sendto()          #發送UDP數據
s.getpeername()     #連接到當前套接字的遠端的地址
s.getsockname()     #當前套接字的地址
s.getsockopt()      #返回指定套接字的參數
s.setsockopt()      #設置指定套接字的參數
s.close()           #關閉套接字

 

  • 面向鎖的套接字方法
s.setblocking()     #設置套接字的阻塞與非阻塞模式
s.settimeout()      #設置阻塞套接字操作的超時時間
s.gettimeout()      #得到阻塞套接字操作的超時時間

 

  • 面向文件的套接字方法
s.fileno()          #套接字的文件描述符
s.makefile()        #創建一個與該套接字相關的文件

 

打電話的流程演示

服務端.py

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
phone.bind(('127.0.0.1',8080)) #插電話卡

phone.listen(5) #開機,backlog

print('starting....')
conn,addr=phone.accept() #接電話
print(conn)
print('client addr',addr)
print('ready to read msg')
client_msg=conn.recv(1024) #收消息
print('client msg: %s' %client_msg)
conn.send(client_msg.upper()) #發消息

conn.close()
phone.close()

 

客戶端.py

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080)) #撥通電話

phone.send('hello'.encode('utf-8')) #發消息

back_msg=phone.recv(1024)
print(back_msg)

phone.close()

 

輸出

服務端:

starting....
<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 65142)>
client addr ('127.0.0.1', 65142)
ready to read msg
client msg: b'hello'

客戶端

b'HELLO'

 

五、基於TCP的套接字

  • tcp服務端 
ss = socket() #創建服務器套接字
ss.bind()      #把地址綁定到套接字
ss.listen()      #監聽鏈接
inf_loop:      #服務器無限循環
    cs = ss.accept() #接受客戶端鏈接
    comm_loop:         #通訊循環
        cs.recv()/cs.send() #對話(接收與發送)
    cs.close()    #關閉客戶端套接字
ss.close()        #關閉服務器套接字(可選)
  • tcp客戶端
cs = socket()    # 創建客戶套接字
cs.connect()    # 嘗試連接服務器
comm_loop:        # 通訊循環
    cs.send()/cs.recv()    # 對話(發送/接收)
cs.close()            # 關閉客戶套接字

socket通信流程與打電話流程類似,我們就以打電話為例來實現一個low版的套接字通信

服務端

import socket
ip_port=('127.0.0.1',9000)  #電話卡
BUFSIZE=1024                #收發消息的尺寸
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5)     #手機待機


conn,addr=s.accept()            #手機接電話
# print(conn)
# print(addr)
print('接到來自%s的電話' %addr[0])

msg=conn.recv(BUFSIZE)             #聽消息,聽話
print(msg,type(msg))

conn.send(msg.upper())          #發消息,說話

conn.close()                    #掛電話

s.close()                       #手機關機

客戶端

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #撥電話

s.send('nitouxiang nb'.encode('utf-8'))         #發消息,說話(只能發送字節類型)

feedback=s.recv(BUFSIZE)                           #收消息,聽話
print(feedback.decode('utf-8'))

s.close()                                       #掛電話

 

輸出

服務端

接到來自127.0.0.1的電話
b'nitouxiang nb' <class 'bytes'>

客戶端

NITOUXIANG NB

 

上述流程的問題是,服務端只能接受一次鏈接,然后就徹底關閉掉了,實際情況應該是,服務端不斷接受鏈接,然后循環通信,通信完畢后只關閉鏈接,服務器能夠繼續接收下一次鏈接,下面是修改版

 服務端

import socket
ip_port = ('127.0.0.1',8081)    #電話卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)  #買手機
s.bind(ip_port) #手機插卡
s.listen(5) #手機待機

while True:                  #新增接收鏈接循環,可以不停的接電話
    conn,addr=s.accept()    #手機接電話
    print('接到來自%s的電話' %addr[0])
    while True:                 ##新增通信循環,可以不斷的通信,收發消息
        msg=conn.recv(BUFSIZE)  #聽消息,聽話
        if len(msg) == 0:break  #如果不加,那么正在鏈接的客戶端突然斷開,recv便不再阻塞,死循環發生
        print(msg,type(msg))
        conn.send(msg.upper())  #發消息,說話
    conn.close()                #掛電話
s.close()               #手機關機

客戶端

import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #撥電話

while True:                             #新增通信循環,客戶端可以不斷發收消息
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    s.send(msg.encode('utf-8'))         #發消息,說話(只能發送字節類型)

    feedback=s.recv(BUFSIZE)                           #收消息,聽話
    print(feedback.decode('utf-8'))

s.close()                                       #掛電話

 

補充:

在重啟服務端時可能會遇到

這個是由於你的服務端仍然存在四次揮手的time_wait狀態在占用地址(如果不懂,請深入研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高並發情況下會有大量的time_wait狀態的優化方法)

解決辦法

方法一

#加入一條socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))

方法二

發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決,
vi /etc/sysctl.conf

編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后執行 /sbin/sysctl -p 讓參數生效。
 
net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;

net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉;

net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。

net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間

 

六、基於UDP的套接字

  • udp服務端
ss = socket()   #創建一個服務器的套接字
ss.bind()       #綁定服務器套接字
inf_loop:       #服務器無限循環
    cs = ss.recvfrom()/ss.sendto() # 對話(接收與發送)
ss.close()                         # 關閉服務器套接字
  • udp客戶端
cs = socket()   # 創建客戶套接字
comm_loop:      # 通訊循環
    cs.sendto()/cs.recvfrom()   # 對話(發送/接收)
cs.close()                      # 關閉客戶套接字

示例

服務端

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

udp_server_client.bind(ip_port)

while True:
    msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(msg,addr)

    udp_server_client.sendto(msg.upper(),addr)

客戶端

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

while True:
    msg=input('>>: ').strip()
    if not msg:continue

    udp_server_client.sendto(msg.encode('utf-8'),ip_port)

    back_msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'),addr)

輸出

客戶端

>>: 123
123 ('127.0.0.1', 9000)
>>: 3
3 ('127.0.0.1', 9000)
>>: 4
4 ('127.0.0.1', 9000)

服務端

b'123' ('127.0.0.1', 53066)
b'3' ('127.0.0.1', 53066)
b'4' ('127.0.0.1', 53066)

 

模擬QQ聊天,多個客戶端和服務端通信

服務端

import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #買手機
udp_server_sock.bind(ip_port)

while True:
    qq_msg,addr=udp_server_sock.recvfrom(1024)
    print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8')))
    back_msg=input('回復消息: ').strip()

    udp_server_sock.sendto(back_msg.encode('utf-8'),addr)

客戶端1

import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    'TOM':('127.0.0.1',8081),
    'JACK':('127.0.0.1',8081),
    '一棵樹':('127.0.0.1',8081),
    '武大郎':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送: ').strip()
        if msg == 'quit':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])

        back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
        print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))

udp_client_socket.close()

客戶端2

import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    'TOM':('127.0.0.1',8081),
    'JACK':('127.0.0.1',8081),
    '一棵樹':('127.0.0.1',8081),
    '武大郎':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送: ').strip()
        if msg == 'quit':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])

        back_msg,addr=udp_client_socket.recvfrom(BUFSIZE)
        print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8')))

udp_client_socket.close()

 

輸出

客戶端1

請選擇聊天對象: JACK
請輸入消息,回車發送: 約不
來自[127.0.0.1:8081]的一條消息:不約
請輸入消息,回車發送: 

客戶端2

請選擇聊天對象: TOM
請輸入消息,回車發送: 123
來自[127.0.0.1:8081]的一條消息:321
請輸入消息,回車發送: 

服務端

來自[127.0.0.1:62851]的一條消息:123
回復消息: 321
來自[127.0.0.1:60378]的一條消息:約不
回復消息: 不約

 

七、recv與recvfrom

發消息,都是將數據發送到己端的發送緩沖中,收消息都是從己端的緩沖區中收。

  • tcp:send發消息,recv收消息
  • udp:sendto發消息,recvfrom收消息

 1.send與sendinto

tcp是基於數據流的,而udp是基於數據報的:

  • send(bytes_data):發送數據流,數據流bytes_data若為空,自己這段的緩沖區也為空,操作系統不會控制tcp協議發空包
  • sendinto(bytes_data,ip_port):發送數據報,bytes_data為空,還有ip_port,所有即便是發送空的bytes_data,數據報其實也不是空的,自己這端的緩沖區收到內容,操作系統就會控制udp協議發包。

 

2.recv與recvfrom

tcp協議:

(1)如果收消息緩沖區里的數據為空,那么recv就會阻塞(阻塞很簡單,就是一直在等着收)

(2)只不過tcp協議的客戶端send一個空數據就是真的空數據,客戶端即使有無窮個send空,也跟沒有一個樣。

(3)tcp基於鏈接通信

  • 基於鏈接,則需要listen(backlog),指定半連接池的大小
  • 基於鏈接,必須先運行的服務端,然后客戶端發起鏈接請求
  • 對於mac系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端在收消息后加上if判斷,空消息就break掉通信循環)
  • 對於windows/linux系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端通信循環內加異常處理,捕捉到異常后就break掉通訊循環)

 

udp協議

(1)如果如果收消息緩沖區里的數據為“空”,recvfrom也會阻塞

(2)只不過udp協議的客戶端sendinto一個空數據並不是真的空數據(包含:空數據+地址信息,得到的報仍然不會為空),所以客戶端只要有一個sendinto(不管是否發送空數據,都不是真的空數據),服務端就可以recvfrom到數據。

(3)udp無鏈接

  • 無鏈接,因而無需listen(backlog),更加沒有什么連接池之說了
  • 無鏈接,udp的sendinto不用管是否有一個正在運行的服務端,可以己端一個勁的發消息,只不過數據丟失
  • recvfrom收的數據小於sendinto發送的數據時,在mac和linux系統上數據直接丟失,在windows系統上發送的比接收的大直接報錯
  • 只有sendinto發送數據沒有recvfrom收數據,數據丟失 

注意:

1.你單獨運行上面的udp的客戶端,你發現並不會報錯,相反tcp卻會報錯,因為udp協議只負責把包發出去,對方收不收,我根本不管,而tcp是基於鏈接的,必須有一個服務端先運行着,客戶端去跟服務端建立鏈接然后依托於鏈接才能傳遞消息,任何一方試圖把鏈接摧毀都會導致對方程序的崩潰。

2.上面的udp程序,你注釋任何一條客戶端的sendinto,服務端都會卡住,為什么?因為服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。

 

基於tcp先制作一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)

客戶端

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)

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='')

服務端

from socket import *
import subprocess

ip_port=('127.0.0.1',8080)
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)

 

輸出

客戶端

>>: ls
1.py
客戶端.py
客戶端1.py
客戶端2.py
服務端.py
>>: ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 78:4f:43:5b:a5:4c 
    inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 
    inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
>>: ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 78:4f:43:5b:a5:4c 
    inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 
    inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:00 
    media: autoselect <full-duplex>
    status: inactive
en3: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:01 
    media: autoselect <full-duplex>
    status: inactive
en2: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500>>: 
>>: 

服務端

客戶端 ('127.0.0.1', 58194)

上述程序是基於tcp的socket,在運行時會發生粘包

 

服務端

from socket import *
import subprocess

ip_port=('127.0.0.1',9003)
bufsize=1024

udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)

while True:
    #收消息
    cmd,addr=udp_server.recvfrom(bufsize)
    print('用戶命令----->',cmd)

    #邏輯處理
    res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
    stderr=res.stderr.read()
    stdout=res.stdout.read()

    #發消息
    udp_server.sendto(stderr,addr)
    udp_server.sendto(stdout,addr)
udp_server.close()

客戶端

from socket import *
ip_port=('127.0.0.1',9003)
bufsize=1024

udp_client=socket(AF_INET,SOCK_DGRAM)


while True:
    msg=input('>>: ').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)

    data,addr=udp_client.recvfrom(bufsize)
    print(data.decode('utf-8'),end='')

上述程序是基於udp的socket,在運行時永遠不會發生粘包

 

注意注意注意:

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

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

 

八、粘包

1.什么是粘包

粘包:發送方發送兩個字符串”hello”+”world”,接收方卻一次性接收到了”helloworld”。

只有TCP有粘包現象,UDP永遠不會粘包。

 

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

 

補充:

分包:發送方發送字符串”helloworld”,接收方卻接收到了兩個字符串”hello”和”world”。

TCP是以段(Segment)為單位發送數據的,建立TCP鏈接后,有一個最大消息長度(MSS)。如果應用層數據包超過MSS,就會把應用層數據包拆分,分成兩個段來發送。這個時候接收端的應用層就要拼接這兩個TCP包,才能正確處理數據。

補充:

一個socket收發消息的原理

 

2.粘包如何產生

TCP為了提高網絡的利用率,會使用一個叫做Nagle的算法。該算法是指,發送端即使有要發送的數據,如果很少的話,會延遲發送。如果應用層給TCP傳送數據很快的話,就會把兩個應用層數據包“粘”在一起,TCP最后只發一個TCP數據包給接收端。

tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。

 

兩種情況下會發生粘包。

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

from socket import *
ip_port=('127.0.0.1',8085)

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()
服務端
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8085)

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


s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
客戶端

輸出

服務端

-----> hellofeng    #出現粘包現象
-----> 

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

from socket import *
ip_port=('127.0.0.1',8089)

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()
服務端
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8089)

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


s.send('hello feng'.encode('utf-8'))
客戶端

輸出

-----> he
-----> llo feng

補充:

recv里指定的1024意思是從緩存里一次拿出1024個字節的數據

send的字節流是先放入己端緩存,然后由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩余空間,那么數據丟失,用sendall就會循環調用send,數據不會丟失

 

3.如何解決粘包問題

為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據

struct模塊 

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

import json,struct
#假設通過客戶端上傳1T:1073741824000的文件a.txt

#為避免粘包,必須自定制報頭
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值

#為了該報頭能傳送,需要序列化並且轉為bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化並轉成bytes,用於傳輸

#為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節里只包含了一個數字,該數字是報頭的長度

#客戶端開始發送
conn.send(head_len_bytes) #先發報頭的長度,4個bytes
conn.send(head_bytes) #再發報頭的字節格式
conn.sendall(文件內容) #然后發真實內容的字節格式

#服務端開始接收
head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的字節格式
x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度

head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
header=json.loads(json.dumps(header)) #提取報頭

#最后根據報頭的內容提取真實的數據,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

 

示例:

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

發送時:

先發報頭長度

再編碼報頭內容然后發送

最后發真實內容

 

接收時:

先手報頭長度,用struct取出來

根據取出的長度收取報頭內容,然后解碼,反序列化

從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容

 

import socket
import subprocess
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8082)) #插電話卡
phone.listen(5) #開機,backlog
while True:
    print('starting....')
    conn,addr=phone.accept()
    print('cliet addr',addr)
    while True:
        try:
            cmd=conn.recv(1024)
            if not cmd:break
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()

            # conn.send(struct.pack('i',len(cmd_res))) #先報報頭
            head_dic={'filename':None,'hash':None,'total_size':len(cmd_res)}
            head_json=json.dumps(head_dic)
            head_bytes=head_json.encode('utf-8')

            #先發送報頭的長度
            conn.send(struct.pack('i',len(head_bytes)))

            #再發送報頭的bytes
            conn.send(head_bytes)

            #最后發送真實的數據
            conn.send(cmd_res)

        except Exception:
            break
    conn.close()
phone.close()
服務端
import socket
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8082)) #撥通電話

while True: #通信循環
    cmd=input('>>: ').strip()
    if not cmd:continue #防止客戶端發空
    phone.send(cmd.encode('utf-8')) #發消息

    #先收報頭的長度
    head_struct=phone.recv(4)
    head_len=struct.unpack('i',head_struct)[0]

    #再收報頭的bytes
    head_bytes=phone.recv(head_len)
    head_json=head_bytes.decode('utf-8')
    head_dic=json.loads(head_json)

    #最后根據報頭里的詳細信息取真實的數據
    print(head_dic)
    total_size=head_dic['total_size']
    recv_size=0
    data=b''
    while recv_size < total_size: #10240 +1
        recv_data=phone.recv(1024)
        data+=recv_data
        recv_size+=len(recv_data)
    print(data.decode('utf-8'))
phone.close()
客戶端

輸出

>>: ls
{'total_size': 69, 'hash': None, 'filename': None}
1.py
c.py
s.py
客戶端.py
客戶端1.py
客戶端2.py
服務端.py

>>: ifconfig
{'total_size': 2990, 'hash': None, 'filename': None}
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 78:4f:43:5b:a5:4c 
    inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 
    inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:00 
    media: autoselect <full-duplex>
    status: inactive
en3: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:01 
    media: autoselect <full-duplex>
    status: inactive
en2: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:04 
    media: autoselect <full-duplex>
    status: inactive
en4: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:05 
    media: autoselect <full-duplex>
    status: inactive
bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    options=63<RXCSUM,TXCSUM,TSO4,TSO6>
    ether e2:00:ec:98:eb:00 
    Configuration:
        id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
        maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
        root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
        ipfilter disabled flags 0x2
    member: en1 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 6 priority 0 path cost 0
    member: en2 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 8 priority 0 path cost 0
    member: en3 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 7 priority 0 path cost 0
    member: en4 flags=3<LEARNING,DISCOVER>
            ifmaxaddr 0 port 9 priority 0 path cost 0
    nd6 options=201<PERFORMNUD,DAD>
    media: <unknown type>
    status: inactive
p2p0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 2304
    ether 0a:4f:43:5b:a5:4c 
    media: autoselect
    status: inactive
awdl0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1484
    ether 76:77:9e:05:58:d1 
    inet6 fe80::7477:9eff:fe05:58d1%awdl0 prefixlen 64 scopeid 0xc 
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 2000
    inet6 fe80::9572:8b04:187d:f5c6%utun0 prefixlen 64 scopeid 0xd 
    nd6 options=201<PERFORMNUD,DAD>
utun1: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1380
    inet6 fe80::5fbd:ded2:9ab2:309c%utun1 prefixlen 64 scopeid 0x12 
    nd6 options=201<PERFORMNUD,DAD>
en5: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether ac:de:48:00:11:22 
    inet6 fe80::aede:48ff:fe00:1122%en5 prefixlen 64 scopeid 0x4 
    nd6 options=281<PERFORMNUD,INSECURE,DAD>
    media: autoselect
    status: active

>>: 
客戶端

服務端

starting....
cliet addr ('127.0.0.1', 59162)

 


免責聲明!

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



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