網絡編程之socket

網絡編程之sockethtml

看到本篇文章的題目是否是很疑惑,what is this?,不要着急,可是記住一說網絡編程,你就想socket,socket是實現網絡編程的工具,那麼什麼是socket,什麼是網絡編程,什麼是網絡,爲何要學習socket,都在下面有講解,你們細細看來!前端

 

本節目錄

  1. 爲何要學習socket
  2. 客戶端\服務端
  3. 網絡通訊的整個流程
  4. 網絡通訊協議(互聯網協議)
  5. osi七層模型
  6. socket
  7. 套接字socket的發展史及分類
  8. 基於TCP和UDP兩個協議下socket的通訊流程
  9. 粘包現象
  10. 粘包的解決方案
  11. 驗證客戶端的鏈接合法性
  12. socketserver模塊實現併發
  13. 網絡編程的做業

 

一 爲何要學習socket

首先咱們python基礎部分已經學完了,而socket是咱們基礎進階的課程,也就是說,你本身如今徹底能夠寫一些小程序了,可是前面的學習和練習,咱們寫的代碼都是在本身的電腦上運行的,雖然咱們學過了模塊引入,文件引入import等等,我能夠在程序中獲取到另外一個文件的內容,對吧,可是那麼忽然有一天,你的朋友和你說:"把你電腦上的一個文件經過你本身寫的程序發送到個人電腦上",這時候怎麼辦?你是否是會想,what?這怎麼搞?就在此時,忽然靈感來了,我能夠經過qq、雲盤、微信等發送給他啊,但是人家說了,讓你用本身寫的程序啊,嗯,這是個問題,此時又來一個靈感,我給他發送文件確定是經過網絡啊,這就產生了網絡,對吧,那我怎麼讓個人程序可以經過網絡來聯繫到個人朋友呢,而且把文件發送給他呢,那麼查了一下,發現網絡通訊經過socket能夠搞,可是怎麼搞呢?首先,查詢結果是對的,socket就是網絡通訊的工具,任何一門語言都有socket,他不是任何一個語言的專有名詞,而是你們經過本身的程序與其餘電腦進行網絡通訊的時候都用它。知道爲何要學習socket了吧~~朋友們~~而你使用本身的電腦和別人的電腦進行聯繫併發送消息或者文件等操做就叫作網絡通訊。python

對於一個小白來說,看到這一節標題的你,此刻的你心裏是拒絕的,不明白在說些什麼。我理解你的心情,不要驚慌、不要着急,且聽我娓娓道來。linux

你們經過上面的內容大體的瞭解了一下什麼是網絡通訊,那麼在咱們的平常生活中,哪裏用到了網絡通訊呢,網絡通訊的整個流程又是什麼樣子的呢?咱們要學的socket是怎麼在網絡中發揮做用的呢?讓咱們懷揣着這 三個問題 來進行下面的學習。程序員

二 客戶端\服務端架構(哪裏用到了網絡通訊)

咱們使用qq、微信和別人聊天,經過瀏覽器來瀏覽頁面、看京東的網站,經過優酷、快播(此處只是懷念一下)看片片啥的等等,經過無線打印機來打印一個word文檔等,只要有無線、有網、有4G,咱們就能好好的聊天,好好的看片片、好好的購物什麼的,對吧,那麼這些操做都叫作網絡通訊,確切來講都須要使用網絡通訊,前提是你要有網(你們記着這個'網',我下面會給你們詳解),原來生活中到處使用了網絡通訊,咱們經過網絡通訊的不一樣形式:好比說qq是咱們下載到電腦或者手機上的應用程序(qq應用程序就是人家騰訊開發的軟件,放到你的電腦或者手機上供你使用的,大概明白應用程序意思就行,不用深究~~),瀏覽器也是咱們下載的應用程序,可是瀏覽器是經過頁面來訪問別人的網站的,而打印機我是經過我電腦上的word來操做使用的。根據這些不一樣的場景或者說不用的溝通方式,在業內劃分了下面兩個架構(架構:就是不一樣的組成結構)。在看下面的幾個架構以前,咱們須要知道什麼是客戶端,什麼是服務端。客戶端:安裝在你電腦上的qq,瀏覽器(360瀏覽器、chrome瀏覽器、IE瀏覽器等),當咱們使用qq發送消息的時候,消息先發送到了騰訊,而後騰訊在轉發到你朋友的qq上,此時你的qq就是客戶端,騰訊就是服務端。當咱們使用瀏覽器來看京東的網站的時候,咱們電腦上的瀏覽器就叫作客戶端,京東就叫作服務端。web

客戶端英文名稱:Client(使用服務端的服務),服務端英文名稱:Server(一直運行着,等待服務別人,不能有一天訪問百度,百度頁面打不開,不行吧。),下面所說的C\S架構就是說的Client\Server架構。算法

    a.硬件C\S架構:打印機。chrome

    b.軟件C\S架構:QQ、微信、優酷、暴風影音、瀏覽器(IE、火狐,360瀏覽器等)。其中瀏覽器又比較特殊,不少網站是基於瀏覽器來進行訪問的,瀏覽器和各個網站服務端進行的通信方式又常被成爲B\S架構(瀏覽器英文名稱:Browser),web開發就是這個,後面你們知道有前端的課程對吧,前端就是瀏覽器上的知識,之後你會常常和瀏覽器打交道,學完前端就能夠進行web開發全棧開發了。若是我把全部的東西都作成應用程序是否是很麻煩啊,要裝不少的軟件對吧,全部就開始有了B\S架構,只須要個瀏覽器就能使用不少的工具了,而且提供了一個統一入口,這也是爲何B\S架構火了起來。可是手機端的仍是用的應用程序多一些,可是手機端B\S架構也是一個趨勢,就像微信的小程序和公衆號,爲何說是一個趨勢呢,不只僅是由於方便由於省錢,而是提供了一個統一的入口,其實微信早就實現了。統一入口是什麼意思呢?就像咱們公司常常用的一個公司內部管理系統,請假、打卡、報銷、查客戶等等,若是這些功能都須要打開一個網頁或者app,是否是很難受啊,那麼公司就作了這麼一個系統,你們在這個系統上關於上班的一些你須要的功能就都能完成了,這就是統一入口。這也是一個開發思想,大程序分紅幾個小程序,開發速度也快,開發一個小功能就能上線,而不須要等着全部的功能所有開發完成才上線,解耦分治思想,公司作開發時這種思想很流行,迭代開發。說多了。。shell

    無論哪一個架構,他們都要進行網絡通訊,基本都要用socket,咱們學習socekt就是爲了完成C\S架構項目的開發編程

三 網絡通訊的整個流程

還記得上面我說過的那個'網'嗎,在這一節就給你們講解,有些同窗對網絡是既熟悉又陌生,熟悉是由於咱們都知道,咱們安裝一個路由器,拉一個網線,或者用無限路由器,連上網線或者連上wifi就可以上網購物、看片片、吃雞了,可是這一系列的神操做究竟是怎麼讓咱們上網了呢?讓咱們起底揭祕!因爲網絡的內容很是的多,本篇博客主要是學socket網絡編程,因此我把網絡這方面的內容放到了我另一篇博客上,這個博客很簡單,不是什麼深刻研究類的博客,沒有學過網絡的或者說對網絡不太熟悉的同窗能夠去看看,地址是網絡通訊的整個流程,有網絡基礎的同窗,能夠直接往下面學習,若是你自認上學時是個學渣,也能夠過去大體溜一眼~~~未來你面向的是開發,全部網絡這一塊對你來說就是大體知道就能夠了,可是之後想在技術上有深造,那麼就須要你深刻的研究一下網絡了,內容很是多,學海無涯~~

別忘了端口+IP可以肯定一臺電腦上的某一個應用程序~~

  那麼咱們經過下面的代碼簡單看一下socket究竟是個什麼樣子,大概怎麼使用:下面的程序就是一個應用程序,和qq啊、微信啊是同樣的,都叫作應用程序。

import socket
#建立一個socket對象
server = socket.socket()  #至關於建立了一部電話
ip_port = ('192.168.111.1',8001) #建立一個電話卡
server.bind(ip_port) #插上電話卡
server.listen(5) #監聽着電話,我能監聽5個,接到一個電話以後,後面還能有四我的給我打電話,可是後面這四我的都要排隊等着,等着我第一個電話掛掉,再來第6個的時候,第六我的的手機會報錯
print('11111')
#等着別人給我打電話,打來電話的時候,我就拿到了和對方的這個連線通道conn和對方的電話號碼addr
conn,addr = server.accept()  #阻塞住,一直等到有人鏈接我,鏈接以後獲得一個元祖,裏面是連線通道conn和對方的地址(ip+端口)
print('22222')
print(conn)
print('>>>>>>>>>',addr)
while True:
    from_client_data = conn.recv(1024) #服務端必須經過二者之間的鏈接通道來收消息
    from_client_data = from_client_data.decode('utf-8')
    print(from_client_data)
    if from_client_data == 'bye':
        break
    server_input = input('明威說>>>>:')
    conn.send(server_input.encode('utf-8'))
    if server_input == 'bye':
        break
conn.close() #掛電話
server.close() #關手機
test_server.py

 

listen(3),這個3的意思是我鏈接着一個,後面還能夠有三個排隊的,也就是支持4我的的服務,可是後面三個要排隊。

#_*_coding:utf-8_*_
import socket
import time

client = socket.socket()
server_ip_port = ('192.168.111.1',8001)

client.connect(server_ip_port)

while True:
    client_input = input('小文說>>>>:')
    client.send(client_input.encode('utf-8')) #給服務端發送消息
    if client_input == 'bye':
        break
    from_server_data = client.recv(1024)

    print('來自服務端的消息:',from_server_data.decode('utf-8'))
    if from_server_data.decode('utf-8') == 'bye':
        break
client.close() #客戶端掛電話
test_client.py

 

注意:先運行server,而後再運行client,而後你會發現client這個文件再輸出臺的地方讓你輸入內容,你輸入一個內容而後回車,你會發現server那邊的控制檯就輸出了以client發送的內容

 

四 網絡通訊協議(互聯網協議)

次日再講這裏,你們次日再看這裏把~~~

網絡通訊協議是網絡傳輸的靈魂,很是重要,協議即準則,準則是傳輸消息的格式要求,那麼咱們從電腦上發出一個消息,究竟是以什麼樣的消息格式發到了對方的手上呢,來看一看這裏>>>,網絡通訊協議

 

五 osi七層模型

互聯網的核心就是由一堆協議組成,協議就是標準,標準就是你們都承認的,全部人都按照這個來,這樣你們都可以互相瞭解,互相深刻了~~~好比全世界人通訊的標準是英語

 

 

五層通訊流程:

 

六 socket

結合上圖來看,socket在哪一層呢,咱們繼續看下圖

socket在內的五層通信流程:

 

