Python Socket

本節內容

  1. socket基礎html

  2. socket中的粘包
  3. 加密?python

1.socket基礎

socket一般也稱做"套接字",用於描述IP地址和端口,是一個通訊鏈的句柄,應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求。linux

socket起源於Unix,而Unix/Linux基本哲學之一就是「一切皆文件」,對於文件用【打開】【讀寫】【關閉】模式來操做。socket就是該模式的一個實現,socket便是一種特殊的文件,一些socket函數就是對其進行的操做(讀/寫IO、打開、關閉)算法

socket和file的區別:shell

  • file模塊是針對某個指定文件進行【打開】【讀寫】【關閉】
  • socket模塊是針對 服務器端 和 客戶端Socket 進行【打開】【讀寫】【關閉】

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

套接字家族的名字:AF_UNIXjson

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

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

套接字家族的名字:AF_INET服務器

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

簡單的先實現個socket交互:

import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen(5)
while True:
    conn,addr = sk.accept()
    while True:
        print('有人進來了',addr)
        re_data = conn.recv(1024)
        conn.send(re_data)
    conn.close()
服務端
import socket

conn = socket.socket()
conn.connect(('127.0.0.1',8088))

while True:
    data = input(':>>')
    conn.send(data.encode())
    re_data = conn.recv(1024)
    print(re_data.decode())
客戶端

來吧,看完代碼,說說socket的詳細用法吧。

Socket Families(地址簇)

socket.AF_UNIX unix本機進程間通訊,經過文件實現進程間的通訊。

socket.AF_INET IPV4協議

socket.AF_INET6 IPV6協議

Socket Types(socket類型)

socket.SOCK_STREAM  #使用FTP協議

socket.SOCK_DGRAM  #使用UDP協議

socket.SOCK_RAW  #原始套接字,普通的套接字沒法處理ICMP、IGMP等網絡報文,而SOCK_RAW能夠;其次,SOCK_RAW也能夠處理特殊的IPv4報文;此外,利用原始套接字,能夠經過IP_HDRINCL套接字選項由用戶構造IP頭。

socket.SOCK_RDM  #是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在須要執行某些特殊操做時使用,如發送ICMP報文。SOCK_RAM一般僅限於高級用戶或管理員運行的程序使用。

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM,proto=0, fileno=None)

參數一:地址簇

  socket.AF_INET IPv4(默認)
  socket.AF_INET6 IPv6

  socket.AF_UNIX 只可以用於單一的Unix系統進程間通訊

參數二:類型

  socket.SOCK_STREAM  流式socket , for TCP (默認)
  socket.SOCK_DGRAM   數據報式socket , for UDP

  socket.SOCK_RAW 原始套接字,普通的套接字沒法處理ICMP、IGMP等網絡報文,而SOCK_RAW能夠;其次,SOCK_RAW也能夠處理特殊的IPv4報文;此外,利用原始套接字,能夠經過IP_HDRINCL套接字選項由用戶構造IP頭。
  socket.SOCK_RDM 是一種可靠的UDP形式,即保證交付數據報但不保證順序。SOCK_RAM用來提供對原始協議的低級訪問,在須要執行某些特殊操做時使用,如發送ICMP報文。SOCK_RAM一般僅限於高級用戶或管理員運行的程序使用。
  socket.SOCK_SEQPACKET 可靠的連續數據包服務

參數三:協議

  0  (默認)與特定的地址家族相關的協議,若是是 0 ,則系統就會根據地址格式和套接類別,自動選擇一個合適的協議

import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 能夠是 AF_UNIX 或 AF_INET。socket_type 能夠是 SOCK_STREAM 或 SOCK_DGRAM。protocol 通常不填,默認值爲 0。

獲取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

獲取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()      獲得阻塞套接字操做的超時時間

面向文件的套接字的函數
s.fileno()          套接字的文件描述符
s.makefile()        建立一個與該套接字相關的文件
socket的其餘用法

下面先基於TCP來簡單實現信息交互。

# 代碼省略,其實就是

  sk = socket.socket(socket.AF_INIT,socket.SOCK_STREAM)

問題:

這個是因爲你的服務端仍然存在四次揮手的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 時間

# 備註:我沒試過
方法二

知識點: 說下TCP和UDP鏈接方式的不一樣。

