一.目的
以实现小项目的方式,来巩固之前学过的Python基本语法以及相关的知识。
二.相关技术:
1.wxpython GUI编程
2.网络编程
3.多线程编程
4.数据库编程
5.简单的将数据导出到Excel表
三.存在的漏洞以及不足
1.由于数据库编码的问题,无法使用中文。
2.在客户端关闭后,其相关的线程仍然存在于服务器的用户线程队列中,所以服务器会错误地往已关闭的客户端传送信息。
3.客户端初始登录并加载历史记录时,会出现每条历史消息后面的回车键丢失的现象,解决的方法是:在加载相邻两条消息之间加个时间间隔,但效果不佳。
四.源码
服务器Server:
1 # -*- coding: UTF-8 -*- 2 3 from socket import * 4 import time 5 import threading 6 import wx 7 import MySQLdb 8 import xlwt 9 from clientthread import ClientThread 10 11 class Server(wx.Frame): 12 def __init__(self,parent=None,id=-1,title='服务器',pos=wx.DefaultPosition,size=(500,300)): 13 14 '''窗口''' 15 wx.Frame.__init__(self,parent,id,title,pos,size=(400,470)) 16 pl = wx.Panel(self) 17 con = wx.BoxSizer(wx.VERTICAL) 18 subcon = wx.FlexGridSizer(wx.HORIZONTAL) 19 sta = wx.Button(pl , size=(133, 40),label='启动服务器') 20 end = wx.Button(pl, size=(133, 40), label='关闭服务器') 21 hist = wx.Button(pl,size=(133,40),label='导出聊天记录') 22 subcon.Add(sta, 1, wx.BOTTOM) 23 subcon.Add(hist, 1, wx.BOTTOM) 24 subcon.Add(end, 1, wx.BOTTOM) 25 con.Add(subcon,1,wx.ALIGN_CENTRE|wx.BOTTOM) 26 self.Text = wx.TextCtrl(pl, size=(400,250),style = wx.TE_MULTILINE|wx.TE_READONLY) 27 con.Add(self.Text, 1, wx.ALIGN_CENTRE) 28 self.ttex = wx.TextCtrl(pl, size=(400,100),style=wx.TE_MULTILINE) 29 con.Add(self.ttex, 1, wx.ALIGN_CENTRE) 30 sub2 = wx.FlexGridSizer(wx.HORIZONTAL) 31 clear = wx.Button(pl, size=(200, 40), label='清空') 32 send = wx.Button(pl, size=(200, 40), label='发送') 33 sub2.Add(clear, 1, wx.TOP | wx.LEFT) 34 sub2.Add(send, 1, wx.TOP | wx.RIGHT) 35 con.Add(sub2, 1, wx.ALIGN_CENTRE) 36 pl.SetSizer(con) 37 '''窗口''' 38 39 '''绑定''' 40 self.Bind(wx.EVT_BUTTON, self.EditClear, clear) 41 self.Bind(wx.EVT_BUTTON, self.SendMessage, send) 42 self.Bind(wx.EVT_BUTTON, self.Start, sta) 43 self.Bind(wx.EVT_BUTTON, self.Break, end) 44 self.Bind(wx.EVT_BUTTON, self.WriteToExcel, hist) 45 '''绑定''' 46 47 '''服务器准备工作''' 48 self.UserThreadList = [] 49 self.onServe = False 50 addr = ('', 21567) 51 self.ServeSock = socket(AF_INET, SOCK_STREAM) 52 self.ServeSock.bind(addr) 53 self.ServeSock.listen(10) 54 '''服务器准备工作''' 55 56 '''数据库准备工作,用于存储聊天记录''' 57 self.db = MySQLdb.connect('localhost', 'root', '123456', 'user_info') 58 self.cursor = self.db.cursor() 59 self.cursor.execute("select * from history order by time") 60 self.Text.SetValue('') 61 for data in self.cursor.fetchall(): #加载历史聊天记录 62 self.Text.AppendText('%s said: %s when %s ' % (data[0], data[2], data[1])) 63 '''数据库准备工作,用于存储聊天记录''' 64 65 66 #将聊天记录导出到EXCEl表中 67 def WriteToExcel(self,event): 68 wbk = xlwt.Workbook() 69 sheet = wbk.add_sheet('sheet 1') 70 self.cursor.execute("select * from history order by time") 71 sheet.write(0, 0, "User") 72 sheet.write(0, 1, "Datetime") 73 sheet.write(0, 5, "Message") 74 index = 0 75 for data in self.cursor.fetchall(): 76 index = index + 1 77 Time = '%s'%data[1] #将datetime转成字符形式,否则直接写入Excel会变成时间戳 78 sheet.write(index,0,data[0]) 79 sheet.write(index,1,Time) #写进EXCEL会变成时间戳 80 sheet.write(index,5,data[2]) 81 wbk.save(r'D:History_Dialog.xls') 82 83 84 #启动服务器的服务线程 85 def Start(self,event): 86 if not self.onServe: 87 '''启动服务线程''' 88 self.onServe = True 89 mainThread = threading.Thread(target=self.on_serving, args=()) 90 mainThread.setDaemon(True) # 解决父线程结束,子线程还继续运行的问题 91 mainThread.start() 92 '''启动服务线程''' 93 94 #关闭服务器 95 def Break(self,event): 96 self.onServe = False 97 98 #服务器主循环 99 def on_serving(self): 100 print '...On serving...' 101 while self.onServe: 102 UserSocket, UserAddr = self.ServeSock.accept() 103 username = UserSocket.recv(1024).decode(encoding='utf-8') #接收用户名 104 userthread = ClientThread(UserSocket, username,self) 105 self.UserThreadList.append(userthread) #将用户线程加到队列中 106 userthread.start() 107 self.ServeSock.close() 108 109 #绑定发送按钮 110 def SendMessage(self,event): 111 if self.onServe and cmp(self.ttex.GetValue(),''): 112 data = self.ttex.GetValue() 113 self.AddText('Server',data,time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) 114 self.ttex.SetValue('') 115 116 117 # 向所有客户端(包括自己)发送信息,同时更新到数据库 118 def AddText(self, source, data,Time): 119 self.cursor.execute("insert into history values("%s","%s","%s")" % (source,Time,data)) #双引号里面有双引号,bug:句子不能有双引号、以及中文 120 self.db.commit() 121 sendData = '%s said: %s when %s ' % (source,data,Time) 122 self.Text.AppendText('%s '%sendData) 123 for user in self.UserThreadList: #bug:客户端关闭了仍然在队列中。如果客户端关闭了,那怎么在服务器判断是否已经关闭了?客户端在关闭之前发一条信息给服务器? 124 user.UserSocket.send(sendData.encode(encoding='utf-8')) 125 126 #绑定清空按钮 127 def EditClear(self,event): 128 self.ttex.Clear() 129 130 131 def main(): 132 app = wx.App(False) 133 Server().Show() 134 app.MainLoop() 135 136 if __name__ == '__main__': 137 main()
服务器的客户线程Clientthread:
1 # -*- coding: UTF-8 -*- 2 3 import threading 4 import time 5 6 class ClientThread(threading.Thread): 7 8 def __init__(self,UserSocket, Username,server): 9 threading.Thread.__init__(self) 10 self.UserSocket = UserSocket 11 self.Username = Username 12 self.server = server 13 self.Loadhist() 14 15 # 加载历史聊天记录 16 def Loadhist(self): 17 self.server.cursor.execute("select * from history order by time") 18 for data in self.server.cursor.fetchall(): 19 time.sleep(0.6) #几条信息同时发,会造成末尾回车键的丢失,所以要有时间间隔 20 sendData = '%s said: %s when %s '%(data[0], data[2], data[1]) 21 self.UserSocket.send(sendData.encode(encoding='utf-8')) 22 23 24 #方法重写,线程的入口 25 def run(self): 26 size = 1024 27 while True: 28 data = self.UserSocket.recv(size) #未解决:客户端断开连接后这里会报错 29 self.server.AddText(self.Username,data.decode(encoding='utf-8'),time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) 30 self.UserSocket.close() #这里都执行不到
客户登录界面Logframe:
1 # -*- coding: UTF-8 -*- 2 3 from socket import * 4 import wx 5 import MySQLdb 6 from client import Client 7 8 class LogFrame(wx.Frame): 9 def __init__(self,parent=None,id=-1,title='登录窗口',pos=wx.DefaultPosition,size=(500,300)): 10 11 '''窗口''' 12 wx.Frame.__init__(self,parent,id,title,pos,size=(400,280)) 13 self.pl = wx.Panel(self) 14 con = wx.BoxSizer(wx.VERTICAL) 15 subcon = wx.FlexGridSizer(2,2,10,10) 16 username = wx.StaticText(self.pl, label="Username:",style=wx.ALIGN_LEFT) 17 password = wx.StaticText(self.pl, label="Password:",style=wx.ALIGN_LEFT) 18 self.tc1 = wx.TextCtrl(self.pl,size=(180,20)) 19 self.tc2 = wx.TextCtrl(self.pl,size=(180,20),style=wx.TE_PASSWORD) 20 subcon.Add(username,wx.TE_LEFT) 21 subcon.Add(self.tc1,1,wx.EXPAND) 22 subcon.Add(password) 23 subcon.Add(self.tc2,1,wx.EXPAND) 24 con.Add(subcon,1,wx.ALIGN_CENTER) 25 subcon2 = wx.FlexGridSizer(1,2,10,10) 26 register = wx.Button(self.pl,label='Register') 27 login = wx.Button(self.pl,label='Login') 28 subcon2.Add(register,1, wx.TOP) 29 subcon2.Add(login,1, wx.TOP) 30 con.Add(subcon2,1,wx.ALIGN_CENTRE) 31 self.pl.SetSizer(con) 32 self.Bind(wx.EVT_BUTTON,self.Register,register) 33 self.Bind(wx.EVT_BUTTON,self.Login,login) 34 '''窗口''' 35 self.isConnected = False 36 self.userSocket = None 37 38 #连接到服务器 39 def ConnectToServer(self): 40 if not self.isConnected: 41 ADDR = ('localhost', 21567) 42 self.userSocket = socket(AF_INET, SOCK_STREAM) 43 try: 44 self.userSocket.connect(ADDR) 45 self.userSocket.send(self.tc1.GetValue().encode(encoding='utf-8')) 46 self.isConnected = True 47 return True 48 except Exception: 49 return False 50 else: 51 return True 52 53 #登录 54 def Login(self,event): 55 if not self.ConnectToServer(): 56 err = wx.MessageDialog(None, '服务器未启动', 'ERROR!', wx.OK) 57 err.ShowModal() 58 err.Destroy() 59 else: 60 username = self.tc1.GetValue() 61 password = self.tc2.GetValue() 62 db = MySQLdb.connect('localhost', 'root', '123456', 'user_info') 63 cursor = db.cursor() 64 cursor.execute("select * from user_list where username='%s' and password='%s'"%(username,password)) 65 if not cursor.fetchone(): 66 err = wx.MessageDialog(None,'用户不存在或密码错误','ERROR!',wx.OK) 67 err.ShowModal() 68 else: 69 self.Close() 70 Client(opSock=self.userSocket, username=username).Show() 71 db.commit() 72 db.close() 73 74 #注册 75 def Register(self,event): 76 if not self.ConnectToServer(): 77 err = wx.MessageDialog(None, '服务器未启动', 'ERROR!', wx.OK) 78 err.ShowModal() 79 err.Destroy() 80 else: 81 username = self.tc1.GetValue() 82 password = self.tc2.GetValue() 83 db = MySQLdb.connect('localhost', 'root', '123456', 'user_info') 84 cursor = db.cursor() 85 cursor.execute("select * from user_list where username='%s'"%username) 86 if not cursor.fetchone(): 87 cursor.execute("insert into user_list(username,password) values('%s','%s')"%(username,password)) 88 else: 89 err = wx.MessageDialog(None, '用户已存在', 'ERROR!', wx.OK) 90 err.ShowModal() 91 db.commit() 92 db.close() 93 94 95 def main(): 96 app = wx.App(False) 97 LogFrame().Show() 98 app.MainLoop() 99 100 if __name__ == '__main__': 101 main()
客户端Client:
1 #/usr/bin/env python 2 # -*- coding: UTF-8 -*- 3 4 import wx 5 import threading 6 from time import ctime 7 8 class Client(wx.Frame): 9 def __init__(self,opSock,username,parent=None,id=-1,title='客户端',pos=wx.DefaultPosition,size=(500,300)): 10 11 '''窗口''' 12 wx.Frame.__init__(self,parent,id,title,pos,size=(400,470)) 13 self.opSock = opSock 14 self.username = username 15 pl = wx.Panel(self) 16 con = wx.BoxSizer(wx.VERTICAL) 17 subcon = wx.FlexGridSizer(wx.HORIZONTAL) 18 sta = wx.Button(pl, size=(200, 40),label='连接') 19 end = wx.Button(pl, size=(200, 40),label='断开') 20 subcon.Add(sta, 1, wx.TOP|wx.LEFT) 21 subcon.Add(end, 1, wx.TOP|wx.RIGHT) 22 con.Add(subcon,1,wx.ALIGN_CENTRE) 23 self.Text = wx.TextCtrl(pl, size=(400,250),style = wx.TE_MULTILINE|wx.TE_READONLY) 24 con.Add(self.Text, 1, wx.ALIGN_CENTRE) 25 self.ttex = wx.TextCtrl(pl, size=(400,100),style=wx.TE_MULTILINE) 26 con.Add(self.ttex, 1, wx.ALIGN_CENTRE) 27 sub2 = wx.FlexGridSizer(wx.HORIZONTAL) 28 clear = wx.Button(pl, size=(200, 40), label='清空') 29 send = wx.Button(pl, size=(200, 40), label='发送') 30 sub2.Add(clear, 1, wx.TOP | wx.LEFT) 31 sub2.Add(send, 1, wx.TOP | wx.RIGHT) 32 con.Add(sub2, 1, wx.ALIGN_CENTRE) 33 pl.SetSizer(con) 34 '''窗口''' 35 36 '''绑定''' 37 self.Bind(wx.EVT_BUTTON, self.EditClear, clear) 38 self.Bind(wx.EVT_BUTTON, self.Send, send) 39 self.Bind(wx.EVT_BUTTON, self.Login, sta) 40 self.Bind(wx.EVT_BUTTON, self.Logout, end) 41 '''绑定''' 42 self.isConnected = False 43 44 #登录 45 def Login(self,event): 46 '''客户端准备工作''' 47 self.isConnected = True 48 t = threading.Thread(target=self.Receive, args=()) 49 t.setDaemon(True) 50 t.start() 51 '''客户端准备工作''' 52 53 #退出 54 def Logout(self,event): 55 self.isConnected = False 56 57 #绑定发送按钮 58 def Send(self,event): 59 if self.isConnected and cmp(self.ttex.GetValue(),''): 60 self.opSock.send(self.ttex.GetValue().encode(encoding='utf-8')) 61 self.ttex.SetValue('') 62 63 #绑定清空按钮 64 def EditClear(self,event): 65 self.ttex.Clear() 66 67 #接收客户端的信息(独立一个线程) 68 def Receive(self): 69 while self.isConnected: 70 data = self.opSock.recv(1024).decode(encoding='utf-8') 71 self.Text.AppendText('%s '%data)