1. 程式人生 > >初學Python——Socket網絡編程

初學Python——Socket網絡編程

客服 for 打電話 字符串 並不會 參數 內核 忽略 發送信息

認識socket

socket本質上就是在2臺網絡互通的電腦之間,架設一個通道,兩臺電腦通過這個通道來實現數據的互相傳遞。我們知道網絡 通信 都 是基於 ip+port(端口) 方能定位到目標的具體機器上的具體服務,操作系統有0-65535個端口,每個端口都可以獨立對外提供服務,如果 把一個公司比做一臺電腦 ,那公司的總機號碼就相當於ip地址, 每個員工的分機號就相當於端口, 你想找公司某個人,必須 先打電話到總機,然後再轉分機 。

建立一個socket必須至少有2端, 一個服務端,一個客戶端, 服務端被動等待並接收請求,客戶端主動發起請求, 連接建立之後,雙方可以互發數據。

基本參數

Socket Families(地址簇)

socket.AF_UNIX 本機進程間通信

socket.AF_INET  IPV4(默認) 

socket.AF_INET6 IPV6

Socket Types(類型)

socket.SOCK_STREAM 流式socket,代表TCP協議(默認)

socket.SOCK_DGRAM 數據報式socket,代表UDP協議

socket方法

sk = socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

建立socket連接對象

sk.bind(address)

  s.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,將所有內容發送出去。

服務端步驟:

步驟:
1.server = socket.socket() 聲明實例,生成連接對象
2.server.bind() 綁定要監聽的端口
3.server.listen() 開始監聽
4.conn,addr = server.accept()等待客戶端發起連接,阻塞
5.接收數據(發送數據)
6.當客戶端斷開連接後,繼續監聽等待下一個客戶端建立連接
……
關閉連接對象

代碼示例:

import socket
"服務器端"

server = socket.socket() # 生成連接對象
server.bind(("localhost",6000)) # 綁定要監聽的端口
server.listen(5) # 開始監聽(最大允許掛起的連接)
while True:
    print("\n服務器在等待...")
    conn,addr = server.accept() # 等待客戶端發起建立連接,起到阻塞作用
    # conn 是客戶端鏈接過來而在服務器端為其生成的連接實例,addr是IP地址+端口
    print("已成功連接")
    print("連接對象:{0},地址:{1}\n".format(conn,addr))
    while True:
        try:
            data = conn.recv(1024)  # 接收數據
            # (如果客服端斷開連接,此步驟將會被無限循環操作,所以一定要有檢查機制)
            if data != b"000001":  # 接收到的信號不是"000001"的話就正常執行
                print("接收客戶端信息:", data.decode())
                msg = input(">>輸入返回客戶端的數據:")
                conn.send(msg.encode(encoding="utf-8")) # 向客戶端發送數據
            else:
                print("該客戶已主動斷開連接")
                break
        except ConnectionResetError as e:
            print("該客戶機異常!已被強迫斷開連接",e)
            break
        else:
            print("It‘s OK !")

server.close()

客戶端步驟:

步驟:
1.client = socket.socket() 聲明實例,生成連接對象
2.client.connect() 與服務器建立連接
3.與服務器交互(發送接收數據)
4.client.close() 斷開連接

代碼示例:

import socket
"通信案例客戶端消息接收與發送"
client = socket.socket() # 聲明socket類型,並生成socket連接對象
try:
    client.connect(("localhost",6000)) # 與服務器建立連接

    while True:
        msg = input(">>輸入要向服務器發送的信息:")
        client.send(msg.encode(encoding="utf-8")) # 向服務器發送信息(只能發送bytes字節類型,不能是str字符類型)
        data = client.recv(1024) # 接收來自服務器的1024個字節
        print("接收來自服務器的數據:",data.decode()) # 打印服務器發送的數據
        chioce = input("按任意鍵繼續,按0退出客戶端")
        if chioce == "0":
            client.send(b"000001") # 發送此信號表明客戶端要斷開連接
            break
        client.close()
except ConnectionRefusedError as e:
    print("服務器還沒開機!請靜候")

需要註意的是:

1.客戶端再發送數據時,要主要服務器接收的大小限制。如果超過了這個限制,超出的部分暫時存在系統緩沖區,第二次接收的時候再輸出剩下的部分。例如服務器端的recv(1024),而客戶端一次發了2024字節,那麽剩下的1000字節存在緩沖區,第二次接收的時候會接收緩沖區的內容,將不會接收新發來的數據,會造成數據錯誤。官方建議一次性不超過8192字節

2.雙發收發數據只能是bytes類型

3.粘包問題,下面講

socket粘包問題

什麽是粘包呢?我們知道發送數據,並且數據量比較大時,並不會一次性發送,即使能一次發送,客戶端也不一定能一次性接收,所以服務器有個緩沖區,等客戶端下次再接收數據的時候再發送給客戶端,所以,就需要將數據分成幾次發送,客戶端分成幾次接收。那又出來問題了,客戶端知道數據(文件)有多大麽?它怎麽知道要接收幾次?當然是要服務器告訴他啦!

於是,我們設計服務器首先發送數據大小(數據),再開始分批發送數據,客戶端先接收文件大小(數據),再開始分批接收。問題就有可能在這裏出現了。如果連續2次send數據,很有可能將兩次的數據黏在一起發送出去,在客戶端也無法將其分開,怎麽辦呢?

