第二个程序——客户端ClientUI

news2024/9/24 9:25:01

简介

在我的上一篇文章中,我已经介绍了如何实现“在线聊天室”中的服务器端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等非常简单基础的工具,因此其修改的空间较大。相信通过小修或大改可以让其适应更多的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176378.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一期Go群问答-并发控制-数据竞争-错误与异常

每周更新Go技术交流群的群问答内容&#xff0c;有需要可发我Go加群讨论学习。 并发控制 waitGroup.done()不是必须写在main方法中吗? 为什么我的协程没有成功等待&#xff1f; 熊&#xff1a;如果用了wait group&#xff0c;请求就直接卡住了&#xff0c;如果只有一个gorou…

Linux C编程一站式学习笔记5

Linux C编程一站式学习笔记 chap5 深入理解函数 文章目录Linux C编程一站式学习笔记 chap5 深入理解函数一.return语句习题二.增量式开发三.递归我猜有递归可视化工具&#xff0c;一搜果真有收获习题GCD(Greatest Common Divisor) 最大公约数Fibonacci相关资源、参考资料嘶&…

在linux中安排mysql

linux安装mysql 检测当前系统中是否安装Mysql数据库 rpm -qa rpm -qa|grep mysql rpm -qa|grep mariadb没有输出就是没有安装 我的这里显示mariadb是安装了的&#xff08;会与mysql冲突&#xff09; 卸载已经安装的软件 rpm -e --nodeps 软件名称 rpm -e --nodeps mariadb-li…

什么是执行董事

一、什么是执行董事执行董事&#xff0c;是指参与经营的董事。作为法定意义上的执行董事&#xff0c;是指规模较小的有限公司在不设立董事会的情况下设立的负责公司经营管理的职务。作为上市公司意义上的执行董事&#xff0c;执行董事并没有明确的法规依据。执行董事和非执行董…

偷偷理解Java和Scala中==和equals()的区别

君霸王&#xff0c;社稷定&#xff0c;君不霸王&#xff0c;社稷不定&#x1f97d; 目录 Java总结 Scala总结 Java中和equals() ---------------------------------------------------------------------------------------------------------------------------------------…

【人工智能原理自学】卷积神经网络:图像识别实战

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本文讲解卷积神经网络&#xff1a;图像识别实战&#xff0c;一起卷起来叭&#xff01; 目录一、“卷”二、LeNet-5网络一、“卷” 这节课我们来看如何把卷积运算融入到神经网络…

【青训营】Go语言的基本语法

一、 配置Go语言及其开发环境 Mac配置&#xff1a;http://t.zoukankan.com/zsy-p-6685889.html https://wenku.baidu.com/view/8aeec92b15fc700abb68a98271fe910ef12daeaf.html?wkts1673764660043&bdQuery%E5%A6%82%E4%BD%95%E9%85%8D%E7%BD%AEgopathmac 二、基础语法 p…

避免用Apache Beanutils进行属性的copy。why?让我们一起一探究竟。

在实际的项目开发中&#xff0c;对象间赋值普遍存在&#xff0c;随着双十一、秒杀等电商过程愈加复杂&#xff0c;数据量也在不断攀升&#xff0c;效率问题&#xff0c;浮出水面。 问&#xff1a;如果是你来写对象间赋值的代码&#xff0c;你会怎么做&#xff1f; 答&#xf…

05 |「链表」刷题

前言 前言&#xff1a;链表面试高频题。 文章目录前言一. 基础回顾二. 高频考题1. 例题1&#xff09;题目链接&#xff08;LeetCode 206 反转链表&#xff09;2&#xff09; 算法思路3&#xff09;源码剖析4&#xff09;时间复杂度2. 习题一. 基础回顾 参考上一讲&#xff1a; …

线性代数[向量]

系列文章目录 第一章 线性代数[初等变换(一)] 第二章 线性代数[初等变换(二)] 第三章 线性代数[初等变换(三)] 第四章 线性代数[矩阵的秩] 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一.引入 二.向…

计网必会:运输层概述、网络层介绍、TCP、UDP、多路复用、多路分解