Socket又稱爲套接字,它是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。當咱們使用不一樣的協議進行通訊時就得使用不一樣的接口,還得處理不一樣協議的各類細節,這就增長了開發的難度,軟件也不易於擴展(就像咱們開發一套公司管理系統同樣,報帳、會議預約、請假等功能不須要單獨寫系統,而是一個系統上多個功能接口,不須要知道每一個功能如何去實現的)。因而UNIX BSD就發明了socket這種東西,socket屏蔽了各個協議的通訊細節,使得程序員無需關注協議自己,直接使用socket提供的接口來進行互聯的不一樣主機間的進程的通訊。這就比如操做系統給咱們提供了使用底層硬件功能的系統調用,經過系統調用咱們能夠方便的使用磁盤(文件操做),使用內存,而無需本身去進行磁盤讀寫,內存管理。socket其實也是同樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套接口,同過這個接口就能夠統1、方便的使用tcp/ip協議的功能了。

其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。

 

七 套接字socket的發展史及分類

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

八 基於TCP和UDP兩個協議下socket的通信流程

1.TCP和UDP對比

TCP(Transmission Control Protocol)可靠的、面向鏈接的協議(eg:打電話)、傳輸效率低全雙工通訊(發送緩存&接收緩存)、面向字節流。使用TCP的應用:Web瀏覽器;文件傳輸程序。

UDP(User Datagram Protocol)不可靠的、無鏈接的服務,傳輸效率高(發送前時延小),一對1、一對多、多對1、多對多、面向報文(數據包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視頻流;IP語音(VoIP)。

直接看圖對比其中差別

 

繼續往下看

TCP和UDP下socket差別對比圖:

 

上面的圖只是讓你們感覺一下TCP和UDP協議下,socket工做流程的不一樣,二者之間的差別是tcp須要鏈接,udp不須要,有些同窗是否是有些迷糊,老師,這裏面的bind、listen啥的都是什麼東西啊,我感受人生是迷茫的!calm down!下面咱們就分開二者,細細學習!

2.TCP協議下的socket

來吧!先上圖!

基於TCP的socket通信流程圖片:

 

雖然上圖將通信流程中的大體描述了一下socket各個方法的做用,可是仍是要總結一下通信流程(下面一段內容)

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

上代碼感覺一下,須要建立兩個文件,文件名稱隨便起,爲了方便看,個人兩個文件名稱爲tcp_server.py(服務端)和tcp_client.py(客戶端),將下面的server端的代碼拷貝到tcp_server.py文件中,將下面client端的代碼拷貝到tcp_client.py的文件中,而後先運行tcp_server.py文件中的代碼,再運行tcp_client.py文件中的代碼,而後在pycharm下面的輸出窗口看一下效果。

server端代碼示例(若是比喻成打電話)

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

client端代碼示例

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

 

socket綁定IP和端口時可能出現下面的問題:

    解決辦法: 

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

 

可是若是你加上了上面的代碼以後仍是出現這個問題:OSError: [WinError 10013] 以一種訪問權限不容許的方式作了一個訪問套接字的嘗試。那麼只能換端口了,由於你的電腦不支持端口重用。

    記住一點,用socket進行通訊,必須是一收一發對應好。

關於setsockopt能夠看這篇文章。關於setsockopt的使用

  提一下:網絡相關或者須要和電腦上其餘程序通訊的程序才須要開一個端口。

  

  在看UDP協議下的socket以前,咱們還須要加一些內容來說:看代碼

    server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()  #在這阻塞,等待客戶端過來鏈接
while True:
    ret = conn.recv(1024)  #接收消息  在這仍是要阻塞,等待收消息
    ret = ret.decode('utf-8')  #字節類型轉換爲字符串中文
    print(ret)
    if ret == 'bye':        #若是接到的消息爲bye,退出
        break
    msg = input('服務端>>')  #服務端發消息
    conn.send(msg.encode('utf-8'))
    if msg == 'bye':
        break

conn.close()
sk.close()
只能與第一個客戶端通訊server端代碼

 

    client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #鏈接服務端

while True:
    msg = input('客戶端>>>')  #input阻塞,等待輸入內容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
sk.close()
只能與第一個客戶端通訊client端代碼

 

你會發現,第一個鏈接的客戶端能夠和服務端收發消息,可是第二個鏈接的客戶端發消息服務端是收不到的

  緣由解釋:
    tcp屬於長鏈接,長鏈接就是一直佔用着這個連接,這個鏈接的端口被佔用了,第二個客戶端過來鏈接的時候,他是能夠鏈接的,可是處於一個佔線的狀態,就只能等着去跟服務端創建鏈接,除非一個客戶端斷開了(優雅的斷開能夠,若是是強制斷開就會報錯,由於服務端的程序還在第一個循環裏面),而後就能夠進行和服務端的通訊了。什麼是優雅的斷開呢?看代碼。
server端代碼:
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #容許地址重用,這個東西都說能解決問題,我很是不建議你們這麼作,容易出問題
sk.bind(('127.0.0.1',8090))
sk.listen()
# 第二步演示,再加一層while循環
while True:    #下面的代碼所有縮進進去,也就是循環創建鏈接,可是無論怎麼聊,只能和一個聊,也就是另一個優雅的斷了以後才能和另一個聊
                #它不能同時和好多人聊,仍是長鏈接的緣由,一直佔用着這個端口的鏈接,udp是能夠的,而後咱們學習udp
    conn,addr = sk.accept()  #在這阻塞,等待客戶端過來鏈接
    while True:
        ret = conn.recv(1024)  #接收消息  在這仍是要阻塞,等待收消息
        ret = ret.decode('utf-8')  #字節類型轉換爲字符串中文
        print(ret)
        if ret == 'bye':        #若是接到的消息爲bye,退出
            break
        msg = input('服務端>>')  #服務端發消息
        conn.send(msg.encode('utf-8'))
        if msg == 'bye':
            break
    conn.close()
優雅的斷開一個client端以後另外一個client端就能夠通訊的代碼

 

client端代碼

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #鏈接服務端

while True:
    msg = input('客戶端>>>')  #input阻塞,等待輸入內容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
# sk.close()
client端代碼

 

強制斷開鏈接以後的報錯信息:

    

 

3.UDP協議下的socket

老樣子!先上圖!

基於UDP的socket通信流程:

 

總結一下UDP下的socket通信流程

  先從服務器端提及。服務器端先初始化Socket,而後與端口綁定(bind),recvform接收消息,這個消息有兩項,消息內容和對方客戶端的地址,而後回覆消息時也要帶着你收到的這個客戶端的地址,發送回去,最後關閉鏈接,一次交互結束

上代碼感覺一下,須要建立兩個文件,文件名稱隨便起,爲了方便看,個人兩個文件名稱爲udp_server.py(服務端)和udp_client.py(客戶端),將下面的server端的代碼拷貝到udp_server.py文件中,將下面cliet端的代碼拷貝到udp_client.py的文件中,而後先運行udp_server.py文件中的代碼,再運行udp_client.py文件中的代碼,而後在pycharm下面的輸出窗口看一下效果。

server端代碼示例

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

 

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

相似於qq聊天的代碼示例:

#_*_coding:utf-8_*_
import socket
ip_port=('127.0.0.1',8081)
udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #DGRAM:datagram 數據報文的意思,象徵着UDP協議的通訊方式
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)
server端

 

#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

qq_name_dic={
    'taibai':('127.0.0.1',8081),
    'Jedan':('127.0.0.1',8081),
    'Jack':('127.0.0.1',8081),
    'John':('127.0.0.1',8081),
}


while True:
    qq_name=input('請選擇聊天對象: ').strip()
    while True:
        msg=input('請輸入消息,回車發送,輸入q結束和他的聊天: ').strip()
        if msg == 'q':break
        if not msg or not qq_name or qq_name not in qq_name_dic:continue
        udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])# 必須帶着本身的地址,這就是UDP不同的地方,不須要創建鏈接,可是要帶着本身的地址給服務端,不然服務端沒法判斷是誰給我發的消息,而且不知道該把消息回覆到什麼地方,由於咱們之間沒有創建鏈接通道

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

 

接下來,給你們說一個真實的例子,也就是實際當中應用的,那麼這是個什麼例子呢?就是咱們電腦系統上的時間,windows系統的時間是和微軟的時間服務器上的時間同步的,而mac本是和蘋果服務商的時間服務器同步的,這是怎麼作的呢,首先他們的時間服務器上的時間是和國家同步的,大家用個人系統,那麼大家的時間只要和我時間服務器上的時間同步就好了,對吧,我時間服務器是否是提供服務的啊,至關於一個服務端,咱們的電腦就至關於客戶端,就是經過UDP來搞的。

咱們自制一個時間服務器的代碼示例:

from socket import *
from time import strftime
import time
ip_port = ('127.0.0.1', 9000)
bufsize = 1024

tcp_server = socket(AF_INET, SOCK_DGRAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)

while True:
    msg, addr = tcp_server.recvfrom(bufsize)
    print('===>', msg)
    stru_time = time.localtime()  #當前的結構化時間
    if not msg:
        time_fmt = '%Y-%m-%d %X'
    else:
        time_fmt = msg.decode('utf-8')
    back_msg = strftime(time_fmt,stru_time)
    print(back_msg,type(back_msg))
    tcp_server.sendto(back_msg.encode('utf-8'), addr)

tcp_server.close()
server端

 

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('當前日期:',str(data,encoding='utf-8'))
client端

 

UDP來個小練習吧:

練習的需求是這樣的:一、服務端須要提供的服務有:接收消息(時間格式的字符串)、將個人本地的時間轉換成接收到的消息的格式(也就是個時間格式的字符串)、發回給客戶端。二、客戶端自行想一下怎麼寫。

TCP協議和UDP協議下socket的基本使用ok了,那咱們來深刻分析一下socket。(這一塊的內容初學者不要看,對socket有些瞭解的同窗能夠研究一下,切記看不懂很正常,不要深究,現階段大家就是學習應用爲主!)>>>>看這裏>>>>socket原理剖析,裏面包含socket中各個方法的做用和方法中的參數。

這裏我列出兩個簡易描述socket各個參數和方法的圖,共你們參考:

socket類型:

  socket各個方法的解釋:

  

 

九 粘包現象

  
  說粘包以前,咱們先說兩個內容,1.緩衝區、2.windows下cmd窗口調用系統指令
   9.1 緩衝區(下面粘包現象的圖裏面還有關於緩衝區的解釋)
    
每一個 socket 被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。

write()/send() 並不當即向網絡中傳輸數據,而是先將數據寫入緩衝區中,再由TCP協議將數據從緩衝區發送到目標機器。一旦將數據寫入到緩衝區,函數就能夠成功返回,無論它們有沒有到達目標機器,也無論它們什麼時候被髮送到網絡,這些都是TCP協議負責的事情。

TCP協議獨立於 write()/send() 函數,數據有可能剛被寫入緩衝區就發送到網絡,也可能在緩衝區中不斷積壓,屢次寫入的數據被一次性發送到網絡,這取決於當時的網絡狀況、當前線程是否空閒等諸多因素,不禁程序員控制。

read()/recv() 函數也是如此,也從輸入緩衝區中讀取數據,而不是直接從網絡中讀取。

這些I/O緩衝區特性可整理以下:

1.I/O緩衝區在每一個TCP套接字中單獨存在;
2.I/O緩衝區在建立套接字時自動生成;
3.即便關閉套接字也會繼續傳送輸出緩衝區中遺留的數據;
4.關閉套接字將丟失輸入緩衝區中的數據。

輸入輸出緩衝區的默認大小通常都是 8K,能夠經過 getsockopt() 函數獲取:

1.unsigned optVal;
2.int optLen = sizeof(int);
3.getsockopt(servSock, SOL_SOCKET, SO_SNDBUF,(char*)&optVal, &optLen);
4.printf("Buffer length: %d\n", optVal);
socket緩衝區解釋

 9.2 windows下cmd窗口調用系統指令(linux下沒有寫出來,你們仿照windows的去摸索一下吧)

    a.首先ctrl+r,彈出左下角的下圖,輸入cmd指令,肯定
      

    b.在打開的cmd窗口中輸入dir(dir:查看當前文件夾下的全部文件和文件夾),你會看到下面的輸出結果。

      

      另外還有ipconfig(查看當前電腦的網絡信息),在windows沒有ls這個指令(ls在linux下是查看當前文件夾下全部文件和文件夾的指令,和windows下的dir是相似的),那麼沒有這個指令就會報下面這個錯誤

      

windows下執行多條指令

 

      

      爲何要說這個系統指令呢,是但願藉助系統指令和指令輸出的結果來模擬一下粘包現象,那什麼是粘包呢?

   今天的內容就先到這裏,明天咱們認識粘包~~,你們好好理解練習一下把。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  9.3 粘包現象(兩種)

    先上圖:(本圖是我作出來爲了讓小白同窗有個大體的瞭解用的,其中不少地方更加的複雜,那就須要未來你們有多餘的精力的時候去作一些深刻的研究了,這裏我就不帶你們搞啦)

    

 

    關於MTU你們能夠看看這篇文章 https://yq.aliyun.com/articles/222535  還有百度百科 MTU百科

    MTU簡單解釋:

MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500個字節,也就是1500B。若是本機一次須要發送的數據比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度

 

關於上圖中提到的Nagle算法等建議你們去看一看Nagle算法、延遲ACK、linux下的TCP_NODELAY和TCP_CORK,這些內容等大家把python學好之後再去研究吧,網絡的內容實在太多啦,也就是說你們須要努力的過程還很長,加油!

  

  超出緩衝區大小會報下面的錯誤,或者udp協議的時候,你的一個數據包的大小超過了你一次recv能接受的大小,也會報下面的錯誤,tcp不會,可是超出緩存區大小的時候,確定會報這個錯誤。

 
   

  9.4 模擬一個粘包現象

    在模擬粘包以前,咱們先學習一個模塊subprocess。
import subprocess
cmd = input('請輸入指令>>>')
res = subprocess.Popen(
    cmd,                     #字符串指令:'dir','ipconfig',等等
    shell=True,              #使用shell,就至關於使用cmd窗口
    stderr=subprocess.PIPE,  #標準錯誤輸出,凡是輸入錯誤指令,錯誤指令輸出的報錯信息就會被它拿到
    stdout=subprocess.PIPE,  #標準輸出,正確指令的輸出結果被它拿到
)
print(res.stdout.read().decode('gbk'))
print(res.stderr.read().decode('gbk'))
subprocess的簡單使用

 

 注意:

        若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼

        且只能從管道里讀一次結果,PIPE稱爲管道。

 

     下面是subprocess和windows上cmd下的指令的對應示意圖:subprocess的stdout.read()和stderr.read(),拿到的結果是bytes類型,因此須要轉換爲字符串打印出來看。
    

    

    好,既然咱們會使用subprocess了,那麼咱們就經過它來模擬一個粘包,終於到模擬粘包現象了,這一天真的是好累。

    tcp粘包演示(一):

      先從上面粘包現象中的第一種開始: 接收方沒有及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 
      server端代碼示例:
cket 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('gbk'),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)
tcp_server.py

 

import socket
ip_port = ('127.0.0.1',8080)
size = 1024
tcp_sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res = tcp_sk.connect(ip_port)
while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    tcp_sk.send(msg.encode('utf-8'))
    act_res=tcp_sk.recv(size)
    print('接收的返回結果長度爲>',len(act_res))
    print('std>>>',act_res.decode('gbk')) #windows返回的內容須要用gbk來解碼,由於windows系統的默認編碼爲gbk
tcp_client.py

 

tcp粘包演示(二):發送數據時間間隔很短,數據也很小,會合到一塊兒,產生粘包

      server端代碼示例:(若是兩次發送有必定的時間間隔,那麼就不會出現這種粘包狀況,試着在兩次發送的中間加一個time.sleep(1))

 

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()
tcp_server.py

 

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)
res=s.connect(ip_port)
s.send('hi'.encode('utf-8'))
s.send('meinv'.encode('utf-8'))
client端

 

 

    示例二的結果:所有被第一個recv接收了
    

 

     udp粘包演示:注意:udp是面向包的,因此udp是不存在粘包的
      server端代碼示例:
import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk = socket.socket(type=socket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090))
msg,addr = sk.recvfrom(1024)
while True:
    cmd = input('>>>>')
    if cmd == 'q':
        break
    sk.sendto(cmd.encode('utf-8'),addr)
    msg,addr = sk.recvfrom(1032)
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg))
    print(msg.decode('utf-8'))

sk.close()
udp_server.py

 

import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk = socket.socket(type=socket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090))
msg,addr = sk.recvfrom(1024)
while True:
    cmd = input('>>>>')
    if cmd == 'q':
        break
    sk.sendto(cmd.encode('utf-8'),addr)
    msg,addr = sk.recvfrom(1024)
    # msg,addr = sk.recvfrom(1218)
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg))
    print(msg.decode('utf-8'))

sk.close()
client端

 

  在udp的代碼中,咱們在server端接收返回消息的時候,咱們設置的recvfrom(1024),那麼當我輸入的執行指令爲‘dir’的時候,dir在我當前文件夾下輸出的內容大於1024,而後就報錯了,報的錯誤也是下面這個:

  

    解釋緣由:是由於udp是面向報文的,意思就是每一個消息是一個包,你接收端設置接收大小的時候,必需要比你發的這個包要大,否則一次接收不了就會報這個錯誤,而tcp不會報錯,這也是爲何ucp會丟包的緣由之一,這個和咱們上面緩衝區那個錯誤的報錯緣由是不同的。  

 
   9.5 TCP會粘包、UDP永遠不會粘包
    看下面的解釋緣由:
發送端能夠是一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時纔會清除緩衝區內容。數據是可靠的,可是會粘包。

 

補充兩個問題

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

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

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

    send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失,通常的小數據就用send,由於小數據也用sendall的話有些影響代碼性能,簡單來說就是還多while循環這個代碼呢。
  
用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送) 

用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比較短,可能會等待和下一次數據一塊兒發送。
 

 

粘包的緣由: 主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的
     
  學到這裏,咱們留一個小做業(作不作是你的事情,個人事情是真心的教會你,但願你尊重本身的努力):實現一個簡單的網盤功能。
    
 
 
 

十 粘包的解決方案

   解決方案(一):
     問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端發一個確認消息給發送端,而後發送端再發送過來後面的真實內容,接收端再來一個死循環接收完全部數據。
     

    看代碼示例:

      server端代碼 
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()
tcp_server.py

 

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'))
tcp_client.py

 

解決方案(二):

    經過struck模塊將須要發送的內容的長度進行打包,打包成一個4字節長度的數據發送到對端,對端只要取出前4個字節,而後對這四個字節的數據進行解包,拿到你要發送的內容的長度,而後經過這個長度來繼續接收咱們實際要發送的內容。不是很好理解是吧?哈哈,不要緊,看下面的解釋~~
       爲何要說一下這個模塊呢,由於解決方案(一)裏面你發現,我每次要先發送一個個人內容的長度,須要接收端接收,並切須要接收端返回一個確認消息,我發送端才能發後面真實的內容,這樣是爲了保證數據可靠性,也就是接收雙方能順利溝通,可是多了一次發送接收的過程,爲了減小這個過程,咱們就要使struck來發送你須要發送的數據的長度,來解決上面咱們所說的經過發送內容長度來 解決粘包的問題
     關於struck的介紹:
       瞭解c語言的人,必定會知道struct結構體在c語言中的做用,不瞭解C語言的同窗也不要緊,不影響,其實它就是定義了一種結構,裏面包含不一樣類型的數據(int,char,bool等等),方便對某一結構對象進行處理。而在網絡通訊當中,大多傳遞的數據是以二進制流(binary data)存在的。當傳遞字符串時,沒必要擔憂太多的問題,而當傳遞諸如int、char之類的基本數據的時候,就須要有一種機制將某些特定的結構體類型打包成二進制流的字符串而後再網絡傳輸,而接收端也應該能夠經過某種機制進行解包還原出原始的結構體數據。python中的struct模塊就提供了這樣的機制,該模塊的主要做用就是對python基本類型值與用python字符串格式表示的C struct類型間的轉化(This module performs conversions between Python values and C structs represented as Python strings.)。
    
    struck模塊的使用:struct模塊中最重要的兩個函數是pack()打包, unpack()解包。
 
    

    pack():#我在這裏只介紹一下'i'這個int類型,上面的圖中列舉除了能夠打包的全部的數據類型,而且struck除了pack和uppack兩個方法以外還有好多別的方法和用法,你們之後找時間能夠去研究一下,這裏我就不作介紹啦,網上的教程不少~~

import struct
a=12
# 將a變爲二進制
bytes=struct.pack('i',a) 
-------------------------------------------------------------------------------
struct.pack('i',1111111111111) 若是int類型數據太大會報錯struck.error
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍

 

pack方法圖解:

      

 

    unpack():

# 注意,unpack返回的是tuple !!

a,=struct.unpack('i',bytes) #將bytes類型的數據解包後,拿到int類型數據

好,到這裏咱們將struck這個模塊將int類型的數據打包成四個字節的方法了,那麼咱們就來使用它解決粘包吧。

  先看一段僞代碼示例:

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,由於bytes只能將字符串類型的數據轉換爲bytes類型的,全部須要先序列化一下這個字典,字典不能直接轉化爲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)
僞代碼 含解釋

 

下面看正式的代碼:

  server端代碼示例:報頭:就是消息的頭部信息,咱們要發送的真實內容爲報頭後面的內容。

import socket,struct,json
import subprocess
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)
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()
        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) #在發真實的內容
        #其實就是連續的將長度和內容一塊兒發出去,那麼整個內容的前4個字節就是咱們打包的後面內容的長度,對吧
        
    conn.close(
tcp_server.py(自定製報頭)

 

client端代碼示例:

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)     #先接收4個字節的數據,由於咱們將要發送過來的內容打包成了4個字節,因此先取出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:    #根據內容的長度來繼續接收4個字節後面的內容。
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)
    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默認gbk編碼
tcp_client.py(自定製報頭)

複雜一些的代碼示例

  server端:

import socket,struct,json
import subprocess
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)

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

        headers={'data_size':len(back_msg)}
        head_json=json.dumps(headers)
        head_json_bytes=bytes(head_json,encoding='utf-8')

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

    conn.close()
