socket編程基礎


socket編程

什么是socket

  • 定義

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

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

  • socket和file的區別:

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

Python 提供了兩個基本的 socket 模塊。py2位大寫,py3全部小寫
第一個是 Socket,它提供了標准的 BSD Sockets API。
第二個是 SocketServer, 它提供了服務器中心類,可以簡化網絡服務器的開發

socket編程實現

  • 流程圖:

  • 說明:

    • 服務端

      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. 小明為了接收電話,他首先得買個手機,此步驟類同創建socket套接字
      2. 小明有了手機,需要辦一張電話卡,此步驟類同綁定套接字搭配監聽的ip和端口
      3. 小明有了手機和電話卡,則手機開機,處於待機狀態 此步驟類同監聽客戶端連接
      4. 當小紅打電話進來之后,需要接電話,此類同於接收客戶端建立連接的請求
    • 小紅

      1. 小紅在和小明打電話前得有個通信工具等等,所以需要找到一部手機,類同創建一個套接字
      2. 小紅需要知道小明的電話號碼,並撥打電話,此步驟就等於客戶端連接服務端
    • 小紅和小明交互

    小紅:你好 此時小紅是發消息,小明此時處於收消息的狀態
    小明:你好 小明收到小紅發的你好消息,做出回應,此時小明開始給小紅發消息,小紅處於收消息狀態
    最后小紅收到了小明的消息,小明此時已經掛斷電話,最后此次通信已斷

    注意此次通信只是一個簡單的交互過程,交互完成之后,則先完成方會主動關系連接。如果要持續通信,請繼續往下看

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

服務端

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方法

    • sk.bind(address)

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

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

    • sk.setblocking(bool)

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

    • sk.accept()

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

    • sk.connect(address)

    連接到address處的套接字。一般,address的格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤。

    • sk.connect_ex(address)

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

    • sk.close()

    關閉套接字

    • sk.recv(bufsize[,flag])

    接受套接字的數據。數據以字符串形式返回,bufsize指定最多可以接收的數量。flag提供有關消息的其他信息,通常可以忽略

    • sk.recvfrom(bufsize[.flag])

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

    • sk.send(string[,flag])

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

    • sk.sendall(string[,flag])

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

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

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

    • sk.settimeout(timeout)

    設置套接字操作的超時期,timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因為它們可能用於連接的操作(如 client 連接最多等待5s )

    • sk.getpeername()

    返回連接套接字的遠程地址。返回值通常是元組(ipaddr,port)。

    • sk.getsockname()

    返回套接字自己的地址。通常是一個元組(ipaddr,port)

    • sk.fileno()

  套接字的文件描述符

  • 案例:模擬ssh

    • 服務端:
import socket
import  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 -n
0.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:fuzj


import socket
import  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 socketserver
import subprocess

class 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:
                break



if __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):
    pass

def handle(self):
    pass

def finish(self):
    pass
SocketServer.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方法)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM