27 網路通訊協議 udp tcp
四 網路通訊協議(網際網路協議)
第二天再講這裡,大家第二天再看這裡把~~~
網路通訊協議是網路傳輸的靈魂,非常重要,協議即準則,準則是傳輸訊息的格式要求,那麼我們從電腦上發出一個訊息,到底是以什麼樣的訊息格式發到了對方的手上呢,來看一看這裡>>>,網路通訊協議
五 osi七層模型
網際網路的核心就是由一堆協議組成,協議就是標準,標準就是大家都認可的,所有人都按照這個來,這樣大家都能夠互相瞭解,互相深入了~~~比如全世界人通訊的標準是英語
五層通訊流程:
六 socket
結合上圖來看,socket在哪一層呢,我們繼續看下圖
socket在內的五層通訊流程:
Socket又稱為套接字,它是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。當我們使用不同的協議進行通訊時就得使用不同的介面,還得處理不同協議的各種細節,這就增加了開發的難度,軟體也不易於擴充套件(就像我們開發一套公司管理系統一樣,報賬、會議預定、請假等功能不需要單獨寫系統,而是一個系統上多個功能介面,不需要知道每個功能如何去實現的)。於是UNIX BSD就發明了socket這種東西,socket遮蔽了各個協議的通訊細節,使得程式設計師無需關注協議本身,直接使用socket提供的介面來進行互聯的不同主機間的程序的通訊。這就好比作業系統給我們提供了使用底層硬體功能的系統呼叫,通過系統呼叫我們可以方便的使用磁碟(檔案操作),使用記憶體,而無需自己去進行磁碟讀寫,記憶體管理。socket其實也是一樣的東西,就是提供了tcp/ip協議的抽象,對外提供了一套介面,同過這個介面就可以統一、方便的使用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)不可靠的、無連線的服務,傳輸效率高(傳送前時延小),一對一、一對多、多對一、多對多、面向報文(資料包),盡最大努力服務,無擁塞控制。使用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.pyView Code
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.pyView Code
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() #關閉伺服器套接字(可選) 解決辦法View Code
但是如果你加上了上面的程式碼之後還是出現這個問題: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端程式碼View Code
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端程式碼View Code
你會發現,第一個連線的客戶端可以和服務端收發訊息,但是第二個連線的客戶端發訊息服務端是收不到的
原因解釋: 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端就可以通訊的程式碼View Code
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端程式碼View Code
強制斷開連線之後的報錯資訊:
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.pyView Code
client端程式碼示例
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)View Code
類似於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端View Code
#_*_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端View Code
接下來,給大家說一個真實的例子,也就是實際當中應用的,那麼這是個什麼例子呢?就是我們電腦系統上的時間,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端View Code
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端View Code
UDP來個小練習吧:
練習的需求是這樣的:1、服務端需要提供的服務有:接收訊息(時間格式的字串)、將我的本地的時間轉換成接收到的訊息的格式(也就是個時間格式的字串)、發回給客戶端。2、客戶端自行想一下怎麼寫。
TCP協議和UDP協議下socket的基本使用ok了,那我們來深入分析一下socket。(這一塊的內容初學者不要看,對socket有些瞭解的同學可以研究一下,切記看不懂很正常,不要深究,現階段你們就是學習應用為主!)>>>>看這裡>>>>socket原理剖析,裡面包含socket中各個方法的作用和方法中的引數。
這裡我列出兩個簡易描述socket各個引數和方法的圖,共大家參考:
socket型別:
socket各個方法的解釋: