定義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
說明: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鏈接,併發送消息,讀消息,回消息的過程,初學者可能一會兒就懵了,請看下面的類比,
類比
經過上面的服務端和客戶端的一個簡單的交互,能夠將其比做打電話,小明是服務端,小紅是客戶端
小紅:你好 此時小紅是發消息,小明此時處於收消息的狀態
小明:你好 小明收到小紅髮的你好消息,作出迴應,此時小明開始給小紅髮消息,小紅處於收消息狀態
最後小紅收到了小明的消息,小明此時已經掛斷電話,最後這次通訊已斷
注意這次通訊只是一個簡單的交互過程,交互完成以後,則先完成方會主動關係鏈接。若是要持續通訊,請繼續往下看
小明
小紅
小紅和小明交互
小紅在和小明打電話前得有個通訊工具等等,因此須要找到一部手機,類同建立一個套接字
小紅須要知道小明的電話號碼,並撥打電話,此步驟就等於客戶端鏈接服務端
小明爲了接收電話,他首先得買個手機,此步驟類同建立socket套接字
小明有了手機,須要辦一張電話卡,此步驟類同綁定套接字搭配監聽的ip和端口
小明有了手機和電話卡,則手機開機,處於待機狀態 此步驟類同監聽客戶端鏈接
當小紅打電話進來以後,須要接電話,此類同於接收客戶端創建鏈接的請求
服務端
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.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
上述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方法)