C/S 既(Client與Server),中文意思:客戶端與服務器端架構,這種架構也是從用戶層面(物理層面)來劃分的html
這裏的客戶端通常泛指客戶端應用程序exe,程序須要先安裝後,才能運行在用戶的的計算機上,對用戶的電腦操做系統環境依賴較大python
B/S既:Browser與Server的意思,中文意思:瀏覽器端與服務器端架構,這種架構是從用戶層面劃分的。算法
Browser瀏覽器,其實也是一種client客戶端,只是這個客戶端不須要你們安裝什麼應用程序,只須要用戶在瀏覽器上經過HTTP請求服務器端相關的資源(網頁資源),客戶端Browser就能鏡像增刪改查編程
網絡基礎json
首先程序必需要啓動,其次,必須有這臺機器的地址,而在互聯網中,一臺計算機的地址,就是使用一串數字表示,好比 78.5.6.29
windows
<details> <summary><mark><font color=darkred>什麼是IP地址?</font></mark></summary> IP地址是指互聯網協議地址(英語:Internet Protocol Address,又譯爲網際協議地址),是IP Address的縮寫。IP地址是IP協議提供的一種統一的地址格式,它爲互聯網上的每個網絡和每一臺主機分配一個邏輯地址,以此來屏蔽物理地址的差別。 IP地址是一個32位的二進制數,一般被分割爲4個「8位二進制數」(也就是4個字節)。IP地址一般用「點分十進制」表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之間的十進制整數。例:點分十進IP地址(100.4.5.6),其實是32位二進制數(01100100.00000100.00000101.00000110)。 </details>設計模式
<details> <summary><mark><font color=darkred>什麼是端口?</font></mark></summary> "端口"是英文port的意譯,能夠認爲是設備與外界通信交流的出口。 </details>瀏覽器
<details> <summary><mark><font color=darkred>在windows查看端口的佔用</font></mark></summary> netstat -aon|findstr "49157" </details>緩存
所以ip
地址精確到具體的一臺電腦,而端口精確到具體的程序。服務器
Socket是應用層與TCP/IP協議組通訊的中間軟件抽象層,他是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議隱藏在Socket接口後面,對於用戶來講,一組簡單的接口,就是所有,讓Socket去組織數據,以符合指定的協議。
<details> <summary><mark><font color=darkred>站在你的角度看socket</font></mark></summary> 其實站在你的角度上看,socket就是一個模塊。咱們經過調用模塊中已經實現的方法創建兩個進程之間的鏈接和通訊。 也有人將socket說成ip+port,由於ip是用來標識互聯網中的一臺主機的位置,而port是用來標識這臺機器上的一個應用程序。 因此咱們只要確立了ip和port就能找到一個應用程序,而且使用socket模塊來與之通訊。 </details>
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 所以,有時人們也把套接字稱爲「伯克利套接字」或「BSD 套接字」。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通信。這也被稱進程間通信,或 IPC。套接字有兩種(或者稱爲有兩個種族),分別是基於文件型的和基於網絡型的
套接字家族的名字:AF_UNIX
unix
一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其餘的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是不多被使用,或者是根本沒有實現,全部地址家族中,AF_INET是使用最普遍的一個,python支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候我麼只使用AF_INET)
Transmisson Control Protocol
)可靠的,面向鏈接的協議(打電話)傳輸效率低全雙工通訊(發送緩存&&接受緩存),面向字節流。使用TCP的應用:WEB瀏覽器;電子郵件;文件傳輸程序User Data Protocol
)不可靠的,無鏈接的服務,傳輸效率高(發送前延時小),一對一,一對多,多對一,面向報文,盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統;視頻流;IP語音(VoIP)。tcp
是基於連接的,必須先啓動服務端,而後再啓動客戶端去連接服務端
import socket sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 買手機 # family = socket.AF_INET 當前基於網絡通訊的 # type = socket.SOCK_STREAM 默認是tcp協議 sk.bind(('127.0.0.1',9000)) # 裝上電話卡 sk.listen() # 開機 while True: conn,addr = sk.accept() # 等電話,等待客戶端來連接我 # conn 就是server和客戶端創建起來的一個鏈接 while True: msg_send = input('>>>') conn.send(msg_send.encode('utf-8')) # 向客戶端發送信息 if msg_send.upper() == 'Q': break # 判斷輸入的爲q 退出 msg = conn.recv(1024).decode('utf-8') #接收客戶端信息 if msg.upper() == 'Q': break print(msg) conn.close() # 掛電話 sk.close() # 關手機 # 不管在server 仍是 client 只要輸入q 就兩邊斷開鏈接 - 掛電話
import socket sk = socket.socket() # 實例化一個socket對象 sk.connect(('127.0.0.1',9000)) while True: msg = sk.recv(1024).decode('utf-8') # 字節 阻塞直到有數據發送過來 if msg.upper() == 'Q': break print(msg) # 字節轉字符串 decode msg_send = input('>>>') # input寫入的是字符串 sk.send(msg_send.encode('utf-8')) # 發送的是字節,字符串轉字節 encode if msg_send.upper() == 'Q': break sk.close()
udp
是無連接的,啓動服務以後能夠直接接受消息,不須要提早創建連接
import socket sk = socket.socket(type=socket.SOCK_DGRAM) #建立一個服務器的套接字 sk.bind(('127.0.0.1',8001)) #綁定服務器套接字 while True: msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8')) send_msg = input('>>>') sk.sendto(send_msg.encode('utf-8'),addr) # 對話(接收與發送) sk.close() # 關閉服務器套接字
import socket sk = socket.socket(type=socket.SOCK_DGRAM) while True: send_msg = input('>>>') sk.sendto(send_msg.encode('utf-8'),('127.0.0.1',8001)) msg,addr = sk.recvfrom(1024) print(msg.decode('utf-8')) sk.close()
import socket ip_port = ('127.0.0.1',9001) 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('utf8'))) back_msg = input('回覆消息:').strip() udp_server_sock.sendto(back_msg.encode('utf8'),addr)
import socket BUFSIZE = 1024 udp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic = { '金老爸':('127.0.0.1',9001), 'Alex':('127.0.0.1',9001) } while True: qq_name = input('請選擇聊天對象:').strip() while True: msg = input('請輸入消息,回車發送,輸入q結束和他的聊天: ').strip() if msg == 'q':break if not msg or not qq_name or not qq_name in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf8'),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('utf8'))) udp_client_socket.close()
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.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 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('utf8') back_msg = strftime(time_fmt) tcp_server.sendto(back_msg.encode('utf8'),addr) tcp_server.close()
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('utf8'),ip_port) data = tcp_client.recv(BUFSIZE) print(data.decode('utf8'))
from socket import * updsocket = socket(type = SOCK_DGRAM) addr = ("192.168.0.168",2425) msg = input('>>>') updsocket.sendto(("1:111:eva:eva:32:%s"%msg).encode('gbk'),addr)
飛秋運行時,會監聽2425端口,因此咱們首先要和本地創建UDP鏈接 1:111:eva:eva:32:要發送的內容 1表示版本號,111標識包號,
eva
表示用戶名,第二個eva
表示主機名,32表示發送消息,後面的表示要發送的消息內容。
socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)
建立socket對象的參數說明:
參數 | 說明 |
---|---|
family | 地址系列應爲AF_INET(默認值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。(AF_UNIX 域其實是使用本地 socket 文件來通訊) |
type | 套接字類型應爲SOCK_STREAM(默認值),SOCK_DGRAM,SOCK_RAW或其餘SOCK_常量之一。<SOCK_STREAM 是基於TCP的,有保障的(即能保證數據正確傳送到對方)面向鏈接的SOCKET,多用於資料傳送。 SOCK_DGRAM 是基於UDP的,無保障的面向消息的socket,多用於在網絡上發廣播信息。 |
proto | 協議號一般爲零,能夠省略,或者在地址族爲AF_CAN的狀況下,協議應爲CAN_RAW或CAN_BCM之一。 |
fileno | 若是指定了fileno,則其餘參數將被忽略,致使帶有指定文件描述符的套接字返回。<br/>與socket.fromfd()不一樣,fileno將返回相同的套接字,而不是重複的。<br/>這可能有助於使用socket.close()關閉一個獨立的插座。 |
咱們如今來看一中現象
# server端 import socket sk = socket.socket() sk.bind(('127.0.0.1', 9000)) sk.listen() conn,addr = sk.accept() for i in range(3): conn.send(b'sbzz') conn.close() sk.close() # 客戶端 import socket sk = socket.socket() sk.connect(('127.0.0.1', 9000)) for i in range(3): print(sk.recv(1024)) sk.close()
上述代碼執行後的結果
原本正常接受的應該是三次sbzz
,可是到第二次的時候,第二次和第三次的sbzz
粘在一塊兒,這種現象就叫粘包
注意:只有TCP有粘包現象,UDP永遠不會粘包
流式傳輸:特指 TCP協議,像流水同樣,無邊界
當發送端緩衝區的長度大於網卡的MTU時,
tcp
會將此次發送的數據拆成幾個數據包發送出去。 MTU是Maximum Transmission Unit
的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500。若是本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度。
TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。 收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。 這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。 對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。 可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。 也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。 而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。 怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。
socket數據傳輸過程當中的用戶態與內核態說明
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。
UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。 不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。 對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。 不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y;x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠。
補充說明:
用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送)
用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比較短,可能會等待和下一次數據一塊兒發送。
發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)
#_*_coding:utf-8_*_ 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()
#_*_coding:utf-8_*_ 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) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
#_*_coding:utf-8_*_ 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(2) #一次沒有收完整 data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
#_*_coding:utf-8_*_ 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) s.send('hello egg'.encode('utf-8'))
黏包現象只發生在tcp協議中:
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據。
import socket import struct def my_send(conn,msg): msgb = msg.encode('utf-8') len_msg = len(msgb) pack_len = struct.pack('i', len_msg) conn.send(pack_len) conn.send(msgb) sk = socket.socket() sk.bind(('127.0.0.1',9002)) sk.listen() conn,addr = sk.accept() msg1 = '你好' msg2 = '吃了麼' my_send(conn,msg1) my_send(conn,msg2) conn.close() sk.close()
import time import socket import struct sk = socket.socket() def my_recv(sk): pack_len = sk.recv(4) len_msg = struct.unpack('i', pack_len)[0] msg = sk.recv(len_msg).decode('utf-8') return msg sk.connect(('127.0.0.1',9002)) for i in range(100000):i*2 msg = my_recv(sk) print(msg) msg = my_recv(sk) print(msg) sk.close()
存在的問題: 程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
剛剛的方法,問題在於咱們咱們在發送
咱們能夠藉助一個模塊,這個模塊能夠把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息以前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就中止,就能恰好很少很多的接收完整的數據了。
該模塊能夠把一個類型,如數字,轉成固定長度的bytes
>>> struct.pack('i',1111111111111) struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍
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)
更詳細的用法
#_*_coding:utf-8_*_ #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = (1, 'abc'.encode('utf-8'), 2.7) values2 = ('defg'.encode('utf-8'),101) s1 = struct.Struct('I3sf') s2 = struct.Struct('4sI') print(s1.size,s2.size) prebuffer=ctypes.create_string_buffer(s1.size+s2.size) print('Before : ',binascii.hexlify(prebuffer)) # t=binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer,0,*values1) s2.pack_into(prebuffer,s1.size,*values2) print('After pack',binascii.hexlify(prebuffer)) print(s1.unpack_from(prebuffer,0)) print(s2.unpack_from(prebuffer,s1.size)) s3=struct.Struct('ii') s3.pack_into(prebuffer,0,123,123) print('After pack',binascii.hexlify(prebuffer)) print(s3.unpack_from(prebuffer,0))
藉助struct模塊,咱們知道長度數字能夠被轉換成一個標準大小的4字節數字。所以能夠利用這個特色來預先發送數據長度。
發送時 | 接收時 |
---|---|
先發送struct轉換好的數據長度4字節 | 先接受4個字節使用struct轉換成數字來獲取要接收的數據長度 |
再發送數據 | 再按照長度接收數據 |
# 接收文件 import json import socket sk = socket.socket() sk.bind(('127.0.0.1',9001)) sk.listen() conn,addr = sk.accept() file_dic = conn.recv(1024).decode('utf-8') dic = json.loads(file_dic) with open(dic['filename'],mode='wb') as f: while dic['filesize']>0: file_content = conn.recv(1024) dic['filesize'] -= len(file_content) f.write(file_content) conn.close() sk.close()
import os import json import socket sk = socket.socket() sk.connect(('127.0.0.1',9001)) # 輸入須要發送的文件,獲取併發送文件大小 file_path = r'D:\ev錄屏保存的視頻\20190719_150518.mp4' file_name = os.path.basename(file_path) file_size = os.path.getsize(file_path) dic = {'filename':file_name,'filesize':file_size} str_dic = json.dumps(dic) dic_b = str_dic.encode('utf-8') sk.send(dic_b) with open(file_path,mode = 'rb') as f: content = f.read() sk.send(content) sk.close()
咱們還能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)
發送時 | 接收時 |
---|---|
先發報頭長度 | 先收報頭長度,用struct取出來 |
再編碼報頭內容而後發送 | 根據取出的長度收取報頭內容,而後解碼,反序列化 |
最後發真實內容 | 從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容 |