1. socket介紹
1.1 socket套接字
python中提供socket.py标準庫,非常底層的接口庫
socket是一種通用的網絡程式設計接口,和網絡層次沒一一對應的關系
1.2 協定族
AF表示Address Family,用于socket()第一個參數
1.3 Socket類型
2. TCP程式設計
socket程式設計,需要兩端,一般來說需要一個服務端、一個用戶端,服務端稱為Server,用戶端稱為Client。這種程式設計模式也稱為CS程式設計。
2.1 TCP服務端程式設計
2.1.1 服務端程式設計步驟
- 建立socket對象
- 綁定IP位址Address和端口Port。bind()方法,IPv4位址為一個二進制組(‘IP位址字元串’,Port)
- 開始監聽,将在指定的IP的端口上監聽,listen()方法。
- 擷取用于傳送資料的socket對象。socket.accept() -> (socket object, address info) accept方法阻塞等待用戶端建立連接配接,傳回一個新的Socket對象和用戶端位址的二進制位址是遠端用戶端的位址,IPv4中它是一個二進制組(clientaddr, port)
- 接收資料:recv(bufsize[, flags]) 使用緩沖區接收資料
- 發送資料:send(bytes)發送資料
TCP server端開發:
# TCP server 端開發
import socket
# import time
server = socket.socket() # TCP 連接配接 IPv4
ip = '127.0.0.1' # 本機回環位址,永遠指向本機
port = 9999 # 建議使用1000以上; TCP 65536種狀态
server.bind((ip, port)) # address,此方法隻能綁定一次
server.listen() # 真正的顯示出端口,監聽不是阻塞函數
# time.sleep(100)
print(server)
# print(server.accept()) # 預設阻塞,不懂千萬不要修改
new_socket, client_info = server.accept()
print(new_socket)
print('new_socket', client_info)
while True:
# new_socket.send(b'server ack')
data = new_socket.recv(1024) # 預設情況下是阻塞的
print(data)
new_socket.send('server ack. data={}'.format(data.decode()))
new_socket.close()
# new2, client2 = server.accept() # 新的連接配接,之前的連接配接已經關閉,并且兩次連接配接可能在不同的程序
# print(new2)
# print('new2', client2)
# data = new2.recv(1024)
# print(data)
# new2.send('server new2 ack. data={}'.format(data.encode()))
# new2.close()
server.close()
實戰——寫一個群聊程式
需求分析
聊天工具是CS程式,C是每一個用戶端client,S是伺服器端server
伺服器應該具有的功能:
- 啟動服務,包括綁定位址和端口,并監聽
- 建立連接配接,能和多個用戶端建立連接配接
- 接收不同使用者的資訊
- 分發,将接收的 某個使用者的資訊轉發到已連接配接的所有用戶端
- 停止服務
- 記錄連接配接的用戶端
代碼實作
# tcp server 端開發
import socket
import threading
import logging
from datetime import datetime
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.address = ip, port
self.event = threading.Event()
self.lock = threading.Lock()
self.clients = {}
def start(self):
self.sock.bind(self.address)
self.sock.listen()
threading.Thread(target=self.accept, name='accept').start()
def accept(self):
while not self.event.is_set():
try:
new_sock, client_info = self.sock.accept() # 阻塞等待連接配接
except Exception as e:
logging.error(e)
with self.lock:
self.clients[client_info] = new_sock
threading.Thread(target=self.rec, name='rec', args=(new_sock, client_info)).start()
def rec(self, sock, client):
while not self.event.is_set():
try:
data = sock.recv(1024) # 阻塞等待接收資訊,接收資訊也可能出現異常
except Exception as e:
logging.error(e)
data = b''
print(data.decode(encoding='cp936'))
if data.strip() == b'quit' or data.strip() == b'': # 用戶端主動斷開連接配接
with self.lock:
self.clients.pop(client) # 将斷開連接配接的客戶ip和端口從字典中移除,因為下文還要周遊字典
sock.close() # 此句比較耗時,可以放在鎖的外面
break
msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] - {}".format(datetime.now(), *client, data.decode(encoding='cp936'))
exc = []
exs = []
with self.lock:
for c, s in self.clients.items(): # 周遊的是時候不允許别人pop和add,是以加鎖
try:
s.send(msg.encode(encoding='cp936')) # 給所有的new_sock發送的資訊。注意此句可能會出現異常,如網絡斷了
except Exception as e:
logging.error(e)
exc.append(c)
exs.append(s)
for c in exc:
self.clients.pop(c)
for s in exs: # 此句比較耗時,是以放在鎖外面
s.close()
def stop(self):
self.event.set()
with self.lock:
values = list(self.clients.values())
self.clients.clear() # 這是一個好習慣
for s in values:
s.close()
self.sock.close()
cs = ChatServer()
cs.start()
while True:
cmd = input(">>>").strip()
if cmd == 'quit':
cs.stop()
break
logging.info(threading.enumerate())
logging.info(cs.clients)
socket常用方法
makefile
socket.makefile(mode=‘r’, buffering=None, *, encoding=None, error=None, newline=None)
建立一個與該套接字相關聯的檔案對象,将recv方法看做讀方法,将send方法看做寫方法。
import socket
s = socket.socket()
s.bind(('127.0.0.1', 9999))
s.listen()
s1, info = s.accept()
print(s1.getpeername()) # ('127.0.0.1', 55934)
print(s1.getsockname()) # ('127.0.0.1', 9999)
f = s1.makefile('rw')
data = f.read(10) # 一次讀取10個位元組
print(data)
msg = 'server rec {}'.format(data)
f.write(msg)
f.flush()
print(f, s1, sep='\n')
f.close()
s1.close()
s.close()
makefile練習
使用makefile改寫群聊類
import datetime
import threading
import socket
import logging
FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.address = ip, port
self.event = threading.Event()
self.clients = {}
self.lock = threading.Lock()
def start(self):
self.sock.bind(self.address)
self.sock.listen()
threading.Thread(target=self.accept, name='accept').start()
def accept(self):
while not self.event.is_set():
new_sock, client_info = self.sock.accept()
new_file = new_sock.makefile('rw')
with self.lock:
self.clients[client_info] = new_file, new_sock # 增加了item,是以必須加鎖,多線程處理同一個資源
threading.Thread(target=self.rec, name='rec', args=(new_file, client_info)).start()
def rec(self, f, client):
while not self.event.is_set():
line = f.readline() # 阻塞等一行來,輸入資料的時候要加換行符
print(line)
if line.strip() == 'quit' or line.strip() == '': # line為字元串。不能再寫成b''和b'quit'了
with self.lock:
_, s = self.clients.pop(client)
f.close()
s.close()
break
msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
# 此處的line不用解碼了,因為readline讀取的是字元串
with self.lock:
for ff, _ in self.clients.values(): # 注意ff不能與上面的f重複
ff.write(msg)
ff.flush()
# def rec(self, f, client):
# while not self.event.is_set():
# try:
# line = f.readline()
# except Exception as e:
# logging.error(e)
# line = 'quit'
# msg = line.strip()
# if msg == 'quit' or msg == '':
# with self.lock:
# _, sock = self.clients.pop(client)
# f.close()
# sock.close()
# logging.info('{} quits.'.format(client))
# break
# msg = '{:%Y/%m/%d %H:%M:%S} [{}: {}] {}'.format(datetime.datetime.now(), *client, line)
# logging.info(msg)
# with self.lock:
# for ff, _ in self.clients.values():
# ff.write()
# ff.flush()
def stop(self):
self.event.set()
# keys = []
with self.lock:
for f, s in self.clients.values():
f.close()
s.close()
self.sock.close()
def main():
cs = ChatServer()
cs.start()
while True:
cmd = input(">>>").strip()
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate())
logging.info(cs.clients)
if __name__ == '__main__':
main()
2.2 用戶端程式設計
2.2.1 用戶端程式設計步驟
- 建立socket對象
- 連接配接到遠端服務端的ip和port,connect()方法
- 傳輸資料:使用send、recv方法發送、接收資料
- 關閉連接配接,釋放資源
import socket
client = socket.socket()
ip_address = ('127.0.0.1', 9999)
client.connect(ip_address) # 直接連接配接伺服器
client.send(b'abc\n')
data = client.recv(1024) # 阻塞等待
client.close()
import socket
import threading
import datetime
import logging
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatClient:
def __init__(self, ip='127.0.0.1', port=9999):
self.client = socket.socket()
self.address = ip, port
self.event = threading.Event()
def start(self):
self.client.connect(self.address)
self.send("I'm ready.")
threading.Thread(target=self.rec, name='rec').start()
def rec(self):
while not self.event.is_set():
try:
data = self.client.recv(1024) # 此句可能會出現異常,如網絡中斷
except Exception as e:
logging.error(e)
break
msg = "{:%Y/%m/%d %H:%M:%S} [{}:{}] {}".format(datetime.datetime.now(), *self.address, data)
logging.info(msg)
def send(self, msg: str):
data = "{}\n".format(msg.strip()).encode()
self.client.send(data)
def stop(self):
self.client.close()
self.event.wait(3)
self.event.set()
logging.info('Client stops')
cc = ChatClient()
cc.start()
while True:
cmd = input(">>>").strip()
if cmd == 'quit':
cc.stop()
break
cc.send(cmd)
内容源碼均在github中的python倉庫