網絡編程——socket編程

1、C/S架構介紹

C/S架構    C:clint(客戶端軟件)
            S:server(服務端軟件)html

  python程序員是應用開發程序員,開發的軟件都是應用軟件,應用軟件必須運行於操做系統之上,操做系統則運行於硬件上,應用軟件不能直接操做硬件,應用軟件對硬件的操做必須調用操做系統接口,由操做系統操做硬件。python

客戶端軟件基於網絡發送消息給服務端軟件流程:
  一、客戶端軟件產生數據,存放於客戶端軟件的內存中,而後調用接口將本身內存中的數據發送/拷貝給操做系統內存
  二、客戶端操做系統收到數據後,按照客戶端軟件指定的協議、調用網卡發送數據
  三、網絡傳輸數據
  四、服務端軟件調用系統接口,想要將數據從操做系統內存拷貝到本身的內存中
  五、服務端操做系統受到4的指令後,使用與客戶端相同的協議,從網卡接收到數據,而後拷貝給服務端軟件linux

  假設我如今要寫一個程序,給另外一臺計算機發數據,必須經過tcp/ip協議 ,但具體的實現過程是什麼呢?我應該怎麼操做才能把數據封裝成tcp/ip的包,又執行什麼指令才能把數據發到對端機器上呢?程序員

  簡而言之,socket這個東東干的事情,就是幫你把tcp/ip協議層的各類數據封裝啦、數據發送、接收等經過代碼已經給你封裝好了,你只須要調用幾行代碼,就能夠給別的機器發消息了。算法

2、計算機網絡

  計算機之間的通訊首先要有物理連接介質,好比網線,交換機,路由器等網絡設備。 shell

  通訊的線路建好以後,只是物理層面有了能夠承載數據的介質,要想通訊,還須要咱們按照某種規則組織咱們的數據,這樣對方在接收到數據後就能夠按照相同的規則去解析出數據。編程

3、TCP/IP

  按照功能不一樣,人們將互聯網協議分爲osi七層或tcp/ip五層或tcp/ip四層。json

  http://www.cnblogs.com/linhaifeng/articles/5937962.html#_label5windows

TCP/IP五層
  一、應用層:規定應用程序的數據格式。
  二、傳輸層:創建端口到端口的通訊設計模式

"""
    端口範圍0-65535,0-1023爲系統佔用端口
    傳輸層有兩種協議,TCP和UDP:
    tcp協議:
    可靠傳輸,TCP數據包沒有長度限制,理論上能夠無限長,可是爲了保證網絡的效率,一般  TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包沒必要再分割。
    udp協議:
    不可靠傳輸,」報頭」部分一共只有8個字節,總長度不超過65,535字節,正好放進一個IP數據包。
"""
傳輸層詳解

  三、網絡層:引入一套新的地址用來區分不一樣的廣播域/子網,這套地址即網絡地址

"""
ip協議:
    一、規定網絡地址的協議叫ip協議,它定義的地址稱之爲ip地址,普遍採用的v4版本即ipv4,它規定網絡地址由32位2進製表示
    二、範圍0.0.0.0-255.255.255.255
    三、一個ip地址一般寫成四段十進制數,例:172.16.10.1
    
子網掩碼:
    所謂」子網掩碼」,就是表示子網絡特徵的一個參數。它在形式上等同於IP地址,也是一個32位二進制數字,
    它的網絡部分所有爲1,主機部分所有爲0。好比,IP地址172.16.10.1,若是已知網絡部分是前24位,
    主機部分是後8位,那麼子網絡掩碼就是11111111.11111111.11111111.00000000,寫成十進制就是255.255.255.0。
"""
網絡層詳解

  四、數據鏈路層:定義了電信號的分組方式,分組方式後來造成了統一的標準,即以太網協議ethernet

"""
    ethernet規定:
        一、一組電信號構成一個數據包,叫作‘幀’
        二、每一數據幀分紅:報頭head和數據data兩部分

    head包含:(固定18個字節)
        發送者/源地址,6個字節
        接收者/目標地址,6個字節
        數據類型,6個字節
    
    data包含:(最短46字節,最長1500字節)
        數據包的具體內容
    
    head長度+data長度=最短64字節,最長1518字節,超過最大限制就分片發送

    mac地址:(標識子網內,一臺機器的位置)
        ethernet規定接入internet的設備都必須具有網卡,發送端和接收端的地址即是指網卡的地址,即mac地址
        每塊網卡出廠時都被燒製上一個世界惟一的mac地址,長度爲48位2進制,一般由12位16進制數表示(前六位是廠商編號,後六位是流水線號)
    
    廣播:
        有了mac地址,同一網絡內的兩臺主機就能夠通訊了(一臺主機經過arp協議獲取另一臺主機的mac地址)
        ethernet採用最原始的方式,廣播的方式進行通訊,即計算機通訊基本靠吼

    ## 以太網協議基於MAC地址的廣播,只能在局域網吼。##
"""
數據鏈路層詳解

  五、物理層:主要是基於電器特性發送高低電壓(電信號),高電壓對應數字1,低電壓對應數字0