tcp_server.py

  client端:

from socket import *
import struct,json

ip_port=('127.0.0.1',8080)
client=socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

while True:
    cmd=input('>>: ')
    if not cmd:continue
    client.send(bytes(cmd,encoding='utf-8'))

    head=client.recv(4)
    head_json_len=struct.unpack('i',head)[0]
    head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
    data_len=head_json['data_size']

    recv_size=0
    recv_data=b''
    while recv_size < data_len:
        recv_data+=client.recv(1024)
        recv_size+=len(recv_data)

    #print(recv_data.decode('utf-8'))
    print(recv_data.decode('gbk')) #windows默認gbk編碼
tcp_client.py

  其實上面複雜的代碼作了個什麼事情呢,就是自定製了報頭:

  

  有同窗問:老師,你爲啥屢次send啊,其實屢次send和將數據拼接起來send一次是同樣的,由於咱們約定好了,你接收的時候先接收4個字節,而後再接收後面的內容

整個流程的大體解釋:
咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的描述信息(大小啊之類的),而後json序列化,而後用struck將序列化後的數據長度打包成4個字節。
咱們在網絡上傳輸的全部數據 都叫作數據包,數據包裏的全部數據都叫作報文,報文裏面不止有你的數據,還有ip地址、mac地址、端口號等等,其實全部的報文都有報頭,這個報頭是協議規定的,看一下

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

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

 

十一 驗證客戶端的連接合法性

  首先,咱們來探討一下,什麼叫驗證合法性, 舉個例子:有一天,我開了一個socket服務端,只想讓我們這個班的同窗使用,可是有一天,隔壁班的同窗過來問了一下我開的這個服務端的ip和端口,而後他是否是就能夠去鏈接我了啊,那怎麼辦,我是否是不想讓他鏈接我啊,我須要驗證一下你的身份,這就是驗證鏈接的合法性,再舉個例子,就像咱們上面說的你的windows系統是否是鏈接微軟的時間服務器來獲取時間的啊,你的mac能到人家微軟去獲取時間嗎,你願意,人家微軟還不肯意呢,對吧,那這時候,你每次鏈接我來獲取時間的時候,我是否是就要驗證你的身份啊,也就是你要帶着你的系統信息,我要判斷你是否是我微軟的windows,對吧,若是是mac,我是否是不讓你連啊,這就是鏈接合法性。若是驗證你的鏈接是合法的,那麼若是我還要對你的身份進行驗證的需求,也就是要驗證用戶名和密碼,那麼咱們還須要進行身份認證。鏈接認證>>身份認證>>ok你能夠玩了。
  
  好大體描述相信你們基本理解了,若是這尚未理解,那麼同窗,我要哭暈在廁所了。
    
 
  若是你想在分佈式系統中實現一個簡單的客戶端連接認證功能,又不像SSL那麼複雜,那麼利用hmac+加鹽的方式來實現,直接看代碼!(SSL,咱們都)
from socket import *
import hmac,os

secret_key=b'Jedan has a big key!'
def conn_auth(conn):
    '''
    認證客戶端連接
    :param conn:
    :return:
    '''
    print('開始驗證新連接的合法性')
    msg=os.urandom(32)#生成一個32字節的隨機字符串
    conn.sendall(msg)
    h=hmac.new(secret_key,msg) 
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)

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

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

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

 

from socket import *
import hmac,os

