第二十八章 網絡編程------Socket

本章目錄html

一.什麼是socketpython

二.爲何須要socketlinux

三.socket的發展程序員

四.python中的socketshell

五.基於TCP的socket編程

六.基於UDP的socketjson

六. 粘包問題詳解windows

七.粘包的解決方案設計模式

 

引入:爲何必定要先學習網絡協議?緩存

之因此學習網絡編程就是爲了讓咱們的程序可以利用網絡來傳輸數據,開發出C/S構架的應用程序

而網絡的核心,就是協議,沒有協議就沒有互聯網,咱們要開發出C/S結構程序則必須遵循這些協議的標準!

就像上帝說能夠幫你完成一個願望,然而上帝和你對話時,你說的話上帝徹底聽不懂,全部你必須瞭解上帝使用的語言,說他能聽懂的話!

一.什麼是socket

Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。

socket在OSI模型中的位置

yaasdcf

二.爲何須要socket

在標準的OIS模型中並無規定說必須有socket層,也就是說不使用socket也能完成通信,是的,的確如此!

那爲何須要socket呢?一個字 懶,程序員都是懶的!

咱們發現尚未開始實現應用程序邏輯,就須要花大把時間來實現各類協議,太特麼費事兒了,就有人專門把協議中一堆複雜的事情進行了封裝,因而socket就誕生了!

有了socket之後,無需本身編寫代碼實現三次握手,四次揮手,ARP請求,打包數據等等,socket已經封裝好了,只須要遵循socket的規定去編程,寫出的程序天然就是遵循tcp/udp標準的。

三.socket的發展

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

四.python中的socket

需明確:關於網絡協議 和socket相關概念,對於全部編程語言都是一致的,區別僅僅是各編程語言的函數名稱不一樣

# 1.導入socket模塊
import socket
# 2.建立socket對象 函數定義以下
socket.socket(socket_family,socket_type,protocal=0)
#socket_family 能夠是 AF_UNIX 或 AF_INET。
#socket_type 能夠是 SOCK_STREAM表示TCP協議 或 SOCK_DGRAM表示UDP協議。
#protocol 通常不填,默認值爲 0。
   
# 2.1獲取TCP 套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 或者 後面的參數都有默認值,能夠不寫,默認建立的是TCP協議socket
tcpSock = socket.socket()

# 2.2獲取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)

要明確一點:不管是客戶端服務器端都使用的都是socket對象

服務端套接字函數
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()      獲得阻塞套接字操做的超時時間

四.1基於TCP的socket

前言:已經明確socket是別人封裝好的接口,使用接口就變的簡單了

按照通信流程編寫代碼便可

1.TCP通信流程

重點是要先接受通話請求 才能創建鏈接進行通話

TCP的通信流程與打電話的過程很是類似

買手機 == socket() 裝進手機卡 == bind() 待機 == listen() 電話來了 接受通話 == accept() 聽 == read() 說 == write() 掛電話 == close()

2.TCP服務端

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('接到來自%s的電話' %addr[0])

msg=conn.recv(BUFSIZE)             #聽消息,聽話
print(msg,type(msg))

conn.send(msg.upper())          #發消息,說話

conn.close()                    #掛電話

s.close()                       #手機關機

服務端

3.TCP客戶端

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

注意TCP中必須先啓動服務器再啓動客戶端,不然客戶端因爲沒法連接服務器,直接報錯!

如上就完成了一個最基本的TCP通信,可是創建是爲了傳輸數據,二傳輸數據不少時候並非一次性就傳輸完成了,須要屢次收發過程,因此須要給代碼加上循環

4.改進版服務器端

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)             #聽消息,聽話
       print(msg,type(msg))
       conn.send(msg.upper())          #發消息,說話
   conn.close()                    #掛電話
s.close()                       #手機關機

5.改進版客戶端

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

6.常見錯誤:

1.端口占用

在調試過程當中,可能會碰見如下錯誤:

問題發生緣由:

1.多是因爲你已經啓動了服務器程序,卻又再次啓動了服務器程序,同一個端口不能被多個進程使用致使!

2.三次握手或四次揮手時,發生了異常致使對方程序已經結束而服務器任然處於time_wait狀態致使!

3.在高併發的場景下,因爲連接的客戶端太多,也會產生大量處於time_wait狀態的連接

解決的方案:

第1種緣由,很簡單關閉以前運行的服務器便可

第2,3中緣由致使的問題,有兩種解決方案:

1.設置服務器重用端口

#加入一條socket配置,重用ip和端口
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,必須在bind前加
phone.bind(('127.0.0.1',8081))

2.經過調整linux內核參數解決(瞭解)

發現系統存在大量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 時間

2.強行關閉連接

(發生錯誤演示,運行上面的改進版的服務器與客戶端,連接成功後直接中止客戶端程序)

