python之socket編程

1、客戶端/服務器架構

1.硬件C/S架構(打印機)python

2.軟件C/S架構算法

  互聯網中到處是C/S架構shell

  如購物網站是服務端,你的瀏覽器是客戶端(B/S架構也是C/S架構的一種)編程

  騰訊做爲服務端爲你提供視頻,你得下個騰訊視頻客戶端才能看它的視頻)json

C/S架構與socket的關係:windows

學習socket就是爲了完成C/S架構的開發設計模式

2、osi七層

須知一個完整的計算機系統是由硬件、操做系統、應用軟件三者組成,具有了這三個條件,一臺計算機系統就能夠本身跟本身玩了(打個單機遊戲,玩個掃雷啥的)瀏覽器

若是你要跟別人一塊兒玩,那你就須要上網了,什麼是互聯網?緩存

互聯網的核心就是由一堆協議組成,協議就是標準,好比全世界人通訊的標準是英語服務器

若是把計算機比做人,互聯網協議就是計算機界的英語。全部的計算機都學會了互聯網協議,那全部的計算機都就能夠按照統一的標準去收發信息從而完成通訊了。

人們按照分工不一樣把互聯網協議從邏輯上劃分了層級,能夠看下圖:

因此,學習socket以前要先學習互聯網協議。

3、socket層

通過上圖和下圖的對比,咱們不難發現socket層的所在。

socket的是什麼呢?

Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。

因此,咱們無需深刻理解tcp/udp協議,socket已經爲咱們封裝好了,咱們只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。

也有人將socket說成ip+port,ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序,ip地址是配置到網卡上的,而port是應用程序開啓的,ip與port的綁定就標識了互聯網中獨一無二的一個應用程序,而程序的pid是同一臺機器上不一樣進程或者線程的標識

4、套接字發展史及分類

套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信,或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的。

基於文件類型的套接字家族

套接字家族的名字:AF_UNIX

unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊

基於網絡類型的套接字家族

套接字家族的名字:AF_INET

(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)。

套接字工做流程

先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束

socket()模塊函數用法

  import socket
  socket.socket(socket_family,socket_type,protocal=0)
  socket_family 能夠是 AF_UNIX 或 AF_INET。socket_type 能夠是 SOCK_STREAM 或 SOCK_DGRAM。protocol 通常不填,默認值爲 0。
  
  獲取tcp/ip套接字
  tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  
  獲取udp/ip套接字
  udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 
 因爲 socket 模塊中有太多的屬性。咱們在這裏破例使用了'from module import *'語句。使用 'from socket import *',咱們就把 socket 模塊裏的全部屬性都帶到咱們的命名空間裏了,這樣能 大幅減短咱們的代碼。
 例如tcpSock = socket(AF_INET, SOCK_STREAM)
View Code
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的鏈接,(阻塞式)等待鏈接的到來

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

公共用途的套接字函數
s.recv() 接收TCP數據
s.send() 發送TCP數據(send在待發送數據量大於己端緩存區剩餘空間時,數據丟失,不會發完)
s.sendall() 發送完整的TCP數據(本質就是循環調用send,sendall在待發送數據量大於己端緩存區剩餘空間時,數據不丟失,循環調用send直到發完)
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() 建立一個與該套接字相關的文件

5、基於TCP的套接字

tcp是基於連接的,必須先啓動服務端,而後再啓動客戶端去連接服務端

tcp服務端:

 ss = socket() #建立服務器套接字
 ss.bind()      #把地址綁定到套接字
 ss.listen()      #監聽連接
 inf_loop:      #服務器無限循環
     cs = ss.accept() #接受客戶端連接
     comm_loop:         #通信循環
         cs.recv()/cs.send() #對話(接收與發送)
     cs.close()    #關閉客戶端套接字
 ss.close()        #關閉服務器套接字(可選)

tcp客戶端:

 cs = socket()    # 建立客戶套接字
 cs.connect()    # 嘗試鏈接服務器
 comm_loop:        # 通信循環
     cs.send()/cs.recv()    # 對話(發送/接收)
 cs.close()            # 關閉客戶套接字

socket通訊流程與打電話流程相似,咱們就以打電話爲例來實現一個low版的套接字通訊:

import socket
ip_port=('127.0.0.1',9000)  #電話卡
BUFSIZE=1024                #收發消息的尺寸
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5)     #手機待機


conn,addr=s.accept()            #手機接電話
# print(conn)
# print(addr)
print('接到來自%s的電話' %addr[0])

msg=conn.recv(BUFSIZE)             #聽消息,聽話
print(msg,type(msg))

conn.send(msg.upper())          #發消息,說話

conn.close()                    #掛電話

s.close()                       #手機關機
服務端
import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #撥電話

s.send('linhaifeng nb'.encode('utf-8'))         #發消息,說話(只能發送字節類型)

feedback=s.recv(BUFSIZE)                           #收消息,聽話
print(feedback.decode('utf-8'))

s.close()                                       #掛電話
客戶端

