day30:TCP&UDP:socket

李博倫發表於2020-08-13

目錄

1.TCP協議和UDP協議

2.什麼是socket?

3.socket正文

  1.TCP基本語法

  2.TCP迴圈發訊息

  3.UDP基本語法

  4.UDP迴圈發訊息

4.黏包

5.解決黏包問題

  1.解決黏包方式一:先傳送接下來要傳送資料的大小

  2.解決黏包方式二:conn.send("00000100".encode())

  3.前戲:struct模組

  4.解決黏包方式三:使用struct模組

1.TCP協議和UDP協議

TCP(Transmission Control Protocol)一種面向連線的、可靠的、傳輸層通訊協議(比如:打電話)

優點:可靠,穩定,傳輸完整穩定,不限制資料大小

缺點:慢,效率低,佔用系統資源高,一發一收都需要對方確認

應用:Web瀏覽器,電子郵件,檔案傳輸,大量資料傳輸的場景

UDP(User Datagram Protocol)一種無連線的,不可靠的傳輸層通訊協議(比如:發簡訊)

優點:速度快,可以多人同時聊天,耗費資源少,不需要建立連線

缺點:不穩定,不能保證每次資料都能接收到

應用:IP電話,實時視訊會議,聊天軟體,少量資料傳輸的場景

2.什麼是socket?

socket的意義:通絡通訊過程中,資訊拼接的工具(中文:套接字)

 

3.socket正文

1.TCP基本語法

服務端

#  ### 服務端
import socket
# 1.建立一個socket物件
sk = socket.socket()

# 2.繫結對應的ip和埠號(讓其他主機在網路中可以找得到)
"""127.0.0.1代表本地ip"""
sk.bind( ("127.0.0.1",9001) )

# 3.開啟監聽
sk.listen()

# 4.建立三次握手
conn,addr  = sk.accept()

# 5.處理收發資料的邏輯
"""recv 接受 send 傳送"""
res = conn.recv(1024)# 最多一次接受 1024 位元組
print(res.decode("utf-8"))

# 6.四次揮手
conn.close()

# 7.退還埠
sk.close()

客戶端

# ### 客戶端
import socket
# 1.建立一個socket物件
sk = socket.socket()

# 2.與伺服器建立連線
sk.connect( ("127.0.0.1",9001) )

# 3.傳送資料(只能傳送二進位制的位元組流)
sk.send("北京昨天迎來了暴雨,如今有車是不行的,還得有船".encode("utf-8"))

# 4.關閉連線
sk.close()

2.TCP迴圈發訊息

服務端

# ### 服務端
import socket 
# 1.建立socket物件
sk = socket.socket()
# # 在bind方法之前加上這句話,可以讓一個埠重複使用
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)

# 2.繫結ip和埠號(在網路中註冊該主機)
sk.bind( ("127.0.0.1" , 9000) )
# 3.開啟監聽
sk.listen()
"""
print(conn)
print(addr)
conn:<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9002), raddr=('127.0.0.1', 53620)>
addr:('127.0.0.1', 53620)
"""

# 5.處理收發資料的邏輯 
"""send(位元組流)"""
"""
conn.send("我去北京先買船".encode("utf-8"))
"""
while True:
    # 4.三次握手
    conn,addr = sk.accept()
    while True:
        res = conn.recv(1024)
        print(res.decode())
        strvar = input("請輸入服務端要給客戶端傳送的內容")
        conn.send(strvar.encode())
        if strvar.upper() == "Q":
            break

# 6.四次揮手
conn.close()

# 7.退還埠
sk.close()

客戶端

# ### 客戶端
import socket 
# 1.建立socket物件
sk = socket.socket()
# 2.連線服務端
sk.connect( ("127.0.0.1" , 9000) )
# 3.收發資料
"""
res = sk.recv(1024) # 一次最多接受1024個位元組
print(res.decode())
"""

while True:
    strvar = input("請輸入您要傳送的內容:")
    sk.send(strvar.encode())
    res = sk.recv(1024)
    if res == b"q" or res == b"Q":
        break
    print(res.decode())

# 4.關閉連線
sk.close()

3.UDP基本語法

服務端

# ### 服務端
import socket
# 1.建立udp物件
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2.繫結地址埠號
sk.bind( ("127.0.0.1",9000) )
# 3.udp伺服器,在一開始只能夠接受資料
msg,cli_addr = sk.recvfrom(1024)

print(msg.decode())
print(cli_addr)

# 服務端給客戶端傳送資料
msg = "我是你老孃,趕緊給我回家吃飯"
sk.sendto(msg.encode(),cli_addr)

# 4.關閉連線
sk.close()

客戶端

# ### 客戶端
import socket
# 1.建立udp物件
sk = socket.socket(type=socket.SOCK_DGRAM)

# 2.收發資料的邏輯

# 傳送資料
msg = "你好,你是mm還是gg"
# sendto( 訊息,(ip,埠號) )
sk.sendto( msg.encode() ,  ("127.0.0.1",9000)  )

# 接受資料
msg,server_addr = sk.recvfrom(1024)
print(msg.decode())
print(server_addr)

# 3.關閉連線
sk.close()

4.UDP迴圈發訊息

服務端

# ### 服務端
import socket
# 1.建立udp物件
sk = socket.socket(type=socket.SOCK_DGRAM)
# 2.繫結地址埠號
sk.bind( ("127.0.0.1",9000) )
# 3.udp伺服器,在一開始只能夠接受資料
while True:
    # 接受訊息
    msg,cli_addr = sk.recvfrom(1024)
    print(msg.decode())
    message = input("服務端給客戶端傳送的訊息是?:")
    # 傳送資料
    sk.sendto(message.encode() , cli_addr)


# 4.關閉連線
sk.close()

客戶端

# ### 客戶端
import socket
# 1.建立udp物件
sk = socket.socket(type=socket.SOCK_DGRAM)

# 2.收發資料的邏輯
while True:
    # 傳送資料
    message = input("客戶端給服務端傳送的訊息是?:")
    sk.sendto(message.encode(), ("127.0.0.1",9000) )
    
    # 接受資料
    msg,addr = sk.recvfrom(1024)
    print(msg.decode("utf-8"))
    

# 3.關閉連線
sk.close()

4.黏包

1.出現黏包的原因

tcp協議在傳送資料時,會出現黏包現象.

1.資料粘包是因為在客戶端/伺服器端都會有一個資料緩衝區,

緩衝區用來臨時儲存資料,為了保證能夠完整的接收到資料,因此緩衝區都會設定的比較大。

2.在收發資料頻繁時,由於tcp傳輸訊息的無邊界,不清楚應該擷取多少長度

導致客戶端/伺服器端,都有可能把多條資料當成是一條資料進行擷取,造成黏包

2.黏包出現的兩種情況

黏包現象一:

在傳送端,由於兩個資料短,傳送的時間隔較短,所以在傳送端形成黏包

黏包現象二:

在接收端,由於兩個資料幾乎同時被髮送到對方的快取中,所有在接收端形成了黏包

總結:

傳送端,包之間時間間隔短 或者 接收端,接受不及時, 就會黏包

核心是因為tcp對資料無邊界擷取,不會按照傳送的順序判斷

3.黏包的應用場景

解決黏包場景:

應用場景在實時通訊時,需要閱讀此次發的訊息是什麼

不需要解決黏包場景:

下載或者上傳檔案的時候,最後要把包都結合在一起,黏包無所謂.

5.解決黏包問題

1.解決黏包方式一:先傳送接下來要傳送資料的大小

服務端

# ### 服務端
import time
import socket
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn,addr = sk.accept()


# 處理收發資料的邏輯

# 先傳送接下來要傳送資料的大小
conn.send("5".encode()) # 傳送5個位元組
# 發完長度之後,再發資料
conn.send("hello".encode())
conn.send(",world".encode())

conn.close()
sk.close()

客戶端

# ### 客戶端
"""
黏包出現的兩種情況:
    (1) 傳送端傳送資料太快
    (2) 接收端接收資料太慢
"""
import socket
import time
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

