socket在python下的使用

李帅啊發表於2024-10-31

socket在python下的使用

- 建立套接字物件
- 套接字物件方法
- socket緩衝區與阻塞
- 粘包(資料的無邊界性)
- 案例之模擬ssh命令
- 案例之檔案上傳

1.1建立套接字物件

Linux中的一切都是檔案,每個檔案都有一個整數型別的檔案描述符;socket也可以視為一個檔案物件,也有檔案描述符。

import socket

sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

# <socket.socket fd=496, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
print(sock)

1.AF為地址族(address family),也就是IP地址型別,常用的有AF_INET和AF_INET6,INET是“Internet”的簡寫,AF_INET表示IPV4地址,例如127.0.0.1;AF_INET6表示ipv6地址。127.0.0.1,他是一個特殊IP地址,表示本機地址,會經常用到。

2.type為資料傳輸方式/套接字型別,常用的有SOCK_STREAM(流格式套接字/面向連線的套接字)和SOCK_DGRAM(資料包套接字/無連線的套接字)。

3.protocol表示傳輸協議,常用的有IPPROTO_TCP和IPPTOTO_UDP,分別表示TCP傳輸協議和UDP傳輸協議。有了地址型別和資料傳輸方式,還不足以決定採用哪種協議嗎?為什麼還需要第三個引數呢?一般情況下有了af和type兩個引數地址就可以建立套接字了,作業系統會自動推演出協議型別,除非遇到這樣的情況:有兩種不同的協議支援同一種地址型別和資料傳輸型別。如果我們不指明使用哪種協議,作業系統是沒有辦法自動推演的。
如果使用sock_stream傳輸資料,那麼滿足這兩個條件的協議只有TCP,因此可以這樣來代用socket():

# IPPROTO_TCP表示TCP協議
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)  

這種套接字稱為 TCP 套接字。

如果使用 SOCK_DGRAM 傳輸方式,那麼滿足這兩個條件的協議只有 UDP,因此可以這樣來呼叫 socket() :

# IPPROTO_UDP表示UDP協議
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)  

這種套接字稱為 UDP 套接字。

上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設為 0,系統會自動推演出應該使用什麼協議,如下所示:

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) # 建立TCP套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)  # 建立UDP套接字 

4、sock = socket.socket()預設建立TCP套接字。

1.2套接字物件方法

服務端:bind方法
socket用來建立套接字物件,確定套接字的各種屬性,然後在伺服器端要用bind()方法將套接字與特定的IP地址和埠的資料才能交給套接字處理。類似的,客戶端也要用connect()方法建立連線。

import socket		#呼叫socket模組
sock = socket.socket()		#呼叫docket
sock.bind(("127.0.0.1",8899))	#建立地址和埠,使用預設tcp協議。

服務端:listen方法
透過listen()方法可以讓套接字進入被動監聽狀態,sock為需要進入監聽狀態的套接字,backlog為請求佇列的最大長度,所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字太會被“喚醒”來響應請求,當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒辦法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再衝緩衝區讀取出來處理。如果不斷有新的請求,他們就按照先後順序在緩衝區中排隊,知道緩衝區滿。這個緩衝區,就稱為請求佇列(request queue)

緩衝區的長度(能存放多少個客戶端請求)可以透過listen()方法和backlog引數指定,但究竟為多少並沒有什麼標準,可以根據你需求來定,併發量小的話可以是10或者20.

如果將backlog的值設定為SOMAXCONN,就由系統來決定請求佇列長度,這個值比較龐大,可能是幾百,或者更多。當請求佇列滿時,就不再接受新的請求。

注意:listen()只是讓套接字處於監聽狀態,並沒有接受請求,接收請求需要使用accept()函式

sock.listen(5)

