Socket編程複習

Socket又稱"套接字",應用程序通常通過"套接字"向網絡發出請求或者應答網絡請求,使主機間或者一臺計算機上的進程間可以通訊。


socket()函數

Python 中,我們用 socket()函數來創建套接字,語法格式如下:

socket.socket([family[, type[, proto]]])

參數

  • family: 套接字家族可以使AF_UNIX或者AF_INET
  • type: 套接字類型可以根據是面向連接的還是非連接分爲SOCK_STREAMSOCK_DGRAM
  • protocol: 一般不填默認爲0.

Socket 對象(內建)方法

函數 描述
服務器端套接字
s.bind() 綁定地址(host,port)到套接字, 在AF_INET下,以元組(host,port)的形式表示地址。
s.listen() 開始TCP監聽。backlog指定在拒絕連接之前,操作系統可以掛起的最大連接數量。該值至少爲1,大部分應用程序設爲5就可以了。
s.accept() 被動接受TCP客戶端連接,(阻塞式)等待連接的到來
客戶端套接字
s.connect() 主動初始化TCP服務器連接,。一般address的格式爲元組(hostname,port),如果連接出錯,返回socket.error錯誤。
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
s.recv() 接收TCP數據,數據以字符串形式返回,bufsize指定要接收的最大數據量。flag提供有關消息的其他信息,通常可以忽略。
s.send() 發送TCP數據,將string中的數據發送到連接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。
s.sendall() 完整發送TCP數據,完整發送TCP數據。將string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None,失敗則拋出異常。
s.recvfrom() 接收UDP數據,與recv()類似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。
s.sendto() 發送UDP數據,將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。
s.close() 關閉套接字
s.getpeername() 返回連接套接字的遠程地址。返回值通常是元組(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一個元組(ipaddr,port)
s.setsockopt(level,optname,value) 設置給定套接字選項的值。
s.getsockopt(level,optname[.buflen]) 返回套接字選項的值。
s.settimeout(timeout) 設置套接字操作的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因爲它們可能用於連接的操作(如connect())
s.gettimeout() 返回當前超時期的值,單位是秒,如果沒有設置超時期,則返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag爲0,則將套接字設爲非阻塞模式,否則將套接字設爲阻塞模式(默認值)。非阻塞模式下,如果調用recv()沒有發現任何數據,或send()調用無法立即發送數據,那麼將引起socket.error異常。
s.makefile() 創建一個與該套接字相關連的文件

簡單實例

服務端

我們使用 socket 模塊的 socket 函數來創建一個 socket 對象。socket 對象可以通過調用其他函數來設置一個 socket 服務。

現在我們可以通過調用 bind(hostname, port) 函數來指定服務的 port(端口)

接着,我們調用 socket 對象的 accept 方法。該方法等待客戶端的連接,並返回 connection 對象,表示已連接到客戶端。

完整代碼如下:

#!/usr/bin/python3
# 文件名:server.py

# 導入 socket、sys 模塊
import socket
import sys

# 創建 socket 對象
serversocket = socket.socket(
            socket.AF_INET, socket.SOCK_STREAM) 

# 獲取本地主機名
host = socket.gethostname()

port = 9999

# 綁定端口號
serversocket.bind((host, port))

# 設置最大連接數,超過後排隊
serversocket.listen(5)

while True:
    # 建立客戶端連接
    clientsocket,addr = serversocket.accept()      

    print("連接地址: %s" % str(addr))
    
    msg='歡迎訪問菜鳥教程!'+ "\r\n"
    clientsocket.send(msg.encode('utf-8'))
    clientsocket.close()

客戶端

接下來我們寫一個簡單的客戶端實例連接到以上創建的服務。端口號爲 9999。

socket.connect(hosname, port ) 方法打開一個 TCP 連接到主機爲 hostname 端口爲 port 的服務商。連接後我們就可以從服務端獲取數據,記住,操作完成後需要關閉連接。

完整代碼如下:

#!/usr/bin/python3
# 文件名:client.py

# 導入 socket、sys 模塊
import socket
import sys

# 創建 socket 對象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 

# 獲取本地主機名
host = socket.gethostname() 

# 設置端口號
port = 9999

# 連接服務,指定主機和端口
s.connect((host, port))

# 接收小於 1024 字節的數據
msg = s.recv(1024)

s.close()

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

現在我們打開兩個終端,第一個終端執行 server.py 文件:

$ python3 server.py

第二個終端執行 client.py 文件:

$ python3 client.py 
歡迎訪問菜鳥教程!

這時我們再打開第一個終端,就會看到有以下信息輸出:

連接地址: ('192.168.0.118', 33397)

Python Internet 模塊

以下列出了 Python 網絡編程的一些重要模塊:

協議 功能用處 端口號 Python 模塊
HTTP 網頁訪問 80 httplib, urllib, xmlrpclib
NNTP 閱讀和張貼新聞文章,俗稱爲"帖子" 119 nntplib
FTP 文件傳輸 20 ftplib, urllib
SMTP 發送郵件 25 smtplib
POP3 接收郵件 110 poplib
IMAP4 獲取郵件 143 imaplib
Telnet 命令行 23 telnetlib
Gopher 信息查找 70 gopherlib, urllib

以下內容轉自 cnblogs 的文章,寫得特別詳細,供參考:

https://www.cnblogs.com/zhangyingai/p/7097922.html

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

 

二、套接字發展史及分類

  套接字起源於 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函數使用

  • 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()        #創建一個與該套接字相關的文件

 

打電話的流程演示

服務端.py

複製代碼

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
phone.bind(('127.0.0.1',8080)) #插電話卡

phone.listen(5) #開機,backlog

print('starting....')
conn,addr=phone.accept() #接電話
print(conn)
print('client addr',addr)
print('ready to read msg')
client_msg=conn.recv(1024) #收消息
print('client msg: %s' %client_msg)
conn.send(client_msg.upper()) #發消息

conn.close()
phone.close()

複製代碼

 

客戶端.py

複製代碼

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080)) #撥通電話

phone.send('hello'.encode('utf-8')) #發消息

back_msg=phone.recv(1024)
print(back_msg)

phone.close()

複製代碼

 

輸出

服務端:

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', 65142)>
client addr ('127.0.0.1', 65142)
ready to read msg
client msg: b'hello'

客戶端

b'HELLO'

 

五、基於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('nitouxiang nb'.encode('utf-8'))         #發消息,說話(只能發送字節類型)

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

s.close()                                       #掛電話

複製代碼

 

輸出

服務端

接到來自127.0.0.1的電話
b'nitouxiang nb' <class 'bytes'>

客戶端

NITOUXIANG NB

 

上述流程的問題是,服務端只能接受一次鏈接,然後就徹底關閉掉了,實際情況應該是,服務端不斷接受鏈接,然後循環通信,通信完畢後只關閉鏈接,服務器能夠繼續接收下一次鏈接,下面是修改版

 服務端

複製代碼

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('接到來自%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()               #手機關機

複製代碼

客戶端

複製代碼

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()                                       #掛電話

複製代碼

 

補充:

在重啓服務端時可能會遇到

這個是由於你的服務端仍然存在四次揮手的time_wait狀態在佔用地址(如果不懂,請深入研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高併發情況下會有大量的time_wait狀態的優化方法)

解決辦法

方法一

#加入一條socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))

方法二

複製代碼

發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決,
vi /etc/sysctl.conf

編輯文件,加入以下內容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然後執行 /sbin/sysctl -p 讓參數生效。
 
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 時間

複製代碼

 

六、基於UDP的套接字

  • udp服務端
ss = socket()   #創建一個服務器的套接字
ss.bind()       #綁定服務器套接字
inf_loop:       #服務器無限循環
    cs = ss.recvfrom()/ss.sendto() # 對話(接收與發送)
ss.close()                         # 關閉服務器套接字
  • udp客戶端
cs = socket()   # 創建客戶套接字
comm_loop:      # 通訊循環
    cs.sendto()/cs.recvfrom()   # 對話(發送/接收)
cs.close()                      # 關閉客戶套接字

示例

服務端

複製代碼

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

udp_server_client.bind(ip_port)

while True:
    msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(msg,addr)

    udp_server_client.sendto(msg.upper(),addr)

複製代碼

客戶端

複製代碼

import socket
ip_port=('127.0.0.1',9000)
BUFSIZE=1024
udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

while True:
    msg=input('>>: ').strip()
    if not msg:continue

    udp_server_client.sendto(msg.encode('utf-8'),ip_port)

    back_msg,addr=udp_server_client.recvfrom(BUFSIZE)
    print(back_msg.decode('utf-8'),addr)

複製代碼

輸出

客戶端

>>: 123
123 ('127.0.0.1', 9000)
>>: 3
3 ('127.0.0.1', 9000)
>>: 4
4 ('127.0.0.1', 9000)

服務端

b'123' ('127.0.0.1', 53066)
b'3' ('127.0.0.1', 53066)
b'4' ('127.0.0.1', 53066)

 

模擬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)

複製代碼

客戶端1

複製代碼

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

qq_name_dic={
    'TOM':('127.0.0.1',8081),
    'JACK':('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()

複製代碼

客戶端2

複製代碼

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

qq_name_dic={
    'TOM':('127.0.0.1',8081),
    'JACK':('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()

複製代碼

 

輸出

客戶端1

請選擇聊天對象: JACK
請輸入消息,回車發送: 約不
來自[127.0.0.1:8081]的一條消息:不約
請輸入消息,回車發送: 

客戶端2

請選擇聊天對象: TOM
請輸入消息,回車發送: 123
來自[127.0.0.1:8081]的一條消息:321
請輸入消息,回車發送: 

服務端

來自[127.0.0.1:62851]的一條消息:123
回覆消息: 321
來自[127.0.0.1:60378]的一條消息:約不
回覆消息: 不約

 

七、recv與recvfrom

發消息,都是將數據發送到己端的發送緩衝中,收消息都是從己端的緩衝區中收。

  • tcp:send發消息,recv收消息
  • udp:sendto發消息,recvfrom收消息

 1.send與sendinto

tcp是基於數據流的,而udp是基於數據報的:

  • send(bytes_data):發送數據流,數據流bytes_data若爲空,自己這段的緩衝區也爲空,操作系統不會控制tcp協議發空包
  • sendinto(bytes_data,ip_port):發送數據報,bytes_data爲空,還有ip_port,所有即便是發送空的bytes_data,數據報其實也不是空的,自己這端的緩衝區收到內容,操作系統就會控制udp協議發包。

 

2.recv與recvfrom

tcp協議:

(1)如果收消息緩衝區裏的數據爲空,那麼recv就會阻塞(阻塞很簡單,就是一直在等着收)

(2)只不過tcp協議的客戶端send一個空數據就是真的空數據,客戶端即使有無窮個send空,也跟沒有一個樣。

(3)tcp基於鏈接通信

  • 基於鏈接,則需要listen(backlog),指定半連接池的大小
  • 基於鏈接,必須先運行的服務端,然後客戶端發起鏈接請求
  • 對於mac系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端在收消息後加上if判斷,空消息就break掉通信循環)
  • 對於windows/linux系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端通信循環內加異常處理,捕捉到異常後就break掉通訊循環)

 

udp協議

(1)如果如果收消息緩衝區裏的數據爲“空”,recvfrom也會阻塞

(2)只不過udp協議的客戶端sendinto一個空數據並不是真的空數據(包含:空數據+地址信息,得到的報仍然不會爲空),所以客戶端只要有一個sendinto(不管是否發送空數據,都不是真的空數據),服務端就可以recvfrom到數據。

(3)udp無鏈接

  • 無鏈接,因而無需listen(backlog),更加沒有什麼連接池之說了
  • 無鏈接,udp的sendinto不用管是否有一個正在運行的服務端,可以己端一個勁的發消息,只不過數據丟失
  • recvfrom收的數據小於sendinto發送的數據時,在mac和linux系統上數據直接丟失,在windows系統上發送的比接收的大直接報錯
  • 只有sendinto發送數據沒有recvfrom收數據,數據丟失 

注意:

1.你單獨運行上面的udp的客戶端,你發現並不會報錯,相反tcp卻會報錯,因爲udp協議只負責把包發出去,對方收不收,我根本不管,而tcp是基於鏈接的,必須有一個服務端先運行着,客戶端去跟服務端建立鏈接然後依託於鏈接才能傳遞消息,任何一方試圖把鏈接摧毀都會導致對方程序的崩潰。

2.上面的udp程序,你註釋任何一條客戶端的sendinto,服務端都會卡住,爲什麼?因爲服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。

 

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

客戶端

複製代碼

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)

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

    s.send(msg.encode('utf-8'))
    act_res=s.recv(BUFSIZE)

    print(act_res.decode('utf-8'),end='')

複製代碼

服務端

複製代碼

from socket import *
import subprocess

ip_port=('127.0.0.1',8080)
BUFSIZE=1024

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)

