基於TCP協議通訊的套接字

什麼是 Socketshell

Socket 是應用層與 TCP/IP 協議通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket 其實就是一個門面模式,它把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,對用戶來講,一組簡單的接口就是所有,讓 Socket 去組織數據,以符合指定的協議。編程

因此,咱們無需深刻理解 TCP/UDP 協議,socket 已經爲咱們封裝好了,咱們只須要遵循 socket 的規定去編程,寫出的程序天然就是遵循 TCP/UDP 標準的。設計模式

套接字的分類:服務器

  基於文件類型的套接字家族:AF_UNIX(在 Unix 系統上,一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程同時運行在同一機器,能夠經過訪問同一個文件系統間接完成通訊)網絡

  基於網絡類型的套接字家族:AF_INET(Python 支持不少種地址家族,可是因爲咱們只關心網絡編程,因此大部分時候咱們只使用 AF_INET)併發

基於 TCP 協議的 socketssh

工做流程:socket

下面咱們舉個打電話的小例子來講明一下tcp

若是你要給你的一個朋友打電話,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就創建起了鏈接,就能夠講話了。等交流結束,掛斷電話結束這次交談。 生活中的場景就解釋了這工做原理。ide

(若是你去一家餐館吃飯,假設那裏的老闆就是服務端,而你本身就是客戶端,當你去吃飯的時候,你確定的知道那個餐館,也就是服務端的地址,可是對於你本身來講,餐館的老闆不須要知道你的地址)

服務端
1)建立套接字描述符(socket)
2)設置服務器的 IP 地址和端口號(須要轉換爲網絡字節序的格式)
3)將套接字描述符綁定到服務器地址(bind)
4)將套接字描述符設置爲監聽套接字描述符(listen),等待來自客戶端的鏈接請求,監聽套接字維護未完成鏈接隊列和已完成鏈接隊列
5)從已完成鏈接隊列中取得隊首項,返回新的已鏈接套接字描述符(accept),若是已完成鏈接隊列爲空,則會阻塞
6)從已鏈接套接字描述符讀取來自客戶端的請求(read / recv)
7)向已鏈接套接字描述符寫入應答(write / send)
8)關閉已鏈接套接字描述符(close),回到第 5 步等待下一個客戶端的鏈接請求

 服務端必須知足至少三點:

  1)綁定一個固定的 IP 和端口號

  2)一直對外提供服務,穩定運行

  3)可以支持併發

客戶端:
1)建立套接字描述符(socket)
2)設置服務器的 IP 地址和端口號(須要轉換爲網絡字節序的格式)
3)請求創建到服務器的 TCP 鏈接並阻塞,直到鏈接成功創建(connect)
4)向套接字描述符寫入請求(write / send)
5)從套接字描述符讀取來自服務器的應答(read / recv)
6)關閉套接字描述符(close)

import socket socket.socket(socket_family, socket_type, proto=0) socket_family 能夠是 AF_UNIX 或 AF_INET。socket_type 能夠是 SOCK_STREAM 或 SOCK_DGRAM。proto 通常不填,默認值爲 0。 獲取TCP/IP套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 獲取UDP/IP套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socket模塊函數用法
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 1. 服務端套接字函數
phone.bind('主機ip地址', 端口號)  # 綁定到(主機,端口號)套接字
phone.listen()  # 開始TCP監聽
phone.accept()  # 被動接受TCP客戶的鏈接,等待鏈接的到來
服務端套接字函數
# 2. 客戶端套接字函數
import socket phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 買手機
phone.connect()  # 主動鏈接服務端的ip和端口
phone.connect_ex()  # connect()函數的擴展版本,出錯的時候返回錯碼,而不是拋出異常
客戶端套接字函數
# 3. 服務端和客戶端的公共用途的嵌套字函數
phone.recv()  # 接受TCP數據
phone.send()  # 發送TCP數據
phone.recvfrom()  # 接受UDP數據
phone.sendto()  # 發送UDP數據
phone.getpeername()  # 接收到當前套接字遠端的地址
phone.getsockname()  # 返回指定套接字的參數
phone.setsockopt()  # 設置指定套接字的參數
phone.close()  # 關閉套接字
服務端和客戶端的公共用途的嵌套字函數
# 面向鎖的套接字方法
phone.setblocking()  # 設置套接字的阻塞與非阻塞模式
phone.settimeout()  # 設置阻塞套接字操做的超時時間
phone.gettimeout()  # 獲得阻塞套接字操做的超時時間
面向鎖的套接字方法
# 面向文件的套接字函數
phone.fileno()  # 套接字的文件描述符
phone.makefile()  # 建立一個與該套接字相關的文件
面向文件的套接字函數

TCP是基於連接的,必須先啓動服務器,而後再啓動客戶端去連接服務端

簡單版

import socket # 1. 建立套接字描述符, 用來創建連接
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(phone) # 2. 設置IP和端口號, 綁定套接字描述符
phone.bind(("127.0.0.1", 8080)) # 3. 將套接字描述符設置爲監聽狀態, 設置同一時刻最大請求數爲5
phone.listen(5) print("start...") # 4. 等待來自客戶端的鏈接
conn, client_addr = phone.accept() # accept有返回值,是一個元組 # 元組的第一個參數是雙向連接的套接字對象(即三次握手的結果), 用來收發消息 # 第二個參數是一個元組,存放客戶端的IP和端口號 # print(conn) # print(client_addr)

