UDP 是无链接的,可以实现服务端与多个客户端进行同时进行通讯,无论先启动哪一端都可以。(qq 聊天 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 = 1024udp_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 = 1024udp_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()
ntp 服务(Network Time Protocol),是用来使计算机时间同步化的一种协议。
需求:基于 UDP 服务实现 ntp 服务,获取服务端时间:
服务端:
from socket import *
import timeip_port = (gethostname(), 8080)
buffer_size = 1024ntp_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 = 1024ntp_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()
利用 socketserver 模块可以实现基于 tcp 服务的套接字并发功能。基于 tcp 的套接字主要有两个关键循环:一个链接循环,一个通讯循环。
sockeserver 模块分为两大类:server 类(解决链接问题),request 类(解决通讯类)。
server 类
request 类
BaseRequestHandler、StreamRequestHandler、DatagramRequestHandler
继承关系
源码分析
self.server
:套接字对象self.request
:一个链接self.client_address
:客户端地址self.request
:是一个元组,第一个元素是客户端发来的数据,第二个元素是服务端的 udp 套接字对象self.client_address
:客户端地址示例
TCP 实现并发
服务端:
import socketserverip_port = ('127.0.0.1', 8080)
buffer_size = 1024class MyServer(socketserver.BaseRequestHandler):def handle(self):print('客户端地址', self.client_address)print('客户端链接', self.request)# 通讯循环while True:try:# 收消息data = self.request.recv(1024)if not data :breakprint('收到客户端%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 = 1024client = socket(AF_INET, SOCK_STREAM)
client.connect(ip_port)while True:msg = input(">>>").strip()if not msg:continueif msg == 'quit':breakclient.send(msg.encode('utf-8'))data = client.recv(buffer_size)print('收到服务端的消息', data.decode('utf-8'))client.close()
socketserver 模块利用进程、线程来实现并发功能,其内部封装了很多类(如:创建socket对象,实现链接循环,以及利用进程、线程实现并发)。我们利用这些接口去实现通讯循环即可。
我们必须定义一个自己的类,并实现 handle() 方法 。需要注意的是我们不需要修改客户端。
UDP 实现并发
服务端:
import socketserverip_port = ('127.0.0.1', 8080)class MyServer(socketserver.BaseRequestHandler):def handle(self):print(self.request)# self.request: (b'ahhd', )# 第一个元素:客户端发来的消息,第二个:服务端 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 = 1024client = socket(AF_INET, SOCK_DGRAM)while True:msg = input('>>>').strip()if not msg:continueif msg == 'quit':breakclient.sendto(msg.encode('utf-8'), ip_port)data, addr = client.recvfrom(buffer_size)print('服务端发来消息', data.decode('utf-8'))client.close()
如果想在分布式系统中实现一个简单的客户端链接认证功能,又不像 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) # 两者比较,相同返回 Truedef data_handler(conn, buffer_size=1024):"""接收数据:param conn:链接:param buffer_size:接收数据大小:return:"""if conn_auth(conn):print('链接不合法')conn.close()returnprint('链接合法,开始通讯')while True:data = conn.recv(buffer_size)if not data:breakprint('客户端发来消息', 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 = 1024server_handler(ip_port, buffer_size)
客户端:
# _*_coding:utf-8_*_
__author__ = 'Hubery_Jun'# 验证密码
secret_key = b'Life is Short, you need Python'from socket import *
import hmacdef 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:continueif data == 'quit':breakclient.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 = 1024client_handler(ip_port, buffer_size)
客户端链接 ('192.168.152.1', 9866)
开始验证链接合法性
链接合法,开始通讯
客户端发来消息 dir
os.urandom(32)
产生一个随机的 32字节长度的字节对象 msg,并发送给客户端验证。自己随便编写一个 key,然后利用 hamc 模块进行算法加密。当客户端不知道 key 时,或者不知道加密方法时,向服务端发起链接请求,就会验证失败。