服務端:accept方法
當套接字處於監聽狀態時,可以透過accept()函式來接收客戶端請求。accept()返回一個新的套接字來和客戶端通訊,addr儲存了客戶端的IP地址和埠號,而sock是伺服器端的套接字,大家注意區分。後面和客戶端通訊是,要使用這個新生成的套接字,而不是原來伺服器端的套接字。

conn,addr=sock.accept()

print("conn:",conn) # conn: <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 64915)>

print("addr:",addr) # addr: ('127.0.0.1', 64915)

客戶端:connect方法
connect()是客戶端程式用來連線伺服器的方法:

import socket
ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)

注意:只有經過connect連線成功後的套接字物件才能呼叫傳送和接收方法(send/recv)。所以服務端的sock物件不能send or recv。

收發資料方法:send和recv

s.recv()
	接收TCP資料,資料以字串形式返回,bufsize指定要接收的最大資料量。flag提供有關訊息的其他訊息,通常可以忽略。

s.send()
	傳送TCP資料,將string中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。

簡單聊天案例

服務端:

import socket

# 1.構建套接字物件
sock = socket.socket()

# 2.繫結:度無端的ip和埠
sock.bind(("127.0.0.1",8899))

# 3.確定最大監聽數
sock.listen(3)

# 4.等待連線
while 1:
    print("等待伺服器連線。。。")
    #獲取客戶端的套接字物件和地址
    conn,addr = sock.accept()
    print(conn,addr)

    #conn:傳送訊息  send 方法   接收資料 recv方法
    try:
        while 1:
            msg = conn.recv(1024)       #
            print(msg.decode())
            if msg.decode() == "quit":
                break
            res = input("響應>>>")
            conn.send(res.encode())
    except ConnectionResetError:
        print("客戶端斷開,連線下一個客戶端")

客戶端:

import socket

# 建立客戶端的套接字物件
sock = socket.socket()

# 連線伺服器
sock.connect(("127.0.0.1",8899))

while 1:
    data = input(">>>")
    sock.send(data.encode())
    if data == "quit":
        break
    res = sock.recv(1024)
    print("伺服器響應",res.decode())

圖解socket函式:

1.3、socket緩衝區與阻塞

1.socket緩衝區

每個socket被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。write()/send()並不立即向網路中傳輸資料,而是現將資料寫入緩衝區中,再由TCP協議將資料從緩衝區傳送到目標及其。一旦將資料寫入到緩衝區,函式就可以成功返回,不管它們有麼有到達目標及其,也不管它們何時被髮送到網路,這些都是TCP協議負責的事情。

TCP協議獨立於write()/send()函式,資料有可能剛被寫入緩衝區就傳送到網路,也可能在緩衝區中不斷積壓,多次寫入的資料被一次性傳送到網路,這取決於當時網路情況、當前執行緒是否空閒等諸多因素,不由程式設計師控制。read()/recv()函式亦是如此,也從輸入緩衝區中讀取資料,而不是直接從網路中讀取。

這些I/O緩衝區特性可整理如下:

  • I/O緩衝區在每個TCP套接字中單獨存在;
  • I/O緩衝區在建立套接字時自動生成;
  • 即使關閉套接字也會繼續傳送輸出緩衝區中遺留的資料;
  • 關閉套接字將丟失輸入緩衝區中的資料。

輸入輸出緩衝區的預設大小一般都是8K!

2、阻塞模式

對於TCP套接字(預設情況下),當使用send()傳送資料時:

1.首先會檢查緩衝區,如果緩衝區的可用空間長度小於要傳送的資料,那麼send()會被阻塞(暫停執行),直到緩衝區中的資料被髮送到目標機器,騰出足夠的空間,才喚醒send()函式繼續寫入資料。
2.如果TCP協議正在向網路傳送資料,那麼輸出緩衝區會被鎖定,不允許寫入,send()也會被阻塞,直到資料傳送完畢緩衝區解鎖,send()才會被喚醒。
3.如果要寫入的資料大於緩衝區的最大長度,那麼將分批寫入。
4.直到所有資料被寫入緩衝區send()才能返回。

當使用recv()讀取資料時:

1.首先會檢查緩衝區,如果緩衝區中有資料,那麼就讀取,否則函式會被阻塞,直到網路上有資料到來。
2.如果要去讀的資料長度 小於緩衝區中的資料長度,那麼就不能一次性將緩衝區中所有的資料讀出,剩餘資料將不斷積壓,直到有recv()函式再次讀取。
3.直到讀取到資料後,recv()函式才會返回,否則就一直被阻塞

TCP套接字預設情況下是阻塞模式,也是常用的。當然也可以更改為非阻塞模式。

粘包(資料的無邊界性)

socket資料的接收和傳送是無關的,recv()不管資料傳送了多少次,都會盡可能多的接收資料,也就是說,recv()和send()的執行次數可能不同。

例如,send()重複執行三次,每次都會傳送字串“abc”,那麼目標機器上的read()/recv()可能分三次接收,每次都接收“abc”;也可能分兩次接收,第一次接收“abcab”,第二次接收“cabc”;也可能一次就接收到字串“abcabc”.

假設我們希望客戶端每次傳送一位學生的學號,讓服務端返回該學生的姓名、住址、成績等資訊,這時候可能就會出現問題,服務端不能區分學生的學號,例如第一次傳送1,第二次傳送3,伺服器可能當成13來處理,返回的資訊顯然是錯誤的。

這就是資料的“粘包”問題,客戶端傳送的多個資料包被當做一個資料包接收,也稱資料的無邊界性,recv()函式不知道資料包的開始或結束標誌(實際上也沒有任何開始或結束標誌),只把它們當做連續的資料流來處理。

服務端

import socket
import time

s = socket.socket()
s.bind(("127.0.0.1",8888))  #暴露的地址埠協議
s.listen(5)     #最大連線數

client,addr = s.accept()    #接收客戶端資訊
time.sleep(10)       #程式睡眠10秒
data = client.recv(1024)       #接收客戶端資料資訊,單次最大接收1024位元組
print(data)

client.send(data) #將客戶端到傳送過來的資料給返回去

客戶端

import socket

s = socket.socket()     #呼叫socket
s.connect(("127.0.0.1",8888))      #連線服務端資訊

data = input(">>>")

s.send(data.encode())   #將輸入資訊轉換位元組碼傳送給服務端
s.send(data.encode())   #將輸入資訊轉換位元組碼傳送給服務端
s.send(data.encode())   #將輸入資訊轉換位元組碼傳送給服務端

res = s.recv(1024)      #列印服務端返回資料
print(res)


上傳檔案

服務端

import socket
import time
import struct
import json
import os
#基礎配置  IP地址 埠 協議 最大監聽數
sock = socket.socket()
sock.bind(("127.0.0.1",8899))
sock.listen(5)

while True:
    print("server is waiting....")
    client_sock,addr = sock.accept()
    print("客戶端%s建立連線"%str(addr))

    while True:
        try:
            msg_len = client_sock.recv(4)       #接收客戶端上傳的檔案,前4位元組
        except Exception:
            print("客戶端斷開")
            break
        msg_len = struct.unpack("i",msg_len)[0]     #解析出客戶端上傳檔案的大小
        info = client_sock.recv(msg_len)            #接收客戶端上傳基本內容,msg_len:上傳檔案的大小
        info_dict = json.loads(info)        #將接收的資料反序列化
        print(info_dict)
        cmd = info_dict.get("cmd")      #拿去客戶端列表中cmd的值,判斷客戶端是上傳/下載檔案,put上傳/get下載

        if cmd == "put":
            """拿出客戶端傳輸過來的基本資訊"""
            file_name = info_dict["file_name"]      #獲取客戶端上傳的名字
            # file_name = file_name
            print(file_name)
            file_size = info_dict["file_size"]      #獲取客戶端上傳的檔案大小

            """儲存客戶端上傳檔案"""
            with open(file_name,"wb") as f:     #以客戶端上傳的名字在服務端建立檔案
                receive_len = 0
                while receive_len < file_size:
                    tmp = client_sock.recv(4095)
                    f.write(tmp)
                    receive_len += len(tmp)

            """驗證上傳檔案是否完整"""
            if  file_size == os.path.getsize(file_name):
                print("檔案上傳完成!!!")
                client_sock.send("上傳完成!".encode())
            else:
                print("檔案上傳不完整,請重新上傳!!!")

        elif cmd == "get":
            """下載"""
            FileName = info_dict["FileName"]
            print(FileName)
            """下載檔案"""
            try:
                f = open(FileName,"rb")

            except Exception:
                KK = "檔案不存在或許可權錯誤!"
                print(KK)
                ll = json.dumps(KK).encode()  # 內容json化和位元組化
                megs = struct.pack("i", len(ll))  # 轉為4位元組,存放ll的大小

                client_sock.send(megs)  # 傳送struct存放的資料    (2121,)
                client_sock.send(ll)  # 傳送具體資料內容           (json後的檔案不存在或許可權錯誤)

            else:   #如果try中的語句沒有引發異常,則執行else中的語句
                """檔案大小,傳送給客戶端"""
                megs = json.dumps(os.path.getsize(FileName))        #判斷髮送給客戶端檔案大小轉為位元組碼,並且json化
                print(megs)
                print(type(megs))
                megs_len = struct.pack("i",int(megs))       #封裝成4位元組
                print(megs_len)
                client_sock.send(megs_len)     #傳送struce封裝的資料      (21232,)
                print("傳送成功")
                """傳送檔案內容給服務端,每次傳送給客戶端4095位元組,傳送完次跳出迴圈"""
                while True:
                    date = f.read(4095)
                    if date == b"":  # 表示傳輸完成,已經拿不到資料了
                        break
                    client_sock.send(date)
                f.close()

客戶端

import socket
import struct
import os
import json
import time
ip_port = ("127.0.0.1",8899)    #服務端地址
sk = socket.socket()
sk.connect(ip_port) #連線服務端

while 1:    #使可以多次上傳
    msg = input("輸入執行命令>>>") #格式 put meinv.jpg
    cmd,params = msg.split(" ")     #以空格拆分使用者輸入命令

    if cmd == "put":
        """上傳檔案"""
        try:
            f = open(params,mode="rb")  #以位元組方式開啟使用者上傳的檔案
        except Exception:
            print("上傳檔案不存在")
            continue
        """獲取上傳基本資訊,名稱 大小"""
        file_name = f.name   #獲取上傳檔名稱,就是params
        file_size = os.path.getsize(params)     #獲取上傳檔案大小

        """將基本資訊打包,json化 傳輸給服務端"""
        data = {}   #將基本資訊存放列表
        data["file_name"] = file_name
        data["file_size"] = file_size
        data["cmd"] = "put"
        msg = json.dumps(data).encode()     #將基本資訊json化
        msg_len = struct.pack("i",len(msg)) #統計基本資訊的大小並將結果進行封裝成4位元組檔案
        # print(data)
        """傳送檔案基本資訊到服務端"""
        sk.send(msg_len)    #將4位元組檔案大小傳送給服務端
        sk.send(msg)    #將序列化後的基本資訊傳輸給服務端

        """傳送檔案內容給服務端,每次傳送給客戶端4095位元組,傳送完次跳出迴圈"""
        while True:
            date = f.read(4095)
            sk.send(date)
            if date == b"":     #表示傳輸完成,已經拿不到資料了
                break

        f.close()       #關閉開啟的檔案
        print(sk.recv(1024).decode())   #列印上傳檔案是否完整

    elif cmd == "get":
        """下載檔案"""

        """打包基本資訊傳送給服務端,名稱 方式,以字典的形式"""
        Data = {}
        Data["cmd"] = "get"
        Data["FileName"] = params
        # print(Data)
        """將資料json化,封裝"""
        msgs = json.dumps(Data).encode()        #將資料Jason化,並轉為位元組碼
        msgs_len = struct.pack("i",len(msgs))   #基本資訊位元組大小
        """傳送資料給服務端"""
        sk.send(msgs_len)       #傳送基本資訊長度到服務端
        sk.send(msgs)   #基本資料傳送給服務端

        """接受服務端資料"""
        # time.sleep(5)
        msg_len = sk.recv(4)  # 接收服務端傳的檔案,前4位元組,內容:傳輸的檔案大小
        msg_len = struct.unpack("i", msg_len)[0]  # 解析出服務端傳檔案的大小
        print(msg_len)
        # info = sk.recv(msg_len)  # 接收客戶端上傳基本內容,msg_len:上傳檔案的大小
        # print(info)
        # info = info.decode()
        # print(info)
        # info_dict = json.loads(info)  # 將接收的資料反序列化,下載檔案大小資料

        """檔案內容下載"""
        with open(params,"wb") as f:     #以客戶端上傳的名字在服務端建立檔案
            receive_len = 0
            while receive_len < msg_len:
                tmp = sk.recv(2048)
                f.write(tmp)
                receive_len += len(tmp)

