简介
在我的上一篇文章中,我已经介绍了如何实现“在线聊天室”中的服务器端ServerUI,服务器端作为整个聊天系统的“中继系统”,负责转发用户的信息到聊天室,可以转发给聊天室中的每一个人(即,群聊),也可以转发给聊天室中某个指定的人(即,私聊)。因此,我们的ClientUI客户端,其功能就是向服务器端发送数据,并从服务器端接受数据的。
思路
我在简介中说过,ClientUI的功能是“发送给”,“接受来自”,其对象都是服务器端,而不是具体的其它客户端。这么做的原因我已在ServerUI中介绍过,此处不妨简单回顾下。由于本系统使用是建立在python socket之间建立的TCP协议上的,因此客户端需要与“它想要联系的人”之间建立TCP连接;然而,由于这是一个在线聊天室,如果让某位客户与每一个接入聊天室的客户都建立连接,那么显然接入聊天室的客户也需要与当前已经在聊天室中的每一位客户建立连接,相当于构成了一个全连接的关系。这样做的话,用代码实现的成本过高,且我认为效率不高;因此使用服务器端ServerUI作为中继,每一个接入聊天室的客户先与服务器建立连接,之后服务器将客户发送的信息进行转发(注意,服务器同样可以监视客户互相发送<包括私发>的信息)。
那么,服务器如何得知要把信息转发给谁,以及这条信息是谁发送的呢?
我们假设客户A想通过服务器S发送给客户B信息。在上一篇文章中,我介绍了socket.py及其中实现的编码器/解码器,以及目的前缀,此处不妨再回顾一下。
- 目的前缀:由于服务器S中通过套接字(由于本程序是在本机上实现的,因此套接字就是客户端的IP地址及端口,总之可以让服务器S得知能够唯一标识用户地址的信息即可)标识每一位客户,并将其存在socs序列中(详见我的上一篇文章),因此如果发送方A能够在其发送的信息中加入它想发送给的目标方的地址(套接字,由于本程序在本机上实现,这个信息我使用的是IP地址。即,将目标方的IP地址作为目的前缀),即:将目标方地址dest和发送信息data进行编码,格式为
dest|data
,将这样一条信息发送给服务器S,那么服务器S可以进行解码,由于S知道发送给它信息的客户是A(发送时包含发送方的套接字信息),而通过解码得知了收方的信息,那么服务器可以将这条信息再次编码,即:将发送方的地址orid和发送的信息data编码为orid|data
发送给接收方,接收方收到信息后通过解码就得知了是谁给它发送了信息。 - 客户A想要给客户B发送消息,必然需要先与服务器S建立连接,同时客户B此时也必然是与服务器S相连接的(即,A和B在聊天室的状态均为“在线”);
- 由于A与S建立了连接,S在监听连接时,知道是哪个套接字想要与其建立连接,通过元组拆包,S可以得知A的IP地址及端口,而B由于同样与A建立了连接,因此S也得知B的套接字信息;这些信息均存放在服务器的socs序列中;
- 服务器的socs序列与数据库中的在库用户相关联,在注册成为在线聊天室用户时,客户可以填写其IP地址及端口,以及昵称,在客户端ClientUI会解析数据库中的信息,并在聊天栏中显示当前在线的用户有哪些。
- 由于通过昵称可以查询到其对应的目的地址,A选择要发送的用户后,输入发送信息点击发送,ClientUI即将目的地址通过目的前缀编码,发送给服务器S;
- 服务器S收到信息后,通过TCP连接得知是A发送给它的,再解码得知目的地位B,将A的目的地址编码发送给B;
- B得到信息后进行解码(这里非常关键,上文提到客户端只能发送给服务器信息,并从服务器接收信息,因此客户端得到的信息,其目的前缀均为发送方<即:知道是谁发给了它信息>地址),得知是谁发给了它信息,以及信息是什么。
ServerUI代码实现
由于socket.py的实现已经在上文提到过并进行了剖析,此处不再复述,详见上一篇文章。
用到的库函数
from tkinter import *
from Socketer import *
from threading import Thread
import pymssql as mysql
import datetime
import inspect
import ctypes
界面初始化Client_init
使用python自带的GUI工具tkinter进行了简单实现,UI界面的实现同样放在了Client_init类中,其中的功能实现则放在了Application_Client_init类中(与ServerUI相同,前者是后者的一个子类)。界面初始化Client_init是服务于客户端登录界面的,由于本程序是在本机上实现的,不支持在局域网内不同主机下的互相通信,因此使用本机的IP地址标识每一个客户即可。故登录时输入<唯一的IP地址, 端口>即可。
class Client_init(object):
client_ip = None
client_port = None
GUI = None
Successfully_Login = False
ip_and_port = None
def __init__(self):
self.GUI = Tk()
self.GUI.title("Client Login")
self.GUI.geometry('450x160')
self.GUI.wm_resizable(False, False)
Label(self.GUI, text='IP地址:', font=(20)).place(relx=.3, y=35, anchor="center")
self.ip = Entry(self.GUI, width=20)
self.ip.place(relx=.6, y=35, anchor="center")
Label(self.GUI, text='端口号:', font=(20)).place(relx=.3, y=70, anchor="center")
self.port = Entry(self.GUI, width=20)
self.port.place(relx=.6, y=70, anchor="center")
Button(self.GUI, width=15, height=1, text='登录',command=self.get_ip_and_port).place(relx=.5, y=120, anchor="center")
self.GUI.mainloop()
"""
👇检查输入的IP地址和端口号是否合法。
"""
def get_ip_and_port(self):
if len(self.ip.get()) == 0 or len(self.port.get()) == 0:
messagebox.showerror(title='Client Login ERROR', message='信息有缺。')
elif len(tuple(self.ip.get().split('.'))) != 4:
messagebox.showerror(title='Client Login ERROR', message='非法的IP地址。')
else:
self.Successfully_Login = True
self.ip_and_port = (self.ip.get(),self.port.get())
self.GUI.destroy()
Application_Client_init的实现。由于初始化界面没有过多的信息,因此没有需要在父类中实现的功能。
class Application_Client_init(Client_init):
def __init__(self):
Client_init.__init__(self)
Client_init界面:
界面设计ClientUI类
此处的ClientUI类才是客户端的主界面,同样使用子类和父类进行实现,父类包含tkinter界面的定义,子类则给出了这些定义的实现。
👇界面设计没什么好说的,如有需要建议您将这段代码自己跑起来,看着弹出的界面再对照着代码进行解读。
class ClientUI(object):
GUI = None
Client_soc = None
text = None
isOn = False
connect = mysql.connect("192.168.2.4", "sa", "123456", "Client")
cur = connect.cursor()
friends = []
def __init__(self,addr):
self.client_ip = addr[0]
self.client_port = int(addr[1])
self.GUI = Tk()
self.GUI.title("Client")
self.GUI.geometry('700x460')
self.GUI.wm_resizable(False,False)
Label(self.GUI, text='IP地址:',font=(20)).place(relx=.3, y=15, anchor="center")
self.ip = Entry(self.GUI, width=20)
self.ip.place(relx=.5, y=15, anchor="center")
Label(self.GUI, text='端口号:' ,font=(20)).place(relx=.3, y=50, anchor="center")
self.port = Entry(self.GUI, width=20)
self.port.place(relx=.5, y=50, anchor="center")
Button(self.GUI,width=15,height=1,text='连接/断开',command=self.connect2server_disconnect).place(relx=.7, y=50, anchor="center")
self.state = Label(self.GUI,text="离线",font=("YouYuan",10),bg='pink').place(relx=.7, y=15, anchor="center")
self.paned_window = PanedWindow(self.GUI, showhandle=False, orient=HORIZONTAL,height=320,borderwidth=2)
self.paned_window.pack(expand=1)
# 左侧frame
self.left_frame = Frame(self.paned_window)
self.paned_window.add(self.left_frame)
self.text = Text(self.left_frame, font=('Times New Roman', 10))
text_y_scroll_bar = Scrollbar(self.left_frame, command=self.text.yview, relief=SUNKEN, width=2)
text_y_scroll_bar.pack(side=RIGHT, fill=Y)
self.text.config(yscrollcommand=text_y_scroll_bar.set)
self.text.pack(fill=BOTH)
self.text.insert(END, '[{}]:等待连接至服务器。\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
self.tsend = Entry(self.GUI, width=50)
self.tsend.place(relx=.4, y=420, anchor="center")
btn = Button(self.GUI, width=12, height=1, text='发送',command=self.send2c).place(relx=.8, y=420, anchor="center")
# 右侧frame
self.right_frame = Frame(self.paned_window)
self.paned_window.add(self.right_frame)
# 右上角Text
self.list_obj = Listbox(self.right_frame, font=("Courier New", 11))
text_y_scroll = Scrollbar(self.right_frame, command=self.list_obj.yview)
self.text_scroll_obj = text_y_scroll
self.list_obj.config(yscrollcommand=text_y_scroll.set)
text_y_scroll.pack(side=RIGHT, fill=Y)
self.list_obj.pack(expand=1,fill=BOTH)
self.GUI.mainloop()
👇界面实现概览。注意:此处输入的IP地址和端口是服务器的IP地址和端口号。
重头戏:ClientUI的实现
ClientUI的实现放在了其子类Application_ClientUI中。
class Application_ClientUI(ClientUI):
def __init__(self,addr):
ClientUI.__init__(self,addr)
def send2c(self):
if not self.isOn:#客户端离线时,点击“发送”按钮是非法的
messagebox.showerror(title="Connection ERROR", message="未连接至服务器。")
return
if len(self.tsend.get()) == 0:
messagebox.showerror(title="Content Empty ERROR",message="所发信息不可为空。")
elif len(self.list_obj.curselection()) == 0:
messagebox.showerror(title="Selection ERROR",message="未选择信息接收人。")
else:
data = self.tsend.get()
raw_data = data
dest = self.list_obj.curselection()#鼠标点击触发事件:获取收方地址
dest_addr = ("BROADCASTING",1)
dest = self.list_obj.get(dest[0]).replace("\n","")
if dest != "广播BROADCASTING":#查看是否以“广播”模式发送消息
for i in range(len(self.friends)):
if dest == self.friends[i][2]:
dest_addr = (self.friends[i][0],self.friends[i][1])
break
data = wencoding(dest_addr,data)#数据编码,将收方地址或模式编码进入所发数据中
try:
self.client_soc.s.send(data.encode("utf-8"))#发送消息
except:
self.connect2server_disconnect()#捕获发送异常
return
if dest != "广播BROADCASTING":#如果不是广播
self.text.insert(END,
'[{}]我:{}(to {})\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), raw_data, dest))
else:
self.text.insert(END,
'[{}]我:{}(广播消息已发送)\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),raw_data))
self.tsend.delete(0,END)#清空发送消息的文本框,以便发送下一条消息
def client_recv(self):
while True:#设置永真循环,并将该函数设置在子线程中,以不断地接收消息
try:
data = self.client_soc.s.recv(1024)
except:
continue
data = data.decode("utf-8")
ori_addr, data = wdecoding(data)#对收到的消息进行解码
ori_id = "Unknown"#初始化发送当前收到消息的发送人的地址信息
for i in range(len(self.friends)):
if self.friends[i][0] == ori_addr[0]:#从数据库查找
ori_id = self.friends[i][2]
break
if ori_addr[0] == '1':#如果是服务器发的
ori_id = "服务器"
self.text.insert(END,'[{}]{}:{}\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
, ori_id,data))
def connect2server_disconnect(self):
if not self.isOn:
if len(self.ip.get()) == 0 or len(self.port.get()) == 0:
messagebox.showerror(title='Server Connecting ERROR', message='信息有缺。')
else:
try:
self.client_soc = Client(self.client_ip,self.client_port)
except:
messagebox.showerror(title="操作过于频繁,请稍后再试", message="操作过于频繁,请稍后再试。")
return
self.host_ip = self.ip.get()
self.host_port = int(self.port.get())
try:
self.client_soc.s.connect((self.host_ip, self.host_port))
messagebox.showinfo(title="Successfully Connected",message="连接成功。")
self.state = Label(self.GUI, text="在线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
self.isOn = True
sql = "select * from " + "c" + self.host_ip.replace(".","")
self.cur.execute(sql)
User_list = self.cur.fetchall()
self.list_obj.insert(END,"广播BROADCASTING" + '\n')
for i in range(len(User_list)):
if User_list[i][0] == self.client_ip:
continue
self.friends.append((User_list[i][0],int(User_list[i][1]),User_list[i][2]))
self.list_obj.insert(END, User_list[i][2] + '\n')
self.text.insert(END,'[{}]:已连接至服务器,IP地址为:{}。\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self.host_ip))
self.client_recv_threading = Thread(target=self.client_recv,args=())
self.client_recv_threading.setDaemon(True)
self.client_recv_threading.start()
except:
messagebox.showerror(title="Connection Failed",message="连接失败,服务器未上线或所输信息有误。")
self.client_soc.s.close()
else:
self.tsend.delete(0,END)
self.text.delete("1.0", "end")
self.list_obj.delete(0,END)
self.client_soc.s.close()
if self.client_recv_threading.is_alive():
stop_thread(self.client_recv_threading)
self.isOn = False
self.state = Label(self.GUI, text="离线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
messagebox.showinfo(title="Successfully disconnected", message="已断开连接。")
由于涉及到多线程,与上一篇文章一样,我们同样按照线程建立的顺序进行讲解。
连接至服务器/断开连接:connect2server_disconnect
def connect2server_disconnect(self):
if not self.isOn:
"如果服务器不在线"
if len(self.ip.get()) == 0 or len(self.port.get()) == 0:
messagebox.showerror(title='Server Connecting ERROR', message='信息有缺。')
else:
try:
"建立客户端套接字,准备连接至服务器。"
self.client_soc = Client(self.client_ip,self.client_port)
except:
messagebox.showerror(title="操作过于频繁,请稍后再试", message="操作过于频繁,请稍后再试。")
return
self.host_ip = self.ip.get()
self.host_port = int(self.port.get())
try:
"连接至服务器。"
self.client_soc.s.connect((self.host_ip, self.host_port))
messagebox.showinfo(title="Successfully Connected",message="连接成功。")
self.state = Label(self.GUI, text="在线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
self.isOn = True
"通过sql从数据库读取用户列表。"
sql = "select * from " + "c" + self.host_ip.replace(".","")
self.cur.execute(sql)
User_list = self.cur.fetchall()
self.list_obj.insert(END,"广播BROADCASTING" + '\n')
"将用户列表插入到在线用户显示框中。"
for i in range(len(User_list)):
if User_list[i][0] == self.client_ip:
continue
self.friends.append((User_list[i][0],int(User_list[i][1]),User_list[i][2]))
self.list_obj.insert(END, User_list[i][2] + '\n')
self.text.insert(END,'[{}]:已连接至服务器,IP地址为:{}。\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self.host_ip))
"👇开设线程client_recv,即客户端上线,开始接收信息。"
self.client_recv_threading = Thread(target=self.client_recv,args=())
self.client_recv_threading.setDaemon(True)
self.client_recv_threading.start()
except:
messagebox.showerror(title="Connection Failed",message="连接失败,服务器未上线或所输信息有误。")
self.client_soc.s.close()
else:
self.tsend.delete(0,END)
self.text.delete("1.0", "end")
self.list_obj.delete(0,END)
self.client_soc.s.close()
if self.client_recv_threading.is_alive():
stop_thread(self.client_recv_threading)
self.isOn = False
self.state = Label(self.GUI, text="离线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
messagebox.showinfo(title="Successfully disconnected", message="已断开连接。")
接收信息:client_recv
由于需要一直接收信息,因此应该设置永真循环并将这个函数放在一个新的线程下。
def client_recv(self):
while True:#设置永真循环,并将该函数设置在子线程中,以不断地接收消息
try:
data = self.client_soc.s.recv(1024)
except:
continue
data = data.decode("utf-8")
ori_addr, data = wdecoding(data)#对收到的消息进行解码
ori_id = "Unknown"#初始化发送当前收到消息的发送人的地址信息
for i in range(len(self.friends)):
if self.friends[i][0] == ori_addr[0]:#从数据库查找
ori_id = self.friends[i][2]
break
if ori_addr[0] == '1':#如果是服务器发的
ori_id = "服务器"
self.text.insert(END,'[{}]{}:{}\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
, ori_id,data))
与客户端界面中“发送”绑定的行为:send2c
def send2c(self):
if not self.isOn:#客户端离线时,点击“发送”按钮是非法的
messagebox.showerror(title="Connection ERROR", message="未连接至服务器。")
return
if len(self.tsend.get()) == 0:
messagebox.showerror(title="Content Empty ERROR",message="所发信息不可为空。")
elif len(self.list_obj.curselection()) == 0:
messagebox.showerror(title="Selection ERROR",message="未选择信息接收人。")
else:
data = self.tsend.get()
raw_data = data
dest = self.list_obj.curselection()#鼠标点击触发事件:获取收方地址
dest_addr = ("BROADCASTING",1)
dest = self.list_obj.get(dest[0]).replace("\n","")
if dest != "广播BROADCASTING":#查看是否以“广播”模式发送消息
for i in range(len(self.friends)):
if dest == self.friends[i][2]:
dest_addr = (self.friends[i][0],self.friends[i][1])
break
data = wencoding(dest_addr,data)#数据编码,将收方地址或模式编码进入所发数据中
try:
self.client_soc.s.send(data.encode("utf-8"))#发送消息
except:
self.connect2server_disconnect()#捕获发送异常
return
if dest != "广播BROADCASTING":#如果不是广播
self.text.insert(END,
'[{}]我:{}(to {})\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), raw_data, dest))
else:
self.text.insert(END,
'[{}]我:{}(广播消息已发送)\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),raw_data))
self.tsend.delete(0,END)#清空发送消息的文本框,以便发送下一条消息
完整的ClientUI
from tkinter import *
from Socketer import *
from threading import Thread
import pymssql as mysql
import datetime
import inspect
import ctypes
def _async_raise(tid, exctype):
"""raises the exception, performs cleanup if needed"""
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
_async_raise(thread.ident, SystemExit)
class Client_init(object):
client_ip = None
client_port = None
GUI = None
Successfully_Login = False
ip_and_port = None
def __init__(self):
self.GUI = Tk()
self.GUI.title("Client Login")
self.GUI.geometry('450x160')
self.GUI.wm_resizable(False, False)
Label(self.GUI, text='IP地址:', font=(20)).place(relx=.3, y=35, anchor="center")
self.ip = Entry(self.GUI, width=20)
self.ip.place(relx=.6, y=35, anchor="center")
Label(self.GUI, text='端口号:', font=(20)).place(relx=.3, y=70, anchor="center")
self.port = Entry(self.GUI, width=20)
self.port.place(relx=.6, y=70, anchor="center")
Button(self.GUI, width=15, height=1, text='登录',command=self.get_ip_and_port).place(relx=.5, y=120, anchor="center")
self.GUI.mainloop()
def get_ip_and_port(self):
if len(self.ip.get()) == 0 or len(self.port.get()) == 0:
messagebox.showerror(title='Client Login ERROR', message='信息有缺。')
elif len(tuple(self.ip.get().split('.'))) != 4:
messagebox.showerror(title='Client Login ERROR', message='非法的IP地址。')
else:
self.Successfully_Login = True
self.ip_and_port = (self.ip.get(),self.port.get())
self.GUI.destroy()
class Application_Client_init(Client_init):
def __init__(self):
Client_init.__init__(self)
class ClientUI(object):
GUI = None
Client_soc = None
text = None
isOn = False
connect = mysql.connect("192.168.2.4", "sa", "123456", "Client")
cur = connect.cursor()
friends = []
def __init__(self,addr):
self.client_ip = addr[0]
self.client_port = int(addr[1])
self.GUI = Tk()
self.GUI.title("Client")
self.GUI.geometry('700x460')
self.GUI.wm_resizable(False,False)
Label(self.GUI, text='IP地址:',font=(20)).place(relx=.3, y=15, anchor="center")
self.ip = Entry(self.GUI, width=20)
self.ip.place(relx=.5, y=15, anchor="center")
Label(self.GUI, text='端口号:' ,font=(20)).place(relx=.3, y=50, anchor="center")
self.port = Entry(self.GUI, width=20)
self.port.place(relx=.5, y=50, anchor="center")
Button(self.GUI,width=15,height=1,text='连接/断开',command=self.connect2server_disconnect).place(relx=.7, y=50, anchor="center")
self.state = Label(self.GUI,text="离线",font=("YouYuan",10),bg='pink').place(relx=.7, y=15, anchor="center")
self.paned_window = PanedWindow(self.GUI, showhandle=False, orient=HORIZONTAL,height=320,borderwidth=2)
self.paned_window.pack(expand=1)
# 左侧frame
self.left_frame = Frame(self.paned_window)
self.paned_window.add(self.left_frame)
self.text = Text(self.left_frame, font=('Times New Roman', 10))
text_y_scroll_bar = Scrollbar(self.left_frame, command=self.text.yview, relief=SUNKEN, width=2)
text_y_scroll_bar.pack(side=RIGHT, fill=Y)
self.text.config(yscrollcommand=text_y_scroll_bar.set)
self.text.pack(fill=BOTH)
self.text.insert(END, '[{}]:等待连接至服务器。\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
self.tsend = Entry(self.GUI, width=50)
self.tsend.place(relx=.4, y=420, anchor="center")
btn = Button(self.GUI, width=12, height=1, text='发送',command=self.send2c).place(relx=.8, y=420, anchor="center")
# 右侧frame
self.right_frame = Frame(self.paned_window)
self.paned_window.add(self.right_frame)
# 右上角Text
self.list_obj = Listbox(self.right_frame, font=("Courier New", 11))
text_y_scroll = Scrollbar(self.right_frame, command=self.list_obj.yview)
self.text_scroll_obj = text_y_scroll
self.list_obj.config(yscrollcommand=text_y_scroll.set)
text_y_scroll.pack(side=RIGHT, fill=Y)
self.list_obj.pack(expand=1,fill=BOTH)
self.GUI.mainloop()
class Application_ClientUI(ClientUI):
def __init__(self,addr):
ClientUI.__init__(self,addr)
def send2c(self):
if not self.isOn:#客户端离线时,点击“发送”按钮是非法的
messagebox.showerror(title="Connection ERROR", message="未连接至服务器。")
return
if len(self.tsend.get()) == 0:
messagebox.showerror(title="Content Empty ERROR",message="所发信息不可为空。")
elif len(self.list_obj.curselection()) == 0:
messagebox.showerror(title="Selection ERROR",message="未选择信息接收人。")
else:
data = self.tsend.get()
raw_data = data
dest = self.list_obj.curselection()#鼠标点击触发事件:获取收方地址
dest_addr = ("BROADCASTING",1)
dest = self.list_obj.get(dest[0]).replace("\n","")
if dest != "广播BROADCASTING":#查看是否以“广播”模式发送消息
for i in range(len(self.friends)):
if dest == self.friends[i][2]:
dest_addr = (self.friends[i][0],self.friends[i][1])
break
data = wencoding(dest_addr,data)#数据编码,将收方地址或模式编码进入所发数据中
try:
self.client_soc.s.send(data.encode("utf-8"))#发送消息
except:
self.connect2server_disconnect()#捕获发送异常
return
if dest != "广播BROADCASTING":#如果不是广播
self.text.insert(END,
'[{}]我:{}(to {})\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), raw_data, dest))
else:
self.text.insert(END,
'[{}]我:{}(广播消息已发送)\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),raw_data))
self.tsend.delete(0,END)#清空发送消息的文本框,以便发送下一条消息
def client_recv(self):
while True:#设置永真循环,并将该函数设置在子线程中,以不断地接收消息
try:
data = self.client_soc.s.recv(1024)
except:
continue
data = data.decode("utf-8")
ori_addr, data = wdecoding(data)#对收到的消息进行解码
ori_id = "Unknown"#初始化发送当前收到消息的发送人的地址信息
for i in range(len(self.friends)):
if self.friends[i][0] == ori_addr[0]:#从数据库查找
ori_id = self.friends[i][2]
break
if ori_addr[0] == '1':#如果是服务器发的
ori_id = "服务器"
self.text.insert(END,'[{}]{}:{}\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
, ori_id,data))
def connect2server_disconnect(self):
if not self.isOn:
if len(self.ip.get()) == 0 or len(self.port.get()) == 0:
messagebox.showerror(title='Server Connecting ERROR', message='信息有缺。')
else:
try:
self.client_soc = Client(self.client_ip,self.client_port)
except:
messagebox.showerror(title="操作过于频繁,请稍后再试", message="操作过于频繁,请稍后再试。")
return
self.host_ip = self.ip.get()
self.host_port = int(self.port.get())
try:
self.client_soc.s.connect((self.host_ip, self.host_port))
messagebox.showinfo(title="Successfully Connected",message="连接成功。")
self.state = Label(self.GUI, text="在线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
self.isOn = True
sql = "select * from " + "c" + self.host_ip.replace(".","")
self.cur.execute(sql)
User_list = self.cur.fetchall()
self.list_obj.insert(END,"广播BROADCASTING" + '\n')
for i in range(len(User_list)):
if User_list[i][0] == self.client_ip:
continue
self.friends.append((User_list[i][0],int(User_list[i][1]),User_list[i][2]))
self.list_obj.insert(END, User_list[i][2] + '\n')
self.text.insert(END,'[{}]:已连接至服务器,IP地址为:{}。\n'.format(
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), self.host_ip))
self.client_recv_threading = Thread(target=self.client_recv,args=())
self.client_recv_threading.setDaemon(True)
self.client_recv_threading.start()
except:
messagebox.showerror(title="Connection Failed",message="连接失败,服务器未上线或所输信息有误。")
self.client_soc.s.close()
else:
self.tsend.delete(0,END)
self.text.delete("1.0", "end")
self.list_obj.delete(0,END)
self.client_soc.s.close()
if self.client_recv_threading.is_alive():
stop_thread(self.client_recv_threading)
self.isOn = False
self.state = Label(self.GUI, text="离线", font=("YouYuan", 10), bg='pink').place(relx=.7, y=15,
anchor="center")
messagebox.showinfo(title="Successfully disconnected", message="已断开连接。")
if __name__ == '__main__':
ip_and_port = Application_Client_init().ip_and_port
if ip_and_port != None:
Application_ClientUI(ip_and_port)
提示与展望
- 本程序没有对登录界面进行实现,即:我的数据库是静态的,在您使用这个程序之前应根据类似的数据库设置配置数据库,并建立自己的表。我建立的数据库名为Client,表名为从c127001,表属性为ip、port、id,类型均为varchar(MAX)。
- 关于python如何连接数据库,这个需要您自己在网上查阅资料。本程序使用的数据库是sql server,当然现在看来使用mysql搭配navicat可能是更好的选择,避免了需多不必要的配置。
- 当前尚不可支持在聊天室中发送表情包等,但由于设计这个程序的目的是完成课内的实验,往年曾设置“在聊天室中支持发送表情包”为加分项。
- 由于我实现的在线聊天程序是在本机上实现的,因此甚至无法支持在局域网下不同设备进行相互通信。但由于实现的功能完整,且使用python实现其可读性较强,使用的也是诸如tkinter、thread、socket等非常简单基础的工具,因此其修改的空间较大。相信通过小修或大改可以让其适应更多的场景。