4、OSI七層模型

  OSI/RM模型(Open System Interconnection / Reference Model)的設計目的是成爲一個全部計算機廠商都能實現的開放網絡模型,來克服使用衆多私有網絡模型所帶來的困難和低效性。
  一、應用層   二、表示層   三、會話層
  四、傳輸層
  五、網絡層
  六、數據鏈路層
  七、物理層

5、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是同一臺機器上不一樣進程或者線程的標識。

5、套接字發展和分類

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

# socket實例化方式
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
# proto=0 可忽略,特殊用途
# fileno=None 可忽略,特殊用途

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

  套接字家族名:AF_UNIX

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

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

  套接字家族名:AF_INET

  全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET

三、socket type類型

socket.SOCK_STREAM   #for tcp
socket.SOCK_DGRAM    #for udp
socket.SOCK_RAW      #原始套接字,普通的套接字沒法處理ICMP、IGMP等網絡報文,而SOCK_RAW能夠;其次,SOCK_RAW也能夠處理特殊的IPv4報文;此外,利用原始套接字,能夠經過IP_HDRINCL套接字選項由用戶構造IP頭。
socket.SOCK_RDM      #是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在須要執行某些特殊操做時使用,如發送ICMP報文。SOCK_RAM一般僅限於高級用戶或管理員運行的程序使用。
socket.SOCK_SEQPACKET   #廢棄了

6、套接字工做流程

  套接字工做流程圖以下:

  

 

  流程解析:服務器端先初始化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)

二、服務端套接字函數

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()        建立一個與該套接字相關的文件
1:用打電話的流程快速描述socket通訊
2:服務端和客戶端加上基於一次連接的循環通訊
3:客戶端發送空,卡主,證實是從哪一個位置卡的
服務端:
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)

conn,addr=phone.accept()
while True:
    data=conn.recv(1024)
    print('server===>')
    print(data)
    conn.send(data.upper())
conn.close()
phone.close()
客戶端:
from socket import *

phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))

while True:
    msg=input('>>: ').strip()
    phone.send(msg.encode('utf-8'))
    print('client====>')
    data=phone.recv(1024)
    print(data)

說明卡的緣由:緩衝區爲空recv就卡住,引出原理圖



4.演示客戶端斷開連接,服務端的狀況,提供解決方法

5.演示服務端不能重複接受連接,而服務器都是正常運行不斷來接受客戶連接的

6:簡單演示udp
服務端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
phone.bind(('127.0.0.1',8082))
while True:
    msg,addr=phone.recvfrom(1024)
    phone.sendto(msg.upper(),addr)
客戶端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
while True:
    msg=input('>>: ')
    phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082))
    msg,addr=phone.recvfrom(1024)
    print(msg)

udp客戶端能夠併發演示
udp客戶端能夠輸入爲空演示,說出recvfrom與recv的區別,暫且不提tcp流和udp報的概念,留到粘包去說
socket實驗推演流程

 

7、基於TCP的套接字 

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

服務端:

import socket

# 一、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字
# print(phone)
"""
<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)>
"""

# 二、綁定手機卡
phone.bind(('127.0.0.1', 9001))  # 本地迴環地址,  端口0-65535(0-1024歸系統使用)

# 三、開機
phone.listen(5)  # 最大掛起的連接數

# 四、等電話連接
print('starting...')
# res = phone.accept()  # 程序卡在這一步
# print(res)  # res包含連接對象
"""
starting...
===》
(<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 55868)>, ('127.0.0.1', 55868))
"""
# 改寫分拆兩個元組元素
conn, client_addr = phone.accept()
print(conn)
print(client_addr)
print('got a new connection from %s' % (client_addr, ))
"""
<socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 55925)>
('127.0.0.1', 65197)
got a new connection from ('127.0.0.1', 65197)
"""

# 五、收發消息
data = conn.recv(1024)  # 一、單位是bytes 二、1024表明接收數據的最大數是1024個bytes
print('客戶端數據', data)

conn.send(data.upper())  # 數據修改成大寫後發送

