1. 设计题目
基于 UDP 协议的 GUI 聊天室实现
2. 开发环境
Windows 10
阿里云 ECS 服务器 ubuntu_18_04_x64(无图形化界面)
3. 开发工具
FinalShell 3.0.10
Idle (Python 3.7.3 Shell)
ecs-workbench 工作台
4. 设计思想
4.1 服务器、系统、编程语言的选择
服务器这里选择的是阿里云 ECS(Elastic Compute Service),是一种弹性可伸缩的 计算服务,支持根据业务波动随时扩展和释放资源,简单概括其优点:
- 提供虚拟防火墙、角色权限控制、内网隔离、防病毒攻击及流量监控等多重安全方案;
- 支持通过内网访问其他阿里云服务,形成丰富的行业解决方案,降低公网流量成本;
- 提供行业通用标准 API,提高易用性和适用性。
系统这里选择的是基于 Linux 内核的 Ubuntu 系统,简单概括六个优点:
- 免费开源;
- 安全稳定;
- 模块化程度高;
- Liunx 系统广泛的硬件支持;
- 多用户,多任务;
- 良好的可移植性。
编程语言选择 Python,其优点有:
- 易于阅读和维护
- 有一个广泛的标准库
- python 支持各种主流数据库之间的交互
- 可扩展性和可移植性
- GUI 编程【图形化界面】
- 可嵌入型【可以将 python 程序嵌入到 c++中】
4.2 GUI(图形用户界面)实现工具
Python 的 GUI 可以通过 Tkinter、PyQt、wxPython、PySide 等实现。
- Tkinter 是 Python 的标准 Tk GUI 工具包的接口,实现较为简单;
- PyQt 是由 Python 语言调用 Qt 图形库实现的一个 Python 模块集,具有 300 多个类,近 6000 个函数和方法,分为 GPL 版本和商业版;
- wxPython 是跨平台 GUI库 wxWidgets 进行 Python 封装后以 Python 模块 形式提供给用户;
- PySide 是 PyQt 的 LGPL 版本,提供与 PyQt 类似的功能与 API 函数。 这些实现 Python GUI 的模块都可以运行在 UNIX、Linux、Windows、Mac 等主 流操作系统之上。
我这里选择实现较为简单的 Tkinter。
4.3 OSI 体系中传输层协议的选择
传输层协议有 TCP、UDP 协议,先了解一下他们:
- TCP 协议:是一种面向连接的、可靠的、基于字节流的传输层通信协议;
- UDP 协议:用户数据报协议,无连接、不可靠。
TCP 与 UDP 基本区别:
- 基于连接与无连接。
- TCP 要求系统资源较多,UDP 较少。
- UDP 程序结构较简单。
- 流模式(TCP)与数据报模式(UDP)。
- TCP 保证数据正确性,UDP 可能丢包。
- TCP 保证数据顺序,UDP 不保证。
- TCP 面向连接(如打电话要先拨号建立连接);UDP 是无连接的,即发 送数据之前不需要建立连接。
- TCP 提供可靠的服务,传送的数据,无差错, 不丢失,不重复; UDP 尽最大努力交付,即不保证可靠交付。
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP 是面向报文的,UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)。
- 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和 多对多的交互通信。
- TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道。
那么我们做聊天室用那种比较好呢,我先去了解了全国最大的社交平台之一 QQ 是如何使用协议的,查阅了一些博客,简单概括就是 UDP 为主 TCP 为辅:
- 用户实际上通过服务器交互,UDP 减少服务负担;
- 超高的并发量,海量的数据包,以效率优先,使用 UDP 协议依靠辅助的算法完成较为可靠的传输控制;
- 网络环境复杂,UDP 包能够穿透大部分的代理服务器;
- 登陆 QQ 后,依靠 TCP 连接保持在线状态。
- 对业务可靠性要求高的用 TCP 协议,不高的用 UDP 协议。
本次实验使用 UDP 协议实现简单的 GUI 聊天室。
4.4 程序的并发
GUI 聊天室支持多人在线聊天,所以程序一定是多进程/多线程并发的,每个进程或者线程负责与一个客户端通讯。Python 中内置了多线程功能支持,封装了对底层操作系统的调度方式,简化了 Python 的多线程编程,我们这里直接使用 socketserver 中的 Threading 模块来完成线程的实现。都说python的线程是假线程,很鸡肋,我不太了解python虚拟机,不多说了。
5. 设计步骤
5.1 访问规则之开通 UDP 协议
首先,阿里云 ECS 服务器默认没有对 UDP 协议授权。要使用 UDP 协议进行通讯就要开通 UDP 协议并设置端口范围。
在云服务器管理控制台,点击网络与安全→安全组:
在安全组列表选择一个实例进行修改,在访问规则中进行手动添加,设置协议类型,端口范围,优先级等等,根据提示完成操作即可。
5.2 服务器端程序实现
对于简单多人聊天室的设计,这里首先考虑对客户端的身份进行判断,是否为新用户,然后进行不同的响应,最后广播给每一个在线的客户端。
# -*- coding: utf-8 -*-
import socketserver
#继承DatagramRequestHandler重写handle
class Chat_server(socketserver.DatagramRequestHandler):
def handle(self):
try:
(data_b,conn)=self.request
addr = self.client_address
print('data_b=',data_b)
#判断是否为新接收的客户端
if addrs.count(addr)==0:
conns.append(conn)
addrs.append(addr)
name_s=data_b.decode('utf-8')
#调用字典类型函数,将组成的键值对放入字典
users.setdefault(addr,name_s)
data='Welcome '+name_s+'!'
data_s=''
else:
name_s=users.get(addr)
data_s=data_b.decode('utf-8')
data=name_s+': '+data_s
print('data=',data)
data_b = data.encode('utf-8')
#将两个列表组合成元组
for cn in zip(conns,addrs):
cn[0].sendto(data_b,cn[1])
if data_s.upper()[0:3]=='BYE':
print('%s is exited!' % name_s)
#删除连接客户端的对象conn
conns.remove(conn)
addrs.remove(addr)
#删除以addr为键的数据
del(users[addr])
except Exception as e:
print('Error is ',e)
#定义列表变量,存储与客户端的连接对象conn
conns=[]
#定义列表变量,存储客户端的地址
addrs=[]
#定义字典变量,存储与客户端的连接对象conn和客户端用户名称name_s
users={}
ip=''
server=socketserver.ThreadingUDPServer((ip,9999),Chat_server)
print('Bind UDP on 9999...')
#以无限循环形式处理客户端请求
server.serve_forever()
5.3 客户端程序实现
对于简单多人聊天室的客户端,首先考虑设计 GUI(图形用户界面),要有输入框, 消息显示框,控制消息发送的按钮,和显示框的滑动条。 对于消息发送的控制,这里设计一个命令按钮的事件响应函数进行消息发送的控制 ,对于消息接收要使接收到的数据可视化。
import socket
import threading
import tkinter as tk
import sys
#命令按钮的事件响应函数
def send_msg():
#从字符串对象中获取按钮的文本
txt=bt_txt.get()
if txt=='Logon':
#设置按钮文本
bt_txt.set('Send')
msg=et_txt.get().strip()
et_txt.set('')
print('msg=',msg)
#将msg内容转化为bytes字节流并保存在msg_b
msg_b=msg.encode('utf-8')
#向服务器发送消息
client.sendto(msg_b, (ip, 9999))
#判断前三个字符
if msg.upper()[0:3]=='BYE':
#设置按钮状态,不在响应鼠标单击
chat_send.config(state=tk.DISABLED)
client.close()
sys.exit(0)
#该函数为线程执行代码,用于从服务器接收消息并显示
def receive_msg():
while True:
try:
#接收服务器发来的消息
(data_b,addr)=client.recvfrom(1024)
#将字节流数据转换为字符串
data_s=data_b.decode('utf-8')
if not data_s:
continue
#调试和监测运行
print('data_s=',data_s)
#插入列表框第一行
chat_list.insert(0,data_s)
#使新插入的数据可视
chat_list.see(0)
except Exception as e:
print('Error is ',e)
print('Exit!')
break
ip='服务器端ip地址'
#先在本地测试
#ip='127.0.0.1'
#建立socket对象,SOCK_STREAM为TCP方式,SOCK_DGRAM为UDP方式
client=socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.connect_ex((ip,9999))
#创建线程,执行自定义函数receive_msg()
t=threading.Thread(target=receive_msg)
t.start()
#创建窗口root
root=tk.Tk()
root.title('Chatting room')
root.geometry('300x350')
root.resizable(width=False,height=True)
#在窗口root中定义框架fm
fm=tk.Frame(root,width=300,height=300)
#在框架fm中定义滚动条scrl
scrl=tk.Scrollbar(fm)
#在框架fm中定义列表框chat_list
chat_list=tk.Listbox(fm,width=300,selectmode=tk.BROWSE)
#设置列表框chat_list纵向滚动由滚动条scrl控制
chat_list.configure(yscrollcommand=scrl.set)
#设置滚动条scrl的命令响应为列表框chat_list的纵向滚动显示
scrl['command']=chat_list.yview
#定义字符串对象,作为命令按钮的文本连续变量
bt_txt=tk.StringVar(value='Logon')
et_txt=tk.StringVar(value='')
#定义单行编辑框并与et_txt绑定
chat_txt=tk.Entry(root,bd=5,width=280,textvariable=et_txt)
#定义按钮并于bt_txt绑定,事件处理函数为send_msg
chat_send=tk.Button(root,textvariable=bt_txt,command=send_msg)
#定义scrl在框架右侧,以纵向充满方式显示
scrl.pack(side=tk.RIGHT, fill=tk.Y)
lb = tk.Label(root,text='')
#显示
chat_txt.pack()
chat_send.pack()
chat_list.pack()
fm.pack()
lb.pack()
#显示主窗口root并接收操作
root.mainloop()
6. 运行结果
这里用了一个 Linux 服务端,四个 Windows 客户端进行测试。 首先,三个玩家进入多人聊天室,大家都可以很流畅的交流,中途加入四号, 依旧可以流畅的交流,然后 4 号又走了,还是可以很流畅的交流,体验感杠杠的!
下图为整体效果,黑色背景为服务端,三个白色框框为客户端,由于 4 号 byebye 了,所以它的客户端关闭了。