12. 基于 UDP 的套接字
UDP 是无链接的,可以实现服务端与多个客户端进行同时进行通讯,无论先启动哪一端都可以。(qq 聊天 UDP 服务)
12.1 基于 udp 套接字模板
UDP 服务端
s = socket(AF_INET, SOCK_DGRAM) # 创建一个服务器的套接字
s.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
data,addr = s.recvfrom(buffer_size) # 接收
s.sendto(data.upper(), addr) # 发送
s.close() # 关闭服务器套接字
UDP 客户端
s = socket(AF_INET, SOCK_DGRAM) # 创建客户端套接字
comm_loop: # 通讯循环
s.sendto('xxx', ip_port) # 发送
data,addr = s.recvfrom(2024) # 接收
s.close() # 关闭客户端套接字
示例:
服务端
from socket import *
ip_port = (gethostname(), 8080)
buffer_size = 1024
udp_server = socket(AF_INET, SOCK_DGRAM)
udp_server.bind(ip_port)
while True:
conn, addr = udp_server.recvfrom(buffer_size)
print('客户端发来信息:', conn.decode('utf-8'))
udp_server.sendto(conn.upper(), addr)
udp_server.close()
客户端
from socket import *
ip_port = (gethostname(), 8080)
buffer_size = 1024
udp_client = socket(AF_INET, SOCK_DGRAM)
while True:
msg = input('>>>').strip()
udp_client.sendto(msg.encode('utf-8'), ip_port)
data, addr = udp_client.recvfrom(buffer_size)
print('服务端发来信息:', data.decode('utf-8'))
udp_client.close()
12.2 基于 UDP 实现 ntp 服务
ntp 服务(Network Time Protocol),是用来使计算机时间同步化的一种协议。
需求:基于 UDP 服务实现 ntp 服务,获取服务端时间:
服务端:
from socket import *
import time
ip_port = (gethostname(), 8080)
buffer_size = 1024
ntp_server = socket(AF_INET, SOCK_DGRAM)
ntp_server.bind(ip_port)
while True:
data, addr = ntp_server.recvfrom(buffer_size) # data: 客户端发来的信息 addr:客户端地址
print('客户端时间格式:', data.decode('utf-8'))
if not data: # 如果客户端输入的为空
fmt = '%Y-%m-%d %X'
else:
fmt = data.decode('utf-8') # 客户端自定义的时间格式
back_time = time.strftime(fmt)
ntp_server.sendto(back_time.encode('utf-8'), addr)
ntp_server.close()
客户端:
from socket import *
ip_port = (gethostname(), 8080)
buffer_size = 1024
ntp_client = socket(AF_INET, SOCK_DGRAM)
while True:
msg = input('>>>') # 客户端输入时间格式
ntp_client.sendto(msg.encode('utf-8'), ip_port)
data, addr = ntp_client.recvfrom(buffer_size)
print('服务端时间:', data.decode('utf-8'))
ntp_client.close()
13. socketserver 实现并发
利用 socketserver 模块可以实现基于 tcp 服务的套接字并发功能。基于 tcp 的套接字主要有两个关键循环:一个链接循环,一个通讯循环。
sockeserver 模块分为两大类:server 类(解决链接问题),request 类(解决通讯类)。
server 类
- 与链接相关:BaseServer、TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer
- 基于多线程实现并发:ThreadingMixIn、ThreadingTCPServer、ThreadingUDPServer
- 基于多进程实现并发:ForkingMinIn、ForkingTCPServer、ForkingUDPServer
request 类
BaseRequestHandler、StreamRequestHandler、DatagramRequestHandler
继承关系
源码分析
- 基于 tcp 的 sockserver 我们自己定义的类中的:
self.server
:套接字对象self.request
:一个链接self.client_address
:客户端地址
- 基于 udp 的 sockserver 我们自己定义的类中的:
self.request
:是一个元组,第一个元素是客户端发来的数据,第二个元素是服务端的 udp 套接字对象self.client_address
:客户端地址
示例
TCP 实现并发
服务端:
import socketserver
ip_port = ('127.0.0.1', 8080)
buffer_size = 1024
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
print('客户端地址', self.client_address)
print('客户端链接', self.request)
# 通讯循环
while True:
try:
# 收消息
data = self.request.recv(1024)
if not data :break
print('收到客户端%s的消息是:%s' %(self.client_address, data.decode('utf-8')))
# 发消息
self.request.send(data.upper())
except Exception as e:
print(e)
# 链接循环
if __name__ == '__main__':
# 内部会产生一个 socket 对象,并连接客户端
s = socketserver.ThreadingTCPServer(ip_port, MyServer) # 第一个参数:ip_port,第二个:自定义的类名
s.serve_forever()
客户端:
from socket import *
ip_port = ('127.0.0.1', 8080)
buffer_size = 1024
client = socket(AF_INET, SOCK_STREAM)
client.connect(ip_port)
while True:
msg = input(">>>").strip()
if not msg:continue
if msg == 'quit':break
client.send(msg.encode('utf-8'))
data = client.recv(buffer_size)
print('收到服务端的消息', data.decode('utf-8'))
client.close()
socketserver 模块利用进程、线程来实现并发功能,其内部封装了很多类(如:创建socket对象,实现链接循环,以及利用进程、线程实现并发)。我们利用这些接口去实现通讯循环即可。
我们必须定义一个自己的类,并实现 handle() 方法 。需要注意的是我们不需要修改客户端。
UDP 实现并发
服务端:
import socketserver
ip_port = ('127.0.0.1', 8080)
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
print(self.request)
# self.request: (b'ahhd', <socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
# 第一个元素:客户端发来的消息,第二个:服务端 socket 对象
print('收到客户端消息是', self.request[0])
self.request[1].sendto(self.request[0].upper(), self.client_address)
if __name__ == '__main__':
s = socketserver.ThreadingUDPServer(ip_port, MyServer)
s.serve_forever()
客户端:
from socket import *
ip_port = ('127.0.0.1', 8080)
buffer_size = 1024
client = socket(AF_INET, SOCK_DGRAM)
while True:
msg = input('>>>').strip()
if not msg:continue
if msg == 'quit':break
client.sendto(msg.encode('utf-8'), ip_port)
data, addr = client.recvfrom(buffer_size)
print('服务端发来消息', data.decode('utf-8'))
client.close()
14. 认证客户端链接合法性
如果想在分布式系统中实现一个简单的客户端链接认证功能,又不像 SSL 那么复杂,那么可以采用 hmac(算法加密)+ 盐(随机密码)的方法来实现。
当服务端受到 syn 洪水攻击(即攻击者利用很多客户端发送空信息,造成服务端奔溃)时,验证客户端链接合法性很有必要。
服务端:
# _*_coding:utf-8_*_
__author__ = 'Hubery_Jun'
from socket import *
import hmac, os
# 验证 key,即 salt
secret_key = b'Life is Short, you need Python'
def conn_auth(conn):
"""
验证链接合法性
:param conn:
:return:
"""
print('开始验证链接合法性')
msg = os.urandom(32) # 随机产生一个 32 字节长的字节对象,用于加密
conn.sendall(msg) # 发送给客户端验证
h = hmac.new(secret_key, msg) # 使用 hmac 模块(msg + salt)进行算法加密
digest = h.digest()
res = conn.recv(len(digest)) # 接收客户端的验证信息
return hmac.compare_digest(res, digest) # 两者比较,相同返回 True
def data_handler(conn, buffer_size=1024):
"""
接收数据
:param conn:链接
:param buffer_size:接收数据大小
:return:
"""
if conn_auth(conn):
print('链接不合法')
conn.close()
return
print('链接合法,开始通讯')
while True:
data = conn.recv(buffer_size)
if not data:break
print('客户端发来消息', data.decode('utf-8'))
conn.sendall(data.upper())
def server_handler(ip_port, buffer_size, back_log=5):
"""
处理链接
:param ip_port: ip + port
:param buffer_size: 接收数据大小
:return:
"""
s = socket(AF_INET, SOCK_STREAM)
s.bind(ip_port)
s.listen(back_log)
while True:
conn, addr = s.accept()
print('客户端链接', addr)
data_handler(conn, buffer_size)
if __name__ == '__main__':
ip_port = (gethostname(), 8080)
buffer_size = 1024
server_handler(ip_port, buffer_size)
客户端:
# _*_coding:utf-8_*_
__author__ = 'Hubery_Jun'
# 验证密码
secret_key = b'Life is Short, you need Python'
from socket import *
import hmac
def conn_auth(client):
"""
验证客户端到服务端的链接
:param client:
:return:
"""
msg = client.recv(32)
h = hmac.new(msg, secret_key)
digest = h.digest()
client.sendall(digest)
def client_handler(ip_port, buffer_size):
"""
通讯循环
:param ip_port: ip + port
:param buffer_size: 数据大小
:return:
"""
client = socket(AF_INET, SOCK_STREAM)
client.connect(ip_port)
conn_auth(client)
while True:
data = input('>>>').strip()
if not data:continue
if data == 'quit':break
client.sendall(data.encode('utf-8'))
res = client.recv(buffer_size)
print('服务端发来消息', res.decode('utf-8'))
client.close()
if __name__ == '__main__':
ip_port = (gethostname(), 8080)
buffer_size = 1024
client_handler(ip_port, buffer_size)
客户端链接 ('192.168.152.1', 9866)
开始验证链接合法性
链接合法,开始通讯
客户端发来消息 dir
- 服务端: 利用
os.urandom(32)
产生一个随机的 32字节长度的字节对象 msg,并发送给客户端验证。自己随便编写一个 key,然后利用 hamc 模块进行算法加密。 - 客户端: 接收服务端发来的 msg,再利用 hmac 模块进行算法加密,然后再把加密后的信息发送给服务端,进行比较。如客户端发过去的信息和服务端加密后的信息相符,则返回 True,链接合法,否则不合法,直接关闭链接。
当客户端不知道 key 时,或者不知道加密方法时,向服务端发起链接请求,就会验证失败。
- AF_INET:基于网络
- SOCK_DGRAM:基于 UDP ,数据报,无链接
- SOCK_STREAM:基于 TCP,数据流
- 发送数据要是字节形式(二进制)
- udp 发送数据,后面要跟 host+port
- udp 接收数据,接收的是个元组(数据,主机+port)
- udp 可以接收多个客户端同时
- tcp:不能实现并发,只能同时服务一个客户端
- udp:可以实现并发,能同时服务多个客户端,因为它没有链接
- recvfrom 接受空也不会报错