加上連接循環與通訊循環

import socket
ip_port=('127.0.0.1',8081)#電話卡
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
s.bind(ip_port) #手機插卡
s.listen(5)     #手機待機


while True:                         #新增接收連接循環,能夠不停的接電話
    conn,addr=s.accept()            #手機接電話
    # print(conn)
    # print(addr)
    print('接到來自%s的電話' %addr[0])
    while True:                         #新增通訊循環,能夠不斷的通訊,收發消息
        msg=conn.recv(BUFSIZE)             #聽消息,聽話

        # if len(msg) == 0:break        #若是不加,那麼正在連接的客戶端忽然斷開,recv便再也不阻塞,死循環發生

        print(msg,type(msg))

        conn.send(msg.upper())          #發消息,說話

    conn.close()                    #掛電話

s.close()                       #手機關機
server改進
import socket
ip_port=('127.0.0.1',8081)
BUFSIZE=1024
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect_ex(ip_port)           #撥電話

while True:                             #新增通訊循環,客戶端能夠不斷髮收消息
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    s.send(msg.encode('utf-8'))         #發消息,說話(只能發送字節類型)

    feedback=s.recv(BUFSIZE)                           #收消息,聽話
    print(feedback.decode('utf-8'))

s.close()                                       #掛電話
client改進

6、基於UDP的套接字

UDP是無連接的,先啓動那一端都不會報錯。

udp服務端:

1 ss = socket()   #建立一個服務器的套接字
2 ss.bind()       #綁定服務器套接字
3 inf_loop:       #服務器無限循環
4     cs = ss.recvfrom()/ss.sendto() # 對話(接收與發送)
5 ss.close()                         # 關閉服務器套接字

udp客戶端

1 cs = socket()   # 建立客戶套接字
2 comm_loop:      # 通信循環
3     cs.sendto()/cs.recvfrom()   # 對話(發送/接收)
4 cs.close()                      # 關閉客戶套接字

udp套接字簡單編程

from socket import *
ip_port = ('127.0.0.1',1234)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM)#數據報
udp_server.bind(ip_port)

while True:
    data,addr = udp_server.recvfrom(buffer_size)

    print(data)
    udp_server.sendto(data.upper(),addr)
服務器端
from socket import *

ip_port = ('127.0.0.1', 1234)
buffer_size = 1024

udp_client = socket(AF_INET, SOCK_DGRAM)  # 數據報


while True:
    msg = input('>>:').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)

    data,addr = udp_client.recvfrom(buffer_size)
    print(data.decode('utf-8'))
客戶端

時間服務器示例

#時間管理器

from socket import *
import time
ip_port = ('127.0.0.1',1234)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM)#數據報
udp_server.bind(ip_port)

while True:
    data,addr = udp_server.recvfrom(buffer_size)
    if not data:
        t='%Y-%m-%d %X'
    else:
        t=data.decode('utf-8')
    back_time = time.strftime(t)
    udp_server.sendto(back_time.encode('utf-8'),addr)
ntp_server
from socket import *

ip_port = ('127.0.0.1', 1234)
buffer_size = 1024

udp_client = socket(AF_INET, SOCK_DGRAM)  # 數據報


while True:
    msg = input('>>:').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)

    data,addr = udp_client.recvfrom(buffer_size)
    print(data.decode('utf-8'))
ntp_client

7、粘包問題

讓咱們基於tcp先編程一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)

from socket import *
import subprocess
import struct
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  #處理quit命令致使的數據爲空,致使死循環
             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')
             conn.send(cmd_res)
         except Exception as e:
             print(e)
             break
server
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'))
     cmd_res=tcp_client.recv(buffer_size)
     print('命令的執行結果是:',cmd_res.decode('gbk'))
 tcp_client.close()
client

上述程序是基於tcp的socket,在運行時會發生粘包。

在基於UDP編程一個遠程執行命令的程序

from socket import *
import subprocess

ip_port=('127.0.0.1',9003)
bufsize=1024

udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)

while True:
    #收消息
    cmd,addr=udp_server.recvfrom(bufsize)
    print('用戶命令----->',cmd)

    #邏輯處理
    res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE)
    stderr=res.stderr.read()
    stdout=res.stdout.read()

    #發消息
    udp_server.sendto(stderr,addr)
    udp_server.sendto(stdout,addr)
udp_server.close()
server_udp
from socket import *
ip_port=('127.0.0.1',9003)
bufsize=1024

udp_client=socket(AF_INET,SOCK_DGRAM)


while True:
    msg=input('>>: ').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)

    data,addr=udp_client.recvfrom(bufsize)
    print(data.decode('utf-8'),end='')
client _udp

上述程序是基於udp的socket,在運行時永遠不會發生粘包

那麼,粘包是怎麼形成的?

 

只有TCP有粘包現象,UDP永遠不會粘包,首先須要掌握一個socket收發消息的原理

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

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

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

此外,發送方引發的粘包是由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協議會幫你封裝上消息頭.

 

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

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

兩種狀況下會發生粘包。

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)

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()
server
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('feng'.encode('utf-8'))
client

接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)

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()
server
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 feng'.encode('utf-8'))
client

拆包的發生狀況

當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。

補充問題一:爲什麼tcp是可靠傳輸,udp是不可靠傳輸

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

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

補充問題二:send(字節流)和recv(1024)及sendall

recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據

send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失.

8、粘包的解決方法

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據。

第一種的解決方法

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

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

因此咱們能夠,爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據,引用struct模塊。

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 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()
server
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編碼
client

咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)

發送時:先發報頭長度,再編碼報頭內容而後發送,最後發真實內容

接收時:先手報頭長度,用struct取出來,根據取出的長度收取報頭內容,而後解碼,反序列化,從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容

9、socketserver實現併發

 

基於tcp的套接字,關鍵就是兩個循環,一個連接循環,一個通訊循環

 

socketserver模塊中分兩大類:server類(解決連接問題)和request類(解決通訊問題)

 

server類:

request類:

繼承關係;

 

 

 

 

如下述代碼爲例,分析socketserver源碼:

ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()

查找屬性的順序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer

  1. 實例化獲得ftpserver,先找類ThreadingTCPServer的__init__,在TCPServer中找到,進而執行server_bind,server_active
  2. 找ftpserver下的serve_forever,在BaseServer中找到,進而執行self._handle_request_noblock(),該方法一樣是在BaseServer中
  3. 執行self._handle_request_noblock()進而執行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),而後執行self.process_request(request, client_address)
  4. 在ThreadingMixIn中找到process_request,開啓多線程應對併發,進而執行process_request_thread,執行self.finish_request(request, client_address)
  5. 上述四部分完成了連接循環,本部分開始進入處理通信部分,在BaseServer中找到finish_request,觸發咱們本身定義的類的實例化,去找__init__方法,而咱們本身定義的類沒有該方法,則去它的父類也就是BaseRequestHandler中找....

源碼分析總結:

基於tcp的socketserver咱們本身定義的類中的

  1.   self.server即套接字對象
  2.   self.request即一個連接
  3.   self.client_address即客戶端地址

基於udp的socketserver咱們本身定義的類中的

  1. self.request是一個元組(第一個元素是客戶端發來的數據,第二部分是服務端的udp套接字對象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  2. self.client_address即客戶端地址

示例代碼:

import socketserver
import struct
import json
import os
class FtpServer(socketserver.BaseRequestHandler):
    coding='utf-8'
    server_dir='file_upload'
    max_packet_size=1024
    BASE_DIR=os.path.dirname(os.path.abspath(__file__))
    def handle(self):
        print(self.request)
        while True:
            data=self.request.recv(4)
            data_len=struct.unpack('i',data)[0]
            head_json=self.request.recv(data_len).decode(self.coding)
            head_dic=json.loads(head_json)
            # print(head_dic)
            cmd=head_dic['cmd']
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(head_dic)
    def put(self,args):
        file_path = os.path.normpath(os.path.join(
            self.BASE_DIR,
            self.server_dir,
            args['filename']
        ))

        filesize = args['filesize']
        recv_size = 0
        print('----->', file_path)
        with open(file_path, 'wb') as f:
            while recv_size < filesize:
                recv_data = self.request.recv(self.max_packet_size)
                f.write(recv_data)
                recv_size += len(recv_data)
                print('recvsize:%s filesize:%s' % (recv_size, filesize))


ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()
server
import socket
import struct
import json
import os



class MYTCPClient:
    address_family = socket.AF_INET

    socket_type = socket.SOCK_STREAM

    allow_reuse_address = False

    max_packet_size = 8192

    coding='utf-8'

    request_queue_size = 5

    def __init__(self, server_address, connect=True):
        self.server_address=server_address
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if connect:
            try:
                self.client_connect()
            except:
                self.client_close()
                raise

    def client_connect(self):
        self.socket.connect(self.server_address)

    def client_close(self):
        self.socket.close()

    def run(self):
        while True:
            inp=input(">>: ").strip()
            if not inp:continue
            l=inp.split()
            cmd=l[0]
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(l)


    def put(self,args):
        cmd=args[0]
        filename=args[1]
        if not os.path.isfile(filename):
            print('file:%s is not exists' %filename)
            return
        else:
            filesize=os.path.getsize(filename)

        head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
        print(head_dic)
        head_json=json.dumps(head_dic)
        head_json_bytes=bytes(head_json,encoding=self.coding)

        head_struct=struct.pack('i',len(head_json_bytes))
        self.socket.send(head_struct)
        self.socket.send(head_json_bytes)
        send_size=0
        with open(filename,'rb') as f:
            for line in f:
                self.socket.send(line)
                send_size+=len(line)
                print(send_size)
            else:
                print('upload successful')




client=MYTCPClient(('127.0.0.1',8080))

client.run()
client
相關文章
相關標籤/搜索