time.sleep(2) # 睡2s,讓其接受速度慢一些,製造黏包效果
# 處理收發資料的邏輯
# 先接受接下來要傳送資料的大小
res = sk.recv(1) # res = "5"
num = int(res.decode()) # num = 5
# 接受num這麼多個位元組數
res1 = sk.recv(num) # 一次最多隻能接收5個位元組
res2 = sk.recv(1024)
print(res1)
print(res2)


sk.close()

2.解決黏包方式二:conn.send("00000100".encode())

服務端:

# ### 服務端
import time
import socket
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn,addr = sk.accept()


# 處理收發資料的邏輯
# 先傳送接下來要傳送資料的大小
conn.send("00000100".encode())
# 發完長度之後,再發資料
msg = "hello" * 20
conn.send(msg.encode())
conn.send(",world".encode())

conn.close()
sk.close()

客戶端:

# ### 客戶端
"""
黏包出現的兩種情況:
    (1) 傳送端傳送資料太快
    (2) 接收端接收資料太慢
"""
import socket
import time
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

time.sleep(2)
# 處理收發資料的邏輯
# 先接受接下來要傳送資料的大小
res = sk.recv(8)
num = int(res.decode())
# 接受num這麼多個位元組數
res1 = sk.recv(num)
res2 = sk.recv(1024)
print(res1)
print(res2)


sk.close()

其實,這兩種寫法都存在一定的限制,並非最完美的解決方案

下面介紹一個模組,用來完美的解決黏包現象

3.前戲:struct模組

struct模組裡有兩個方法:

pack :把任意長度數字轉化成具有固定4個位元組長度的位元組流

unpack :把4個位元組值恢復成原來的數字,返回最終的是元組

import struct

# pack
# i => int 要轉化的當前資料是整型
res1 = struct.pack("i",999999999)
print(res1,len(res1)) # b'\xff\xc9\x9a;' 4
res2 = struct.pack("i",1)
print(res2,len(res2)) # b'\x01\x00\x00\x00' 4
res3 = struct.pack("i",4399999)
print(res3,len(res3)) # b'\x7f#C\x00' 4
# pack 的範圍 -2147483648 ~ 2147483647 21個億左右
res4 = struct.pack("i",2100000000) 
print(res4,len(res4)) # b'\x00u+}' 4


# unpack
# i => 把對應的資料轉換成int整型
tup = struct.unpack("i",res)
print(tup) # (2100000000,)
print(tup[0]) # 2100000000

4.解決黏包方式三:使用struct模組

服務端

# ### 服務端
import time
import socket
import struct
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind( ("127.0.0.1",9000) )
sk.listen()

conn,addr = sk.accept()


# 處理收發資料的邏輯
strvar = input("請輸入你要傳送的資料")
msg = strvar.encode()
length = len(msg) # 你輸入字串的長度
res = struct.pack("i",length) # 無論長度是多少,res都是固定4個位元組長度的位元組流
print("---",res)

# 第一次傳送的是位元組長度
conn.send(res)

# 第二次傳送真實的資料
conn.send(msg)

# 第三次傳送真實的資料
conn.send("世界真美好123".encode())



conn.close()
sk.close()

客戶端

# ### 客戶端
"""
黏包出現的兩種情況:
    (1) 傳送端傳送資料太快
    (2) 接收端接收資料太慢
"""
import socket
import time
import struct
sk = socket.socket()
sk.connect( ("127.0.0.1",9000) )

time.sleep(2)
# 處理收發資料的邏輯

# 第一次接受的是位元組長度
n = sk.recv(4) # 接收到4個位元組長度的位元組流
tup = struct.unpack("i",n) # 將4個位元組長度的位元組流轉化成數字
n = tup[0] # n就是長度


# 第二次接受真實的資料
res = sk.recv(n)
print(res.decode())

# 第三次接受真實的資料
res = sk.recv(1024)
print(res.decode())
sk.close()

struct如何做到控制接受位元組數的呢?