1. 程式人生 > >python基礎之socket程式設計

python基礎之socket程式設計

一 客戶端/伺服器架構

1.硬體C/S架構(印表機)

2.軟體C/S架構

  網際網路中處處是C/S架構

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

  騰訊作為服務端為你提供視訊,你得下個騰訊視訊客戶端才能看它的視訊)

C/S架構與socket的關係:

我們學習socket就是為了完成C/S架構的開發

二 osi七層

引子:

須知一個完整的計算機系統是由硬體、作業系統、應用軟體三者組成,具備了這三個條件,一臺計算機系統就可以自己跟自己玩了(打個單機遊戲,玩個掃雷啥的)

如果你要跟別人一起玩,那你就需要上網了,什麼是網際網路?

網際網路的核心就是由一堆協議組成,協議就是標準,比如全世界人通訊的標準是英語

如果把計算機比作人,網際網路協議就是計算機界的英語。所有的計算機都學會了網際網路協議,那所有的計算機都就可以按照統一的標準去收發資訊從而完成通訊了。

人們按照分工不同把網際網路協議從邏輯上劃分了層級,

為何學習socket一定要先學習網際網路協議:

1.首先:本節課程的目標就是教會你如何基於socket程式設計,來開發一款自己的C/S架構軟體

2.其次:C/S架構的軟體(軟體屬於應用層)是基於網路進行通訊的

3.然後:網路的核心即一堆協議,協議即標準,你想開發一款基於網路通訊的軟體,就必須遵循這些標準。

4.最後:就讓我們從這些標準開始研究,開啟我們的socket程式設計之旅

                                                                              

                                                           圖1

三 socket層

在圖1中,我們沒有看到Socket的影子,那麼它到底在哪裡呢?還是用圖來說話,一目瞭然。 


                   圖2

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

六 套接字工作流程

       一個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就建立起了連線,就可以講話了。等交流結束,結束通話電話結束此次交談。 生活中的場景就解釋了這工作原理。

      

                                           圖3       

先從伺服器端說起。伺服器端先初始化Socket,然後與埠繫結(bind),對埠進行監聽(listen),呼叫accept阻塞,等待客戶端連線。在這時如果有個客戶端初始化一個Socket,然後連線伺服器(connect),如果連線成功,這時客戶端與伺服器端的連線就建立了。客戶端傳送資料請求,伺服器端接收請求並處理請求,然後把迴應資料傳送給客戶端,客戶端讀取資料,最後關閉連線,一次互動結束

socket()模組函式用法

 1 import socket
 2 socket.socket(socket_family,socket_type,protocal=0)
 3 socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,預設值為 0。
 4 
 5 獲取tcp/ip套接字
 6 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 7 
 8 獲取udp/ip套接字
 9 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
10 
11 由於 socket 模組中有太多的屬性。我們在這裡破例使用了'from module import *'語句。使用 'from socket import *',我們就把 socket 模組裡的所有屬性都帶到我們的名稱空間裡了,這樣能 大幅減短我們的程式碼。
12 例如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實驗推演流程

七 基於TCP的套接字

tcp是基於連結的,必須先啟動服務端,然後再啟動客戶端去連結服務端

tcp服務端

1 ss = socket() #建立伺服器套接字
2 ss.bind()      #把地址繫結到套接字
3 ss.listen()      #監聽連結
4 inf_loop:      #伺服器無限迴圈
5     cs = ss.accept() #接受客戶端連結
6     comm_loop:         #通訊迴圈
7         cs.recv()/cs.send() #對話(接收與傳送)
8     cs.close()    #關閉客戶端套接字
9 ss.close()        #關閉伺服器套接字(可選)

tcp客戶端

1 cs = socket()    # 建立客戶套接字
2 cs.connect()    # 嘗試連線伺服器
3 comm_loop:        # 通訊迴圈
4     cs.send()/cs.recv()    # 對話(傳送/接收)
5 cs.close()            # 關閉客戶套接字

socket通訊流程與打電話流程類似,我們就以打電話為例來實現一個low版的套接字通訊

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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()                       #手機關機
服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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()                                       #掛電話
客戶端

加上鍊接迴圈與通訊迴圈

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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()                       #手機關機
服務端改進版
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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是無連結的,先啟動哪一端都不會報錯

udp服務端

1 ss = socket()   #建立一個伺服器的套接字
2 ss.bind()       #繫結伺服器套接字
3 inf_loop:       #伺服器無限迴圈
4     cs = ss.recvfrom()/ss.sendto() # 對話(接收與傳送)
5 ss.close()                         # 關閉伺服器套接字

udp客戶端

cs = socket()   # 建立客戶套接字
comm_loop:      # 通訊迴圈
    cs.sendto()/cs.recvfrom()   # 對話(傳送/接收)
cs.close()                      # 關閉客戶套接字

udp套接字簡單示例

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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)
udp服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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)
udp客戶端

qq聊天(由於udp無連線,所以可以同時多個客戶端去跟服務端通訊)

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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)
udp服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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

服務端執行結果

客戶端1執行結果

客戶端2執行結果

時間伺服器

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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客戶端

九 粘包現象

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

注意注意注意:

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結果

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
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.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=act_res.stderr.read()
        stdout=act_res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)
服務端