zhangan

QQ聊天软件代码功能编写


一,Tkinter聊天界面编写

 1,聊天软件客户端界面开发-1

  • Tkinter的模块(“TK接口”)是标准的Python接口从Tk的GUI工具包
  • https://i.cnblogs.com/EditPosts.aspx?opt=1   ###Tkinter官方文档:关于相关函数具体看文档介绍
  • Tkinter模块:自带的,跨平台
    • 做GUI:界面软件
    • 不难,但是复杂,东西多,窗口控件多,设计,美感,抠图
  • 界面开发设计
    • 设置窗口:像素大小
    • 拜访控件:按钮,复选框,文本框
  • root控件流程:
    • 1:导入模块   import tkinter
    • 2:创建主窗口root = tkinter.Tk()
    • 3:创建其他窗口以及控件(按钮,文本框,等等)
    • 4:摆放到root上
    • 5:开启root的事件循环:root.mainloop()
    • 6:如果不需要该窗口,可以使用root.destory()

  • 组件:
    • 文本框
      • tkinter.Text()
        • insert():插入数据 
          • tkinter.END,从末尾插入
        • delete(‘0.0’,tkinter.END):删除全部文本框数据
        • tag_config(\'标签名\',foreground=\'颜色\',):标签配置,设置字体颜色, 第一个字符串参数是标签名,第二个是颜色
        • get(\'0.0\',thinter.END):阅读拿出文本框的内容
    • 按钮
      • tkinter.Button()
        • text:按钮名字
        • command:回调函数,按钮点击时的工作任务列表框
    • 列表框
      • tkinter.Listbox()
        • insert(tkinter.END, 追加的数据)
  • 事件绑定
    • 双击事件:<Double-Button-1>
    • 事件绑定的函数必须有一个参数,这个参数就是事件
    • self.right_listbox.curselection()
      • 返回当前选择的列表框索引位置值,返回结果为单个数据的元组
      • index = self.listbox_user.curselection()[0]
    • self.talk_user_name = self.listbox_user.get(index)
      • index   #获取的当前列表框中的索引位置的值

 

  • root.title(\'**\')
    • 设置上标字符
  • grid()
    • x,y轴的摆放方式
    • row:行
    • column:列
    • rowspan:行宽,跨行数
    • pady:表格间距,x轴
    • padx:y轴的表格间距
  • sticky
    • 对其方向,上北下南左西右东N,S,W,E
  • grid_propagate(0)
    • 0代表禁止容器窗口缩放
  • root.resizable(width=False,height=Flase)
    • 禁止窗口缩放
  • root.winfo_screenwidth()
    • 自动获取用户的屏幕宽度
  • root.winfo_screenheight()
    • 自动获取用户的屏幕高度
  • root.geometry("%dx%d%+d%+d" % (width, height, xoffset, yoffset))
    • 设置控件所处的屏幕位置
  • tkinter.Label(所属的主窗口,text=\'名字\',font=("黑体",9, "bold"))
    • 添加标签lable
  • tkinter.Entry()
    • 单行文本框
    • 获取数据直接使用get() 函数即可,不需要传参。
  • from tkinter import messagebox   ##展示报错信息
    • \'showerror\', \'showinfo\', \'showwarning\'
      • title=None, message=None
    • messagebox.showerror(title=\'登录失败\',message=\'登录失败\')

  

  


 

  • 客户端登录及聊天窗口代码编写整合如下:
    from tkinter import Tk
    import tkinter
    import time
    from multiprocessing.pool import ThreadPool
    import socket
    from tkinter import messagebox
    import pickle
    import _pickle
    #scroolbar
    class TalkRoot:
    	\'\'\'
    		实现用户聊天的主要界面
    	\'\'\'
    	def __init__(self,user_name,client,user_id):
    		self.root = Tk()
    		self.root.title(\'欢迎你:%s\' % user_name)
    		self.client = client
    		self.user_id = user_id
    		#-----------------创建进程池----------------------
    		self.thread_pool = ThreadPool(5) #5个线程的线程池
    		#-------功能变量----------------------
    		self.talk_user_name = \'\' #你要聊天的人
    		self.client = client	#套接字
            #------------------设置窗口所处的用户界面位置及窗口大小在屏幕中间------------
    		self.root_width=550
    		self.root_height=420
    		self.user_screen_width = self.root.winfo_screenwidth()#自动获取用户的屏幕宽度
    		self.user_screen_height = self.root.winfo_screenheight()#用户的屏幕高
    		self.root.geometry("%dx%d%+d%+d" %
    			(self.root_width,
    			self.root_height,
    			(self.user_screen_width - self.root_width) / 2,
    			(self.user_screen_height - self.root_height) / 2,)
    			) 		
    		#-------容器--------
    		#上: 输出
    		self.frame_top = tkinter.Frame(width=380,height=270)
    		#中: 输入
    		self.frame_center = tkinter.Frame(width=380,height=100)
    		#下: 按钮
    		self.frame_bottom = tkinter.Frame(width=380,height=30)
    		#右: 列表
    		self.frame_right = tkinter.Frame(width=170,height=400)
    
    		#--------控件--------
    		#上: 输出
    		self.text_output = tkinter.Text(self.frame_top,height=260)
    		self.text_output.tag_config(\'time_stamp\',foreground=\'green\')  #标签配置颜色
    		self.text_output.tag_config(\'self_msg\',foreground=\'blue\')#标签配置颜色
    		self.text_output.tag_config(\'other_msg\',foreground=\'red\')#标签配置颜色
    		#中: 输入
    		self.text_input = tkinter.Text(self.frame_center)
    		self.text_input.bind(\'<Return>\',self.button_send_msg)
    		#下: 按钮
    		self.button_send = tkinter.Button(self.frame_bottom,text=\'发送\',command=self.button_send_msg) #发送按钮
    		self.button_cancle = tkinter.Button(self.frame_bottom,text=\'取消\',command=self.fc_button_cancle) #取消按钮
    		#右: 列表
    		self.listbox_user = tkinter.Listbox(self.frame_right,width=150,height=25)
    		self.listbox_user.bind(\'<Double-Button-1>\',self.get_talk_user)#绑定双击事件
    		#--------摆放生效容器---------
    		self.frame_top.grid(row=0,column=0,padx=2,pady=5)
    		self.frame_center.grid(row=1,column=0,padx=2,pady=5)
    		self.frame_bottom.grid(row=2,column=0,sticky=tkinter.E)
    		self.frame_right.grid(row=0,column=1,rowspan=3,padx=5,pady=5)
    
    		#--------摆放生效控件---------
    		self.text_output.grid()
    		self.text_input.grid()
    		self.button_send.grid(row=0,column=1,padx=300)
    		self.button_cancle.grid(row=0,column=0)
    		self.listbox_user.grid()
    
    		#--------禁止容器窗口缩放grid_--------------------
    		self.frame_top.grid_propagate(0)
    		self.frame_center.grid_propagate(0)
    		self.frame_bottom.grid_propagate(0)
    		self.frame_right.grid_propagate(0)
    		#------------专门开一个接受消息聊天的线程------------
    		self.thread_pool.apply_async(func=self.check_recv_msg) #非阻塞线程
    		#------------窗口循环---------------------------------------
    		self.root.mainloop()
    
    	def check_recv_msg(self):
    		while True:	#循环窗口聊天
    			try:
    				data = pickle.loads(self.client.recv(1024))
    			except _pickle.UnpicklingError:
    				messagebox.showinfo(title=\'服务端消息错误\',message=\'服务端发来无法解析的错误\')
    				self.root.destroy()
    				exit()
    			except Exception as e:
    				messagebox.showinfo(title=\'服务端断开\',message=\'服务端断开\')
    				self.root.destroy()
    				exit()
    			if data.get(\'flag\') == \'flush\':
    				#服务端返回最新在线用户
    				self.thread_pool.apply_async(func=self.flush_onlien_user,args=(data,))
    			elif data.get(\'flag\') == \'forward\':
    				#服务端返回别人发来的消息
    				self.thread_pool.apply_async(func=self.show_other_msg,args=(data,))
    			elif not data:
    				#服务端发来空消息,断开连接
    				exit()
    
    	def show_other_msg(self,data):
    		\'\'\'
    			展示别人发来的消息
    		\'\'\'
    		send_name = data.get(\'data\')[\'send_name\']
    		other_msg = data.get(\'data\')[\'message\']
    		print(\'别人发来了消息:\',other_msg)
    		time_stamp = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime() )  + \'\n\'
    		self.text_output.insert(tkinter.END,time_stamp,\'green\')
    		self.text_output.insert(tkinter.END,\'%s:%s\' % (send_name,other_msg),\'red\')
    
    	def flush_onlien_user(self,data):
    		self.listbox_user.delete(0,tkinter.END) #清空在线用户列表
    		for user in data.get(\'data\'):
    			#user : {id:name}
    			id_ = list(user.keys())[0]
    			name = list(user.values())[0]
    			self.listbox_user.insert(tkinter.END, \'%s:%s\' % (name,id_))
    
    	def button_send_msg(self,events=None):
    		self_msg = self.text_input.get(\'0.0\',tkinter.END)
    		self.text_input.delete(\'0.0\',tkinter.END)
    		if not self_msg.isspace():
    			time_stamp = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime() )  + \'\n\'
    			self.text_output.insert(tkinter.END,time_stamp,\'time_stamp\')
    			self.text_output.insert(tkinter.END,\' \' + self_msg,\'self_msg\')
    		self.thread_pool.apply_async(func=self.clear_input_text)
    		#id:name
    		if self.talk_user_name:
    			print(\'捕捉到在线用户,准备发送消息\')
    			recv_id = self.talk_user_name.split(\':\')[0] 
    			print(recv_id)
    			send_data = {
    				\'flag\': \'send\',
    				\'data\': {
    					\'send_id\': self.user_id,
    					\'recv_id\': recv_id,
    					\'message\': self_msg,
    				}
    			}
    			self.client.send(pickle.dumps(send_data))
    		else:
    			\'\'\'
    				群发功能
    			\'\'\'
    			send_data = {
    				\'flag\': \'part\',
    				\'data\': {
    					\'send_id\': self.user_id,
    					\'message\': self_msg,
    				}
    			}
    
    	def clear_input_text(self):
    		time.sleep(0.1)
    		self.text_input.delete(\'0.0\',tkinter.END)
    		
    	def get_talk_user(self,events):
    		\'\'\'
    			获取当前聊天用户
    		\'\'\'
    		if self.listbox_user.size() != 0:
    			index = self.listbox_user.curselection()[0]
    			self.talk_user_name = self.listbox_user.get(index)
    			print(self.talk_user_name)
    
    	def fc_button_cancle(self):
    		\'\'\'
    			聊天界面的销毁
    				销毁窗口
    				客户端套接字释放
    		\'\'\'
    		exit()
    
    	def __del__(self):
    		self.client.close() #套接字释放
    		self.root.destroy() #销毁窗口
    
    class LoginRoot():
    	def __init__(self):
    		self.root = tkinter.Tk()
    		self.root.title(\'登陆\')
    		self.root.resizable(width=False,height=False) #禁止窗口缩放调整
    
    		self.root_width = 197
    		self.root_height = 75
    
    		#自适应,首先要获取到用户的屏幕分辨率
    		self.user_screen_width = self.root.winfo_screenwidth() #用户的屏幕也宽度
    		self.user_screen_height = self.root.winfo_screenheight() #用户的屏幕高度
    
    		self.root.geometry("%dx%d%+d%+d" % #设置窗口所处的用户界面位置及窗口大小
    			(self.root_width,
    			self.root_height,
    			(self.user_screen_width - self.root_width) / 2,
    			(self.user_screen_height - self.root_height) / 2,)
    			)
    
    		#创建提示label
    		self.label_id = tkinter.Label(self.root,text=\'I D:\',font=("黑体",10, "bold")) # font设置字体样式
    		self.label_name = tkinter.Label(self.root,text=\'昵称:\',font=("黑体",10, "bold"))
    		#摆放提示label
    		self.label_id.grid(row=0,column=0,sticky=tkinter.W)
    		self.label_name.grid(row=1,column=0,)
    		#创建输入单行文本框
    		self.entry_id = tkinter.Entry(self.root)
    		self.entry_name = tkinter.Entry(self.root)
    		#摆放文本框
    		self.entry_id.grid(row=0,column=1)
    		self.entry_name.grid(row=1,column=1)
    		#创建功能按钮
    		self.button_login = tkinter.Button(self.root,text=\'登陆\',command=self.user_login)
    		self.button_cancle = tkinter.Button(self.root,text=\'取消\',command=self.user_cancle)
    		#摆放功能按钮
    		self.button_login.grid(row=2,column=1,sticky=tkinter.E)
    		self.button_cancle.grid(row=2,column=0,sticky=tkinter.W)
    		self.root.mainloop()
    
    	def user_login(self):
    		\'\'\'
    			要确定用户提供的ID是唯一的
    			把昵称传递给TalkRoot
    		\'\'\'
    		user_id = self.entry_id.get()
    		user_name = self.entry_name.get()
    
    		send_login_msg = {
    			\'flag\': \'login\',
    			\'data\':{
    				\'id\': user_id,
    				\'name\': user_name
    			}
    		}
    		ip = \'192.168.1.101\'
    		port = 8000
    		try:
    			self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    			self.client.connect( (ip,port) )
    		except Exception as e:
    			print(\'登陆连接时的错误:\',e)
    			messagebox.showerror(title=\'登陆失败\',message=\'登陆失败\')
    		else:
    			self.client.send(pickle.dumps(send_login_msg))
    			data = pickle.loads(self.client.recv(1024))
    			if data.get(\'flag\') == \'login\':
    				if data.get(\'data\')[\'state\']:
    					#登陆成功
    					self.root.destroy()
    					tkr = TalkRoot(user_name,self.client,user_id)
    				else:
    					self.client.close()
    					messagebox.showwarning(title=\'登陆失败\',message=\'%s\' % (data.get(\'data\')[\'message\']))
    
    	def user_cancle(self):
    		self.root.destroy()
    
    def main():
    	lgr = LoginRoot()
    
    
    if __name__ == \'__main__\':
    	main()

二,聊天类软件后台架构及请求数据设计 

一,服务端功能

  • 1:接收用户登陆
    • 主线程,消息收集
    • 服务端套接字,唯一ID的校验
  • 2:告知在线用户
    • 另开线程,遍历访问服务端保存的客户端序列数据
  • 3:处理在线用户的消息发送
    • 主线程,消息收集 一旦捕获到了一个消息是要发送给别人的
    • 另开线程,处理消息转发,发给另外一个在线的套接字

二,客户端功能

  • 1:登陆
    • 套接字,发送ID和昵称即可
  • 2:接收处理服务端发来的在线用户列表
  • 3:发送消息,展示消息

三,请求数据结构体 / 客户端

  • 1:pickle.dumps() #打包二进制数据体
    •   
    • { #客户端登陆发送的数据体
          \'flag\': \'login\', #数据标志位
          \'data\': {
              \'id\': \'123456\', #用户ID
              \'name: \'xxxxx\', #用户昵称
          }
      }
      
      { #客户端发送数据体
          \'flag\': \'send\' #数据标志位
          \'data\': {
          \'send_id\': \'123455\', #发送者的ID
              \'recv_id\': \'123456\', #接收人的ID号
              \'message: \'xxxxx\', #发给别人的数据
          }
      }
      
      { #客户端注册数据体
          \'flag\': \'register\', #数据标志位
          \'data\': {
              \'id\': \'123456\', #用户ID
              \'name: \'xxxxx\', #用户昵称
              \'password\': \'xxxxx\', #
          }
      }

四,服务端保存在线用户数据体

  • 1:直接连进来的,不进入这个数据体,不算有效用户
  • 2:某个线程,捕捉这个套接字发来的第一个login的数据体
  • 3:判断ID:
    • ID不重复:登陆保存
    • ID重复:当用户发送重复id的时,返回的数据
      •   
        {
            \'flag\':\'login\',
            \'data\': {
                \'state\':True,/False
                \'message\':\'登陆成功\'
            }
        }  
  • 4:成功登陆的用户保存在字典中的数据结构体
    • #字典:
      dict_onlien_user = {
          xxxx:{
              \'name\':\'666\',
              \'socket\':client,
          },
          xxxx:{,
              name:\'666\',
              socket:client,
          },
      }
    • onlien_user.get(recv_id)[\'socket\'].send(message)  #如果再转发消息的时候,准确快速的找到接收者 
  • 5:成功的用户发来的数据,要给别人发消息,服务端怎么保存这个数据?
    •   
      { #客户端发送数据体
          \'flag\': \'send\' #数据标志位
          \'data\': {
              \'send_id\': \'123455\', #发送者的ID
              \'recv_id\': \'123456\', #接收人的ID号
              \'message: \'xxxxx\', #发给别人的数据
          }
      }
  • 6:客户端接收服务端发来的在线用户数据结构体
    • list_onlien_user = [
          {\'id1\':\'name1\'},
          {\'id2\':\'name2\'},
      }
  • 7: 刷新的用户列表数据是一个列表,现在要更改
    • { #服务端发给客户端的数据体
          \'flag\': \'flush\' #数据标志位
          \'data\': [在线用户列表]
      }
      
  • 8: 发给其他用户的数据,怎么发过去?
    • { #服务端发给客户端的数据体
          \'flag\': \'forward\' #数据标志位
          \'data\': {
              \'send_name\': \'123455\', #发送者的ID
              \'message\': \'123456\', #接收人的ID号
          }
      }

五,代码编写如下:

#服务端
import socket
from multiprocessing.pool import ThreadPool
import pickle
import copy
import time

class TalkServer:
    def __init__(self,ip,port):
        self.ip = ip
        self.port = port
        self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket_server.setblocking(0) #服务端套接字设置为非阻塞
        self.socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #设置端口复用
        self.socket_server.bind( (self.ip, self.port) )

        self.dict_online_user = {} #保存未来的在线用户字典
        self.list_online_user = [] #保存即将发送给别人在现在用户列表
        self.thread_pool = ThreadPool(10)

        self.is_flush = False #用来判断是否需要刷新在线用户的

    def run(self, num_=5):
        self.socket_server.listen(num_)
        #2:用户发送给别人的消息处理
        self.thread_pool.apply_async(func=self.inform_online_user_list)
        self.thread_pool.apply_async(func=self.check_send_data)
        while True:
            \'\'\'
                接收用户来访
            \'\'\'
            try:
                client,client_addr = self.socket_server.accept()
            except BlockingIOError:
                pass
            else:
                print(\'有人来了:[%s|%s]\' % client_addr)
                #没报错,那么就是用户真正的连接到了,accept捕捉到了返回值
                #1:用户登陆发来的消息处理,开一个线程去等待他发来对应的数据,或者说判断这个ID是否是重复的
                self.thread_pool.apply_async(func=self.check_login_data,args=(client,))
                #login数据

    def inform_online_user_list(self):
        \'\'\'
            告知客户端现在的在线用户,就是直接发送一个在线用户的列表
        \'\'\'
        while True:
            if self.is_flush:
                time.sleep(1)
                print(\'刷新在线用户\')
                send_online_user_msg = {
                    \'flag\': \'flush\',
                    \'data\': self.list_online_user,
                }

                for id in self.dict_online_user:
                    client = self.dict_online_user[id][\'socket\']
                    client.send(pickle.dumps(send_online_user_msg))
                self.is_flush = False

    def check_send_data(self):
        \'\'\'
            处理用户的发送数据,要发给别人拉
            还会碰到用户发来的断开连接的数据
        \'\'\'
        while True:
            list_onlien_user_bak = copy.copy(self.list_online_user)
            for data in list_onlien_user_bak: #[{\'id1\':\'name1\'},{\'id1\':\'name1\'}...]
                id = list(data.keys())[0]
                client = self.dict_online_user[id][\'socket\'] #套接字拿到
                name = self.dict_online_user[id][\'name\']
                #print(\'当前遍历到的在线用户,%s:%s\' % (id,name))
                try:
                    recv_msg = client.recv(1024) #非阻塞形式获取客户端发来的数据
                except BlockingIOError as e:
                    pass #当前该在线用户并没有发送任何消息
                except ConnectionResetError as e:
                    \'\'\'
                        客户端断开连接
                    \'\'\'
                    pass
                else:
                    #1:判断是否是断开的数据
                    if not recv_msg:
                        print(\'离开了:%s,%s\' % (id,name))
                        client.close()
                        del self.dict_online_user[id]
                        self.list_online_user.remove(data)
                        self.is_flush = True
                    else:
                        \'\'\'
                            { #发送数据体
                                \'flag\': \'send\' #数据标志位
                                \'data\': {
                                	\'send_id\': \'123455\', #发送者的ID
                                    \'recv_id\': \'123456\', #接收人的ID号
                                    \'message: \'xxxxx\', #发给别人的数据
                                }
                            }
                        \'\'\'
                        clear_recv_msg = pickle.loads(recv_msg)
                        if clear_recv_msg.get(\'flag\') == \'send\': #确实是要给别人发送消息
                            clear_recv_data = clear_recv_msg.get(\'data\')
                            if clear_recv_data: #发来的数据中确实有data
                                send_id = clear_recv_data.get(\'send_id\')
                                recv_id = clear_recv_data.get(\'recv_id\')
                                message = clear_recv_data.get(\'message\')

                                if self.dict_online_user.get(recv_id):
                                    recv_socket = self.dict_online_user.get(recv_id)[\'socket\'] #接收者的套接字
                                else:
                                    continue
                                send_name = self.dict_online_user[send_id][\'name\']#发送者的名字
                                \'\'\'
                                { #服务端发给客户端的数据体
                                    \'flag\': \'forward\' #数据标志位
                                    \'data\': {
                                    	\'send_name\': \'123455\', #发送者的ID
                                        \'message\': \'123456\', #接收人的ID号
                                    }
                                }
                                \'\'\'
                                forward_data = {
                                    \'flag\': \'forward\',
                                    \'data\': {
                                        \'send_name\': send_name,
                                        \'message\': message,
                                    }
                                }
                                recv_socket.send(pickle.dumps(forward_data))
                                print(\'消息转发完毕:\n\',forward_data)

    def check_login_data(self,client):
        \'\'\'
            { #登陆数据体
                \'flag\': \'login\', #数据标志位
                \'data\': {
                    \'id\': \'123456\', #用户ID
                    \'name: \'xxxxx\', #用户昵称
                }
            }
        \'\'\'
        recv_msg = client.recv(1024) #接收用户发来的login数据体
        if recv_msg: #发来的数据是有效的
            clear_recv_msg = pickle.loads(recv_msg)
            if clear_recv_msg.get(\'flag\') == \'login\': #发来的确实是login的数据
                clear_recv_data = clear_recv_msg.get(\'data\')
                if clear_recv_data: #发来的数据中确实有data
                    id = clear_recv_data.get(\'id\')
                    name = clear_recv_data.get(\'name\')
                    #判断ID是否重复
                    if id in self.dict_online_user:
                        print(\'[%s:%s]该用户出现重复ID\' % (id, name))
                        send_login_msg = {
                            \'flag\': \'login\',
                            \'data\': {
                                \'state\': False,
                                \'message\': \'ID使用重复\',
                            }
                        }
                        client.send(pickle.dumps(send_login_msg)) #给客户端返回错误信息
                    elif not id or not name:
                        print(\'[%s:%s]该用户所需数据为空\' % (id, name))
                        send_login_msg = {
                            \'flag\': \'login\',
                            \'data\': {
                                \'state\': False,
                                \'message\': \'ID或名字为空\',
                            }
                        }
                        client.send(pickle.dumps(send_login_msg)) #给客户端返回错误信息
                    else:
                        \'\'\'
                        dict_onlien_user = {
                            xxxx:{
                                \'name\':\'666\',
                                \'socket\':client,
                            ...
                            },
                        \'\'\'
                        client.setblocking(0) #真正存储在在线字典里的用户套接字是个非阻塞的
                        self.dict_online_user[id] = {
                            \'name\': name,
                            \'socket\': client,
                        }
                        self.list_online_user.append( {id:name} )
                        #刷新在线用户
                        print(\'在线用户已添加\')
                        self.is_flush = True
                        send_login_msg = {
                            \'flag\': \'login\',
                            \'data\': {
                                \'state\': True,
                                \'message\': \'登陆成功\',
                            }
                        }
                        client.send(pickle.dumps(send_login_msg))
                        return
        client.close() #用户数据无效,关闭连接

def main():
    ip = \'\'
    port = 8000
    tks = TalkServer(ip, port)
    tks.run()

if __name__ == \'__main__\':
    main() 

  运行结果:

 

分类:

技术点:

相关文章: