Python網路編程下篇

socket編程python


中篇對socket的搭建服務端與客戶端的鏈接進行了代碼實現化,以及socket內置方法的認識及運用。算法

粘包現象的出現shell

在中篇中,對於tcp和udp製做了一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)編程

在tcp下:在運行時會發生粘包緩存

在udp下:在運行時永遠不會發生粘包服務器

什麼是粘包網絡

在上篇中,對於socket的收發消息的原理進行了一些闡釋數據結構

所謂粘包,主要仍是由於接受方不知道消息之間的界限,不知道一次提取多少字節的數據所形成的。併發

此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。異步

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

粘包有倆種現象

1、發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包

from socket import *
ip_port=('127.0.0.1',8082)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

conn,addr=tcp_server.accept()

data1=conn.recv(buffer_size)  #指定buffer_size ,獲得的結果就是經過Nagle算法,隨機接收次數。
print('第1次數據',data1)

data2=conn.recv(buffer_size)
print('第2次數據',data2)

data3=conn.recv(buffer_size)
print('第3次數據',data3)
服務端
from socket import *
import time

ip_port=('127.0.0.1',8082)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('world'.encode('utf-8'))
tcp_client.send('egon'.encode('utf-8'))
客戶端
第1次數據 b'helloworldegon'
第2次數據 b''
第3次數據 b''
服務端與客戶端鏈接的結果

2、客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包

from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

conn,addr=tcp_server.accept()

data1=conn.recv(1)
print('第1次數據',data1)

# data2=conn.recv(5)
# print('第2次數據',data2)
#
# data3=conn.recv(1)
# print('第3次數據',data3)
服務端
from socket import *
import time
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024  #接收的數據只有1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

tcp_client.send('helloworldegon'.encode('utf-8'))

time.sleep(1000)
客戶端
第1次數據 b'h'
第2次數據 b'ellow'  #發送的數據過大,接收的數據設置的較小,就會出現致使粘包 
第3次數據 b'o'
服務端與客戶端鏈接的結果

補充知識:

一、tcp是可靠傳輸

  tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的。

二、udp是不可靠傳輸

   udp發送數據,對端是不會返回確認信息的,所以不可靠。

基於中篇的實現遠程命令的例子,做出解決粘包的方法

#low版解決粘包版本服務端
from socket import *
import subprocess
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_server=socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr=tcp_server.accept()
    print('新的客戶端連接',addr)
    while True:
        #收消息
        try:
            cmd=conn.recv(buffer_size)
            if not cmd:break
            print('收到客戶端的命令',cmd)

            #執行命令,獲得命令的運行結果cmd_res
            res=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                 stderr=subprocess.PIPE,
                                 stdout=subprocess.PIPE,
                                 stdin=subprocess.PIPE)
            err=res.stderr.read()
            if err:
                cmd_res=err
            else:
                cmd_res=res.stdout.read()

            #發送消息
            if not cmd_res:
                cmd_res='執行成功'.encode('gbk')

            length=len(cmd_res)  #計算長度
            conn.send(str(length).encode('utf-8')) #把長度發給客戶端
            client_ready=conn.recv(buffer_size)    #卡着一個recv
            if client_ready == b'ready':  #若是收到客戶端的ready消息,就說明準備好了。
                conn.send(cmd_res)        #就能夠send給客戶端發送消息啦!
        except Exception as e:
            print(e)
            break
low版解決粘包版本服務端
#low版解決粘包版客戶端
from socket import *
ip_port=('127.0.0.1',8080)
back_log=5
buffer_size=1024

tcp_client=socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    if cmd == 'quit':break
    tcp_client.send(cmd.encode('utf-8'))
    #解決粘包
    length=tcp_client.recv(buffer_size)  #接收發送過來的長度(1024*8=8192,2**8192=能夠接收的長度)
    tcp_client.send(b'ready')   #客戶端再send給服務端,告訴服務端我準備好啦!

    length=int(length.decode('utf-8'))  #先解碼,轉成字符串的長度
    #解決思路:就是提早發一個頭過去,告訴客戶端須要接收的長度(分兩步:一、發送發度 二、再次發送數據)
    recv_size=0   #接收的尺寸
    recv_msg=b''  #最後要拼接起來
    while recv_size < length:  #要收多大?,要先判斷接收的尺寸<length
        recv_msg += tcp_client.recv(buffer_size)  #接收到的數據,拼接buffer_size,
        recv_size=len(recv_msg) #1024  #衡量本身接收了多少數據,有沒有收完(統計recv_msg的長度)
    print('命令的執行結果是 ',recv_msg.decode('gbk'))
tcp_client.close()
low版解決粘包版客戶端

通過以上代碼處理,再次進行 ipconfig   dir這些命令則能夠恢復正常,不會出現粘包問題

總結:

(爲什麼low):  程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗。

接下來,有一種新的方式處理粘包的問題