案例-模擬ssh命令

服務端

import socket
import subprocess
import struct

socket = socket.socket()
socket.bind(("127.0.0.1", 8899))  # 建立服務端IP地址,埠,預設tcp協議
socket.listen(5)  # 最大監聽數

while 1:
    client,addr = socket.accept()   #接收客戶端連線資訊
    print("客戶端%s建立連線"%str(addr))
    while 1:
        try:
            cmd = client.recv(1024)
            cmd_res_bytes = subprocess.getoutput(cmd.decode())        #使用subprocess模組去自己記憶體中拿去客戶端輸入的命令資訊
        except :
            print("客戶端%s退出"%str(addr))
            client.close()  #斷開連線
            break       #跳出本次迴圈

        print(cmd_res_bytes)    #列印輸出客戶端傳送過來的命令資訊
        cmd_res_bytes = cmd_res_bytes.encode()
        print("len:",len(cmd_res_bytes))   #檢視輸出字串長度,便於驗證傳輸是否一致
        data_len =   len(cmd_res_bytes)    #長度賦值變數
        data_len_pack = struct.pack("i",data_len)   #傳送輸出內容大小到客戶端,客戶端取固定長度,解決粘包問題

        #打包資料長度傳送
        client.send(data_len_pack)      #傳送長度到客戶端
        client.send(cmd_res_bytes)      #傳送資料到客戶端

客戶端

import socket
import time
import struct

sk = socket.socket()
sk.connect(("127.0.0.1",8899))      #連線服務端

while 1:
    data = input("輸入執行命名>>>")       #輸入遠端連線的命令
    sk.send(data.encode())      #傳送資料過去,位元組碼

    #接收資料長度
    time.sleep(2)       #觸發粘包
    data_len = sk.recv(4)       #傳送過來的位元組大小,解決粘包
    total_len = struct.unpack("i",data_len)[0]      #解壓內容為元組,傳輸字串的大小值
    ret = b""           #接收到的內容
    receive_len = 0         #本地從0計算
    while receive_len < total_len:      #當本地接收到的位元組數不小於傳輸過的位元組數時,表示檔案傳輸完成        #
        temp = sk.recv(1024)    #單次拿去資料長度
        receive_len += len(temp)        #接收位元組大小做累加
        ret += temp         #接收內容做累加
    print(ret.decode())     #列印傳輸過來的內容
    print(len(ret))         #列印接收到的大小
    print("total_len",total_len)

相關文章