網絡編程

1. 軟件開發的架構

我們瞭解的涉及到兩個程序之間通訊的應用大致可以分爲兩種:

第一種是應用類:qq、微信、網盤、優酷這一類是屬於需要安裝的桌面應用

第二種是web類:比如百度、知乎、博客園等使用瀏覽器訪問就可以直接使用的應用

這些應用的本質其實都是兩個程序之間的通訊。而這兩個分類又對應了兩個軟件開發的架構:

C/S架構

在這裏插入圖片描述
C/S即:Client與Server ,中文意思:客戶端與服務器端架構,這種架構也是從用戶層面(也可以是物理層面)來劃分的。

這裏的客戶端一般泛指客戶端應用程序EXE,程序需要先安裝後,才能運行在用戶的電腦上,對用戶的電腦操作系統環境依賴較大。

B/S架構

在這裏插入圖片描述
B/S即:Browser與Server,中文意思:瀏覽器端與服務器端架構,這種架構是從用戶層面來劃分的。

Browser瀏覽器,其實也是一種Client客戶端,只是這個客戶端不需要大家去安裝什麼應用程序,只需在瀏覽器上通過HTTP請求服務器端相關的資源(網頁資源),客戶端Browser瀏覽器就能進行增刪改查。

2. socket編程

計算機網絡一節中,我們已瞭解到Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。它把TCP/IP層複雜的操作抽象爲幾個簡單的接口供應用層調用來實現進程在網絡中通信。
在這裏插入圖片描述

2.1 socket通信流程

在這裏插入圖片描述

  • 1 服務器根據地址類型(ipv4,ipv6)、socket類型、協議創建socket

  • 2 服務器爲socket綁定ip地址和端口號

  • 3 服務器socket監聽端口號請求,隨時準備接收客戶端發來的連接,這時候服務器的socket並沒有被打開

  • 4 客戶端創建socket

  • 5 客戶端打開socket,根據服務器ip地址和端口號試圖連接服務器socket

  • 6 服務器socket接收到客戶端socket請求,被動打開,開始接收客戶端請求,直到客戶端返回連接信息。這時候socket進入阻塞狀態,所謂阻塞即accept()方法一直等到客戶端返回連接信息後才返回,開始接收下一個客戶端連接請求

  • 7 客戶端連接成功,向服務器發送連接狀態信息

  • 8 服務器accept方法返回,連接成功

  • 9 客戶端向socket寫入信息(或服務端向socket寫入信息)

  • 10 服務器讀取信息(客戶端讀取信息)

  • 11 客戶端關閉

  • 12 服務器端關閉

sk.bind(address)

  #s.bind(address) 將套接字綁定到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。

sk.listen(backlog)

  #開始監聽傳入連接。backlog指定在拒絕連接之前,可以掛起的最大連接數量。

      #backlog等於5,表示內核已經接到了連接請求,但服務器還沒有調用accept進行處理的連接個數最大爲5
      #這個值不能無限大,因爲要在內核中維護連接隊列

sk.setblocking(bool)

  #是否阻塞(默認True),如果設置False,那麼accept和recv時一旦無數據,則報錯。

sk.accept()

  #接受連接並返回(conn,address),其中conn是新的套接字對象,可以用來接收和發送數據。address是連接客戶端的地址。

  #接收TCP 客戶的連接(阻塞式)等待連接的到來

sk.connect(address)

  #連接到address處的套接字。一般,address的格式爲元組(hostname,port),如果連接出錯,返回socket.error錯誤。

sk.connect_ex(address)

  #同上,只不過會有返回值,連接成功時返回 0 ,連接失敗時候返回編碼,例如:10061

sk.close()

  #關閉套接字

sk.recv(bufsize[,flag])

  #接受套接字的數據。數據以字符串形式返回,bufsize指定最多可以接收的數量。flag提供有關消息的其他信息,通常可以忽略。

sk.recvfrom(bufsize[.flag])

  #與recv()類似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。

sk.send(string[,flag])

  #將string中的數據發送到連接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。即:可能未將指定內容全部發送。

sk.sendall(string[,flag])

  #將string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None,失敗則拋出異常。

      #內部通過遞歸調用send,將所有內容發送出去。

sk.sendto(string[,flag],address)

  #將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。該函數主要用於UDP協議。

sk.settimeout(timeout)

  #設置套接字操作的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因爲它們可能用於連接的操作(如 client 連接最多等待5s )

sk.getpeername()

  #返回連接套接字的遠程地址。返回值通常是元組(ipaddr,port)。

sk.getsockname()

  #返回套接字自己的地址。通常是一個元組(ipaddr,port)

sk.fileno()

  #套接字的文件描述符