當客服端與服務器連接成功後,若是一方沒有執行close,而是直接強行終止程序(或是遇到異常被迫終止),都會致使另外一方發送問題,

在windows下,接收數據的一方在recv函數處將拋出異常

Traceback (most recent call last):
 File "C:/Users/jerry/PycharmProjects/untitled/TCP/server.py", line 9, in <module>
   conn.recv(1024)
ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的鏈接。

linux下,不會拋出異常會致使接收數據的一方,recv方法不斷的收到空消息,形成死循環

要使應用程序可以在不一樣平臺正常工做,那須要分別處理這兩個問題

解決方案以下:

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()          
   while True:                        
       try:
           msg=conn.recv(BUFSIZE)            
           #linux不會拋出異常,會接收到空消息,這裏加以判斷
           if not msg:
               conn.close()
               break
           print(msg,type(msg))
           conn.send(msg.upper())        
      except ConnectionResetError:
      #只要異常發生則意味着對方以及關閉了,服務器也相應的關閉該連接
           conn.close()
           break
   conn.close()              
s.close()                      

至此TCP通信模板程序就完成了,能夠不斷的接收新的連接,不斷的收發消息,而且不會由於客戶端強制關閉而異常退出!

五.基於UDP的socket

1.UDP 通信流程

UDP通信流程與對講機很是相似

買傳呼機 == socket() 固定對講頻道 == bind() 收信號 == recvfrom() 發信號 == sendto() 因爲不須要創建 鏈接因此省去 TCP的listen()和accept()這兩步

2.UDP服務器端

import socket
ip_port=('127.0.0.1',9000) # 固定通信頻道
BUFSIZE=1024
#在TCP中socket的初始化參數能夠省略, 由於默認建立的就是TCP協議的socket
#而UDP則必須手動指定相關參數
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) # 發信息

3.UDP客戶端

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) #收消息

udp是無連接的,先啓動哪一端都不會報錯,即便對方地址根本不存在也不會報錯,強制關閉任何一方也沒有任何問題

另外,因爲無鏈接的特色,服務器不須要針對摸個客戶端進行循環,只要循環的接收便可,誰發來的消息均可以被處理,基於這個特色咱們能夠編寫一個UDP程序,實現多個客戶端同時與服務器通信

4.UDP聊天服務器

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)

5.UDP聊天客戶端

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

