Socket(也稱做套接字)是一組接口,是應用層與 TCP/IP協議族 通訊的中間軟件抽象層,它對TCP/IP協議進行了實現,應用層須要網絡通訊,直接調用這些接口便可~
從應用層的角度,也能夠簡單地將 Socket 理解爲 ip+port,ip用來定位互聯網中的一臺主機,port用來定位該主機上的應用程序,因此經過 ip+port 可以找到須要通訊的另外一個程序,通訊過程的底層由 Socket 模塊實現~html
套接字家族名稱:AF_UNIX算法
基於文件的套接字就是經過對同一個文件的讀寫來完成進程間的通訊。若兩個進程運行在同一臺服務器上,使用這種方式來通訊效率更高~shell
套接字家族名稱:AF_INET編程
跨越網絡的通訊使用 AF_INET,還有AF_INET6,用於ipv6。經常使用的就這幾個,剩下的無需關心~json
建立TCP鏈接時,由客戶端主動發起鏈接,創建鏈接以後,基於這個鏈接開始通訊。TCP鏈接是可靠的鏈接~
服務端緩存
import socket sk = socket.socket() sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sk.bind(('127.0.0.1', 8888)) # 綁定套接字 sk.listen(10) # 監聽鏈接,10表示 server 端最多同時響應10個鏈接 conn, _ = sk.accept() # 接收鏈接 msg = conn.recv(1024) # 接收 client 端發來的信息 print(msg) conn.send(b'hi') # 向客戶端發送信息 conn.close() # 關閉鏈接 sk.close() # 關閉 server端套接字
說明:
setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 表示對IP地址和端口進行重用,可避免端口已被佔用的問題(上一次服務端程序退出以後,其使用的端口還未徹底關閉)服務器
OSError: [Errno 48] Address already in use
這裏的 socket.socket() 省略了默認參數,等同於 socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
family 指定 套接字家族,能夠是AF_UNIX或者AF_INET
type 指定套接字類型,是面向鏈接的(SOCK_STREAM)仍是非鏈接(SOCK_DGRAM),基於TCP的鏈接使用 SOCK_STREAM,基於UDP的使用 SOCK_DGRAM~網絡
客戶端多線程
import socket sk = socket.socket() ip_addr = ('127.0.0.1', 8888) sk.connect(ip_addr) sk.send(b'hello') rece_msg = sk.recv(1024) print(rece_msg) sk.close()
TCP是基於可靠的鏈接,而且通訊雙方都以流的形式發送數據,相對於TCP,UDP則是面向無鏈接的協議。
使用UDP協議通訊,不須要創建鏈接,只須要知道對方的IP地址和端口號,就能夠直接發送數據包。使用UDP傳輸數據是不可靠的,發送端只管發送,並不會對數據包的到達進行確認,但相對於TCP,它的優勢是速度快~
服務端併發
import socket udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_sk.bind(('127.0.0.1', 8888)) msg, addr = udp_sk.recvfrom(1024) # 接受數據 print(msg) udp_sk.sendto(u'你好'.encode('utf-8'), addr) # 發送數據 udp_sk.close()
建立 socket 時指定套接字類型 爲 socket.SOCK_DGRAM。綁定端口以後(bind),不須要 listen,即可以直接 recvfrom 接受客戶端的數據
客戶端
import socket udp_sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ip_addr = ('127.0.0.1', 8888) udp_sk.sendto(b'from udp client', ip_addr) msg, addr = udp_sk.recvfrom(1024) print(msg.decode('utf-8')) udp_sk.close()
客戶端建立基於UDP的Socket後,不須要鏈接(connect),可直接使用sendto發送數據
服務端套接字函數 s.bind() 綁定(主機,端口號)到套接字,在AF_INET下,以元組(host,port)的形式表示地址。 s.listen(n) 開始TCP監聽,你表示能同時創建的鏈接數據 s.accept() 被動接受TCP客戶的鏈接,(阻塞式)等待鏈接的到來 客戶端套接字函數 s.connect() 主動初始化TCP服務器鏈接,通常address的格式爲元組(hostname,port),若是鏈接出錯,返回socket.error錯誤。 s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常 公共用途的套接字函數 s.recv(bufsize) 接收TCP數據,數據以bytes形式返回,bufsize指定要接收的最大數據量。 s.send(string) 發送TCP數據,將string(bytes)中的數據發送到鏈接的套接字。返回值是要發送的字節數量,該數量可能小於string的字節大小。 s.sendall(string) 完整發送TCP數據。將string中的數據發送到鏈接的套接字,但在返回以前會嘗試發送全部數據。成功返回None,失敗則拋出異常。 s.recvfrom(buffer) 接收UDP數據,與recv()相似,但返回值是(data,address)。其中data是包含接收數據的字符串,address是發送數據的套接字地址。 s.sendto(string, addr) 發送UDP數據,將數據發送到套接字,addr是形式爲(ipaddr,port)的元組,指定遠程地址。返回值是發送的字節數。 s.getpeername() 鏈接到當前套接字的遠端的地址 s.getsockname() 當前套接字的地址 s.getsockopt() 返回指定套接字的參數 s.setsockopt() 設置指定套接字的參數 s.close() 關閉套接字 面向鎖的套接字方法 s.setblocking() 設置套接字的阻塞與非阻塞模式 s.settimeout() 設置阻塞套接字操做的超時時間 s.gettimeout() 獲得阻塞套接字操做的超時時間 面向文件的套接字的函數 s.fileno() 返回套接字的文件描述符 s.makefile() 建立一個與該套接字相關的文件
簡單說明一下 send 和 sendall 方法的區別:
上面已經說明 send(string) 方法發送的數據量可能小於string的字節大小,就是說可能沒法發送string中的全部數據,因此簡單起見,通常使用sendall方法。以下兩種方式是等價的:
# sendall sock.sendall('Hello world\n') # send buffer = 'Hello world\n' while buffer: bytes = sock.send(buffer) buffer = buffer[bytes:]
以下示例是遠程執行命令的程序,客戶端發送命令到服務器端,服務器端執行完成後,將結果返回給客戶端~
服務端
ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_sk_server.bind(ip_port) tcp_sk_server.listen(5) while True: conn, addr = tcp_sk_server.accept() print("from %s:%s" % (addr[0], addr[1])) while True: cmd = conn.recv(BUFSIZE) # recv 方法會阻塞,直到接收到數據;如果鏈接關閉,則會接收一個 b'',程序繼續日後執行 if len(cmd) == 0: break res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() if stderr: send_mes = stderr else: send_mes = stdout conn.sendall(send_mes)
客戶端
import socket ip_port = ('127.0.0.1', 8888) BUFSIZE = 100 tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_client.connect_ex(ip_port) while True: cmd = input(">>: ").strip() if len(cmd) == 0: continue if cmd == 'quit': break tcp_sk_client.send(cmd.encode('utf-8')) cmd_res = tcp_sk_client.recv(BUFSIZE) print(cmd_res.decode('utf-8')) tcp_sk_client.close()
注意客戶端接收數據的 BUFSIZE 調整成了100,以下是客戶端執行的返回結果:
>>: ls /tmp VMwareDnD com.apple.launchd.Q7QRr2IZct com.apple.launchd.gOYBDHgesI powerlog >>: ifconfig lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIM >>: ls /tmp ESTAMP> inet 127.0.0.1 netmask 0xff000000 inet6 ::1 prefixlen 128 inet6 fe80::1%lo0 prefixlen 6 >>:
第一個命令 'ls /tmp' 完整的輸出了,可是第二個命令因爲其返回數據的長度大於100,只顯示了一部分,剩下沒有顯示的在下一個返回結果中輸出,而下一個命令的返回結果應該還在緩存中,這就是黏包現象
TCP協議是基於數據流的,數據發送以後,數據的長度對於客戶端的應用程序而言是不可見的,客戶端程序在從緩衝區提取數據的時候不知道一段數據從哪裏開始到哪裏結束,這就形成了黏包現象。
還有就是TCP協議中的Nagle算法,將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包,發送。這也是形成客戶端接收數據時產生黏包現象的緣由~
總之面向流的通訊(基於TCP協議的通訊)是無消息保護邊界的,這是形成黏包的主要原應~
UDP協議是無鏈接的,面向消息的。因爲UDP支持的是一對多模式,接收端的套接字緩衝區採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中有消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。
因此基於UDP協議的通訊是不會有黏包現象的。客戶端使用 recvfrom(bufsize) 接收bufsize 大小的數據後,這個消息剩下的數據就丟失了。下一次接收數據從另外一個消息的開頭開始提取。而基於TCP協議的通訊,客戶端使用 recv(bufsize) 接收 bufsize 大小的數據後,剩下的數據會依舊留在緩存中,下一次接收數據會從上一次結束的地方開始提取~
綜上所述,基於TCP協議的通訊,如下兩種狀況下會出現黏包:
狀況_1
發送端將屢次間隔較小且數據量小的數據,合併成一個大的數據塊發送出去,形成接收端的黏包~
服務端(接收端)
import socket ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sk.bind(ip_port) sk.listen(5) conn, addr = sk.accept() info_1 = conn.recv(BUFSIZE) info_2 = conn.recv(BUFSIZE) print('info_1: %s' % info_1.decode('utf-8')) print('info_2: %s' % info_2.decode('utf-8')) conn.close()
客戶端(發送端)
import socket ip_port = ('127.0.0.1', 8888) sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) sk.connect(ip_port) sk.send(b'hello') sk.send(b'hi') sk.close()
在服務端(接收端)的輸出結果:
info_1: hellohi info_2:
狀況_2
接收端在接收數據時只接收了一部分,下一次接收數據時,會接受到上一次遺留的數據~
服務端(接收端)
import socket ip_port = ('127.0.0.1', 8888) # BUFSIZE = 1024 sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sk.bind(ip_port) sk.listen(5) conn, addr = sk.accept() info_1 = conn.recv(2) # 這裏將 BUFSIZE 僅設置成2字節 info_2 = conn.recv(5) print('info_1: %s' % info_1.decode('utf-8')) print('info_2: %s' % info_2.decode('utf-8')) conn.close()
客戶端(發送端)
import socket ip_port = ('127.0.0.1', 8888) sk = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) sk.connect(ip_port) sk.send(b'hello') sk.send(b'hi') sk.close()
在服務端(接收端)的輸出結果:
info_1: he info_2: llohi
第一次發送的數據出如今下一次的接收中,上述給出的遠程執行命令的程序就屬於這種狀況~
解決方式,就是在發送數據前提早通知數據的長度,這樣在接收數據時僅接收指定長度的數據,從而避免黏包~
服務端
import socket import subprocess ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_sk_server.bind(ip_port) tcp_sk_server.listen(5) while True: conn, addr = tcp_sk_server.accept() print("from %s:%s" % (addr[0], addr[1])) while True: cmd = conn.recv(BUFSIZE) if len(cmd) == 0: break res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() if stderr: send_mes = stderr else: send_mes = stdout data_length = len(send_mes) conn.send(str(data_length).encode('utf-8')) back_data = conn.recv(BUFSIZE).decode('utf-8') if back_data == 'OK': conn.sendall(send_mes) conn.close()
客戶端
import socket ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_client.connect_ex(ip_port) while True: cmd = input(">>: ").strip() if len(cmd) == 0: continue if cmd == 'quit': break tcp_sk_client.send(cmd.encode('utf-8')) # 接收長度信息 data_length = int(tcp_sk_client.recv(BUFSIZE).decode('utf-8')) tcp_sk_client.send('OK'.encode('utf-8')) cmd_res = b'' recv_length = 0 while recv_length < data_length: cmd_res += tcp_sk_client.recv(BUFSIZE) recv_length += BUFSIZE print(cmd_res.decode('utf-8')) tcp_sk_client.close()
在這個示例中,雖然服務端提早發送了數據的長度信息,可是這個長度信息的長度,客戶端任然不知道,這樣如果發送完數據的長度信息以後,直接發送數據,依舊會存在黏包現象;由於 數據的長度信息 的數據量很小,發送端在發送的時候可能會將其和後面的部分數據 合併成一個大的數據塊發送出去,接收端不知道 長度信息 的數據長度,兩個數據之間也沒有邊界。
而在這個示例中,發送端在發送完數據的長度以後,接收了客戶端(接收端)的反饋信息(tcp_sk_client.send('OK'.encode('utf-8'))),確認是"OK"以後再發送數據信息。這就至關於 兩個數據之間 有了 邊界(發送端 在發送數據長度 和 發送數據 之間有一個 recv 動做),繞過了黏包問題~
這種解決方式存在一個問題,就是發送端和接收端多了一次交互過程,這可能會放大網絡延遲帶來的性能損耗~
另外一種方式就是使用struct模塊來解決黏包問題~
struct 模塊能夠一個類型的數據轉成固定長度的 bytes ~
import struct struct_num = struct.pack('i', 123) print(struct_num) # b'{\x00\x00\x00' num = struct.unpack('i', struct_num) print(num[0]) # 123
'i' 表示 要轉換的類型是 int類型~
關於struct模塊的具體使用方式可參見:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
使用struct 模塊,將數據的長度轉換成一個4字節數字。這樣接收端就知道了表示數據長度的數據量的大小,直接接收便可,省去了發送接送反饋信息這一步~
服務端
import socket import subprocess import struct ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 tcp_sk_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_sk_server.bind(ip_port) tcp_sk_server.listen(5) while True: conn, addr = tcp_sk_server.accept() print("from %s:%s" % (addr[0], addr[1])) while True: cmd = conn.recv(BUFSIZE) if len(cmd) == 0: break res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() if stderr: send_mes = stderr else: send_mes = stdout conn.send(struct.pack('i', len(send_mes))) conn.sendall(send_mes) conn.close()
客戶端
import socket import struct ip_port = ('127.0.0.1', 8888) BUFSIZE = 1024 tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_client.connect_ex(ip_port) while True: cmd = input(">>: ").strip() if len(cmd) == 0: continue if cmd == 'quit': break tcp_sk_client.send(cmd.encode('utf-8')) # 接收長度信息 data_length = struct.unpack('i', tcp_sk_client.recv(4))[0] cmd_res = b'' recv_length = 0 while recv_length < data_length: cmd_res += tcp_sk_client.recv(BUFSIZE) recv_length += BUFSIZE print(cmd_res.decode('utf-8')) tcp_sk_client.close()
發送端發送完長度信息後,可直接發送數據,由於接收端知道長度信息的數據量(4字節),可直接接收~
其實解決黏包問題的關鍵就是讓接收端知道將要接收的數據的長度,從而很少很多的接收數據~
SocketServer 對 socket 進行了封裝,內部使用 IO多路複用 以及 「多線程」 和 「多進程」 ,從而實現併發處理多個客戶端請求的 Socke t服務端。
在 SocketServer 中有這些類:ThreadingTCPServer、TCPServer、ForkingTCPServer 是基於TCP實現的。
TCPServer 並非併發的,在接收到請求後逐一進行處理,若是前一個的 handle 沒有結束,那麼其餘的請求將不會處理。
ThreadingTCPServer 和 ForkingTCPServer 則能夠併發處理請求,其實現原理是 沒接收到一個請求就開啓一個線程或者進程進行處理。ThreadingTCPServer 經過創建新線程來運行handle,ForkingTCPServer 則經過創建新進程來運行 handle ~
遠程執行命令的程序 經過 socketserver 來實現
服務端
import socketserver import subprocess import json import struct IP, PORT = '127.0.0.1', 9999 BUFSIZE = 1024 class MyServer(socketserver.BaseRequestHandler): def handle(self): print("hello %s" % self.request) while True: cmd = self.request.recv(BUFSIZE) if len(cmd) == 0: break res = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() if stderr: send_mes = stderr else: send_mes = stdout res_heads = {'data_len': len(send_mes)} res_heads_json = json.dumps(res_heads).encode('utf-8') self.request.send(struct.pack('i', len(res_heads_json))) self.request.send(res_heads_json) self.request.sendall(send_mes) if __name__ == '__main__': socketserver.TCPServer.allow_reuse_address = True server = socketserver.ThreadingTCPServer((IP, PORT), MyServer) server.serve_forever()
客戶端
import socket import struct import json ip_port = ('127.0.0.1', 9999) BUFSIZE = 1024 tcp_sk_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) tcp_sk_client.connect_ex(ip_port) while True: cmd = input(">>: ").strip() if len(cmd) == 0: continue if cmd == 'quit': break tcp_sk_client.send(cmd.encode('utf-8')) # 接收 head 長度信息 head_length = struct.unpack('i', tcp_sk_client.recv(4))[0] res_heads = json.loads(tcp_sk_client.recv(head_length).decode('utf-8')) cmd_res = b'' recv_length = 0 while recv_length < res_heads['data_len']: cmd_res += tcp_sk_client.recv(BUFSIZE) recv_length += BUFSIZE print(cmd_res.decode('utf-8')) tcp_sk_client.close()
上述示例中經過一個字典(dict)來標識數據的一些信息(例如長度),發送端在發送數據時,先發送這個字典的長度,再發送這個字典(字典中攜帶了數據長度),最後發送數據信息~ .................^_^