Python網絡編程中篇

socket編程javascript


Python 提供了兩個級別訪問的網絡服務。java

  • 低級別的網絡服務支持基本的 Socket,它提供了標準的 BSD Sockets API,能夠訪問底層操做系統Socket接口的所有方法。
  • 高級別的網絡服務模塊 SocketServer, 它提供了服務器中心類,能夠簡化網絡服務器的開發。

Socket是什麼 ?

  • Socket又稱"套接字",應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求,使主機間或者一臺計算機上的進程間能夠通信。

socket()函數

  • Python 中,咱們用 socket()函數來建立套接字,語法格式以下:
?
1
2
import socket
socket.socket([family[, type [, proto]]])
  • 參數

    family: 套接字家族可使AF_UNIX或者AF_INETpython

    type: 套接字類型能夠根據TCP(面向鏈接)的仍是UDP(非鏈接)分爲SOCK_STREAMSOCK_DGRAMlinux

    protocol: 通常不填默認爲0.shell

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() 綁定地址(host,port)到套接字, 在AF_INET下,以元組(host,port)的形式表示地址。
s.listen() 開始TCP監聽。backlog指定在拒絕鏈接以前,操做系統能夠掛起的最大鏈接數量。該值至少爲1,大部分應用程序設爲5就能夠了。
s.accept() 被動接受TCP客戶端鏈接,(阻塞式)等待鏈接的到來
客戶端套接字
s.connect() 主動初始化TCP服務器鏈接,。通常address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
s.recv() 接收TCP數據,數據以字符串形式返回,bufsize指定要接收的最大數據量。flag提供有關消息的其餘信息,一般能夠忽略。
s.send() 發送TCP數據,將string中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。
s.sendall() 完整發送TCP數據,完整發送TCP數據。將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。
s.recvform() 接收UDP數據,與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。
s.sendto() 發送UDP數據,將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。
s.close() 關閉套接字
s.getpeername() 返回鏈接套接字的遠程地址。返回值一般是元組(ipaddr,port)。
s.getsockname() 返回套接字本身的地址。一般是一個元組(ipaddr,port)
s.setsockopt(level,optname,value) 設置給定套接字選項的值。
s.getsockopt(level,optname[.buflen]) 返回套接字選項的值。
s.settimeout(timeout) 設置套接字操做的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。通常,超時期應該在剛建立套接字時設置,由於它們可能用於鏈接的操做(如connect())
s.gettimeout() 返回當前超時期的值,單位是秒,若是沒有設置超時期,則返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 若是flag爲0,則將套接字設爲非阻塞模式,不然將套接字設爲阻塞模式(默認值)。非阻塞模式下,若是調用recv()沒有發現任何數據,或send()調用沒法當即發送數據,那麼將引發socket.error異常。
s.makefile() 建立一個與該套接字相關連的文件

辣麼,如今能夠搭建一個簡單的服務端與客戶端之間的通訊編程

基於TCP的套接字windows

服務端服務器

咱們使用 socket 模塊的 socket 函數來建立一個 socket 對象。socket 對象能夠經過調用其餘函數來設置一個 socket 服務。cookie

如今咱們能夠經過調用 bind(hostname, port) 函數來指定服務的 port(端口)網絡

接着,咱們調用 socket 對象的 accept 方法。該方法等待客戶端的鏈接,並返回 connection 對象,表示已鏈接到客戶端。

複製代碼
#!/usr/bin/python3
# 文件名:server.py

# 導入 socket模塊
import socket

# 建立 socket 對象
serversocket = socket.socket(
            socket.AF_INET, socket.SOCK_STREAM) 

# 獲取本地主機名
host = socket.gethostname()

port = 8080

# 綁定端口
serversocket.bind((host, port))

# 設置最大鏈接數,超事後排隊
serversocket.listen(5)

while True:
    # 創建客戶端鏈接
    clientsocket,addr = serversocket.accept()      

    print("鏈接地址: %s" % str(addr))
    
    msg='welcome to JinCheng!'+ "\r\n"
    clientsocket.send(msg.encode('utf-8'))
    clientsocket.close()
複製代碼