qq_name_dic={
   '狗哥':('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()

客戶端1

客戶端2

服務器:

 

六. 粘包問題

什麼是粘包?

粘包指的是數據與數據之間沒有明確的分界線,致使不能正確讀取數據!

要理解粘包問題,須要先了解TCP協議傳輸數據時的具體流程,TCP協議也稱之爲流式協議(UDP稱爲數據報協議)

應用程序沒法直接操做硬件,應用程序想要發送數據則必須將數據交給操做系統,而操做系統須要須要同時爲全部應用程序提供數據傳輸服務,也就意味着,操做系統不可能立馬就能將應用程序的數據發送出去,就須要爲應用程序提供一個緩衝區,用於臨時存放數據,具體流程以下:

發送方:

當應用程序調用send函數時,應用程序會將數據從應用程序拷貝到操做系統緩存,再由操做系統從緩衝區讀取數據併發送出去

接收方:

對方計算機收到數據也是操做系統先收到,至於應用程序什麼時候處理這些數據,操做系統並不清楚,因此一樣須要將數據先存儲到操做系統的緩衝區中,當應用程序調用recv時,其實是從操做系統緩衝區中將數據拷貝到應用程序的過程

上述過程對於TCP與UDP都是相同的不一樣之處在於:

UDP:

UDP在收發數據時是基於數據包的,即一個包一個包的發送,包與包之間有着明確的分界,到達對方操做系統緩衝區後也是一個一個獨立的數據包,接收方從操做系統緩衝區中將數據包拷貝到應用程序

這種方式存在的問題:

1.發送方發送的數據長度每一個操做系統會有不一樣的限制,數據超過限制則沒法發送

2.接收方接收數據時若是應用程序的提供的緩存容量小於數據包的長度將形成數據丟失,而緩衝區大小不可能無限大

TCP:

當咱們須要傳輸較大的數據,或須要保證數據完整性時,最簡單的方式就是使用TCP協議了

與UDP不一樣的是,TCP增長了一套校驗規則來保證數據的完整性,會將超過TCP包最大長度的數據拆分爲多個TCP包 並在傳輸數據時爲每個TCP數據包指定一個順序號,接收方在收到TCP數據包後按照順序將數據包進行重組,重組後的數據全都是二進制數據,且每次收到的二進制數據之間沒有明顯的分界

基於這種工做機制TCP在三種狀況下會發送粘包問題

1.當單個數據包較小時接收方可能一次性讀取了多個包的數據

2.當總體數據較大時接收方可能一次僅讀取了一個包的一部份內容

3.另外TCP協議爲了提升效率,增長了一種優化機制,會將數據較小且發送間隔較短的數據合併發送,該機制也會致使發送方將兩個數據包粘在一塊兒發送

七.粘包的解決方案

理解了粘包問題後,接下來就是如何來解決它了

1.基礎解決方案

首先明確只有TCP會出現粘包問題,之因此粘包是由於接收方不知道一次該接收的數據長度,那如何才能讓接收方知道數據的長度呢?

解決方案:

在發送數據前先發送數據長度

客戶端:
import socket

c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
   cmd = input(">>>:").strip()
   c.send(cmd.encode("utf-8"))

   data = c.recv(1024)
   length = int(data.decode("utf-8"))
   print(length)
   size = 0
   res = b""
   while size < length:
       temp = c.recv(1024)
       size += len(temp)
       res += temp
   print(res.decode("gbk"))
服務器:
import socket
import subprocess
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()

while True:
   client, addr = server.accept()
   while True:
       cmd = client.recv(1024).decode("utf-8")
       p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
       data = p.stdout.read()+p.stderr.read()
       length = str(len(data))
       client.send(length.encode("utf-8"))
       print(length)
       client.sendall(data)

上述方案 看起來解決了粘包問題

可是因爲negle優化機制的存在,長度信息和數據仍是有可能會粘包,而接受方並不知道長度信息具體幾個字節,因此如今的問題是如何可以長度信息作成一個固定長度的bytes數據

咱們能夠將字符串拼接爲一個固定長度的字符 可是這樣太麻煩,struct模塊爲咱們提供了一個功能,能夠將整數類型轉換爲固定長度的bytes,此時就派上用場了

# struct模塊使用
import  struct
# 整型轉bytes
res =  struct.pack("i",100)
print(res)
print(len(res))

# bytes轉整型
res2 = struct.unpack("i",res) # 返回一個元組
print(res2)
print(res2[0])

解決方案修正

客戶端:
import socket
import struct
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
   cmd = input(">>>:").strip()
   c.send(cmd.encode("utf-8"))

   data = c.recv(4)
   length = struct.unpack("i",data)[0]
   
   print(length)
   size = 0
   res = b""
   while size < length:
       temp = c.recv(1024)
       size += len(temp)
       res += temp
   print(res.decode("gbk"))
服務器:
import socket
import subprocess
import struct
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()

while True:
   client, addr = server.accept()
   while True:
       cmd = client.recv(1024).decode("utf-8")
       p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)
       data = p.stdout.read()+p.stderr.read()
       length = len(data)
       len_data = struct.pack("i",length)
       client.send(len_data)

       print(length)
       client.send(data)

 

2.自定義報頭解決粘包

上述方案已經完美解決了粘包問題,可是擴展性不高,例如咱們要實現文件上傳下載,不光要傳輸文件數據,還須要傳輸文件名字,md5值等等,如何能實現呢?

具體思路:

發送端:

1.先將全部的額外信息打包到一個頭中

2.而後先發送頭部數據

3.最後發送真實數據

接收端:

1.接收固定長度的頭部長度數據

2.根據長度數據獲取頭部數據

3.根據頭部數據獲取真實數據

 

CMD程序自定義報頭:

客戶端:
import socket
import struct
import json
c = socket.socket()
c.connect(("127.0.0.1",8888))
while True:
   cmd = input(">>>:").strip()
   c.send(cmd.encode("utf-8"))

   # 頭部數據
   data = c.recv(4)
   head_length = struct.unpack("i",data)[0]
   head_data = c.recv(head_length).decode("utf-8")
   head = json.loads(head_data)
   print(head)

   # 真實數據長度
   data_length = head["data_size"]

   #接收真實數據
   size = 0
   res = b""
   while size < data_length:
       temp = c.recv(1024)
       size += len(temp)
       res += temp

   print(res.decode("gbk"))
服務器:



import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(("127.0.0.1",8888))
server.listen()

while True:
   client, addr = server.accept()
   while True:
       cmd = client.recv(1024).decode("utf-8")
       p = subprocess.Popen(cmd,shell=True,stdout=-1,stderr=-1)

       # 真實數據
       data = p.stdout.read() + p.stderr.read()
       
       # 頭部數據
       head = {"data_size":len(data),"額外信息":"額外的值"}
       head_data = json.dumps(head).encode("utf-8")
       #頭部長度
       head_len = struct.pack("i",len(head_data))

       #逐個發送
       client.send(head_len)
       client.send(head_data)
       client.send(data)

至此粘包問題就完美解決了

相關文章
相關標籤/搜索