# 六、掛電話
conn.close()

# 七、關機
phone.close()
"""
客戶端數據 b'hello'
"""

客戶端:

import socket
# 一、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字
#print(phone)

# 二、撥號
phone.connect(('127.0.0.1', 8080))  # 服務端先啓動後啓動客戶端

# 三、發、收消息
phone.send('hello'.encode('utf-8'))
data = phone.recv(1024)
print(data)

# 四、關閉
phone.close()
"""
b'HELLO'
"""

  上面是以打電話爲例實現了一個low版的套接字通訊,可是若是須要屢次發送,須要加上通訊循環。

import socket
# 一、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字
# 二、綁定手機卡
phone.bind(('127.0.0.1', 8080))  # 本地迴環地址,  端口0-65535(0-1024歸系統使用)
# 三、開機
phone.listen(5)  # 最大掛起的連接數(TCP協議須要)
# 四、等電話連接
print('starting...')
conn, client_addr = phone.accept()  # accept是創建連接
print(client_addr)  # 客戶端一啓動打印 ('127.0.0.1',56641)

# 五、收發消息
while True:  # 通訊循環
    data = conn.recv(1024)  # 一、單位是bytes 二、1024表明接收數據的最大數是1024個bytes
    print('客戶端數據', data)

    conn.send(data.upper())  # 數據修改成大寫後發送
# 六、掛電話
conn.close()
# 七、關機
phone.close()
"""
starting...
('127.0.0.1', 56641)
客戶端數據 b'hello'
客戶端數據 b'xiugeng'
"""
服務端-添加通信循環
import socket
# 一、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字
#print(phone)

# 二、撥號
phone.connect(('127.0.0.1', 8080))  # 服務端先啓動後啓動客戶端

# 三、發、收消息
while True:  # 通訊循環
    msg = input('>>:').strip()
    phone.send(msg.encode('utf-8'))
    data = phone.recv(1024)  # 收到服務端大寫以後的字符
    print(data)

# 四、關閉
phone.close()
"""
>>:hello
b'HELLO'
>>:xiugeng
b'XIUGENG'
"""
客戶端-添加通訊循環

  解決重啓服務端時,服務端仍存在四次揮手的time_wait狀態佔用地址的狀況(深刻研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高併發狀況下會有大量的time_wait狀態的優化方法),沒法重啓服務端,修改代碼以下:

import socket

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

# 解決重啓服務端時,服務端仍存在四次揮手的time_wait狀態佔用地址的狀況:
# 方法一:加入一條socket配置,重用ip和端口
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  # 在bind前,啓動關閉服務端時,系統端口沒有回收,能夠重用端口
# 方法二:調整linux內核參數
"""
在 /etc/sysctl.conf文件中添加:
net.ipv4.tcp_syncookies = 1  # 表示開啓SYN Cookies,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉
net.ipv4.tcp_tw_reuse = 1   # 表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉
net.ipv4.tcp_tw_recycle = 1  #表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉
net.ipv4.tcp_fin_timeout = 30   # 修改系統默認的TIMEOUT時間

執行 /sbin/sysctl -p讓參數生效
"""

phone.bind(('127.0.0.1', 8080))
phone.listen(5)  # 最大掛起的連接數
print('starting...')
conn, client_addr = phone.accept()
print(client_addr)

while True:  # 通訊循環
    try:
        data = conn.recv(1024)  # 客戶端發出空消息,服務端沒有收到
        if not data:break #  # 適用與Linux操做系統
        print('客戶端數據', data)

        conn.send(data.upper())
    except ConnectionResetError: # 適用於windiws的操做系統
        break

conn.close()
phone.close()
服務端-bug修復

 

import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字

phone.connect(('127.0.0.1', 8080))  # 服務端先啓動後啓動客戶端

while True:  # 通訊循環
    msg = input('>>:').strip()  # msg=''
    # 修改辦法
    if not msg:continue  # 輸入空,不發送消息
    phone.send(msg.encode('utf-8'))  # phone.send(b'')
    # print('has send') # 定位不能發空的緣故
    data = phone.recv(1024)  # 服務端沒有回消息,所以收不到消息
    print(data.decode('utf-8'))

phone.close()
客戶端-bug修復
net.ipv4.tcp_syncookies = 1 表示開啓SYN Cookies。當出現SYN等待隊列溢出時,啓用cookies來處理,可防範少許SYN攻擊,默認爲0,表示關閉;
net.ipv4.tcp_tw_reuse = 1 表示開啓重用。容許將TIME-WAIT sockets從新用於新的TCP鏈接,默認爲0,表示關閉;
net.ipv4.tcp_tw_recycle = 1 表示開啓TCP鏈接中TIME-WAIT sockets的快速回收,默認爲0,表示關閉。
net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間

  添加連接循環,當上一個連接結束後,等待下一個客戶端接入。