# 5. 收/發消息, 1024是接收的最大字節數bytes
data = conn.recv(1024) print("收到客戶端的數據", data) conn.send(data.upper()) # 6. 關閉雙向連接的套接字對象
conn.close() # 7. 關閉套接字描述符
phone.close()
服務端
import socket # 1. 建立套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 鏈接服務端的IP地址和端口號
phone.connect(("127.0.0.1", 8080)) # 3. 發/收消息
phone.send("hello".encode("utf-8"))    # 只能發bytes類型
data = phone.recv(1024) print("收到服務端的消息", data) # 4. 關閉套接字描述符
phone.close()
客戶端

因爲 socket 模塊中有太多的屬性。在這裏破例使用了 'from module import *' 語句。使用 'from socket import *',就把 socket 模塊裏的全部屬性都帶到命名空間裏了,這樣能大幅減短代碼。
例如 tcpSock = socket(AF_INET, SOCK_STREAM)

通訊循環

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() # 通訊循環
while True: data = conn.recv(1024) conn.send(data.upper()) conn.close() server.close()
服務端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: msg = input("請輸入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客戶端

可是這樣寫有一個 bug,當你手動結束客戶端的程序運行時,服務端也會跟着崩潰

由於 conn 表明的是一個雙向鏈接,只有服務端和客戶端都正常運行的時候,conn 纔有意義,然而此時客戶端是非正常的斷開,服務端還在使用沒有意義的 conn 作 recv 操做,沒法收到消息,因此在 Windows 上直接崩潰,而在 Linux 上,相同的操做服務端會一直處於收空的狀態

補救措施是,在 Windows 系統上捕捉異常,在 Linux 系統上加上判斷

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) conn, client_addr = server.accept() # 通訊循環
while True: try: data = conn.recv(1024) # 針對Linux系統
        if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服務端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: msg = input("請輸入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客戶端

連接通訊循環

這樣雖然解決了崩潰問題,可是當手動結束客戶端時,服務端仍是會跟着結束,因此在服務端等待客戶端的鏈接前加上循環,從而達到 「連接 + 通訊」 循環

from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 連接循環
while True: conn, client_addr = server.accept() # 通訊循環
    while True: try: data = conn.recv(1024) # 針對Linux系統
            if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服務端
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: msg = input("請輸入: ").strip() client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客戶端

但這樣作,服務端每次只能針對於一個客戶端,只有當這個客戶端的收發消息結束後才能給下一個客戶端服務,沒法達到併發的效果,這個後面學到併發時再講

其實還有一個問題,當客戶端傳一個空消息時,會發生阻塞狀態,由於發空的時候服務端時沒法收到的(空時是什麼都沒有),服務端收不到,沒法返回給客戶端,因此客戶端處於阻塞狀態。補救方法是不讓客戶端輸入空

from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(("127.0.0.1", 8080)) # 通訊循環
while True: msg = input("請輸入: ").strip() if len(msg) == "0": continue client.send(msg.encode("utf-8")) data = client.recv(1024) print(data) client.close()
客戶端
from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 鏈接循環
while True: conn, client_addr = server.accept() # 通訊循環
    while True: try: data = conn.recv(1024) # 針對Linux系統
            if len(data) == 0: break conn.send(data.upper()) except ConnectionResetError: break conn.close() server.close()
服務端

 模擬ssh實現遠程執行命令

當使用客戶端遠程鏈接服務器時,在客戶端上執行命令,服務器會返回命令執行的結果給客戶端,那麼該如何實現呢?

from socket import *
import subprocess server = socket(AF_INET, SOCK_STREAM) server.bind(("127.0.0.1", 8080)) server.listen(5) # 鏈接循環
while True: conn, client_addr = server.accept() # 通訊循環
    while True: try: cmd = conn.recv(1024)  # cmd = b'dir'
            # # 針對Linux系統
            if len(cmd) == 0: break
            # 命令的執行結果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() server.close()
服務端
import socket # 1. 建立套接字描述符
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 鏈接服務端的IP地址和端口號
phone.connect(("127.0.0.1", 8080)) # 3. 發/收消息
phone.send("hello".encode("utf-8"))    # 只能發bytes類型
data = phone.recv(1024) print("收到服務端的消息", data) # 4. 關閉套接字描述符
phone.close()
客戶端

可是目前這樣有一個侷限性,我將接收端數據的最大字節數設置爲1024,當發送端發的數據量小於接收端的1024時,能夠被徹底接收,可是發送端的數據量大於1024時,就只能接收1024條數據,那麼多出的那些數據該如何處理呢?

首先客戶端發送一條執行命令給服務端,讓服務端接收,這裏命令的字節數大多數狀況不會大於1024,因此能夠被徹底接收,暫不考慮,當服務端接收了命令執行後,會將命令的執行結果發送給客戶端,讓客戶端接收,這裏命令的執行結果是頗有可能大於1024個字節的,例如:tasklist,在終端上顯示的最後一條是本身,而在上面所寫的兩個文件中只能顯示幾條結果,很顯然是大於1024的

但這時再輸入 dir 時,居然是 tasklist 沒有執行完的繼續顯示,再輸入其它命令,仍是 tasklist 沒有執行完的繼續顯示,這發生了什麼?

這就是待解決的粘包問題,下一節將會學習

相關文章
相關標籤/搜索