socket編程初級

什麼是socket

  • 定義python

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

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

  • socket和file的區別:服務器

    • file模塊是針對某個指定文件進行【打開】【讀寫】【關閉】網絡

    • socket模塊是針對 服務器端 和 客戶端Socket 進行【打開】【讀寫】【關閉】多線程

  • python相關併發

Python 提供了兩個基本的 socket 模塊。py2位大寫,py3所有小寫
第一個是 Socket,它提供了標準的 BSD Sockets API。
第二個是 SocketServer, 它提供了服務器中心類,能夠簡化網絡服務器的開發ssh

socket編程實現

  • 流程圖:socket

  • 說明:tcp

    • 服務端

      1.服務端須要導入socket模塊,並建立套接字(實例化爲一個對象)

      import socket
      s = socket.socket()

       

      2.綁定套接字s到本地IP和端口

      ip_port = ('127.0.0.1',8080)
      s.bind(ip_port)

       

      3.監聽鏈接

      s.listen(0)
      
      PS:0表示緩衝區可掛起的鏈接數量 0表示不限制,1表示 可掛起一個,那麼意思就是鏈接一個、掛起一個,第三個再鏈接的話,就沒法鏈接,會超時

       

      4.接收客戶端創建鏈接的請求

      conn,addr = s.accept()
      PS:conn爲一個客戶端和服務器創建的鏈接,addr爲客戶端ip

       

      5.接收客戶端的消息,並作相應處理

      recv_data = conn.recv(1024)
      send_data = recv_data.upper()  #將客戶端發送的內容轉換爲大寫,注意。python3裏面客戶端發送的都是二進制數據,python2裏能夠發送字符串

       

      6.給客戶端回消息

      conn.send(send_data)

       

      7.關閉鏈接

      conn.close()

       

    • 客戶端
      1.建立套接字

      import socket
      
      s = socket.socket()

       

      2.鏈接服務端

      ip_port = ('127.0.0.1',8080)
      s.connect(ip_port)

       

      3.給服務端發送消息

      send_data = input('請輸入: ')
      s.send(send_data.encode())  #注意py3發送的數據須要轉換爲二進制,不能直接發送字符串

       

      4.接收服務端消息,並打印

      recv_data = s.recv(1024)print(recv_data.decode())  #服務端迴應的是二進制,因此須要轉換爲字符串

       

      5.關閉鏈接

      s.close()

       

以上就是一個簡單的客戶端和服務端socket鏈接,併發送消息,讀消息,回消息的過程,初學者可能一會兒就懵了,請看下面的類比,

  • 類比

    經過上面的服務端和客戶端的一個簡單的交互,能夠將其比做打電話,小明是服務端,小紅是客戶端

    小紅:你好 此時小紅是發消息,小明此時處於收消息的狀態
    小明:你好 小明收到小紅髮的你好消息,作出迴應,此時小明開始給小紅髮消息,小紅處於收消息狀態
    最後小紅收到了小明的消息,小明此時已經掛斷電話,最後這次通訊已斷

    注意這次通訊只是一個簡單的交互過程,交互完成以後,則先完成方會主動關係鏈接。若是要持續通訊,請繼續往下看

    • 小明

    • 小紅

    • 小紅和小明交互

  1. 小紅在和小明打電話前得有個通訊工具等等,因此須要找到一部手機,類同建立一個套接字

  2. 小紅須要知道小明的電話號碼,並撥打電話,此步驟就等於客戶端鏈接服務端

  1. 小明爲了接收電話,他首先得買個手機,此步驟類同建立socket套接字

  2. 小明有了手機,須要辦一張電話卡,此步驟類同綁定套接字搭配監聽的ip和端口

  3. 小明有了手機和電話卡,則手機開機,處於待機狀態 此步驟類同監聽客戶端鏈接

  4. 當小紅打電話進來以後,須要接電話,此類同於接收客戶端創建鏈接的請求

實現服務端保持鏈接,不受客戶端斷開而斷開,並實現客戶端和服務端持續交互過程

服務端

複製代碼

import socket

ip_port = ('127.0.0.1',8080)

s = socket.socket()
s.bind(ip_port)

s.listen(0)while True:    #這次while循環用於客戶端斷開鏈接以後,從新循環創建新鏈接
    conn,addr = s.accept()    while True:    #此while循環用於客戶端和服務器持續交互
       recv_data = conn.recv(1024)       if not recv_data: break   #判斷消息是否爲空,當消息爲空時,跳出循環,若是不判斷的話,客戶端那邊若是主動斷開鏈接,將會致使服務端處於一個不停的收消息的死循環中,由於鏈接已斷開,處於非阻塞狀態
       send_data = recv_data.upper()  #將客戶消息轉換爲大寫
       conn.send(send_data)
    conn.close()

複製代碼

 

客戶端:

複製代碼

import socket

s = socket.socket()

ip_port = ('127.0.0.1',8080)


s.connect(ip_port)while True:
    send_data = input('請輸入: ')    if send_data == 'exit':break
    elif send_data == '':continue
    s.send(send_data.encode())
    recv_data = s.recv(1024)    print(recv_data.decode())
s.close()

複製代碼

 

運行服務端和客戶端,效果以下:

複製代碼

請輸入: hello
HELLO
請輸入: Jeck
JECK
請輸入: 123
123請輸入: 
請輸入: exit

Process finished with exit code 0

複製代碼

 

socket模塊功能

  • socket 類型

    socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)

    • 參數一:地址簇
         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 ,則系統就會根據地址格式和套接類別,自動選擇一個合適的協議

  • socket方法

    將套接字綁定到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。

    是否阻塞(默認True),若是設置False,那麼accept和recv時一旦無數據,則報錯。

    接受鏈接並返回(conn,address),其中conn是新的套接字對象,能夠用來接收和發送數據。address是鏈接客戶端的地址。
    接收TCP 客戶的鏈接(阻塞式)等待鏈接的到來

    鏈接到address處的套接字。通常,address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。

    同上,只不過會有返回值,鏈接成功時返回 0 ,鏈接失敗時候返回編碼,例如:10061

    關閉套接字

    接受套接字的數據。數據以字符串形式返回,bufsize指定最多能夠接收的數量。flag提供有關消息的其餘信息,一般能夠忽略

    與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。

    將string中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。即:可能未將指定內容所有發送。

    將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。內部經過遞歸調用send,將全部內容發送出去。

    將數據發送到套接字,address是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。該函數主要用於UDP協議。

    設置套接字操做的超時期,timeout是一個浮點數,單位是秒。值爲None表示沒有超時期。通常,超時期應該在剛建立套接字時設置,由於它們可能用於鏈接的操做(如 client 鏈接最多等待5s )

    返回鏈接套接字的遠程地址。返回值一般是元組(ipaddr,port)。

    返回套接字本身的地址。一般是一個元組(ipaddr,port)

    • sk.fileno()

    • sk.getsockname()

    • sk.getpeername()

    • sk.settimeout(timeout)

    • sk.sendto(string[,flag],address)

    • sk.sendall(string[,flag])

    • sk.send(string[,flag])

    • sk.recvfrom(bufsize[.flag])

    • sk.recv(bufsize[,flag])

    • sk.close()

    • sk.connect_ex(address)

    • sk.connect(address)

    • sk.accept()

    • sk.listen(backlog)
        開始監聽傳入鏈接。backlog指定在拒絕鏈接以前,能夠掛起的最大鏈接數量。backlog等於5,表示內核已經接到了鏈接請求,但服務器尚未調用accept進行處理的鏈接個數最大爲5,這個值不能無限大,由於要在內核中維護鏈接隊列

    • sk.setblocking(bool)

    • sk.bind(address)

  套接字的文件描述符

  • 案例:模擬ssh

    • 服務端:

複製代碼

import socketimport  subprocess

ip_port = ('127.0.0.1',8080)

s = socket.socket()
s.bind(ip_port)

s.listen(0)while True:
    conn,addr = s.accept()    while True:        try:
            recv_data = conn.recv(1024)            if not recv_data: break
            p = subprocess.Popen(str(recv_data,encoding='utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)    #執行shell命令,並將標準輸出和錯誤輸出放到緩衝區
            res = p.stdout.read()            if not res:
                send_data = p.stderr.read()            else:
                send_data = res

            data_size = len(send_data)
            conn.send(send_data)        except Exception:            break
    conn.close()

複製代碼

 

* 客戶端

複製代碼

import socket

ip_port = ('127.0.0.1',8080)


s = socket.socket()

s.connect(ip_port)while True:

    send_data = input('>>:  ')    if send_data == 'exit':exit()    elif not send_data:continue
    s.send(bytes(send_data,encoding='utf-8'))
    recv_data = s.recv(1024)    print(recv_data.decode())
s.close()

複製代碼

 

執行結果:

複製代碼

>>:  df -h
Filesystem      Size  Used Avail Use% Mounted on/dev/disk1      112G   51G   62G  45% /

>>:  netstat -lnt
Active Internet connections
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)    
tcp4       0      0  172.16.23.42.57334     23.83.227.252.8023     ESTABLISHED
tcp4       0      0  127.0.0.1.1080         127.0.0.1.57333        ESTABLISHED
tcp4       0      0  127.0.0.1.57333        127.0.0.1.1080         ESTABLISHED
tcp4       0      0  127.0.0.1.8080         127.0.0.1.57332        ESTABLISHED
tcp4       0      0  127.0.0.1.57332        127.0.0.1.8080         ESTABLISHED
tcp4       0      0  172.16.23.42.57328     223.252.199.7.80       CLOSE_WAIT 
tcp4       0      0  172.16.23.42.57269     163.177.72.143.993     ESTABLISHED
tcp4       0      0  10.255.0.10.57047      203.130.45.175.9000    ESTABLISHED
tcp4      27      0  172.16.23.42.57045     163.177.90.125.993     CLOSE_WAIT 
tcp4       0      0  172.16.23.42.56988     114.215.186.163.443    ESTABLISHED
tcp4      27      0  172.16.23.42.56632     163.177.72.143.993     CLOSE_WAIT 
tcp4       0      0  10.255.0.10.56374      10.2
>>:  route -n0.7.12.22          ESTABLISHED
tcp4      27      0  172.16.23.42.56229     163.177.90.125.993     CLOSE_WAIT 
tcp4       0      0  10.255.0.10.54889      203.130.45.175.9000    ESTABLISHED
tcp4       0      0  10.255.0.10.54605      203.130.45.173.6929    ESTABLISHED
tcp4       0      0  10.255.0.10.53228      10.20.7.12.22          ESTABLISHED
tcp4       0      0  10.255.0.10.53122      203.130.45.175.9000    ESTABLISHED
tcp4       0      0  172.16.23.42.52902     42.62.89.250.1194      ESTABLISHED
tcp4       0      0  127.0.0.1.1337         127.0.0.1.52901        ESTABLISHED
tcp4       0      0  127.0.0.1.52901        127.0.0.1.1337         ESTABLISHED
tcp4       0      0  172.16.23.42.52899     17.172.232.10.5223     ESTABLISHED
tcp4       0      0  172.16.23.42.52855     17.252.236.157.5223    ESTABLISHED
tcp4       0      0  172.16.23.42.52790     223.252.199.6.6003     ESTABLISHED
tcp4       0      0  172.16.23.42.50124     223.167.82.210.80      ESTABLISHED
tcp4       0      0  172.16.23.42.50026     1

複製代碼

 

從結果中發現,執行df -h 返回正常結果,執行netstat -lnt返回了一半的結果,繼續執行命令,仍然返回的是netstat -lnt的結果,這就發生了粘包現象

  • 粘包解決

    所謂粘包現象就是服務端把數據發過來以後,客戶端接收時會按必定大小來接收,決定此操做的是s.recv(1024),1024是每次接收的包大小,第一次沒有接收完的話,第二次會繼續接收原來的數據包,這就是粘包現象,解決辦法就是,服務端在發送數據時,現告訴客戶端本次數據的大小,而後再發送數據,客戶端收到數據大小以後,循環接收數據,知道接收完成再終止這次循環,這樣就能夠拿到全部的數據,解決了粘包現象

    • 服務端改造:

複製代碼

#!/usr/bin/env python# -*- coding: UTF-8 -*-#pyversion:python3.5#owner:fuzjimport socketimport  subprocess

ip_port = ('127.0.0.1',8080)

s = socket.socket()
s.bind(ip_port)

s.listen(0)while True:
    conn,addr = s.accept()    while True:        try:
            recv_data = conn.recv(1024)            if not recv_data: break
            p = subprocess.Popen(str(recv_data,encoding='utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            res = p.stdout.read()            if not res:
                send_data = p.stderr.read()            else:
                send_data = res

            data_size = len(send_data)   #計算數據大小
            conn.send(bytes(str(data_size),encoding='utf-8'))  #發送數據大小
            res = conn.recv(1024)  #接收客戶端狀態
            conn.send(send_data)   #發送數據        except Exception:            break
    conn.close()

複製代碼

 

* 客戶端改造:

複製代碼

import socket

ip_port = ('127.0.0.1',8080)


s = socket.socket()

s.connect(ip_port)while True:

    send_data = input('>>:  ')    if send_data == 'exit':exit()    elif not send_data:continue
    s.send(bytes(send_data,encoding='utf-8'))

    recv_size = 0
    data = b''
    data_size = str(s.recv(1024),encoding='utf-8')  #接收數據大小
    s.send(bytes('ok',encoding='utf-8'))  #發送此時的狀態    while recv_size < int(data_size):    #循環接收數據,直到接收完全部數據
        recv_data = s.recv(1024)
        data += recv_data
        recv_size += len(recv_data)    print(str(data,encoding='utf-8'))

s.close()

複製代碼

 

運行結果:發現已經解決上述問題

複製代碼

>>:  df -h
Filesystem      Size  Used Avail Use% Mounted on/dev/disk1      112G   51G   62G  45% /

>>:  netstat -lnt
Active Internet connections
Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)    
tcp4       0      0  172.16.23.42.57476     223.252.199.7.80       CLOSE_WAIT 
tcp4       0      0  127.0.0.1.8080         127.0.0.1.57475        ESTABLISHED
tcp4       0      0  127.0.0.1.57475        127.0.0.1.8080         ESTABLISHED
tcp4       0      0  172.16.23.42.57474     223.252.199.7.80       LAST_ACK   
tcp4       0      0  172.16.23.42.57465     23.83.227.252.8023     ESTABLISHED
tcp4       0      0  127.0.0.1.1080         127.0.0.1.57464        ESTABLISHED
tcp4       0      0  127.0.0.1.57464        127.0.0.1.1080         ESTABLISHED
tcp4       0      0  172.16.23.42.57461     23.83.227.252.8023     ESTABLISHED
tcp4       0      0  127.0.0.1.1080         127.0.0.1.57460        ESTABLISHED
tcp4       0      0  127.0.0.1.57460        127.0.0.1.1080         ESTABLISHED
tcp4       0      0  172.16.23.42.57455     163.177.72.143.993     CLOSE_WAIT 
tcp4       0      0  10.255.0.10.57047      203.130.45.175.9000    ESTABLISHED
tcp4      27      0  172.16.23.42.57045     163.177.90.125.993     CLOSE_WAIT 
tcp4       0      0  172.16.23.42.56988     114.215.186.163.443    ESTABLISHED
tcp4      27      0  172.16.23.42.56632     163.177.72.143.993     CLOSE_WAIT 
tcp4       0      0  10.255.0.10.56374      10.20.7.12.22          ESTABLISHED
tcp4      27      0  172.16.23.42.56229     163.177.90.125.993     CLOSE_WAIT 
tcp4       0      0  10.255.0.10.54889      203.130.45.175.9000    ESTABLISHED
tcp4       0      0  10.255.0.10.54605      203.130.45.173.6929    ESTABLISHED
tcp4       0      0  10.255.0.10.53228      10.20.7.12.22          ESTABLISHED
tcp4       0      0  10.255.0.10.53122      203.130.45.175.9000    ESTABLISHED
tcp4       0      0  172.16.23.42.52902     42.62.89.250.1194      ESTABLISHED
tcp4       0      0  127.0.0.1.1337         127.0.0.1.52901        ESTABLISHED
tcp4       0      0  127.0.0.1.52901        127.0.0.1.1337         ESTABLISHED
tcp4       0      0  172.16.23.42.52899     17.172.232.10.5223     ESTABLISHED
tcp4       0      0  172.16.23.42.52855     17.252.236.157.5223    ESTABLISHED
tcp4       0      0  172.16.23.42.52790     223.252.199.6.6003     ESTABLISHED
tcp4       0      0  172.16.23.42.50124     223.167.82.210.80      ESTABLISHED
tcp4       0      0  172.16.23.42.50026     123.151.10.187.14000   ESTABLISHED
tcp4       0      0  172.16.23.42.49612     163.177.90.125.993     ESTABLISHED
tcp4       0      0  127.0.0.1.49871        127.0.0.1.49375        ESTABLISHED
tcp4       0      0  127.0.0.1.49375        127.0.0.1.49871        ESTABLISHED
tcp4       0      0  127.0.0.1.49871        127.0.0.1.49370        ESTABLISHED
tcp4       0      0  127.0.0.1.49370        127.0.0.1.49871        ESTABLISHED
tcp4       0      0  192.168.123.164.49282  112.90.83.61.443       ESTABLISHED

複製代碼

 

socketserver 實現支持多客戶端

上述ssh模擬客戶端只能支持必定數量的客戶端,受s.listen(0)參數限制。下面能夠實現支持多客戶端操做

SocketServer內部使用 IO多路複用 以及 「多線程」 和 「多進程」 ,從而實現併發處理多個客戶端請求的Socket服務端。即:每一個客戶端請求鏈接到服務器時,Socket服務端都會在服務器是建立一個「線程」或者「進程」 專門負責處理當前客戶端的全部請求

  • ThreadingTCPServer

    ThreadingTCPServer實現的Soket服務器內部會爲每一個client建立一個 「線程」,該線程用來和客戶端進行交互

  • 實現步驟:

    • 1.建立一個類,並繼承SocketServer.BaseRequestHandler 的類

    • 2.在新類中須要建立一個handle的方法

    • 3.啓動ThreadingTCPServer

代碼以下:

複製代碼

import socketserverimport subprocessclass MyServer(socketserver.BaseRequestHandler):  #繼承    def handle(self):   #handle方法。注意此時send和recv時調用的self.request方法
        self.request.sendall(bytes('Welcome',encoding='utf-8'))        while True:            try:
                recv_data = self.request.recv(1024)                if not recv_data: break
                p = subprocess.Popen(str(recv_data, encoding='utf-8'), shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)
                res = p.stdout.read()                if not res:
                    send_data = p.stderr.read()                else:
                    send_data = res                if not send_data:
                    send_data = 'no output'.encode()

                data_size = len(send_data)
                self.request.send(bytes(str(data_size), encoding='utf-8'))
                self.request.recv(1024)
                self.request.send(send_data)            except Exception:                breakif __name__ == '__main__':

    server = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer)    #啓動server
    server.serve_forever()

複製代碼

 

PS:SocketServer.BaseRequestHandler類源碼:其定義了三個方法:setup(),handle()he finish()
執行順序爲:setup(0-->handle()-->finish()
```

複製代碼

class BaseRequestHandler:def __init__(self, request, client_address, server):
    self.request = request
    self.client_address = client_address
    self.server = server
    self.setup()    try:
        self.handle()    finally:
        self.finish()def setup(self):    passdef handle(self):    passdef finish(self):    passSocketServer.BaseRequestHandler

複製代碼

 

```

  • ThreadingTCPServer源碼剖析

  • 內部調用流程

    • 啓動服務端程序

    • 執行 TCPServer.__init__ 方法,建立服務端Socket對象並綁定 IP 和 端口

    • 執行 BaseServer.__init__ 方法,將自定義的繼承自SocketServer.BaseRequestHandler 的類 MyRequestHandle賦值給 self.RequestHandlerClass

    • 執行 BaseServer.server_forever 方法,While 循環一直監聽是否有客戶端請求到達 ...

    • 當客戶端鏈接到達服務器

    • 執行 ThreadingMixIn.process_request 方法,建立一個 「線程」 用來處理請求

    • 執行 ThreadingMixIn.process_request_thread 方法

    • 執行 BaseServer.finish_request 方法,執行 self.RequestHandlerClass() 即:執行 自定義 MyRequestHandler 的構造方法(自動調用基類BaseRequestHandler的構造方法,在該構造方法中又會調用 MyRequestHandler的handle方法)

相關文章
相關標籤/搜索