import socket

"""
須要服務端一直提供服務,
"""
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 8080))
phone.listen(5)  # 最大掛起的連接數

print('starting...')
"""
須要服務端一直提供服務,再添加一個循環。服務端的工做主要是:一、創建連接;二、通訊
須要注意的是:服務端能夠一直提供服務,可是不能併發。只有等前一個連接斷了後,下一個客戶端才能連接
"""
while True:  # 連接循環
    # 建立連接
    conn, client_addr = phone.accept()
    print(client_addr)

    while True:  # 通訊循環
        try:
            data = conn.recv(1024)
            if not data:break
            print('客戶端數據', data)

            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

phone.close()
服務端-添加連接循環
import socket

phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於網絡通信的,基於TCP協議的一個套接字

phone.connect(('127.0.0.1', 8080))  # 服務端先啓動後啓動客戶端

while True:  # 通訊循環
    msg = input('>>:').strip()  # msg=''
    # 修改辦法
    if not msg:continue  # 輸入空,不發送消息
    phone.send(msg.encode('utf-8'))  # phone.send(b'')
    # print('has send') # 定位不能發空的緣故
    data = phone.recv(1024)  # 服務端沒有回消息,所以收不到消息
    print(data.decode('utf-8'))

phone.close()
客戶端-添加連接循環

8、subprocess模塊 

  以前學過os模塊來執行系統命令:

import os

cmd = os.popen('df -h')
cmd = cmd.read()
print(cmd)

  subprocess命令執行的結果就是bytes,無需轉變格式就能夠給客戶端、服務端使用。

 

import subprocess
obj = subprocess.Popen('dxxxs', shell=True,
                 stdout=subprocess.PIPE,  # stdout:正確結果;管道
                 stderr=subprocess.PIPE)   # stderr:錯誤結果;管道

print(obj)
print('stdout 1---->: ', obj.stdout.read().decode('utf-8'))  # 正確管道內容

print('stderr 1---->: ', obj.stderr.read().decode('utf-8'))  # 錯誤管道內容

"""
<subprocess.Popen object at 0x10401ada0>
stdout 1---->:  
stderr 1---->:  /bin/sh: dxxxs: command not found
"""

 

9、基於UDP協議的套接字

  udp不須要通過3次握手和4次揮手,不須要提早創建連接,直接發送數據便可。

服務端:

# import socket
from socket import *   # 儘可能少用這種導入方式,會將全部名字加入名稱空間,容易致使重複
# server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)   # 能夠看到須要引用的socket的模塊很是多。改用from socket import *導入
server = socket(AF_INET, SOCK_DGRAM)  # SOCK_STREAM指的流式協議,SOCK_DGRAM指得是數據報協議(但凡發數據,就已是完整的數據報)
server.bind(('127.0.0.1', 8080))

# server.listen(5)  # 掛起的連接數,TCP協議須要,UDP不須要

# while True:
#     conn, addr = server.accept()  # 用來創建連接,UDP不須要

while True:
    data, client_addr = server.recvfrom(1024)   # 收消息
    print(data)

    server.sendto(data.upper(), client_addr)    # 發消息,取收消息的地址
server.close()

客戶端:

from socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>: ').strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))  # 發消息

    data, server_addr = client.recvfrom(1024)   # 收消息
    print(data, server_addr)

client.close()

  以qq聊天爲例:

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)
服務端
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    '狗哥alex':('127.0.0.1',8081),
    '瞎驢':('127.0.0.1',8081),
    '一棵樹':('127.0.0.1',8081),
    '武大郎':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送: ').strip()
        if msg == 'quit':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()
udp客戶端1
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    '狗哥alex':('127.0.0.1',8081),
    '瞎驢':('127.0.0.1',8081),
    '一棵樹':('127.0.0.1',8081),
    '武大郎':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送: ').strip()
        if msg == 'quit':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()
udp客戶端2

  時間服務器

from socket import *
from time import strftime

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

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

while True:
    msg,addr=tcp_server.recvfrom(bufsize)
    print('===>',msg)
    
    if not msg:
        time_fmt='%Y-%m-%d %X'
    else:
        time_fmt=msg.decode('utf-8')
    back_msg=strftime(time_fmt)

    tcp_server.sendto(back_msg.encode('utf-8'),addr)

tcp_server.close()
ntp服務端
from socket import *
ip_port=('127.0.0.1',9000)
bufsize=1024

tcp_client=socket(AF_INET,SOCK_DGRAM)



while True:
    msg=input('請輸入時間格式(例%Y %m %d)>>: ').strip()
    tcp_client.sendto(msg.encode('utf-8'),ip_port)

    data=tcp_client.recv(bufsize)

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

tcp_client.close()
ntp客戶端

10、SSH遠程模擬及粘包現象

  模擬ssh遠程執行命令:

注意:

res=subprocess.Popen(cmd.decode('utf-8'),
           shell=True,
           stderr=subprocess.PIPE,
           stdout=subprocess.PIPE)

的結果的編碼是以當前所在的系統爲準的,若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼

且只能從管道里讀一次結果

注意:命令ls -l ; lllllll ; pwd 的結果是既有正確stdout結果,又有錯誤stderr結果

服務端-ssh
客戶端-ssh

  基於tcp的socket,ssh遠程運行時,會發生粘包的問題。致使下次執行命令時,會輸出上次服務器回傳的沒執行完的內容。

  再基於udp製做一個遠程執行命令的程序:

import socket
import subprocess

ip_port = ('127.0.0.1', 9001)  # 元組
bufsize = 1024

udp_server = socket.socket(socket.AF_INET, socket.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)
    stdout = res.stdout.read()
    stderr = res.stderr.read()

    # 發消息
    udp_server.sendto(stderr, addr)
    udp_server.sendto(stdout, addr)

udp_server.close()
服務端-UDP_ssh
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    msg = input('>>: ').strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))  # 發消息

    data, server_addr = client.recvfrom(1024)   # 收消息
    print(data, server_addr)

client.close()
客戶端-UDP_ssh

11、粘包 

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

  此外,發送方引發的粘包是由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時纔會清除緩衝區內容。數據是可靠的,可是會粘包。

兩種狀況下會發生粘包

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

import time
import socket

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

client.connect(("127.0.0.1", 9002))

# 使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。
client.send('hello'.encode('utf-8'))
client.send('world'.encode('utf-8'))
客戶端-連續發送消息
import time
import socket
import subprocess

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

server.bind(("127.0.0.1", 9002))  # bind()內爲元組,0-65535:0-1024供操做系統使用
server.listen(5)

conn, addr = server.accept()

res1 = conn.recv(1024)   # 第一次收
print('第一次', res1)
res2 = conn.recv(1024)   # 第二次收
print('第二次', res2)
"""
第一次 b'helloworld'
第二次 b''
"""
# 發生了粘包現象
服務端-收消息

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

import time
import socket

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

client.connect(("127.0.0.1", 9002))

# 使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。
client.send('hello'.encode('utf-8'))
# time.sleep(1)  # 休息一秒看是否還粘包
time.sleep(5)
client.send('world'.encode('utf-8'))
客戶端-間隔五秒發消息
import time
import socket
import subprocess

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

server.bind(("127.0.0.1", 9002))  # bind()內爲元組,0-65535:0-1024供操做系統使用
server.listen(5)

conn, addr = server.accept()
# 客戶端間隔五秒發送,服務端第一次僅接收一個字符,服務端發生粘包
res1 = conn.recv(1)   # 第一次收
print('第一次', res1)
res2 = conn.recv(1024)   # 第二次收
print('第二次', res2)
"""
第一次 b'h'
第二次 b'ello'
"""
服務端-收消息發生粘包

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

  tcp的數據傳輸請參考文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的

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

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

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

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

  sendall就會循環調用send,數據不會丟失

12、先提示長度後發送消息解決粘包

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

import time
import socket
import struct
import json

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

client.connect(("127.0.0.1", 9003))

while True:
    """一、發命令"""
    cmd = input('>>: ').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
    """二、拿結果"""
    # 第一步:先收報頭長度
    obj= client.recv(4)  # 接收四個bytes
    header_size = struct.unpack('i', obj)[0]
    # 第二步:再收報頭
    header_bytes = client.recv(header_size)

    # 第三步:從報頭中解析出對真實數據的描述信息(數據長度)
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['total_size']

    # 第四步:接收真實的數據
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = client.recv(1024)    # 最大不能超過操做系統緩存大小,一次收不完,屢次收
        recv_data += res
        recv_size += len(res)  # 最後一次收的時候將不是1024,將來若是要查看進度的時候有問題

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

client.close()
客戶端-SHH添加報頭

 

# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import subprocess
import struct
import json

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

server.bind(("127.0.0.1", 9003))  # bind()內爲元組,0-65535:0-1024供操做系統使用
server.listen(5)

