异步Websocket构建聊天室
目录
Websocket技术背景
Websockec简介
实现websocket通信程序
实验环境:
服务端(阿里云ESC,VPC网络):
客户端1(本机):
通信模型:
实现功能逻辑:
源代码:
服务端源代码:
客户端源代码:
实验效果:
总结
Websocket技术背景
在websocket协议诞生之前,应用层中用于web通信的协议基本由HTTP主导,毫无疑问,HTTP就是构建现代Web的基石,但是http存在比较致命的性能问题——它是一种请求-响应模型,这意味着他是一种短连接的通信协议,在get多个web资源时需要依靠多次请求响应来轮询,这种通信导致了一种极其低效的通信问题,在实时通信时尤为明显,因此websocket,基于socket优秀的长连接特性得以被扩展到应用层并得到广泛使用
Websockec简介
WebSocket 是一种基于TCP协议之上的应用层协议,2011年已被IETF的RFC文档标准化,相比socket与socketserver,他天然支持全双工通信、异步通信,通信效率极高,且由于是应用层标准协议,因此大部分浏览器能够原生支持websocket协议,对浏览器兼容性极强
实现websocket通信程序
实验环境:
服务端(阿里云ESC,VPC网络):
操作系统:Ubuntu22.04
私网IP:172.29.42.239
公网IP:47.111.23.151
编程语言:Python
数据库:Mysql
客户端1(本机):
操作系统:Windows11
编程语言:Python
通信模型:
C/S,由服务端处理具体的业务逻辑并转发客户端之间的消息
纯异步通信逻辑
实现功能逻辑:
完整的注册登录逻辑
欢迎逻辑,分为发送给新用户自己的单播欢迎逻辑和通告他人新用户登录的欢迎逻辑
显示与隐藏IP归属地,缺省隐藏
shell命令调用(非交互式,交互式有点危险)
心跳报文逻辑,5秒hello,15秒dead
获取在线用户逻辑
获取系统级信息逻辑,如主机名、操作系统、分区信息以及每个分区的具体情况、内存信息以及内存占用详情、CPU信息、当前进程信息等
帮助逻辑,通过发送/help命令获取到用户可使用的全部命令
递归目录树逻辑
每条消息显示当前时间逻辑
PS:以上,shell命令调用、获取系统级信息这些逻辑的命令,不能对自己使用,只能对他人,另一个客户端
源代码:
服务端源代码:
import datetime
from websockets.asyncio.server import ServerConnection,serve
import asyncio
from aioconsole import aprint,ainput
import json
from ip2region.binding.python import xdbSearcher
from datetime import datetime
import pymysql
from textwrap import dedentclient_list={}
client_heartbeat={}
location = False#数据库查询
async def db_select(user):db_conn = pymysql.connect(user='root',password='root',host='127.0.0.1',database='user_info')cur = db_conn.cursor()sql = ' select passwd from user_pwd where username = %s 'cur.execute(sql,user)pwd = cur.fetchall()db_conn.close()return pwd#数据库更新
async def db_update(user,pwd):db_conn = pymysql.connect(user='root', password='root', host='127.0.0.1', database='user_info')cur = db_conn.cursor()sql = 'update user_pwd set passwd = %s where username = %s ;'cur.execute(sql, (pwd, user))db_conn.commit()db_conn.close()#数据库插入
async def db_insert (user,pwd):db_conn = pymysql.connect(user='root', password='root', host='127.0.0.1', database='user_info')cur = db_conn.cursor()sql = 'insert into user_pwd(username,passwd) values(%s,%s)'cur.execute(sql, (user,pwd))db_conn.commit()db_conn.close()#注册逻辑
async def register(websocket_conn,region,city,name_pwd):try:parts = name_pwd.split(' ', 1)username = parts[0]passwd = parts[1]print (parts)pwd = await db_select(username)if pwd:await websocket_conn.send('亲爱的用户,您的账号已注册,无需再次重复注册,直接登录即可')await broadcast_welcome(websocket_conn, region, city, choose='登录')if not pwd:await db_insert(username, passwd)client_list[username] = websocket_conntry:for client_name, ws_conn in client_list.items():print ('进来了3')if username == client_name:print ('进来了4')await ws_conn.send(f'欢迎你,【{username}】,来自{region}{city}的朋友,欢迎你加入我们的战略会议室!\n输入help获取帮助')else:print('进来了5')await ws_conn.send(f'欢迎来自{region}{city}的新同伴【{username}】加入了我们的战略会议室!\n输入help获取帮助')except Exception as e:print (f'注册逻辑中的欢迎消息逻辑捕获到异常报错:{e}')except Exception as e:print (f'注册逻辑捕获到异常报错:{e}')#登录逻辑
async def login(websocket_conn,region,city,name_pwd):try:parts = name_pwd.split(' ', 1)name = parts[0]passwd = parts[1]pwd = await db_select(name)try:if not pwd:await websocket_conn.send('亲爱的用户,您的账号尚未注册,请注册后再加入我们的会议。')await broadcast_welcome(websocket_conn,region,city,choose='注册')if pwd:pwd = pwd[0][0]if passwd != pwd:await websocket_conn.send('不好意思,亲爱的用户,您的密码输入错误,请重新尝试。')await broadcast_welcome(websocket_conn, region, city,choose='登录')if str(passwd) == str(pwd):await websocket_conn.send(f'登录成功!')client_list[name] = websocket_connfor client_name, ws_conn in client_list.items():if name == client_name:await ws_conn.send(f'欢迎你,【{name}】,来自{region}{city}的朋友,欢迎你加入我们的战略会议室!\n输入help获取帮助')if name != client_name:await ws_conn.send(f'欢迎来自{region}{city}的新同伴【{name}】加入了我们的战略会议室!\n请输入help获取帮助')except Exception as e:print (f'登录逻辑中的条件判定捕获到异常报错:{e}')except Exception as e:print (f'登录逻辑捕获到异常报错:{e}')#欢迎逻辑,应用了注册和登录逻辑
async def broadcast_welcome(websocket_conn,region,city,choose):global client_listtry:name_pwd = await websocket_conn.recv()if '注册' in choose:await register(websocket_conn, region, city, name_pwd)if '登录' in choose:await login(websocket_conn,region, city,name_pwd)except Exception as e:print (f'欢迎逻辑中捕获到异常报错:{e}')#服务器通常转发逻辑
async def forwarding_other(sender_name,sender_msg,city):global client_listglobal locationtry:if sender_msg != 'hold,connect,status' \and not sender_msg.startswith('/cmd') \and not sender_msg.startswith('/cmd_return') \and sender_msg != '/client_online' \and not sender_msg.startswith('/get') \and sender_msg != '/help' \and not sender_msg.startswith('/get_info') \and not sender_msg.startswith('/get_cpu_info') \and not sender_msg.startswith('/change_pwd'):time_now = datetime.now().strftime('%H:%M:%S')show_location_forward_msg = f'\r《{city}|({time_now})|{sender_name}>>{sender_msg}'normal_forward_msg = f'\r({time_now})|{sender_name}>>{sender_msg}'for client_name,ws_conn in client_list.items():if sender_msg == '/get_location':location = Trueif sender_msg == '/disable_location':location = Falseif client_name != sender_name and location == False:await ws_conn.send(normal_forward_msg)if client_name != sender_name and location == True:await ws_conn.send(show_location_forward_msg)except Exception as e:print (f'转发逻辑捕获到异常报错:{e}')#Shell命令调用
async def sys_cmd_unicast(sender_name,sender_msg):global client_listtry:if sender_msg.startswith('/cmd') or sender_msg.startswith('/cmd_return'):parts = sender_msg.split(' ', 2)remote_user = parts[1]if sender_msg.startswith('/return_cmd'):remote_conn = client_list[remote_user]cmd_return = parts[2]remote_msg = cmd_returnawait remote_conn.send(remote_msg)if sender_msg.startswith('/cmd'):remote_conn = client_list[remote_user]cmd = parts[2]remote_msg = f'/cmd {sender_name} {cmd}'await remote_conn.send(remote_msg)except Exception as e:print (f'处理cmd命令的逻辑捕获到异常报错:{e}')#归属地查询
async def get_ip_location(ip):# 指定 .xdb 文件路径#D:/Python_Script/.venv/Lib/site-packages/ip2region/data/ip2region.xdb 我windows上存放位置#/usr/local/lib/python3.11/dist-packages/ip2region/data/ip2region.xdb 我ubuntu上存放位置try:dbfilepath = "/usr/local/lib/python3.11/dist-packages/ip2region/data/ip2region.xdb"# 初始化搜索对象searcher = xdbSearcher.XdbSearcher(dbfile=dbfilepath)# 查询 IP 所在地区result1 = searcher.search(ip)location_parts = [part for part in result1.split('|') if part != '0' and part !='内网IP']if not location_parts:return '内网IP'return location_partsexcept Exception as e:print (f'归属地查询库的调用逻辑捕获到异常报错{e}')#心跳报文死亡消息的广播逻辑
async def keepalive_broadcast_send(msg):global client_listtry:for client_name,ws_conn in client_list.items():await ws_conn.send(msg)except Exception as e:print (f'死亡广播逻辑中捕获到异常报错:{e}')#监听心跳报文
async def listen_keepalive(sender_msg,sender_name):global client_listglobal client_heartbeattry:if sender_msg == 'hold,connect,status':client_heartbeat[sender_name] = datetime.now()need_remove_client = []for name, last_time in client_heartbeat.items():if (datetime.now() - last_time).total_seconds() > 15:need_remove_client.append(name)for name in need_remove_client:ws_conn = client_list[name]del client_list[name]del client_heartbeat[name]await ws_conn.close()time_now = datetime.now().strftime('%H:%M:%S')dead_msg = f'噢!亲爱的【{name}】,我们曾引以为傲的一员,在北京时间{time_now}的时刻永远地停止了心跳,离开了这个世界,朋友们,留给我们的时间不多了,敌人日益强大,我们的同伴却在不断减少。'await keepalive_broadcast_send(dead_msg)except Exception as e:print (f'心跳报文逻辑中捕获的异常报错')#显示IP归属地逻辑
async def show_ip_location(sender_msg,region,city,sender_name):global client_listtry:if sender_msg == '/get_location':location = f'{region}|{city}'msg = f'/post_location {location}'ws_conn = client_list[sender_name]await ws_conn.send(msg)except Exception as e:print (f'显示IP归属地逻辑中捕获到异常报错:{e}')
#获取在线用户逻辑
async def get_online_client(sender_msg,sender_name):global client_listtry:name_list = ''if sender_msg == '/client_online' :times = 0for name in client_list:if times == 0 :name_list += nametimes +=1elif times > 0:name_list += '-' + nameawait aprint(name_list)msg = f'/online_client_list {name_list}'ws_conn = client_list[sender_name]await ws_conn.send(msg)except Exception as e:print (f'获取在线用户逻辑中捕获到异常报错:{e}')#获取信息逻辑
async def get_info(sender_msg,sender_name):global client_listtry:if sender_msg.startswith('/get_sys_info') :parts = sender_msg.split(' ', 1)remote_user = parts[1]ws_conn = client_list[remote_user]cmd = dedent("""system = platform.system()version = platform.version()arch=platform.architecture()[0]arch=arch.replace('bit','')hostname=platform.node()print('')print('系统信息:')print(f'主机名:{hostname}')print(f'操作系统:{arch}位{system}{version}')""")msg = f'/post_info {sender_name} {cmd}'await ws_conn.send(msg)if sender_msg.startswith('/get_part_info'):parts = sender_msg.split(' ', 1)remote_user = parts[1]ws_conn = client_list[remote_user]cmd = dedent("""all=psutil.disk_partitions()print('')print('分区信息:')for i in all:dict_info = psutil.disk_usage(i[0])total = int(dict_info[0]/1024/1024/1024)used = int(dict_info[1]/1024/1024/1024)used_percent = dict_info[3]print(f'盘符:{i[0]} 文件系统:{i[2]} 分区总空间:{total}GB 分区已用空间{used}GB 分区已用空间占比{used_percent}%')""")msg = f'/post_info {sender_name} {cmd}'await ws_conn.send(msg)if sender_msg.startswith('/get_memory_info'):parts = sender_msg.split(' ', 1)remote_user = parts[1]ws_conn = client_list[remote_user]cmd = dedent("""print('')print('内存信息')all = psutil.virtual_memory()sum_memory=int(all[0]/1024/1024/1024)print(f'总内存:{sum_memory}GB')available_memory=int(all[1]/1024/1024/1024)print (f'可用内存:{available_memory}GB')used=int(all[3]/1024/1024/1024)print(f'已用内存{used}GB')used_percent=all[2]print(f'已用内存占比:{used_percent}%')""")msg = f'/post_info {sender_name} {cmd}'await ws_conn.send(msg)if sender_msg.startswith('/get_cpu_info'):parts = sender_msg.split(' ', 1)remote_user = parts[1]ws_conn = client_list[remote_user]cmd =dedent("""cpu_per_count = psutil.cpu_count()cpu_freq = psutil.cpu_freq()[2]/1000print('')print('CPU信息:')print(f'CPU核心数: {psutil.cpu_count(logical=False)}')print(f"CPU线程数: {cpu_per_count}")print(f"频率: {cpu_freq}GHz")""")msg = f'/post_info {sender_name} {cmd}'await ws_conn.send(msg)if sender_msg.startswith('/get_process_info'):parts = sender_msg.split(' ',1)remote_user=parts[1]ws_conn = client_list[remote_user]cmd = dedent("""print('')print('当前进程信息:')for i in psutil.process_iter():print (i)""")msg = f'/post_info {sender_name} {cmd}'await ws_conn.send(msg)except Exception as e:print (f'获取信息逻辑中捕获到异常报错{e}')
#帮助逻辑
async def help_cmd(sender_name,sender_msg):global client_listtry:if sender_msg == '/help':ws_conn = client_list[sender_name]dilimiter = "-" * 50help_content = f"""\r用户命令\r{dilimiter}\r获取CPU信息: /get_cpu_info [用户名]\r获取系统信息: /get_sys_info [用户名]\r获取内存信息: /get_memory_info [用户名]\r获取分区信息: /get_part_info [用户名]\r获取进程: /get_process_info [用户名]\r非交互式调用Shell命令并获取执行结果: /cmd [用户名] [shell命令]\r获取在线用户列表:/client_online\r退出会议: [q] or [Q]\r显示IP归属地信息: /get_location\r隐藏IP归属地信息: /disable_location\r获取递归目录树: /get_path_tree [用户名]\rps:输入命令的时候不用填中括号[]\r帮助: /help\r{dilimiter}"""await ws_conn.send(help_content)except Exception as e:print (f'帮助逻辑中捕获到异常报错:{e}')#递归目录树的逻辑
async def get_path_tree(sender_name,sender_msg):global client_listtry:if sender_msg.startswith('/get_path_tree'):#/get_path_tree remote_name remote_pathprint ('tree,我来了')parts = sender_msg.split(' ',2)remote_name = parts[1]ws_conn = client_list[remote_name]remote_path = parts[2]msg = f'/get_path_tree {sender_name} {remote_path}'await ws_conn.send(msg)except Exception as e:print (f'递归目录树的逻辑捕获到异常报错{e}')async def change_pwd(sender_name,sender_msg):global client_listtry:if sender_msg.startswith('/change_pwd'):#/change_pwd new_pwdparts = sender_msg.split(' ',2)pwd = parts[1]ws_conn = client_list[sender_name]await db_update(sender_name,pwd)await ws_conn.send('密码修改成功!')except Exception as e:print (f'改密逻辑捕获到异常报错:{e}')async def server_handle(websocket_conn:ServerConnection):address_port = websocket_conn.remote_addressip_address = address_port[0]location_parts = await get_ip_location(ip_address)if location_parts == '内网IP':region = '本地'city = '内网IP'else:region = location_parts[1]city = location_parts[2]try:choose = await websocket_conn.recv()await broadcast_welcome(websocket_conn,region,city,choose)except Exception as e:print (f'登录|注册的入口逻辑捕获到异常报错{e}')try:async for msg_dict_json in websocket_conn:msg_dict = json.loads(msg_dict_json)await aprint(msg_dict)sender_name = msg_dict['Name']sender_msg = msg_dict['Message']async with asyncio.TaskGroup() as TG:TG.create_task(sys_cmd_unicast(sender_name,sender_msg))TG.create_task(forwarding_other(sender_name,sender_msg,city))TG.create_task(listen_keepalive(sender_msg,sender_name))TG.create_task(show_ip_location(sender_msg,region,city,sender_name))TG.create_task(get_online_client(sender_msg,sender_name))TG.create_task(get_info(sender_msg,sender_name))TG.create_task(help_cmd(sender_name,sender_msg))TG.create_task(get_path_tree(sender_name,sender_msg))TG.create_task(change_pwd(sender_name,sender_msg))except Exception as e:print (f'双向通信阶段捕获到异常报错{e}')
async def main():try:async with serve(server_handle,'172.29.42.239',12345):print("WebSocket 服务器已启动,监听中...")await asyncio.Future()except Exception as e:print (f'启动服务逻辑捕获到异常报错:{e}')if __name__ == '__main__':asyncio.run(main())
客户端源代码:
import os.path
import websockets
import asyncio
from aioconsole import aprint,ainput
import json
import subprocess
import sys
from io import StringIO
import psutil
import platform
import cpuinfo
from datetime import datetimeregion = ''
city = ''
location = Falseasync def send(ws_conn,name):global regionglobal cityglobal locationmsg = ''while 1:name_msg = {}time_now = datetime.now().strftime('%H:%M:%S')if location:msg = await ainput(f'《{region}{city}|({time_now})|{name}>>')if not location:msg = await ainput(f'({time_now})|{name}>>')if msg.upper() == 'Q':await ws_conn.close()await sys.exit()if msg == '/disable_location':location = Falseelse:name_msg['Name'] = namename_msg['Message'] = msgname_msg = json.dumps(name_msg)await ws_conn.send(name_msg)#获取递归目录树
async def get_path_tree(path,depth=1):tree = ''dirname = os.path.basename(path)tree = tree +depth * '|-----' + dirname + '\n'files = os.listdir(path)for file in files:filepath = os.path.join(path,file)if os.path.isdir(filepath):tree = await get_path_tree(filepath,depth+1)if os.path.isfile(filepath):tree = tree + depth * '|-----' + '|-----' + file + '\n'return treeasync def recv(ws_conn,name):global locationglobal cityglobal regionwhile 1:msg = await ws_conn.recv()time_now = datetime.now().strftime('%H:%M:%S')if msg.startswith('/cmd'):parts = msg.split(' ',2)sender_name =parts[1]cmd_raw = parts[2]cmd_raw_split_num = cmd_raw.count(' ')if cmd_raw_split_num != 0:cmd = cmd_raw.split(' ',cmd_raw_split_num)else:cmd = cmd_rawresult = subprocess.run(cmd,text=True,shell=True,capture_output=True)cmd_result = result.stdoutmsg = f'/return_cmd {sender_name} {cmd_result}'name_unicate_msg = {}name_unicate_msg['Name'] = namename_unicate_msg['Message'] = msgname_unicate_msg_json = json.dumps(name_unicate_msg)await ws_conn.send(name_unicate_msg_json)if msg.startswith('/post_location'):parts = msg.split(' ',1)location_parts = parts[1]location_parts = location_parts.split('|',1)region = location_parts[0]city = location_parts[1]location = Trueif msg.startswith('/post_info'):parts = msg.split(' ',2)sender_name = parts[1]cmd = parts[2]output = StringIO()sys.stdout = outputexec(cmd)result_cmd = output.getvalue()sys.stdout = sys.__stdout__msg = f'/result_cmd {sender_name} {result_cmd}'print (msg)name_unicate_msg = {}name_unicate_msg['Name'] = namename_unicate_msg['Message'] = msgname_unicate_msg_json = json.dumps(name_unicate_msg)await ws_conn.send(name_unicate_msg_json)if msg.startswith('/get_path_tree'):parts = msg.split(' ',2)sender_name = parts[1]local_path = parts[2]tree = await get_path_tree(local_path)print (tree)msg = f'/result_cmd {sender_name} {tree}'name_unicate_msg = {}name_unicate_msg['Name'] = namename_unicate_msg['Message'] = msgname_unicate_msg_json = json.dumps(name_unicate_msg)await ws_conn.send(name_unicate_msg_json)if msg.startswith('/online_client_list'):parts = msg.split(' ',1)name_list = parts[1]n=1split_num = name_list.count('-')if split_num >0:name_list = name_list.split('-',split_num)n = 0for online_user in name_list:if n == 0:await aprint(f'\n在线用户列表:')n += 1if n > 0 :await aprint(f' 用户{n}:{online_user}')elif msg.startswith('/'):if location:await aprint(f'《{region}{city}|{name}>>', end='', flush=True)else:await aprint(f'{name}>>',end='',flush=True)else:if location:await aprint(f'\r{msg}\n《{region}{city}|({time_now})|{name}>>', end='', flush=True)else:await aprint(f'\r{msg}\n({time_now})|{name}>>', end='', flush=True)#客户端注册逻辑
async def register(ws_conn):await aprint("-" * 50)await aprint('注册阶段')await aprint("-" * 50)name = await ainput('请输入用户名:\n')pwd = await ainput('请输入登录代码:\n')await aprint("-" * 50)name_pwd = f'{name} {pwd}'await ws_conn.send(name_pwd) # 发送账号密码return name#服务端登录逻辑
async def login(ws_conn):await aprint("-" * 50)await aprint('登录阶段')await aprint("-" * 50)name = await ainput('请输入用户名:\n')pwd = await ainput('请输入登录代码:\n')await aprint("-" * 50)name_pwd = f'{name} {pwd}'await ws_conn.send(name_pwd) # 发送账号密码return nameasync def create_name(ws_conn,choose):while 1:if '注册' in choose:name = await register(ws_conn)print (name)return nameif '登录' in choose:name = await login(ws_conn)msg = await ws_conn.recv()await aprint (msg)if '输入错误' in msg:continueif '尚未注册' in msg:await create_name(ws_conn,choose='注册')if '登录成功' in msg:return nameasync def keepalive(ws_conn,name):keepalive_packets = {}content = 'hold,connect,status'keepalive_packets['Name'] = namekeepalive_packets['Message'] = contentwhile 1:keepalive_packets_json = json.dumps(keepalive_packets)await ws_conn.send(keepalive_packets_json)await asyncio.sleep(5)async def client_handle():url = 'ws://47.111.23.151:12345'async with websockets.connect(url) as ws_conn:await aprint (r"""(`-').-> (`-') _ (`-') (`-') _ (`-') <-. (`-') (`-') _ (`-') _ (`-') _ <-. (`-')_ ( OO)_ ( OO).-/ _ <-.(OO ) ( OO).-/ ( OO).-> \(OO )_ ( OO).-/ ( OO).-/ ( OO).-> (_) \( OO) ) .->
(_)--\_) (,------. \-,-----. ,------,) (,------. / '._ ,--./ ,-.)(,------. (,------. / '._ ,-(`-') ,--./ ,--/ ,---(`-')
/ _ / | .---' | .--./ | /`. ' | .---' |'--...__) | `.' | | .---' | .---' |'--...__) | ( OO) | \ | | ' .-(OO )
\_..`--. (| '--. /_) (`-') | |_.' | (| '--. `--. .--' | |'.'| |(| '--. (| '--. `--. .--' | | ) | . '| |)| | .-, \
.-._) \ | .--' || |OO ) | . .' | .--' | | | | | | | .--' | .--' | | (| |_/ | |\ | | | '.(_/
\ / | `---. (_' '--'\ | |\ \ | `---. | | | | | | | `---. | `---. | | | |'-> | | \ | | '-' | `-----' `------' `-----' `--' '--' `------' `--' `--' `--' `------' `------' `--' `--' `--' `--' `-----'
""")await aprint(" ┌──────────────────────────────────────────────────────────────────────┐")await aprint(" │ • Secret Meeting v1.0 • │")await aprint(" │ Forever │")await aprint(" │⮞ WebSocket Successful Connect to Server@47.111.23.151 │")await aprint(" └──────────────────────────────────────────────────────────────────────┘")while 1:option = input('【注册】(register)还是【登录】(login),请做出你的选择\n1.注册\n2.登录\n')if option == '1':choose = '注册'breakif option == '2':choose = '登录'breakelse:continueawait ws_conn.send(choose)name = await create_name(ws_conn,choose)msg_welcome = await ws_conn.recv()await aprint(msg_welcome)async with asyncio.TaskGroup() as TG:TG.create_task(send(ws_conn,name))TG.create_task(recv(ws_conn,name))TG.create_task(keepalive(ws_conn,name))if __name__ == '__main__':asyncio.run(client_handle())
实验效果:
服务端刚运行,开始监听:
客户端初次进入聊天室:
客户端注册后:
客户端获取帮助:
客户端注册登录后,服务端的心跳开始运作:
总结:
这些功能各位可以自己去尝试,我就不一一演示了,源码自取即可。