import socket,time,subprocess,pickle,struct
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客戶端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,
                            stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()

        l=struct.pack('i',len(ret))
        conn.sendall(l+ret)
        # conn.send(str(len(ret)).encode('utf-8'))
    conn.close()
服務端
import socket,time,struct

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

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

    s.send(msg.encode('utf-8'))

    l=s.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d = s.recv(1024)
        data += r_d
        r_s += len(r_d)

    print(data.decode('gbk'))
客戶端

在解決了TCP的粘包問題,那麼又該怎麼解決TCP的併發問題

 

SocketServer是基於socket寫成的一個更強大的模塊。

 

SocketServer簡化了網絡服務器的編寫。它有4個類:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer。這4個類是同步進行處理的,另外經過ForkingMixIn和ThreadingMixIn類來支持異步。

在python3中該模塊是socketserver

在python2中該模塊是Socketserver

服務器

  服務器要使用處理程序,必須將其出入到服務器對象,定義了5個基本的服務器類型(就是「類」)。BaseServer,TCPServer,UnixStreamServer,UDPServer,UnixDatagramServer。注意:BaseServer不直接對外服務。

 服務器:

  要使用處理程序,必須將其傳入到服務器的對象,定義了四個基本的服務器類。

(1)TCPServer(address,handler)   支持使用IPv4的TCP協議的服務器,address是一個(host,port)元組。Handler是BaseRequestHandler或StreamRequestHandler類的子類的實例。

(2)UDPServer(address,handler)   支持使用IPv4的UDP協議的服務器,address和handler與TCPServer中相似。

(3)UnixStreamServer(address,handler)   使用UNIX域套接字實現面向數據流協議的服務器,繼承自TCPServer。

(4)UnixDatagramServer(address,handler)  使用UNIX域套接字實現數據報協議的服務器,繼承自UDPServer。

 

這四個類的實例都有如下方法。

一、s.socket   用於傳入請求的套接字對象。

二、s.sever_address  監聽服務器的地址。如元組("127.0.0.1",80)

三、s.RequestHandlerClass   傳遞給服務器構造函數並由用戶提供的請求處理程序類。

四、s.serve_forever()  處理無限的請求  #無限處理client鏈接請求

五、s.shutdown()   中止serve_forever()循環

 

SocketServer模塊中主要的有如下幾個類:

一、BaseServer    包含服務器的核心功能與混合類(mix-in)的鉤子功能。這個類主要用於派生,不要直接生成這個類的類對象,能夠考慮使用TCPServer和UDPServer類。

二、TCPServer     基本的網絡同步TCP服務器

三、UDPServer     基本的網絡同步UDP服務器

四、ForkingTCPServer      是ForkingMixIn與TCPServer的組合

五、ForkingUDPServer    是ForkingMixIn與UDPServer的組合

六、ThreadingUDPServer  是ThreadingMixIn和UDPserver的組合

七、ThreadingTCPServer   是ThreadingMixIn和TCPserver的組合

八、BaseRequestHandler   必須建立一個請求處理類,它是BaseRequestHandler的子類並重載其handle()方法。

九、StreamRequestHandler        實現TCP請求處理類的

十、DatagramRequestHandler  實現UDP請求處理類的

十一、ThreadingMixIn  實現了核心的線程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。

十二、ForkingMixIn     實現了核心的進程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。

  

建立服務端的步驟:

1:首先必須建立一個請求處理類

2:它是BaseRequestHandler的子類

3:該請求處理類是BaseRequestHandler的子類並從新寫其handle()方法

4:必需要有一個handle()方法,規則定義死的

 

實例化  請求處理類傳入服務器地址和請求處理程序類

最後實例化調用serve_forever()  #無限處理client請求

 

記住一個原則:對tcp來講:self.request=conn

在這裏做一個簡單的小例子

# TCP下實現的併發
import socketserver
class Myserver(socketserver.BaseRequestHandler): # 必需要繼承這個類
    def handle(self): # 必需要有這個方法
        print(self.request) # 至關於conn
        print(self.client_address) # 鏈接過來的客戶端地址
        while True:
            try:
                data = self.request.recv(1024)
                if not data:break
                print("收到來自%s的消息是: %s" %(self.client_address,data.decode("utf-8")))
                nr = input(">>>")
                self.request.sendall(nr.encode("utf-8"))
            except Exception:
                break

if __name__ == '__main__':
    # ip_port = input("請輸入ip和端口")
    obj = socketserver.ThreadingTCPServer(("127.0.0.1",6060),Myserver)
    obj.serve_forever()
服務端
import socket
ip_port = ("127.0.0.1",6060)
buffer_size = 1024
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(ip_port)

while True:
    nr = input(">>>").strip() #
    if not nr:continue
    s.sendall(bytes(nr, encoding="utf-8"))
    res = s.recv(buffer_size)
    print("來自遠方的消息",str(res, encoding="utf-8"))
客戶端

 

源碼剖析。。。未完待續……

相關文章
相關標籤/搜索