secret_key=b'Jedan has a big key!'
def conn_auth(conn):
    '''
    驗證客戶端到服務器的連接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

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

    conn_auth(tcp_socket_client)

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

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

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

 

介紹代碼中使用的兩個方法:

   一、os.urandom(n)
    其中os.urandom(n) 是一種bytes類型的隨機生成n個字節字符串的方法,並且每次生成的值都不相同。再加上md5等加密的處理,就可以成內容不一樣長度相同的字符串了。
os.urandom(n)函數在python官方文檔中作出了這樣的解釋

函數定位: Return a string of n random bytes suitable for cryptographic use. 
意思就是,返回一個有n個byte那麼長的一個string,而後很適合用於加密。

而後這個函數,在文檔中,被歸結於os這個庫的Miscellaneous Functions,意思是不一樣種類的函數(也能夠說是混種函數) 
緣由是: This function returns random bytes from an OS-specific randomness source. (函數返回的隨機字節是根據不一樣的操做系統特定的隨機函數資源。即,這個函數是調用OS內部自帶的隨機函數的。有特異性)

 

使用方法:

import os
from hashlib import md5

for i in range(10):
    print md5(os.urandom(24)).hexdigest()

二、hmac: 咱們徹底能夠用hashlib來實現,可是學個新的嗎,沒什麼很差的,這個操做更方便一些。

    Python自帶的hmac模塊實現了標準的Hmac算法,咱們首先須要準備待計算的原始消息message,隨機key,哈希算法,這裏採用MD5,使用hmac的代碼以下:
import hmac
message = b'Hello world'
key = b'secret'
h = hmac.new(key,message,digestmod='MD5')
print(h.hexdigest())
比較兩個密文是否相同,能夠用hmac.compare_digest(密文、密文),然會True或者False。

 

可見使用hmac和普通hash算法很是相似。hmac輸出的長度和原始哈希算法的長度一致。須要注意傳入的key和message都是bytes類型,str類型須要首先編碼爲bytes

def hmac_md5(key, s):
    return hmac.new(key.encode('utf-8'), s.encode('utf-8'), 'MD5').hexdigest()

class User(object):
    def __init__(self, username, password):
        self.username = username
        self.key = ''.join([chr(random.randint(48, 122)) for i in range(20)])
        self.password = hmac_md5(self.key, password)

 

十二 socketserver模塊實現併發

  爲何要講socketserver?咱們以前寫的tcp協議的socket是否是一次只能和一個客戶端通訊,若是用socketserver能夠實現和多個客戶端通訊。它是在socket的基礎上進行了一層封裝,也就是說底層仍是調用的socket,在py2.7裏面叫作SocketServer也就是大寫了兩個S,在py3裏面就小寫了。後面咱們要寫的FTP做業,須要用它來實現併發,也就是同時能夠和多個客戶端進行通訊,多我的能夠同時進行上傳下載等。
 
  那麼咱們先看socketserver怎麼用呢,而後在分析,先看下面的代碼
import socketserver                              #一、引入模塊
class MyServer(socketserver.BaseRequestHandler): #二、本身寫一個類,類名本身隨便定義,而後繼承socketserver這個模塊裏面的BaseRequestHandler這個類

    def handle(self):                            #三、寫一個handle方法,必須叫這個名字
        #self.request                            #六、self.request 至關於一個conn

        self.request.recv(1024)                  #七、收消息
        msg = '親,學會了嗎'
        self.request.send(bytes(msg,encoding='utf-8')) #八、發消息

        self.request.close()                     #九、關閉鏈接

        # 拿到了咱們對每一個客戶端的管道,那麼咱們本身在這個方法裏面的就寫咱們接收消息發送消息的邏輯就能夠了
        pass
if __name__ == '__mian__':
    #thread 線程,如今只須要簡單理解線程,彆着急,後面很快就會講到啦,看下面的圖
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)#四、使用socketserver的ThreadingTCPServer這個類,將IP和端口的元祖傳進去,還須要將上面我們本身定義的類傳進去,獲得一個對象,至關於咱們經過它進行了bind、listen
    server.serve_forever()                       #五、使用咱們上面這個類的對象來執行serve_forever()方法,他的做用就是說,個人服務一直開啓着,就像京東同樣,不能關閉網站,對吧,而且serve_forever()幫咱們進行了accept


#注意:
#有socketserver 那麼有socketclient的嗎?
#固然不會有,我要做爲客戶去訪問京東的時候,京東幫我也客戶端了嗎,客戶端是否是在咱們本身的電腦啊,而且socketserver對客戶端沒有過高的要求,只須要本身寫一些socket就好了。

 

ThreadingTCPServer,多線程,簡單解釋:看圖

  

  經過上面的代碼,咱們來分析socket的源碼:(你們還記得面向對象的繼承嗎,來,實戰的時候來啦)

在整個socketserver這個模塊中,其實就幹了兩件事情:一、一個是循環創建連接的部分,每一個客戶連接均可以鏈接成功  2、一個通信循環的部分,就是每一個客戶端連接成功以後,要循環的和客戶端進行通訊。
看代碼中的:server=socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)

還記得面向對象的繼承嗎?來,你們本身嘗試着看看源碼:

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

實例化獲得server,先找ThreadMinxIn中的__init__方法,發現沒有init方法,而後找類ThreadingTCPServer的__init__,在TCPServer中找到,在裏面建立了socket對象,進而執行server_bind(至關於bind),server_active(點進去看執行了listen)
找server下的serve_forever,在BaseServer中找到,進而執行self._handle_request_noblock(),該方法一樣是在BaseServer中
執行self._handle_request_noblock()進而執行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),而後執行self.process_request(request, client_address)
在ThreadingMixIn中找到process_request,開啓多線程應對併發,進而執行process_request_thread,執行self.finish_request(request, client_address)
上述四部分完成了連接循環,本部分開始進入處理通信部分,在BaseServer中找到finish_request,觸發咱們本身定義的類的實例化,去找__init__方法,而咱們本身定義的類沒有該方法,則去它的父類也就是BaseRequestHandler中找....
源碼分析總結:

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

  self.server即套接字對象
  self.request即一個連接
  self.client_address即客戶端地址
基於udp的socketserver咱們本身定義的類中的

  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)>)
  self.client_address即客戶端地址

一個完整的sockeserver代碼示例:

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

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

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

 

import socket

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

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

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

 

十三 網絡編程的做業

   好了同窗們,到了這兒,咱們的網絡編程socket就講完了,大體就是這些內容,給你們留個做業:(你的努力的成果你本身是看的到的~!)
  加粗的是必需要作的,傾斜的是比較有難度的,你們別放鬆呀。
     1. 多用戶同時登錄
    2. 用戶登錄,加密認證
    3. 上傳/下載文件,保證文件一致性
    4. 傳輸過程當中現實進度條
    5. 不一樣用戶家目錄不一樣,且只能訪問本身的家目錄
    6. 對用戶進行磁盤配額、不一樣用戶配額可不一樣
    7. 用戶登錄server後,可在家目錄權限下切換子目錄
    8. 查看當前目錄下文件,新建文件夾
    9. 刪除文件和空文件夾
    10. 充分使用面向對象知識
    11. 支持斷點續傳

  簡單分析一下實現方式:

  1.字符串操做以及打印 —— 實現上傳下載的進度條功能

  2.socketserver —— 實現ftp server端和client端的交互

  3.struct模塊 —— 自定製報頭解決文件上傳下載過程當中的粘包問題

  4.hashlib或者hmac模塊 —— 實現文件的一致性校驗和用戶密文登陸

  5.os模塊 —— 實現目錄的切換及查看文件文件夾等功能

  6.文件操做 —— 完成上傳下載文件及斷點續傳等功能

 

看一下流程圖:

  

相關文章
相關標籤/搜索