我們可以讓服務器每次發送數據後,接收來自客戶端的確認,這樣會強制清空緩沖區,就不會造成粘包。當然,基於上面講的方法,只需要在發送正式數據之前接收確認就好。

最後,如果希望100%確認雙發收發數據是否一致,可以采用MD5校驗。

服務器端步驟:

1.讀取文件名
2.檢測文件是否存在
3.打開文件
4.檢測文件大小
5.將文件大小發給客戶端
6.確認
7.開始邊讀邊發(循環發送)
8.發送MD5

代碼:

import socket,os,hashlib

ser = socket.socket()
ser.bind(("localhost",5000))
ser.listen()

while True:
    try:
        print("正在等待客戶端連接...")
        conn,addr = ser.accept()
        print("已連接,new conn:",addr)
        while True:
            data = conn.recv(8192)
            filename = data.decode()
            print("尋找文件",filename)
            if os.path.isfile(filename):
                conn.send(b"01")
                conn.recv(1024)
                f = open(filename,"rb")
                m = hashlib.md5()
                file_size = os.stat(filename).st_size
                conn.send(str(file_size).encode(encoding="utf-8"))
                client_ack = conn.recv(1024)  # 接收確認信息
                if client_ack == b"1":
                    print("開始發送數據")
                for line in f:
                    m.update(line)
                    conn.send(line)
                f.close()
                conn.send(m.hexdigest().encode(encoding="utf-8")) # 發送MD5值
            else:
                conn.send(b"00") #表示文件不存在
                print("該文件不存在!")
    except ConnectionResetError:
        print("該客戶端已斷開連接")


ser.close()
print("服務器已關閉")

客戶端步驟:

1.發送接收文件請求,同時將文件名發送給服務器

2.接收文件長度

3.本地新建同名文件,循環接收數據,並將其寫入文件

4.同時更新本地MD5值

5.接收數據完畢後,再接收服務器的MD5值,與本地MD5值進行比較

代碼:

import socket,hashlib


def receive1(client):
    "真正的數據接收"
    while True:
        res = b""
        res = res + client.recv(1024)
    return res

def receive(client,filename):
    "接收處理"
    m = hashlib.md5()
    rece_res_size = int(client.recv(1024).decode())  # 接收的結果長度,轉成int型
    client.send(b"1")
    rece_size = 0
    res = b""
    filename = filename.decode()
    f = open(filename + ".new","wb")
    while rece_size < rece_res_size:
        if rece_res_size - rece_size >1024: # 如果不是最後一次接收數據
            size = 1024
        else: # 最後一次接收數據
            size = rece_res_size - rece_size
        a = client.recv(size) # 循環接收數據
        res = res + a
        rece_size = len(res)
        m.update(a)
        f.write(a)
        print("發送數據量:{0},接收數據量:{1}".format(rece_res_size,rece_size))
    else:
        serves_md5 = client.recv(1024).decode()
        print("服務器MD5:",serves_md5)
        print("客戶端MD5:",m.hexdigest())
        if serves_md5 == m.hexdigest():
            print("文件接收並校驗完畢!")
    res.decode()
    f.close()

def main():
    client = socket.socket()
    try:
        client.connect(("localhost", 5000))
        while True:
            filename = input("請輸入需要的文件名").strip().encode(encoding="utf-8")
            if len(filename) == 0:
                print("輸入為空,重新輸入")
                continue
            client.send(filename)
            is_file = client.recv(1024)
            if is_file == b"01":
                client.send(b"OK")
                receive(client,filename) # 調用函數接收數據,返回結果res(bytes)
            else:
                print(" {0} 文件不存在!".format(filename.decode()))

    except ConnectionRefusedError:
        print("等待服務器開機")
    client.close()

main()

socketserver

什麽是socketserver?為什麽需要它呢?

我們在前面的文章中普通的socket並不能同時處理多個客戶端,當一個客戶端在與服務器連接時,其它客戶端只能排隊。而sockerserver則不同,它可以並發地處理多個客戶端請求。

import socketserver
‘‘‘
每一個客戶端請求過來,都會實例化 MyTCPHandler
‘‘‘

class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        "跟客戶端所有的交互都是在handle裏完成的"
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{0} wrote:".format(self.client_address[0]))
                print(self.data)
                if not self.data:
                    print("輸入為空")
                    self.request.send(bytes("輸入為空", "utf-8"))
                else:
                    self.request.send(self.data.upper())
            except ConnectionResetError :
                print("客戶已斷開連接")
                break

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    #server = socketserver.TCPServer((HOST, PORT), MyTCPHandler) # 實例化一對一的連接對象
    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) # 實例化多並發的連接對象(多線程)
    server.serve_forever()

客戶端並沒有什麽區別:

import socket

HOST, PORT = "localhost", 9999
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
while True:
    data = input("輸入字符")
    try:
        sock.sendall(bytes(data + "\n", "utf-8"))

        received = str(sock.recv(1024), "utf-8")
    finally:
        print("Sent:     {0}".format(data))
        print("Received: {0}".format(received))

sock.close()

初學Python——Socket網絡編程