第二个程序——客户端ClientUI

简介

在我的上一篇文章中,我已经介绍了如何实现“在线聊天室”中的服务器端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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值