2.2 基於TCP協議的socket

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址綁定到套接字
sk.listen()          #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024)  #接收客戶端信息
print(ret)       #打印客戶端信息
conn.send(b'hi')        #向客戶端發送信息
conn.close()       #關閉客戶端套接字
sk.close()        #關閉服務器套接字(可選)

client端

import socket
sk = socket.socket()           # 創建客戶套接字
sk.connect(('127.0.0.1',8898))    # 嘗試連接服務器
sk.send(b'hello!')
ret = sk.recv(1024)         # 對話(發送/接收)
print(ret)
sk.close()            # 關閉客戶套接字

問題:你可能會在重啓服務端時遇到報錯
在這裏插入圖片描述

#加入一條socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898))  #把地址綁定到套接字
sk.listen()          #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024)   #接收客戶端信息
print(ret)              #打印客戶端信息
conn.send(b'hi')        #向客戶端發送信息
conn.close()       #關閉客戶端套接字
sk.close()        #關閉服務器套接字(可選)

注意:tcp是基於鏈接的,必須先啓動服務端,然後再啓動客戶端去鏈接服務端

2.3 基於UDP協議的socket

server端

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #創建一個服務器的套接字
udp_sk.bind(('127.0.0.1',9000))        #綁定服務器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 對話(接收與發送)
udp_sk.close()                         # 關閉服務器套接字

client端

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

注意:udp是無鏈接的,啓動服務之後可以直接接受消息,不需要提前建立鏈接

2.4 實例(初戀的故事)

在這裏插入圖片描述
背景:在上學的路上,有個屌絲阿帥,想追女神阿芳,悶騷型,心想:老子就給她一次機會,不把握就算了…

阿芳(server端)

import socket
ip_port = ('127.0.0.1',9997)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)

print ('server waiting...')

conn,addr = sk.accept()
client_data = conn.recv(1024)
print (str(client_data,"utf8"))
conn.sendall(bytes('你是一個好人,但是我們真不合適!',encoding="utf-8"))

sk.close()

阿帥(client端)

import socket
ip_port = ('127.0.0.1',9997)

sk = socket.socket()
sk.connect(ip_port)


sk.sendall(bytes('阿芳,俺喜歡你',encoding="utf8"))

server_reply = sk.recv(1024)
print (str(server_reply,"utf8"))

在這裏插入圖片描述
背景:又有個屌絲,名叫田大,也想追女神阿芳,內心強大,臭不要臉,於是…

田大(server端)

import socket
ip_port = ('127.0.0.1',8888)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(2)
print ("服務端啓動...")
conn,address = sk.accept()
while True:
    client_data=conn.recv(1024)
    if str(client_data,"utf8")=='exit':
        break
    print (str(client_data,"utf8"))
    server_response=input(">>>")
    conn.sendall(bytes(server_response,"utf8"))

conn.close()

阿芳(client端)

import socket
ip_port = ('127.0.0.1',8888)
sk = socket.socket()
sk.connect(ip_port)
print ("客戶端啓動:")

while True:
    inp = input('>>>')
    sk.sendall(bytes(inp,"utf8"))
    if inp == 'exit':
        break
    server_response=sk.recv(1024)
    print (str(server_response,"utf8"))
sk.close()

固然田大臉皮夠厚,但是落花有意流水無情,阿芳誓死不從,不給機會和你聊也白搭(server端也需要有while)。

背景:某天,阿芳孤獨難耐,決定調整狀態想和多人聊一聊找個合適的(雖然不是併發,但是可以在不重新開啓server的前提下與多人聊天),於是田大抓住了機會,倆人一頓深聊後…田大放棄了。

阿芳(server端)

import socket

ip_port = ('127.0.0.1',8870)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(2)
print ("服務端啓動...")

while True:
    conn,address = sk.accept()
    print(address)
    while True:
        try:
            client_data=conn.recv(1024)
        except:
            print("意外中斷")
            break
        print (str(client_data,"utf8"))

        server_response=input(">>>")
        conn.sendall(bytes(server_response,"utf8"))

    conn.close()

田大(client端)

import socket
ip_port = ('127.0.0.1',8888)
sk = socket.socket()
sk.connect(ip_port)
print ("客戶端啓動:")

while True:
    inp = input('>>>')
    sk.sendall(bytes(inp,"utf8"))
    if inp == 'exit':
        break
    server_response=sk.recv(1024)
    print (str(server_response,"utf8"))
sk.close()

這樣如果客戶端發的數據就是空數據的話豈不是也意外退出,那不就bug啦?

其實不用擔心,如果客戶端send了一個空數據後客戶端繼續向下執行,而server端的recv方法會繼續阻塞,直到接收到一個非空數據纔會繼續向下執行。