while True:
    conn,addr=tcp_socket_server.accept()
    print('客戶端',addr)

    while True:
        cmd=conn.recv(BUFSIZE)
        if len(cmd) == 0:break

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

        stderr=res.stderr.read()
        stdout=res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)

複製代碼

 

輸出

客戶端

複製代碼

>>: ls
1.py
客戶端.py
客戶端1.py
客戶端2.py
服務端.py
>>: ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 78:4f:43:5b:a5:4c 
    inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 
    inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
>>: ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
    inet 127.0.0.1 netmask 0xff000000 
    inet6 ::1 prefixlen 128 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether 78:4f:43:5b:a5:4c 
    inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 
    inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255
    nd6 options=201<PERFORMNUD,DAD>
    media: autoselect
    status: active
en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:00 
    media: autoselect <full-duplex>
    status: inactive
en3: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500
    options=60<TSO4,TSO6>
    ether e2:00:ec:98:eb:01 
    media: autoselect <full-duplex>
    status: inactive
en2: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500>>: 
>>: 

複製代碼

服務端

客戶端 ('127.0.0.1', 58194)

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

 

服務端

複製代碼

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

複製代碼

客戶端

複製代碼

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='')

複製代碼

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

 

注意注意注意:

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

的結果的編碼是以當前所在的系統爲準的,如果是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼且只能從管道里讀一次結果

 

八、粘包

1.什麼是粘包

粘包:發送方發送兩個字符串”hello”+”world”,接收方卻一次性接收到了”helloworld”。

只有TCP有粘包現象,UDP永遠不會粘包。

 

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

 

補充:

分包:發送方發送字符串”helloworld”,接收方卻接收到了兩個字符串”hello”和”world”。

TCP是以段(Segment)爲單位發送數據的,建立TCP鏈接後,有一個最大消息長度(MSS)。如果應用層數據包超過MSS,就會把應用層數據包拆分,分成兩個段來發送。這個時候接收端的應用層就要拼接這兩個TCP包,才能正確處理數據。

補充:

一個socket收發消息的原理

 

2.粘包如何產生

TCP爲了提高網絡的利用率,會使用一個叫做Nagle的算法。該算法是指,發送端即使有要發送的數據,如果很少的話,會延遲發送。如果應用層給TCP傳送數據很快的話,就會把兩個應用層數據包“粘”在一起,TCP最後只發一個TCP數據包給接收端。

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

 

兩種情況下會發生粘包。

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

 服務端

 客戶端

輸出

服務端

-----> hellofeng    #出現粘包現象
-----> 

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

 服務端

 客戶端

輸出

-----> he
-----> llo feng

補充:

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

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

 

3.如何解決粘包問題

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

struct模塊 

該模塊可以把一個類型,如數字,轉成固定長度的bytes

複製代碼

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)

複製代碼

 

示例:

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

發送時:

先發報頭長度

再編碼報頭內容然後發送

最後發真實內容

 

接收時:

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

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

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

 

 服務端

 客戶端

輸出

 客戶端

服務端

starting....
cliet addr ('127.0.0.1', 59162)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章