tcp:send發消息,recv收消息

udp:sendto發消息,recvfrom收消息

1.tcp協議:

(1)若是收消息緩衝區裏的數據爲空,那麼recv就會阻塞

(2)tcp基於連接通訊,若是一端斷開了連接,那另一端的連接也跟着完蛋recv將不會阻塞,收到的是空

2.udp協議

(1)若是收消息緩衝區裏的數據爲「空」,recvfrom不會阻塞

(2)recvfrom收的數據小於sendinto發送的數據時,數據丟失

(3)只有sendinto發送數據沒有recvfrom收數據,數據丟失

注意:

1.你單獨運行上面的udp的客戶端,你發現並不會報錯,相反tcp卻會報錯,由於udp協議只負責把包發出去,對方收不收,我根本無論,而tcp是基於連接的,必須有一個服務端先運行着,客戶端去跟服務端創建連接而後依託於連接才能傳遞消息,任何一方試圖把連接摧毀都會致使對方程序的崩潰。

2.上面的udp程序,你註釋任何一條客戶端的sendinto,服務端都會卡住,爲何?由於服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。

3.總結:

1.udp的sendinto不用管是否有一個正在運行的服務端,能夠己端一個勁的發消息

2.udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠

3.tcp的協議數據不會丟,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。

2.socket中的粘包

讓咱們先基於socket來實現一個遠程執行命令的程序(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解碼

且只能從管道里讀一次結果

----<TCP中的粘包現象

先給你看看tcp中的粘包,讓你張張見識。~~

import socket
import subprocess
sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen(5)
while True:
    conn,addr = sk.accept()
    while True:
        print('有人進來了',addr)
        data = conn.recv(1024)
        print(data.decode())
        ret = subprocess.Popen(data.decode(),stdin=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               stdout=subprocess.PIPE,shell=True)
        err = ret.stderr.read()
        if err:
            re_data = err
        else:
            re_data = ret.stdout.read()
        conn.send(re_data)
    conn.close()
服務端
import socket

conn = socket.socket()
conn.connect(('127.0.0.1',8088))

while True:
    data = input(':>>')
    conn.send(data.encode())
    re_data = conn.recv(1024)
    print(re_data.decode('gbk'))
客戶端

來,運行讓你看看什麼是粘包

 

能夠看出在運行netstat -anlp時的命令貌似沒有顯示全,輸入ls的時候上一個命令的顯示會打印。why??

不急不急,在來看看udp的,等我5秒,我給你擼出來。~~~

(出了點小問題,6秒完成,哎!)

import socket
import subprocess
sk = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
sk.bind(('127.0.0.1',8088))

while True:

    data,addr = sk.recvfrom(4096)
    print('有人進來了',addr)
    print(data.decode())
    ret = subprocess.Popen(data.decode(),stdin=subprocess.PIPE,
                           stderr=subprocess.PIPE,
                           stdout=subprocess.PIPE,shell=True)
    err = ret.stderr.read()
    if err:
        re_data = err
    else:
        re_data = ret.stdout.read()
    sk.sendto(re_data, addr)
服務端
import socket

conn = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)


while True:
    data = input(':>>')
    conn.sendto(data.encode(),('127.0.0.1',8088))
    data,addr=conn.recvfrom(4096)
    print(data.decode('gbk'))
客戶端

有報錯盡然,算了。就當咱們成功了吧,你試試,說不許你比我牛逼。

2.1 什麼是粘包

說了這麼多你知道什麼是粘包了嗎?無論懂不懂,莫裝逼,先看我寫的吧。

須知:只有TCP有粘包現象,UDP永遠不會粘包,爲什麼,且聽我娓娓道來首先須要掌握一個socket收發消息的原理。

發送端能夠是一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協議會幫你封裝上消息頭,實驗略

2.2 發生粘包的兩種狀況

第一種:發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8088))
sk.listen(5)

conn,addr = sk.accept()
data1 = conn.recv(1024)
data2 = conn.recv(1024)
print('data1:',data1)
print('data2:',data2)
conn.close()
sk.close()
服務端
import socket

conn = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.connect(('127.0.0.1',8088))
conn.send(b'data1')
conn.send(b'data2')
客戶端

執行結果以下:

第二種:接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)

import socket

sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.bind(('127.0.0.1',8088))
sk.listen(5)

conn,addr = sk.accept()
data1 = conn.recv(10)
data2 = conn.recv(10)
print('data1:',data1)
print('data2:',data2)
conn.close()
sk.close()
服務端
import socket
conn=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.connect(('127.0.0.1',8088))
conn.send(b'12345678901234567890')
conn.close()
客戶端

執行結果以下:

拆包的發生狀況

當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。

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

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

而udp發送數據,對端是不會返回確認信息的,所以不可靠

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

recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據

send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失

2.3 解決粘包的方法

2.3.1 第一種方法

來個思路,在發送數據包以前,發送數據包的大小,客戶端收到後數據後給服務器回覆200確認消息,接下來客戶端按照服務器返回的數據包大小進行數據的接收。對對度~~~

# 第一次使用subprocess模塊執行客戶端命令,執行'netstat -an'時總是卡住,後期再排查吧。

import socket
import subprocess

sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen(5)
while True:
    conn,addr = sk.accept()
    while conn:
        comm = conn.recv(1024)
        res = subprocess.Popen(comm.decode(),stdout=subprocess.PIPE,
                               stdin=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               shell=True
                               )
        err = res.stderr.read()
        if err:
            ret = err
        else:
            ret = res.stdout.read()
        print(ret)
        print(len(ret))
        conn.send(str(len(ret)).encode('utf-8'))
        status_code = conn.recv(1024)
        print(status_code)
        if status_code == b'200':
            conn.sendall(ret)
服務端
import socket

conn = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.connect(('127.0.0.1',8088))
while True:
    comm = input(':>>').strip()
    conn.send(comm.encode())
    datesize = conn.recv(1024)  # 接受返回數據的大小
    conn.send(b'200')
    countsize = 0
    data = b''
    while  countsize < int(datesize.decode()):
        res = conn.recv(1024)
        data += res
        countsize+=len(res)
    print(data.decode('gbk'))
客戶端

還有個思路就是在發送數據以前先發送一個數據報頭,報頭中包含數據的各類信息...好了.點到爲止...

but...

程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗

強插一個知識

from terminaltables import AsciiTable
table_data = [
    ['Heading1', 'Heading2'],
    ['row1 column1', 'row1 column2'],
    ['row2 column1', 'row2 column2'],
    ['row3 column1', 'row3 column2'],
     ]
table = AsciiTable(table_data)

print(table.table)
terminaltables

2.3.2 第二種方法

利用模塊struct解決粘包的問題(使用os.popen模塊執行客戶端發來的命令)

import socket
import struct
import os

sk = socket.socket()
sk.bind(('127.0.0.1',8088))
sk.listen(5)
while True:
    conn,addr = sk.accept()
    while conn:
        comm = conn.recv(1024)
        ret_obj = os.popen(comm.decode())
        ret = ret_obj.read()
        print(ret)
        str_data = struct.pack('i',len(ret))
        conn.send(str_data)
        conn.sendall(ret.encode())
服務端
import socket
import struct

conn = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.connect(('127.0.0.1',8088))
while True:
    comm = input(':>>').strip()
    conn.send(comm.encode())
    size_bytes = conn.recv(4) # 接受返回數據的大小
    datesize = struct.unpack('i',size_bytes)
    print(datesize)
    countsize = 0
    data = b''
    while  countsize < datesize[0]:
        res = conn.recv(1024)
        data += res
        countsize+=len(res)
    print(data.decode('utf-8'))
客戶端

代碼寫的不詳細,但我思路清晰。

1.製做一個報頭信息,數據類型爲字典類型,報頭中存放着真實數據的信息(數據大小,MD5校驗碼,文件名等等的消息)

2.在發送真實數據以前先發送兩個數據,一個是報頭的大小,一個是報頭數據(發送的爲json數據信息)

3.報頭信息大小經過struct模塊發送固定字節大小的信息,裏邊存放的時報頭數據的大小。

4.客戶端根據收到的數據獲取報頭的大小,並接受報頭大小的數據,反序列化後獲得真實數據的信息。繼續接收真實數據。

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)
思路代碼

 

3.加密去看這個吧。http://www.cnblogs.com/40kuai/articles/6473804.html

相關文章
相關標籤/搜索