socket是基於C/S架構的,也就是說進行socket網絡編程,通常需要編寫兩個py文件,一個服務端,一個客戶端。
首先,導入Python中的socket模塊: import socket
Python中的socket通信邏輯如下圖所示(圖片來自網絡):
這張邏輯圖,是整個socket編程中的重點的重點,你必須將它理解、喫透,然後刻在腦海裏,真正成爲自己記憶的一部分!很多人說怎麼都學不會socket編程,歸根到底的原因就是沒有“死記硬背”知識點。
在Python中,import socket後,用socket.socket()方法來創建套接字,語法格式如下:
sk = socket.socket([family[, type[, proto]]])
參數說明:
- family: 套接字家族,可以使AF_UNIX或者AF_INET。
- type: 套接字類型,根據是面向連接的還是非連接分爲SOCK_STREAM或SOCK_DGRAM,也就是TCP和UDP的區別。
- protocol: 一般不填默認爲0。
直接socket.socket(),則全部使用默認值。
下面是具體的參數定義:
通過s = socket.socket()方法,我們可以獲得一個socket對象s,也就是通常說的獲取了一個“套接字”,該對象具有一下方法:
注意事項:
1.Python3以後,socket傳遞的都是bytes類型的數據,字符串需要先轉換一下,string.encode()即可;另一端接收到的bytes數據想轉換成字符串,只要bytes.decode()一下就可以。
2.在正常通信時,accept()和recv()方法都是阻塞的。所謂的阻塞,指的是程序會暫停在那,一直等到有數據過來。
socket編程思路:
服務端:
1.創建套接字,綁定套接字到本地IP與端口:socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.bind()
2.開始監聽連接:s.listen()
3.進入循環,不斷接受客戶端的連接請求:s.accept()
4.接收傳來的數據,或者發送數據給對方:s.recv() , s.sendall()
5.傳輸完畢後,關閉套接字:s.close()
客戶端:
1.創建套接字,連接服務器地址:socket.socket(socket.AF_INET,socket.SOCK_STREAM) , s.connect()
2.連接後發送數據和接收數據:s.sendall(), s.recv()
3.傳輸完畢後,關閉套接字:s.close()
Python的socket編程,通常可分爲TCP和UDP編程兩種,前者是帶連接的可靠傳輸服務,每次通信都要握手,結束傳輸也要揮手,數據會被檢驗,是使用最廣的通用模式;後者是不帶連接的傳輸服務,簡單粗暴,不加控制和檢查的一股腦將數據發送出去的方式,但是傳輸速度快,通常用於安全和可靠等級不高的業務場景,比如文件下載。
TCP編程
服務器端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
ip_port = ('127.0.0.1', 9999)
sk = socket.socket() # 創建套接字
sk.bind(ip_port) # 綁定服務地址
sk.listen(5) # 監聽連接請求
print('啓動socket服務,等待客戶端連接...')
conn, address = sk.accept() # 等待連接,此處自動阻塞
while True: # 一個死循環,直到客戶端發送‘exit’的信號,才關閉連接
client_data = conn.recv(1024).decode() # 接收信息
if client_data == "exit": # 判斷是否退出連接
exit("通信結束")
print("來自%s的客戶端向你發來信息:%s" % (address, client_data))
conn.sendall('服務器已經收到你的信息'.encode()) # 回饋信息給客戶端
conn.close() # 關閉連接
客戶端:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
ip_port = ('127.0.0.1', 9999)
s = socket.socket() # 創建套接字
s.connect(ip_port) # 連接服務器
while True: # 通過一個死循環不斷接收用戶輸入,併發送給服務器
inp = input("請輸入要發送的信息: ").strip()
if not inp: # 防止輸入空信息,導致異常退出
continue
s.sendall(inp.encode())
if inp == "exit": # 如果輸入的是‘exit’,表示斷開連接
print("結束通信!")
break
server_reply = s.recv(1024).decode()
print(server_reply)
s.close() # 關閉連接
上面這個例子,基本能夠展示出socket通信的機制。套接字的創建和關閉,服務器的綁定和監聽,客戶端的連接,這些都是固定套路,沒什麼難點。關鍵之處在於循環內部的收發邏輯,這裏纔是重點,需要根據你自己的業務需求,正確編寫。這個過程中,一定要注意,收發是一一對應的,有發就要有收,並且recv()方法默認是阻塞的。
大家可以在自己的環境中測試上面的例子,並加入更多的內容,不斷地進行嘗試。然而試過之後,你們會發現,雖然服務器和客戶端在一對一的情況下,工作良好,但是,如果有多個客戶端同時連接同一個服務器呢?結果可能不太令人滿意,因爲服務器無法同時對多個客戶端提供服務。爲什麼會這樣呢?因爲Python的socket模塊,默認情況下創建的是單進程單線程,同時只能處理一個連接請求,如果要實現多用戶服務,那麼需要使用多線程機制。
下面我們使用Python內置的threading模塊,配合socket模塊創建多線程服務器。客戶端的代碼不需要修改,可以繼續使用。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import threading # 導入線程模塊
def link_handler(link, client):
"""
該函數爲線程需要執行的函數,負責具體的服務器和客戶端之間的通信工作
:param link: 當前線程處理的連接
:param client: 客戶端ip和端口信息,一個二元元組
:return: None
"""
print("服務器開始接收來自[%s:%s]的請求...." % (client[0], client[1]))
while True: # 利用一個死循環,保持和客戶端的通信狀態
client_data = link.recv(1024).decode()
if client_data == "exit":
print("結束與[%s:%s]的通信..." % (client[0], client[1]))
break
print("來自[%s:%s]的客戶端向你發來信息:%s" % (client[0], client[1], client_data))
link.sendall('服務器已經收到你的信息'.encode())
link.close()
ip_port = ('127.0.0.1', 9999)
sk = socket.socket() # 創建套接字
sk.bind(ip_port) # 綁定服務地址
sk.listen(5) # 監聽連接請求
print('啓動socket服務,等待客戶端連接...')
while True: # 一個死循環,不斷的接受客戶端發來的連接請求
conn, address = sk.accept() # 等待連接,此處自動阻塞
# 每當有新的連接過來,自動創建一個新的線程,
# 並將連接對象和訪問者的ip信息作爲參數傳遞給線程的執行函數
t = threading.Thread(target=link_handler, args=(conn, address))
t.start()
啓動這個多線程服務器,然後多運行幾個客戶端,可以很明顯地看到,服務器能夠同時與多個客戶端通信,基本達到我們的目的。
UDP編程:
相對TCP編程,UDP編程就簡單多了,當然可靠性和安全性也差很多。由於UDP沒有握手和揮手的過程,因此accept()和connect()方法都不需要。下面是一個簡單的例子:
# 服務端
import socket
ip_port = ('127.0.0.1', 9999)
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
sk.bind(ip_port)
while True:
data = sk.recv(1024).strip().decode()
print(data)
if data == "exit":
print("客戶端主動斷開連接!")
break
sk.close()
# 客戶端
import socket
ip_port = ('127.0.0.1', 9999)
sk = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
while True:
inp = input('發送的消息:').strip()
sk.sendto(inp.encode(), ip_port)
if inp == 'exit':
break
sk.close()