而stop_button的退出會使client_data成爲一個空數據繼續向下執行,所以可以依據代碼意外退出。

2.5 實例(模擬qq聊天室)

server端

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)

client端

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

qq_name_dic={
    'aa':('127.0.0.1',8081),
    'bb':('127.0.0.1',8081),
    'vv':('127.0.0.1',8081),
    'dd':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送,輸入q結束和他的聊天: ').strip()
        if msg == 'q':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()

socket參數的詳解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

在這裏插入圖片描述

3. 黏包現象

讓我們基於tcp先製作一個遠程執行命令的程序:
server端

import socket
import subprocess
ip_port = ('127.0.0.1',8879)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)
print ("服務端啓動...")
while True:
    conn,address = sk.accept()
    while True:
        try:

            client_data=conn.recv(1024)
        except Exception:
            break
        print (str(client_data,"utf8"))
        print ("waiting...")
        # server_response=input(">>>")
        # conn.sendall(bytes(server_response,"utf8"))
        cmd=str(client_data,"utf8").strip()
        cmd_call=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE)
        cmd_result=cmd_call.stdout.read()
        if len(cmd_result)==0:
            cmd_result=b"no output!"
        conn.sendall(cmd_result)
        print('send data size',len(cmd_result))
        print('******************')
        print('******************')
        print('******************')

    conn.close()

client端

import socket
ip_port = ('127.0.0.1',8879)
sk = socket.socket()
sk.connect(ip_port)
print ("客戶端啓動:")
while True:
    inp = input('cdm:>>>').strip( )
    if len(inp)==0:
        continue
    if inp=="q":
        break
    sk.sendall(bytes(inp,"utf8"))
    server_response=sk.recv(1024)
    print (str(server_response,"gbk"))
    print('receive data size',len(server_response))
    if inp == 'exit':
        break
sk.close()

同時執行多條命令之後,得到的結果很可能只有一部分,在執行其他命令的時候又接收到之前執行的另外一部分結果,這種顯現就是黏包。

注意:只有TCP有粘包現象,UDP永遠不會粘包

3.1 黏包成因

TCP協議中的數據傳遞

1、tcp協議的拆包機制

當發送端緩衝區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。 MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500。如果本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生很多數據包碎片,增加丟包率,降低網絡速度。

2、面向流的通信特點和Nagle算法

TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合併成一個大的數據塊,然後進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
對於空消息:tcp是基於數據流的,於是收發的消息不能爲空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被髮送,udp協議會幫你封裝上消息頭髮送過去。
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時纔會清除緩衝區內容。數據是可靠的,但是會粘包。

基於tcp協議特點的黏包現象成因

在這裏插入圖片描述

發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。
也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。
而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。
怎樣定義消息呢?可以認爲對方一次性write/send的數據爲一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩衝區。

例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束

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

UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。
不會使用塊的合併優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
對於空消息:tcp是基於數據流的,於是收發的消息不能爲空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被髮送,udp協議會幫你封裝上消息頭髮送過去。
不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y;x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠。

補充說明: udp和tcp一次發送數據長度的限制

用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,如果發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送)
用TCP協議發送時,由於TCP是數據流協議,因此不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不一定會一次性發送出去,如果這段數據比較長,會被分段發送,如果比較短,可能會等待和下一次數據一起發送。

3.2 會發生黏包的兩種情況

情況一 發送方的緩存機制
發送端需要等緩衝區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)

server

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

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()

client

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)


s.send('hello'.encode('utf-8'))
s.send('egg'.encode('utf-8'))
情況二 接收方的緩存機制
接收方不及時接收緩衝區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的數據,產生粘包)

server端

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

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()

client端

#_*_coding:utf-8_*_
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)


s.send('hello egg'.encode('utf-8'))

總結
黏包現象只發生在tcp協議中:

1.從表面上看,黏包問題主要是因爲發送方和接收方的緩存機制、tcp協議面向流通信的特點。

2.實際上,主要還是因爲接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的

3.3 黏包的解決方案

解決方案一
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。

在這裏插入圖片描述
server端

import socket,time
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'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))

client端

import socket,subprocess
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()
        data_length=len(ret)
        conn.send(str(data_length).encode('utf-8'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()

存在的問題:
程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗

解決方案進階
剛剛的方法,問題在於我們我們在發送

我們可以藉助一個模塊,這個模塊可以把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息之前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了。

struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes
>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍

在這裏插入圖片描述

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)
import struct
import binascii
import ctypes

values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')

print(s1.size,s2.size)
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)


s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)

print('After pack',binascii.hexlify(prebuffer))
print(s1.unpack_from(prebuffer,0))
print(s2.unpack_from(prebuffer,s1.size))