print('starting...')
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    while True:  # 通信循環
        try:
            """一、收命令"""
            cmd = conn.recv(8096)
            if not cmd:break
            """二、拿到結果"""
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            """三、命令結果返回客戶端"""
            # 第一步:製做固定長度的報頭
            header_dic = {
                'filename': 'a.txt',
                'md5': 'xxdxxx',
                'total_size': len(stdout) + len(stderr)
            }
            header_json = json.dumps(header_dic)  # dump和dumps的區別
            header_bytes = header_json.encode('utf-8')

            # 第二步:先發送報頭的長度
            conn.send(struct.pack('i', len(header_bytes)))
            # 第三步:將報頭髮給客戶端
            conn.send(header_bytes)
            # conn.send(str(total_size).encode('utf-8'))  # 整型轉換爲字符串,編碼後發出

            # 第四步:發送真實數據
            # conn.send(stdout+stderr)  # 因爲粘包原理可優化改寫以下
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break
    conn.close()

server.close()
服務端-SHH添加報頭

  這種方法雖然解決了粘包的問題,但實際上是比較差的解決方法。

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

十3、加報頭來解決粘包問題

  爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據。

struct模塊

  該模塊的功能就是把一個類型,例如數據,轉化爲固定長度的bytes,以下所示:

import struct

# 格式:struct.pack(fmt, *args)
# 設置格式後,把數字轉成固定長度的bytes類型
# fmt='i'指的是整型數字
res = struct.pack('i', 1280)    # 打包
print(res, type(res), len(res))
"""
b'\x00\x05\x00\x00' <class 'bytes'> 4
"""

res1 = struct.pack('i', 1380)
print(res1, type(res1), len(res1))
"""
b'd\x05\x00\x00' <class 'bytes'> 4
"""

obj = struct.unpack('i', res)   # 解包
print(obj)  # obj是元組,第一個值就是打包的值
print(obj[0])
"""
(1280,)
1280
"""
struct.pack的使用
# res_i = struct.pack('i', 123000000000)
# print(res_i, len(res_i))
"""
struct.error: 'i' format requires -2147483648 <= number <= 2147483647
"""
res_l = struct.pack('l', 123000000000)  # l模式下,數字8個bytes,範圍更大。
print(res_l, len(res_l))
"""
b'\x00\x0e_\xa3\x1c\x00\x00\x00' 8
"""
整型模式和長整型模式
header_dic = {
    'filename': 'a.txt',
    'md5': 'xxdxxx',
    'total_size': 7678685767586786686876868687565756576655657565675657657657656757657565756757557575
}
header_json = json.dumps(header_dic)
# print(type(header_json))  # <class 'str'>
header_bytes = header_json.encode('utf-8')
#print(type(header_bytes))   # <class 'bytes'>

struct.pack('i', len(header_bytes))  # 把報頭的長度打成固定長度
運用json處理特別長的數據

  

  使用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)

  如今運用struct模塊,能夠將ssh程序進一步優化:

# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import subprocess
import struct

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

server.bind(("127.0.0.1", 9001))  # bind()內爲元組,0-65535:0-1024供操做系統使用
server.listen(5)

print('starting...')
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    while True:  # 通信循環
        try:
            """一、收到客戶端的命令"""
            cmd = conn.recv(8096)
            if not cmd:break
            """二、執行命令,拿到結果"""
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()
            """三、命令結果返回客戶端"""
            # 第一步:製做固定長度的報頭
            total_size = len(stdout) + len(stderr)  # 整型
            header = struct.pack('i', total_size)  # 隱患:struct的i 的取值區間是-2147483648~2147483648

            # 第二步:將報頭髮給客戶端
            conn.send(header)
            # conn.send(str(total_size).encode('utf-8'))  # 整型轉換爲字符串,編碼後發出

            # 第三步:發送真實數據
            # conn.send(stdout+stderr)  # 因爲粘包原理可優化改寫以下
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError:
            break
    conn.close()

server.close()
服務端-struct_ssh
# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import struct

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

client.connect(("127.0.0.1", 9001))

while True:
    """一、發命令"""
    cmd = input('>>: ').strip()
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
    """二、拿結果"""
    # 第一步:拿到數據的長度——>即先收報頭
    header = client.recv(4)

    # 第二步:從報頭中解析出對真實數據的描述信息(數據長度)
    total_size = struct.unpack('i', header)[0]  # 解包以後爲元組,取第一項即打包的內容

    # 第三步:接收真實的數據
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = client.recv(1024)    # 最大不能超過操做系統緩存大小,一次收不完,屢次收
        recv_data += res
        recv_size += len(res)  # 最後一次收的時候將不是1024,將來若是要查看進度的時候有問題

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

