基於Tcp的socket編程


基於Tcp的socket編程

最簡單的基於tcp的循環通信

'''
最簡單的基於tcp的循環通信,s和c互發消息,但無法實現多個c與s通信
'''
# server端

import socket
sk = socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 避免服務器重啟時報address already in use錯誤
sk.bind(('127.0.0.1',8081))  # (‘ip’,端口)--元組
sk.listen(5)  # 監聽

while True:  # 鏈接循環
    print('正在等待客戶端連接')
    # 連接堵塞,等待客戶端連接,沒有就一直卡在這。
    conn, addr = sk.accept()  # 接收別人的電話,  conn連接,addr地址
    print(f'{addr}已連接!')
    while True:  # 通信循環
        # 通信堵塞,等待客戶端發送的信息,沒有就一直可在這。
        ret = conn.recv(1024).decode('utf8')
        if ret == 'bye':
            break
        print(ret)
        info = input('>>>')
        conn.send(info.encode('utf8'))

    conn.close()    # 掛電話
sk.close()     # 關手機


# client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8081))  # 撥打別人的手機號

while True:
    info = input('>>>')
    sk.send(info.encode('utf8'))
    ret = sk.recv(1024).decode('utf8')
    print(ret)

sk.close()

subprocess模塊補充

#subprocess 執行系統命令的模塊

import subprocess
#執行系統dir命令,把執行的正確結果放到管道中
obj=subprocess.Popen('tasklistdd',shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
#拿到正確結果的管道,讀出里面的內容
ss=obj.stdout.read()
err=obj.stderr.read()
print('錯誤信息',str(err,encoding='gbk'))
#因為windows用的gbk編碼,用gbk解碼
print(str(ss,encoding='gbk'))

TCP協議完成遠程執行cmd命令

'''
利用TCP協議完成遠程執行cmd命令
'''

# server 端
import socket
import subprocess
soc=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
soc.bind(('127.0.0.1',8001))
soc.listen(3)
while True:
    print('等待客戶端連接')
    conn,addr=soc.accept()
    print('有個客戶端連接上了',addr)
    while True:
        try:
            data=conn.recv(1024)
            if len(data)==0:
                break
            print(data)
            obj = subprocess.Popen(str(data,encoding='utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            #執行正確的結果 b 格式,gbk編碼(windows平台)
            msg=obj.stdout.read()
            #把執行的結果通過網絡傳給c端
            conn.send(msg)
        except Exception:
            break
    # 關閉通道
    conn.close()
# 關閉套接字
soc.close()


# client 端
import socket
soc=socket.socket()
soc.connect(('127.0.0.1',8001))
while True:
    in_s=input('請輸入要執行的命令:')
    soc.send(in_s.encode('utf-8'))
    data=soc.recv(1024)
    print(str(data,encoding='gbk'))
#粘包:tcp會把數據量較小,時間間隔較短的數據,當做同一個包發送

粘包問題

上述遠程執行cmd命令時,如果cmd命令輸出結果過長,就會發生粘包現象,一次接收不完,就會分次接收,導致數據接收不完全

注意:只有TCP有粘包現象,UDP永遠不會粘包,為何,且聽我娓娓道來。

什么是粘包

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

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

  • TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
  • UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
  • TCP是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略

udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠

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

TCP發送數據的四種情況

假設客戶端分別發送了兩個數據包D1和D2給服務端,由於服務端一次讀取到的字節數是不確定的,故可能存在以下4種情況。

  1. 服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包;
  2. 服務端一次接收到了兩個數據包,D1和D2粘合在一起,被稱為TCP粘包;
  3. 服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩余內容,這被稱為TCP拆包;
  4. 服務端分兩次讀取到了兩個數據包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩余內容D1_2和D2包的整包。

特例:如果此時服務端TCP接收滑窗非常小,而數據包D1和D2比較大,很有可能會發生第五種可能,即服務端分多次才能將D1和D2包接收完全,期間發生多次拆包。

為何tcp是可靠傳輸,udp是不可靠傳輸

tcp在數據傳輸時,發送端先把數據發送到自己的緩存中,然后協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則重新發送數據,所以tcp是可靠的

而udp發送數據,對端是不會返回確認信息的,因此不可靠

粘包的兩種情況

  • 發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
  • 接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)

struck模塊補充

import struct
#把一個數字打包成固定長度的4字節
obj=struct.pack('i',1098)
print(obj)  # b'J\x04\x00\x00'
print(len(obj))  # 4

l=struct.unpack('i',obj)[0] 
print(l)  # 1098

解決粘包的方法

  • 擴大單次接收的字節數,單文件過大根本沒用,很雞肋!
  • 我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)

主要講第二個方法

發送時:

先發報頭長度

再編碼報頭內容然后發送

最后發真實內容

接收時:

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

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

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

# server

import socket
import subprocess
import struct

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.bind(('127.0.0.1', 8001))
soc.listen(3)
while True:
    print('等待客戶端連接')
    conn, addr = soc.accept()
    print('有個客戶端連接上了', addr)
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0:
                break
            print(data)
            obj = subprocess.Popen(str(data, encoding='utf-8'), shell=True, stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            # 執行正確的結果 b 格式,gbk編碼(windows平台)
            msg = obj.stdout.read()
            # 發送的時候需要先把長度計算出來
            # 頭必須是固定長度
            # 先發4位,頭的長度
            import json
            dic = {'size': len(msg)}
            dic_bytes = (json.dumps(dic)).encode('utf-8')
            # head_count是4個字節的長度
            head_count = struct.pack('i', len(dic_bytes))
            conn.send(head_count)
            # 發送頭部內容
            conn.send(dic_bytes)
            # 發了內容
            conn.send(msg)
        except Exception:
            break
    conn.close()
soc.close()


# client

import socket
import struct
import json

soc = socket.socket()
soc.connect(('127.0.0.1', 8001))
while True:
    in_s = input('請輸入要執行的命令:')
    soc.send(in_s.encode('utf-8'))
    # 頭部字典的長度
    head_dic_len = soc.recv(4)
    # 解出真正的長度
    head_l = struct.unpack('i', head_dic_len)[0]
    # byte 字典的長度
    # 收真正的頭部字典
    dic_byte = soc.recv(head_l)
    head = json.loads(dic_byte)
    l = head['size']
    count = 0
    data_total = b''
    while count < l:
        if l < 1024:  # 如果接受的數據小於1024 ,直接接受數據大小
            data = soc.recv(l)
        else:  # 如果接受的數據大於1024
            if l - count >= 1024:  # 總數據長度-count(目前收到多少,count就是多少) 如果還大於1024  ,在收1024
                data = soc.recv(1024)
            else:  # 總數據長度-count(目前收到多少,count就是多少) 如果小於1024,只收剩下的部分就可
                data = soc.recv(l - count)
        data_total += data
        count += len(data)
    print(str(data_total, encoding='gbk'))


免責聲明!

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



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