客戶端

接下來咱們寫一個簡單的客戶端實例鏈接到以上建立的服務。

socket.connect(hosname, port ) 方法打開一個 TCP 鏈接到主機爲 hostname 端口爲 port 的服務商。鏈接後咱們就能夠從服務端後期數據,記住,操做完成後須要關閉鏈接。

複製代碼
 
  
#!/usr/bin/python3 # 文件名:client.py

# 導入 socket模塊
import socket

# 建立 socket 對象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 獲取本地主機名
host = socket.gethostname()

# 設置端口好
port = 8080

# 鏈接服務,指定主機和端口
s.connect((host, port))

# 接收小於 1024 字節的數據
msg = s.recv(1024)

s.close()

print (msg.decode('utf-8'))
複製代碼

上面的服務端與客戶端實現了"本身"與"本身"之間的簡單通訊

辣麼,基於上述也能夠實現一種模擬現實"打電話"的通訊

服務端

咱們想實現打電話的通訊,首先服務端要不斷接受鏈接,而後循環通訊,實現一種交互式的收發,通訊完畢後只關閉連接,服務器可以繼續接收下一次連接

import socket
ip_port = ("127.0.0.1",8080)# 電話卡
back_log = 5
buffer_size = 1024

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)#買手機
s.bind(ip_port)   #手機插卡
s.listen(back_log)#手機待機

while True:                 #新增接收連接循環,能夠不停的接電話
    conn,address = s.accept()#手機接電話
    # print(conn)
    print('接到來自%s的電話' %address)
    while True:#新增通訊循環,能夠不斷的通訊,收發消息
        try:   #新增捕捉異常的,若是不加,那麼正在連接的客戶端忽然斷開,recv便再也不阻塞,死循環發生
            ret = str(conn.recv(buffer_size),encoding="utf-8")
            print("from>>> " ,ret)  #收到來自於客戶端的消息
            nr = input(">>>")        #交互式發送,發給客戶端消息
            conn.sendall(bytes(nr, encoding="utf-8"))
        except Exception:
            break

    #conn.close() #掛電話(可選)

#s.close()        #手機關機(可選)
服務端

客戶端

import socket
ip_port = ("127.0.0.1",8080)
buffer_size = 1024
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)# 買手機
s.connect(ip_port)                                  # 撥電話

while True: # 新增通訊循環,客戶端能夠不斷髮收消息
    nr = input(">>>").strip() #
    if not nr:continue # 若是輸入爲空,則返回循環
    s.sendall(bytes(nr, encoding="utf-8"))# 發消息,說話(只能發送字節類型)
    res = s.recv(buffer_size)              # 收消息,聽話
    print("來自遠方的消息",str(res, encoding="utf-8"))

#s.close()                                 # 掛電話(可選)
客戶端

基於上次例子可能會遇到一些問題:

這個是因爲你的服務端仍然存在四次揮手的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遠程執行命令

# 基於tcp協議實現的遠程鏈接命令工做
from socket import *
import subprocess
ip_port = ("127.0.0.1",8080)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()
    print("客戶端鏈接",addr)
    while True:
        try:
            cmd = conn.recv(buffer_size)
            if not cmd:break
            print("收到客戶端的命令是:",cmd)
            res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
                             stderr=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                             stdin=subprocess.PIPE)
            err = res.stderr.read()
            if err:
                cmd_res = err
            else:
                cmd_res = res.stdout.read()
            if not cmd_res:
                cmd_res = "命令執行OK".encode("gbk")
            conn.send(cmd_res)

        except Exception as e:
            print(e)
            break
    conn.close()
服務端
from socket import *
import subprocess
ip_port = ("127.0.0.1",8080)
buffer_size = 1024

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    msg = input(">>>")
    if not msg:continue
    if msg == "q":break #若是輸入的是q 則發送的是空 服務端會進入死循環
    tcp_client.send(msg.encode("utf-8"))
    cmd_res = tcp_client.recv(buffer_size)
    print("命令行的結果是",cmd_res.decode("gbk"))

tcp_client.close()
客戶端

基於UDP的套接字

udp的socket修改了一些參數

服務端

複製代碼
from socket import *

udp_server = socket(AF_INET,SOCK_DGRAM) # 基於網絡,  數據報
udp_server.bind(("127.0.0.1",8080,))

while True:
    data,addr = udp_server.recvfrom(1024)
    print("來自於:",addr,data.decode("utf-8"))

    msg = input(">>>")
    udp_server.sendto(msg.encode("utf-8"),addr)
複製代碼

客戶端

複製代碼
from socket import *
ip_port=("192.168.12.34",8899)

udp_client = socket(AF_INET,SOCK_DGRAM) # 基於網絡,  數據報

while True:
    msg = input(">>>")
    udp_client.sendto(msg.encode("utf-8"),ip_port)  # 發送於誰

    data,addr=udp_client.recvfrom(1024)             # 接收來自於誰
    print(data.decode("utf-8"))
複製代碼

對UDP進行的一些闡釋 若是發送的內容爲空,例如直接回車,不會阻塞。在TCP下按下回車則會阻塞,則要做出一些處理。UDP不會出現粘包現象,再講解粘包時會做出解釋。並且UDP能夠直接實現併發,支持多用戶同時通訊服務端

基於udp套接字咱們能夠做出一些小功能

實現ntp時間服務器

from socket import *
import time

udp_server = socket(AF_INET,SOCK_DGRAM) # 基於網絡,  數據報
udp_server.bind(("127.0.0.1",8080,))

while True:
    data,addr = udp_server.recvfrom(1024)
    print("來自於:",addr,data.decode("utf-8"))
    if not data:
        gs = "%F %X"
    else:
        gs = data.decode("utf-8")
    sj = time.strftime(gs)
    udp_server.sendto(sj.encode("utf-8"),addr)
服務端
from socket import *
ip_port=("127.0.0.1",8080)

udp_client = socket(AF_INET,SOCK_DGRAM) # 基於網絡,  數據報

while True:
    msg = input(">>>")
    udp_client.sendto(msg.encode("utf-8"),ip_port)  # 發送於誰

    data,addr=udp_client.recvfrom(1024)              # 接收來自於誰
    print("標準時間爲:",data.decode("utf-8"))
客戶端

基於udp套接字模擬實現UDP遠程執行命令

# 基於udp實現的遠程命令行
from socket import *
import subprocess

ip_port = ("127.0.0.1",8080)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)

while True:
    #
    cmd,addr = udp_server.recvfrom(buffer_size)
    print("收到客戶端的命令是:", cmd)
    res = subprocess.Popen(cmd.decode("utf-8"),shell=True,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE,
                     stdin=subprocess.PIPE)
    err = res.stderr.read()
    if err:
        cmd_res = err
    else:
        cmd_res = res.stdout.read()
    if not cmd_res: # 若是這個命令的返回值是空的,則會阻塞.因此要人爲的添加一些操做
        cmd_res = "ok".encode("utf-8")
    #
    udp_server.sendto(cmd_res,addr)
服務端

 

from socket import *
import subprocess

ip_port = ("127.0.0.1",8080)
buffer_size = 1024
udp_client = socket(AF_INET,SOCK_DGRAM)

while True:
    msg = input(">>>")
    udp_client.sendto(msg.encode("utf-8"), ip_port)

    data,addr = udp_client.recvfrom(buffer_size)
    print("命令的結果是" ,data.decode("gbk"))
客戶端

注:tcp和udp實現的遠程執行命令由於系統的分別,可能會產生一些差別性。在windows上面,由於將命令交給shell去操做,因此shell底層是用gbk編碼的,再獲得shell處理返回的結果時,則要用gbk去解碼拿到結果。

關於tcp的recv和udp的recvfrom的一些區別

一、收發原理詳解:

發消息:都是將數據發送到己端的發送緩衝區中

收消息:都是從己端的緩衝區中收

二、發消息兩者相似,收消息確實有區別的?

tcp協議:send發消息,recv收消息

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

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

udp協議:sendto發消息,recvfrom收消息

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

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

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

注意:

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

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

總結:

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

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

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

相關文章
相關標籤/搜索