文章目录运输层概念回顾TCP 和UDP概述介绍网络层TCP UDP网络层的联系多路复用和多路分解多路复用和多路分解的中国话理解TCP的多路复用和多路分解与UDP的区别HTTP 会话Web服务器和TCPUDP的多路复用和多路分解源端口号的用处的中国话理解UDP套接字无连接运输UDP 的优势运输层概念…

【C语言进阶】指针进阶(详细版)

目录 一、字符指针 二、指针数组 三、数组指针 1、数组指针的定义 2、&数组名和数组名的区别 3、数组指针的使用 四、数组传参和指针传参 1、一维数组传参 2、一级指针传参 3、二维数组传参 4、二级指针传参 五、函数指针 1、函数指针的定义 2、函数指针的使用 六、…

如何与他人交流

上期我们讲了打破预期,顺应主体,我的别人交流,只有在不把别人当成对象(工具人),而是把对方当成主体的情况下(让别人感受到尊重),这是相互尊重的终极本质,也是唯一方法.把别人当人看.认同对方,对方也会认同你.自信从何而来自信本意为相信自己,所以自信本应该是由内而外的事物,但…

【MySQL进阶】MySQL事务详解

序号系列文章5【MySQL基础】字符集与校对集详解6【MySQL基础】MySQL单表操作详解7【MySQL基础】运算符及相关函数详解8【MySQL基础】MySQL多表操作详解文章目录前言1&#xff0c;事务概念2&#xff0c;事务四大特性2.1&#xff0c;原子性2.2&#xff0c;一致性2.3&#xff0c;隔…

ORB SLAM3 ubuntu18.04 ROS 运行 段错误 (核心已转储) 踩坑及解决

问题猜测及解决&#xff1a;opencv版本兼容性 项目版本&#xff1a;ORB SLAM3 V1.0版本 CPU&#xff1a;13600K (大小核架构不知是否会影响) 电脑环境&#xff1a;ubuntu18.04 ROS运行 相机&#xff1a;D435 i opencv版本&#xff1a;3.2 and 4.6 前提&#xff1a;编译无报错&a…

Java 23种设计模式(3.创建者模式-原型模式)

1.概述 用一个已经创建的实例作为原型&#xff0c;通过复制该原型对象来创建一个和原型对象相同的新对象。 2.结构 原型模式包含如下角色&#xff1a; 抽象原型类&#xff1a; 规定了具体原型对象必须实现的的 clone() 方法。 具体原型类&#xff1a; 实现抽象原型类的 clon…

多线程(5)

文章目录前言 &#xff1a;常见锁策略了1.悲观锁 VS 乐观锁2. 轻量级锁 VS 重量级锁3.自旋锁 VS 挂起等待锁4. 读写锁 VS 普通的互斥锁5. 公平锁 和 非公平锁6. 可重入锁 VS 不可重入锁CAS1. CAS 的应用场景2. CAS 的典型问题 : ABA 问题synchronized 原理1.锁升级 / 锁膨胀2.锁…

Ae 效果详解:发光

效果/风格化/发光Effects/Stylize/Glow发光 Glow效果可找到图像中的较亮部分&#xff0c;然后使那些像素和周围的像素变亮&#xff0c;以创建漫射的发光光环。可以创建两种颜色&#xff08;颜色 A 和颜色 B &#xff09;之间的渐变发光&#xff0c;并可通过复制发光效果以创建更…

RESTful开发风格 与 SpringMVC跨域访问

RESTful REST&#xff1a;表现层状态转换&#xff0c;资源在网络中以某种表现形式进行状态转移RESTful 是基于 REST理念 的一套开发风格&#xff0c;是具体的开发规则&#xff0c;如果一个架构符合REST 原则&#xff0c;就称为 RESTful 架构。 RESTful 开发规范&#xff1a;…

【leetcode】学了栈和队列却觉得无用武之地?试试这几道题目吧!

目录 0.写在前面 1.leetcode.20 有效的括号 2.leetcode.225 用队列实现栈 3.用栈实现队列 4.设计循环队列 0.写在前面 这些题目所用语言为C语言&#xff0c;由于C语言未提供栈和队列的数据结构&#xff0c;所以需要我们手动实现栈和队列。此外熟练掌握栈和队列的性质对解…