python socket + tkinter实现网络聊天室

python socket + tkinter实现网络聊天室

转载:http://www.huqj.top/article?id=169

最近突然想用socket做个聊天室程序,之前用java写过一个文件传输的程序,这次就用python做一下,顺便也学习一下python的界面设计。界面库选择了python自带的tkinter。

    总的来说聊天室功能比较简单,只是一个练习用的demo,但是其中一些关于tk和socket的东西值得记录一下。最终的功能包括:注册、登录、显示聊天室在线成员、聊天。先来几张运行截图:

image.png

image.png  image.png

image.png

因为代码比较少,所以服务端和客户端的代码写在一起的,整个代码结构如下:

image.png

其中只有Server.py是服务器端的代码,服务器端没有界面。其它文件的作用分别如下:

  •  Client.py  负责和Server的socket通信,主要是收发数据

  • LoginPanel.py  登陆界面

  • Main.py  客户端的入口,负责调度各个界面之间的切换和调用client发送接收数据

  • MainPanel.py  聊天室主界面

  • MD5  md5算法

  • RegisterPanel.py  注册界面

  • data  目录,存放用户帐号密码数据

  • image  目录,存放图标

完整的代码和打包的exe文件可以从 https://download.csdn.net/download/qq_32216775/10903517 下载。

启动方式为:

1)先启动Server.exe开始监听端口(12323),或者直接用python运行Server.py

2)再启动一个或多个Main.exe打开客户端的界面,或者直接用python运行Main.py

这里再代码中写的是直接连接本地(127.0.0.1)的服务器,如果需要测试客户端连接远程服务器,只需要将Client.py中第10行的ip地址换成服务器地址即可,换端口也是同理。

image.png

一些值得记录的技术细节:

①tkinter界面上显示图片的方法:

使用tkinter模块的PhotoImage类,和Lbael组件即可,代码参考下面

form_frame = Frame(self.login_frame, bg="#333333")
user_img = PhotoImage(file="image\\user.png", master=self.login_frame)
key_img = PhotoImage(file="image\\key.png", master=self.login_frame)
user_img_label = Label(form_frame, image=user_img, width=30, height=30, bg="#333333")
key_img_label = Label(form_frame, image=key_img, width=30, height=30, bg="#333333")
user_img_label.grid(row=0, column=0, padx=5)
key_img_label.grid(row=1, column=0, padx=5)

②tkinter的Entry组件实现密码框效果:

这个还是可以使用Entry组件,只需要加上属性show="*",即可将输入显示为"*"

Entry(form_frame, textvariable=self.key, show="*", bg="#e3e3e3", width=30) \
    .grid(row=1, column=2, ipady=1)

③Text+Scrollbar组件实现聊天记录的效果:

这种效果主要考虑两个方面,一个是用不同的颜色显示区别其他人的消息和自己的消息,这个可以使用Text组件的tag_configure方法设置不同的标签,第二个是实现消息刷新时自动将滚动条滚动到底部,这个主要是通过Text的see函数实现,主要代码见下面

......
 
# 显示消息的文本框不可编辑,当需要修改内容时再修改版为可以编辑模式 NORMAL
self.message_text.config(state=DISABLED)
self.message_text.tag_configure('greencolor', foreground='green')
self.message_text.tag_configure('bluecolor', foreground='blue')
 
......
 
# 接受到消息,在文本框中显示,自己的消息用绿色,别人的消息用蓝色
def recv_message(self, user, content):
    self.message_text.config(state=NORMAL)
    title = user + " " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + "\n"
    if user == self.username:
        self.message_text.insert(END, title, 'greencolor')
    else:
        self.message_text.insert(END, title, 'bluecolor')
    self.message_text.insert(END, content + "\n")
    self.message_text.config(state=DISABLED)
    # 滚动到最底部
    self.message_text.see(END)

④python md5加密的方法:

import hashlib
 
# md5加密方法
def gen_md5(_str):
    hl = hashlib.md5()
    hl.update(_str.encode(encoding='utf-8'))
    return hl.hexdigest()

⑤python中int型数字和4字节数组之间的转换方法:

因为在socket传输的过程中,有些变长的数据,例如消息,所以这里我设计的传输协议在传输变长字符串的时候会先发送一个4字节的数据表示接下来会传多少数据,因此需要有一个方法来转换int数值和字节数组,这里使用的是int.from_bytes和int.to_bytes方法,如下

# 将数字number转换成4字节数组,byteorder表示字节数组中高位在前还是低位在前
int(number).to_bytes(4, byteorder='big')
 
# 从4个字节的数组中转成数字,_conn.recv(4)返回的就是字节数组
int.from_bytes(_conn.recv(4), byteorder='big')

⑥python程序打包成可执行文件:

使用pyinstaller库,可以方便的打包python项目成exe文件,其原理就是将python解释器以及程序都打包起来,所以效率上可能会比直接运行python文件低。使用方法:1)下载安装pyinstaller库。2)使用pyinstaller打包程序。

pip install pyinstaller
 
pyinstaller -F Server.py

其中-F选项是指打包成一个exe文件,如果不加这个选项会打包成一个附加有很多dll文件的文件夹。然后会发现当前目录下多出了build文件夹和dist目录,dist目录下就有我们需要的可执行文件。


最后贴上客户端的控制流程代码Main.py

from LoginPanel import LoginPanel
from MainPanel import MainPanel
from RegisterPanel import RegisterPanel
from Client import ChatClient
import MD5
from tkinter import messagebox
from threading import Thread
import time
 
 
def send_message():
    print("send message:")
    content = main_frame.get_send_text()
    if content == "" or content == "\n":
        print("空消息,拒绝发送")
        return
    print(content)
    # 清空输入框
    main_frame.clear_send_text()
    client.send_message(content)
 
 
def close_sk():
    print("尝试断开socket连接")
    client.sk.close()
 
 
def close_main_window():
    close_sk()
    main_frame.main_frame.destroy()
 
 
def close_login_window():
    close_sk()
    login_frame.login_frame.destroy()
 
 
# 关闭注册界面并打开登陆界面
def close_reg_window():
    reg_frame.close()
    global login_frame
    login_frame = LoginPanel(login, register, close_login_window)
    login_frame.show()
 
 
# 关闭登陆界面前往主界面
def goto_main_frame(user):
    login_frame.close()
    global main_frame
    main_frame = MainPanel(user, send_message, close_main_window)
    # 新开一个线程专门负责接收并处理数据
    Thread(target=recv_data).start()
    main_frame.show()
 
 
def login():
    print("点击登录按钮")
    user, key = login_frame.get_input()
    # 密码传md5
    key = MD5.gen_md5(key)
    if user == "" or key == "":
        messagebox.showwarning(title="提示", message="用户名或者密码为空")
        return
    print("user: " + user + ", key: " + key)
    if client.check_user(user, key):
        # 验证成功
        goto_main_frame(user)
    else:
        # 验证失败
        messagebox.showerror(title="错误", message="用户名或者密码错误")
 
 
# 登陆界面前往注册界面
def register():
    print("点击注册按钮")
    login_frame.close()
    global reg_frame
    reg_frame = RegisterPanel(close_reg_window, register_submit, close_reg_window)
    reg_frame.show()
 
 
# 提交注册表单
def register_submit():
    print("开始注册")
    user, key, confirm = reg_frame.get_input()
    if user == "" or key == "" or confirm == "":
        messagebox.showwarning("错误", "请完成注册表单")
        return
    if not key == confirm:
        messagebox.showwarning("错误", "两次密码输入不一致")
        return
    # 发送注册请求
    result = client.register_user(user, MD5.gen_md5(key))
    if result == "0":
        # 注册成功,跳往登陆界面
        messagebox.showinfo("成功", "注册成功")
        close_reg_window()
    elif result == "1":
        # 用户名重复
        messagebox.showerror("错误", "该用户名已被注册")
    elif result == "2":
        # 未知错误
        messagebox.showerror("错误", "发生未知错误")
 
 
# 处理消息接收的线程方法
def recv_data():
    # 暂停几秒,等主界面渲染完毕
    time.sleep(1)
    while True:
        try:
            # 首先获取数据类型
            _type = client.recv_all_string()
            print("recv type: " + _type)
            if _type == "#!onlinelist#!":
                print("获取在线列表数据")
                online_list = list()
                for n in range(client.recv_number()):
                    online_list.append(client.recv_all_string())
                main_frame.refresh_friends(online_list)
                print(online_list)
            elif _type == "#!message#!":
                print("获取新消息")
                user = client.recv_all_string()
                print("user: " + user)
                content = client.recv_all_string()
                print("message: " + content)
                main_frame.recv_message(user, content)
        except Exception as e:
            print("接受服务器消息出错,消息接受子线程结束。" + str(e))
            break
 
 
def start():
    global client
    client = ChatClient()
    global login_frame
    login_frame = LoginPanel(login, register, close_login_window)
    login_frame.show()
 
 
if __name__ == "__main__":
    start()

 

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值