s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
print(s3.unpack_from(prebuffer,0))
使用struct解決黏包
藉助struct模塊,我們知道長度數字可以被轉換成一個標準大小的4字節數字。因此可以利用這個特點來預先發送數據長度。

在這裏插入圖片描述

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()


        conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度
        conn.sendall(back_msg) #在發真實的內容

    conn.close()
#_*_coding:utf-8_*_
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('utf-8'))
    print(data.decode('gbk')) #windows默認gbk編碼

我們還可以把報頭做成字典,字典裏包含將要發送的真實數據的詳細信息,然後json序列化,然後用struck將序列化後的數據長度打包成4個字節(4個自己足夠用了)
在這裏插入圖片描述

4. socket的更多方法介紹

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

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

公共用途的套接字函數
s.recv() 接收TCP數據
s.send() 發送TCP數據
s.sendall() 發送TCP數據
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() 創建一個與該套接字相關的文件

官方文檔對socket模塊下的socket.send()和socket.sendall()解釋如下:

  • socket.send(string[, flags])
    send()的返回值是發送的字節數量,這個數量值可能小於要發送的string的字節數,也就是說可能無法發送string中所有的數據。如果有錯誤則會拋出異常
  • socket.sendall(string[, flags])
    嘗試發送string的所有數據,成功則返回None,失敗則拋出異常。

下面兩段代碼是等價的:

#sock.sendall(‘Hello world\n’)

#buffer = ‘Hello world\n’
#while buffer:
#bytes = sock.send(buffer)
#buffer = buffer[bytes:]

5. 驗證客戶端鏈接的合法性

如果你想在分佈式系統中實現一個簡單的客戶端鏈接認證功能,又不像SSL那麼複雜,那麼利用hmac+加鹽1 的方式來實現

服務端:

#_*_coding:utf-8_*_
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    認證客戶端鏈接
    :param conn:
    :return:
    '''
    print('開始驗證新鏈接的合法性')
    msg=os.urandom(32)
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)

def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('該鏈接不合法,關閉')
        conn.close()
        return
    print('鏈接合法,開始通信')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,bufsize,backlog=5):
    '''
    只處理鏈接
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新連接[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)

客戶端(合法)

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    驗證客戶端到服務器的鏈接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)

客戶端(非法:不知道加密方式)

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)

客戶端(非法:不知道secret_key)

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os

secret_key=b'linhaifeng bang bang bang1111'
def conn_auth(conn):
    '''
    驗證客戶端到服務器的鏈接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)


6. socketserver

雖說用Python編寫簡單的網絡程序很方便,但複雜一點的網絡程序還是用現成的框架比較好。這樣就可以專心事務邏輯,而不是套接字的各種細節。SocketServer模塊簡化了編寫網絡服務程序的任務。同時SocketServer模塊也是Python標準庫中很多服務器框架的基礎。

socketserver模塊可以簡化網絡服務器的編寫,Python把網絡服務抽象成兩個主要的類,一個是Server類,用於處理連接相關的網絡操作,另外一個則是RequestHandler類,用於處理數據相關的操作。並且提供兩個MixIn 類,用於擴展 Server,實現多進程或多線程。

Server類:
它包含了種五種server類,BaseServer(不直接對外服務)。TCPServer使用TCP協議,UDPServer使用UDP協議,還有兩個不常使用的,即UnixStreamServer和UnixDatagramServer,這兩個類僅僅在unix環境下有用(AF_unix)。
在這裏插入圖片描述
server端

import socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        self.request.sendall(self.data.upper())

if __name__ == "__main__":
    HOST, PORT = "127.0.0.1", 9999

    # 設置allow_reuse_address允許服務器重用地址
    socketserver.TCPServer.allow_reuse_address = True
    # 創建一個server, 將服務地址綁定到127.0.0.1:9999
    server = socketserver.TCPServer((HOST, PORT),Myserver)
    # 讓server永遠運行下去,除非強制停止程序
    server.serve_forever()

client端

import socket

HOST, PORT = "127.0.0.1", 9999
data = "hello"

# 創建一個socket鏈接,SOCK_STREAM代表使用TCP協議
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect((HOST, PORT))          # 鏈接到客戶端
    sock.sendall(bytes(data + "\n", "utf-8")) # 向服務端發送數據
    received = str(sock.recv(1024), "utf-8")# 從服務端接收數據

print("Sent:     {}".format(data))
print("Received: {}".format(received))


  1. “加鹽”。具體來說就是在原有材料(用戶自定義密碼)中加入其它成分(一般是用戶自有且不變的因素),以此來增加系統複雜度。當這種鹽和用戶密碼相結合後,再通過摘要處理,就能得到隱蔽性更強的摘要值。 ↩︎

發佈了41 篇原創文章 · 獲贊 9 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章