client.close()
客戶端-struct_ssh

十4、經過socket來實現文件傳輸 

普通版本:

# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import subprocess
import struct
import os
import json

share_dir = "/Users/hqs/PycharmProjects/startMyPython3.0/第六章-網絡編程/8_文件傳輸/server/share"
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(("127.0.0.1", 9001))  # bind()內爲元組,0-65535:0-1024供操做系統使用
server.listen(5)

print('starting...')
while True:
    conn, client_addr = server.accept()
    print(client_addr)

    while True:  # 通信循環
        try:
            """一、收到客戶端的命令"""
            res = conn.recv(8096)  # b'get a.txt'
            if not res:break
            """二、解析命令,提取相應命令參數"""
            cmds = res.decode('utf-8').split()  # ['get', 'a.txt']
            filename = cmds[1]
            """三、以讀的方式打開文件,讀取文件內容發送給客戶端"""
            # with open(filename, 'rb') as f:  直接讀文件傳輸會有粘包問題
            # 第一步:製做固定長度的報頭
            header_dic = {
                'filename': filename,   # ''
                'md5': 'xxdxxx',
                'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))  # 文件大小
            }
            header_json = json.dumps(header_dic)
            header_bytes = header_json.encode('utf-8')

            # 第二步:發送報頭的長度
            conn.send(struct.pack('i', len(header_bytes)))

            # 第三步:再發報頭
            conn.send(header_bytes)

            # 第四步:發送真實數據
            with open('%s/%s'% (share_dir, filename), 'rb') as f:
                # conn.send(f.read())   # 一下全讀取,文件有幾個T時會有問題
                for line in f:
                    conn.send(line)   # 一行行發都會粘在一塊兒,且節省內存

        except ConnectionResetError:
            break
    conn.close()

server.close()
服務端
# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import struct
import json

download_dir = '/Users/hqs/PycharmProjects/startMyPython3.0/第六章-網絡編程/8_文件傳輸/client/download'
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(("127.0.0.1", 9001))

while True:
    """一、發命令"""
    cmd = input('>>: ').strip()   # get a.txt軟件自定義命名標準
    if not cmd:continue
    client.send(cmd.encode('utf-8'))
    """二、以寫的方式打開新文件,接收服務端發來的文件內容寫入新文件中"""
    # 第一步:先收報頭長度
    header = client.recv(4)
    header_size = struct.unpack('i', header)[0]

    # 第二步:收取報頭
    header_bytes = client.recv(header_size)

    # 第三步:從報頭中解析出對真實數據的描述信息(數據長度)
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']

    # 第四步:接收真實的數據
    with open('%s/%s' % (download_dir, filename), 'wb') as f:  # 在同一臺機器上,相同的路徑會致使文件清空,須要指定目錄
        recv_size = 0
        # recv_data = b''   拼接字符串不須要了
        while recv_size < total_size:
            line = client.recv(1024)    # 最大不能超過操做系統緩存大小,一次收不完,屢次收
            f.write(line)   # 一邊收一邊寫
            recv_size += len(line)  # 最後一次收的時候將不是1024,將來若是要查看進度的時候有問題
            print('總大小: %s 已下載:%s' % (total_size, recv_size))
client.close()
客戶端

函數版本:

# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import subprocess
import struct
import os
import json

share_dir = "/Users/hqs/PycharmProjects/startMyPython3.0/第六章-網絡編程/9_文件傳輸函數優化版本/server/share"


def get(conn, cmds):
    filename = cmds[1]

    """三、以讀的方式打開文件,讀取文件內容發送給客戶端"""
    # with open(filename, 'rb') as f:  直接讀文件傳輸會有粘包問題
    # 第一步:製做固定長度的報頭
    header_dic = {
        'filename': filename,  # ''
        'md5': 'xxdxxx',
        'file_size': os.path.getsize(r'%s/%s' % (share_dir, filename))  # 文件大小
    }
    header_json = json.dumps(header_dic)
    header_bytes = header_json.encode('utf-8')

    # 第二步:發送報頭的長度
    conn.send(struct.pack('i', len(header_bytes)))

    # 第三步:再發報頭
    conn.send(header_bytes)

    # 第四步:發送真實數據
    with open('%s/%s' % (share_dir, filename), 'rb') as f:
        # conn.send(f.read())   # 一下全讀取,文件有幾個T時會有問題
        for line in f:
            conn.send(line)  # 一行行發都會粘在一塊兒,且節省內存


def put(conn, cmds):...


def run():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server.bind(("127.0.0.1", 9001))  # bind()內爲元組,0-65535:0-1024供操做系統使用
    server.listen(5)

    print('starting...')
    while True:
        conn, client_addr = server.accept()
        print(client_addr)

        while True:  # 通信循環
            try:
                """一、收到客戶端的命令"""
                res = conn.recv(8096)  # b'get a.txt'
                if not res:break
                """二、解析命令,提取相應命令參數"""
                cmds = res.decode('utf-8').split()  # ['get', 'a.txt']
                if cmds[0] == 'get':
                    get(conn, cmds)
                elif cmds[0] == 'put':
                    put(conn, cmds)


            except ConnectionResetError:
                break
        conn.close()

    server.close()


if __name__ == '__main__':
    run()
服務端
# -*- coding:utf-8 -*-
__author__ = 'Qiushi Huang'

import time
import socket
import struct
import json

download_dir = '/Users/hqs/PycharmProjects/startMyPython3.0/第六章-網絡編程/9_文件傳輸函數優化版本/client/download'


def get(client, cmds):
    """二、以寫的方式打開新文件,接收服務端發來的文件內容寫入新文件中"""
    # 第一步:先收報頭長度
    header = client.recv(4)
    header_size = struct.unpack('i', header)[0]

    # 第二步:收取報頭
    header_bytes = client.recv(header_size)

    # 第三步:從報頭中解析出對真實數據的描述信息(數據長度)
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']

    # 第四步:接收真實的數據
    with open('%s/%s' % (download_dir, filename), 'wb') as f:  # 在同一臺機器上,相同的路徑會致使文件清空,須要指定目錄
        recv_size = 0
        # recv_data = b''   拼接字符串不須要了
        while recv_size < total_size:
            line = client.recv(1024)  # 最大不能超過操做系統緩存大小,一次收不完,屢次收
            f.write(line)  # 一邊收一邊寫
            recv_size += len(line)  # 最後一次收的時候將不是1024,將來若是要查看進度的時候有問題
            print('總大小: %s 已下載:%s' % (total_size, recv_size))


def put(client, cmds):...


def run():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    client.connect(("127.0.0.1", 9001))

    while True:
        """一、發命令"""
        inp = input('>>: ').strip()   # get a.txt軟件自定義命名標準
        if not inp:continue
        client.send(inp.encode('utf-8'))

        cmds = inp.split()  # ['get', 'a.txt']
        if cmds[0] == 'get':
            get(client, cmds)
        elif cmds[0] == 'put':
            put(client, cmds)

    client.close()


if __name__ == '__main__':
    run()
客戶端

面向對象版本:

import socket
import struct
import json
import subprocess
import os

class MYTCPServer:
    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

    server_dir='file_upload'

    def __init__(self, server_address, bind_and_activate=True):
        """Constructor.  May be extended, do not override."""
        self.server_address=server_address
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if bind_and_activate:
            try:
                self.server_bind()
                self.server_activate()
            except:
                self.server_close()
                raise

    def server_bind(self):
        """Called by constructor to bind the socket.
        """
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
        self.server_address = self.socket.getsockname()

    def server_activate(self):
        """Called by constructor to activate the server.
        """
        self.socket.listen(self.request_queue_size)

    def server_close(self):
        """Called to clean-up the server.
        """
        self.socket.close()

    def get_request(self):
        """Get the request and client address from the socket.
        """
        return self.socket.accept()

    def close_request(self, request):
        """Called to clean up an individual request."""
        request.close()

    def run(self):
        while True:
            self.conn,self.client_addr=self.get_request()
            print('from client ',self.client_addr)
            while True:
                try:
                    head_struct = self.conn.recv(4)
                    if not head_struct:break

                    head_len = struct.unpack('i', head_struct)[0]
                    head_json = self.conn.recv(head_len).decode(self.coding)
                    head_dic = json.loads(head_json)

                    print(head_dic)
                    #head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
                    cmd=head_dic['cmd']
                    if hasattr(self,cmd):
                        func=getattr(self,cmd)
                        func(head_dic)
                except Exception:
                    break

    def put(self,args):
        file_path=os.path.normpath(os.path.join(
            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.conn.recv(self.max_packet_size)
                f.write(recv_data)
                recv_size+=len(recv_data)
                print('recvsize:%s filesize:%s' %(recv_size,filesize))


tcpserver1=MYTCPServer(('127.0.0.1',8080))

tcpserver1.run()
服務端
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()
客戶端
相關文章
相關標籤/搜索