基于Python的聊天室
基于Python的聊天室
文章目录
本项目是一个在线的聊天室项目,主要运用了python的tkinter图形库、threading多线程库,以及soket库实现网络聊天室,主要实现了群聊和私聊功能,还有发送表情包功能。项目分为服务器端和客户端,采用Tcp协议进行网络数据传输。服务端主要用于存放用户连接信息包括用户的ip地址和端口和用户信息,客户端发送消息时经过服务器转发給其他用户。
一、引言
1.1 背景和意义
随着互联网时代的到来,人与人之间的联系更加紧密。在现实生活中需要一个契机才有机会进行深入的沟通,人们对彼此的了解仅仅是浅层的,而在线的聊天则可以让我们抛开一切外在的东西,用语言去了解一个人的内在。
1.2 系统要实现的功能
1.2.1 用户登录
用户使用默认的服务器端口地址,输入昵称,接着点击‘登录’按钮,实现登录聊天系统。
1.2.2 群发消息
用户点击‘用户列表’,然后点击‘群发’,输入信息后点击‘发送’按钮实现群发消息。
1.2.3 一对一聊天
用户点击‘用户列表’,然后点击发送的对象,此时聊天窗口的标题变为当前用户指向目标用户,此时输入信息后点击‘发送’按钮实现一对一聊天。
1.2.4 发送表情
用户点击‘表情’按钮,此时点击需要发送的表情即可发送
二、系统结构
2.1 系统结构图
本项目分为服务器端和客户端,采用Tcp协议进行网络数据传输。服务端主要用于存放用户连接信息包括用户的ip地址和端口和用户信息,如下图所示
2.2 系统实现原理
系统采用C/S模式进行实现
2.2.1服务器端实现原理
登录和接收数据实现原理 :采用tcp协议,对8888端口进行监听,每当客户端的连接请求到来时,为其创建一个新的线程,判断该连接的用户是否在服务器的在线用户数组中,若不存在,则将用户的信息和用户的连接封装为一个在线用户存入用户数组中;若存在用户,则此次连接必然为发送数据的连接,因而服务器对客户端发送过来的数据进行解析,解析出其中的消息和用户
发送数据实现原理:服务端启动时,创建一个新的线程,用于监听消息队列中的数据是否为空,如果不为空,则调用发送数据的函数,以此往复。
2.2.2 客户端实现原理
发送数据实现原理:客户端数据以(消息内容:;当前用户:;目标用户)
的格式发送给服务器端,由服务器将消息转发给目标用户。
接收数据实现原理:客户端接收到数据以后,对数据进行解析,判断其是否为表情消息,若是则在存有表情的字典中找到其对应的图片,将其显示到聊天信息中;若不是表情,则直接将消息显示到聊天信息中。
2.3 系统技术分析
2.3.1 tkinter 图形用户界面库
Tkinter: Tkinter 模块(Tk 接口)是 Python 的标准 Tk GUI 工具包的接口 .Tk 和Tkinter 可以在大多数的 Unix 平台下使用,同样可以应用在 Windows 和 Macintosh系统里。Tk8.0 的后续版本可以实现本地窗口风格,并良好地运行在绝大多数平台中。
2.3.2 threading 多线程
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
多线程详细介绍
2.3.3 socket 网络编程
Python 提供了两个级别访问的网络服务。
低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets
API,可以访问底层操作系统Socket接口的全部方法。
高级别的网络服务模块 SocketServer,它提供了服务器中心类,可以简化网络服务器的开发。
Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求,使主机间或者一台计算机上的进程间可以通讯。
Python 中,我们用 socket()函数来创建套接字,语法格式如下:
socket.socket([family[, type[, proto]]])
参数:
family
: 套接字家族可以使AF_UNIX
或者AF_INET
type
: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM
或SOCK_DGRAM
protocol
: 一般不填默认为0.
三、实现代码
3.1服务器端代码
3.1.1确定全局变量
IP = ''
PORT = 8888
que = queue.Queue() # 用于存放客户端发送的信息的队列
users = [] # 用于存放在线用户的信息 [conn, user, addr]
lock = threading.Lock() # 创建锁, 防止多个线程写入数据的顺序打乱
3.1.2实现多线程接收数据
class ChatServer(threading.Thread):
def __init__(self, port):
threading.Thread.__init__(self)
self.ADDR = ('', port)
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 用于接收所有客户端发送信息的函数
def tcp_connect(self, conn, addr):
pass;
# 每当接收到一个socket 连接就为其创建并启动一个新的线程
def run(self):
self.s.bind(self.ADDR)
self.s.listen(5)
print('服务器正在运行中...')
q = threading.Thread(target=self.sendData)
q.start()
while True:
conn, addr = self.s.accept()
t = threading.Thread(target=self.tcp_connect, args=(conn, addr))
t.start()
self.s.close()
3.1.3处理接收到的数据
# 将接收到的信息(ip,端口以及发送的信息)存入que队列
def recv(self, data, addr):
lock.acquire()
try:
que.put((addr, data))
finally:
lock.release()
# 用于接收所有客户端发送信息,并将数据保存到消息队列中
def tcp_connect(self, conn, addr):
# 连接后将用户信息添加到users列表
user = conn.recv(1024) # 接收用户名
user = user.decode()
for i in range(len(users)):
if user == users[i][1]:
print('User already exist')
user = '' + user + '_2'
if user == 'no':
user = addr[0] + ':' + str(addr[1])
users.append((conn, user, addr))
print(' 新的连接:', addr, ':', user, end='') # 打印用户名
d = onlines() # 有新连接则刷新客户端的在线用户显示
self.recv(d, addr)
try:
while True:
data = conn.recv(1024)
data = data.decode()
self.recv(data, addr) # 保存信息到队列
conn.close()
except:
print(user + ' 断开连接')
self.delUsers(conn, addr) # 将断开用户移出users
conn.close()
3.1.4将在线用户存入online列表并返回
def onlines():
online = []
for i in range(len(users)):
online.append(users[i][1])
return online
3.1.5删除用户
# 判断断开用户在users中是第几位并移出列表, 刷新客户端的在线用户显示
def delUsers(self, conn, addr):
a = 0
for i in users:
if i[0] == conn:
users.pop(a)
print(' 在线用户: ', end='') # 打印剩余在线用户(conn)
d = onlines()
self.recv(d, addr)
print(d)
break
a += 1
3.1.6发送消息
# 将队列que中的消息发送给所有连接到的用户
def sendData(self):
while True:
if not que.empty():
data = ''
reply_text = ''
message = que.get() # 取出队列第一个元素
if isinstance(message[1], str): # 如果data是str则返回Ture
for i in range(len(users)):
# user[i][1]是用户名, users[i][2]是addr, 将message[0]改为用户名
for j in range(len(users)):
if message[0] == users[j][2]:
print(' this: message is from user[{}]'.format(j))
data = ' ' + users[j][1] + ':' + message[1]
break
users[i][0].send(data.encode())
# data = data.split(':;')[0]
if isinstance(message[1], list): # 同上
# 如果是list则打包后直接发送
data = json.dumps(message[1])
for i in range(len(users)):
try:
users[i][0].send(data.encode())
except:
pass
3.1.7 主函数
#监听服务器线程是否处于运行状态
if __name__ == '__main__':
cserver = ChatServer(PORT)
cserver.start()
while True:
time.sleep(1)
if not cserver.isAlive():
print("Chat connection lost...")
sys.exit(0)
3.2 客户端代码
3.2.1确定全局变量
IP = ''
PORT = ''
user = ''
listbox1 = '' # 用于显示在线用户的列表框
ii = 0 # 用于判断是开还是关闭列表框
users = [] # 在线用户列表
chat = '【群发】' # 聊天对象, 默认为群聊
3.2.2登录窗口实现
# 登陆窗口
loginRoot = tkinter.Tk()
loginRoot.title('聊天室')
loginRoot['height'] = 110
loginRoot['width'] = 270
loginRoot.resizable(0, 0) # 限制窗口大小
IP1 = tkinter.StringVar()
IP1.set('127.0.0.1:8888') # 默认显示的ip和端口
User = tkinter.StringVar()
User.set('')
# 服务器标签
labelIP = tkinter.Label(loginRoot, text='地址:端口')
labelIP.place(x=20, y=10, width=100, height=20)
entryIP = tkinter.Entry(loginRoot, width=80, textvariable=IP1)
entryIP.place(x=120, y=10, width=130, height=20)
# 用户名标签
labelUser = tkinter.Label(loginRoot, text='昵称')
labelUser.place(x=30, y=40, width=80, height=20)
entryUser = tkinter.Entry(loginRoot, width=80, textvariable=User)
entryUser.place(x=120, y=40, width=130, height=20)
# 登录按钮
def login(*args):
global IP, PORT, user
IP, PORT = entryIP.get().split(':') # 获取IP和端口号
PORT = int(PORT) # 端口号需要为int类型
user = entryUser.get()
if not user:
tkinter.messagebox.showerror('温馨提示', message='请输入任意的用户名!')
else:
loginRoot.destroy() # 关闭窗口
loginRoot.bind('<Return>', login) # 回车绑定登录功能
but = tkinter.Button(loginRoot, text='登录', command=login)
but.place(x=100, y=70, width=70, height=30)
loginRoot.mainloop()
3.2.3 与服务器建立连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
if user:
s.send(user.encode()) # 发送用户名
else:
s.send('no'.encode()) # 没有输入用户名则标记no
3.2.4 创建聊天窗口
# 聊天窗口
# 创建图形界面
root = tkinter.Tk()
root.title(user) # 窗口命名为用户名
root['height'] = 400
root['width'] = 580
root.resizable(0, 0) # 限制窗口大小
# 创建多行文本框
listbox = ScrolledText(root)
listbox.place(x=5, y=0, width=570, height=320)
# 文本框使用的字体颜色
listbox.tag_config('red', foreground='red')
listbox.tag_config('blue', foreground='blue')
listbox.tag_config('green', foreground='green')
listbox.tag_config('pink', foreground='pink')
listbox.insert(tkinter.END, '欢迎加入聊天室 !', 'blue')
# 创建多行文本框, 显示在线用户
listbox1 = tkinter.Listbox(root)
listbox1.place(x=445, y=0, width=130, height=320)
def showUsers():
global listbox1, ii
if ii == 1:
listbox1.place(x=445, y=0, width=130, height=320)
ii = 0
else:
listbox1.place_forget() # 隐藏控件
ii = 1
# 查看在线用户按钮
button1 = tkinter.Button(root, text='用户列表', command=showUsers)
button1.place(x=485, y=320, width=90, height=30)
# 创建输入文本框和关联变量
a = tkinter.StringVar()
a.set('')
entry = tkinter.Entry(root, width=120, textvariable=a)
entry.place(x=5, y=350, width=570, height=40)
3.2.5 发送数据
def send(*args):
# 没有添加的话发送信息时会提示没有聊天对象
users.append('【群发】')
print(chat)
if chat not in users:
tkinter.messagebox.showerror('温馨提示', message='没有聊天对象!')
return
if chat == user:
tkinter.messagebox.showerror('温馨提示', message='自己不能和自己进行对话!')
return
mes = entry.get() + ':;' + user + ':;' + chat # 添加聊天对象标记
s.send(mes.encode())
a.set('') # 发送后清空文本框
# 创建发送按钮
button = tkinter.Button(root, text='发送', command=send)
button.place(x=515, y=353, width=60, height=30)
root.bind('<Return>', send) # 绑定回车发送信息
3.2.6 实现私聊
# 私聊功能
def private(*args):
global chat
# 获取点击的索引然后得到内容(用户名)
indexs = listbox1.curselection()
index = indexs[0]
if index > 0:
chat = listbox1.get(index)
# 修改客户端名称
if chat == '【群发】':
root.title(user)
return
ti = user + ' --> ' + chat
root.title(ti)
# 在显示用户列表框上设置绑定事件
listbox1.bind('<ButtonRelease-1>', private)
3.2.7 接收数据并打印
# 用于时刻接收服务端发送的信息并打印
def recv():
global users
while True:
data = s.recv(1024)
data = data.decode()
# 没有捕获到异常则表示接收到的是在线用户列表
try:
data = json.loads(data)
users = data
listbox1.delete(0, tkinter.END) # 清空列表框
number = (' 在线用户数: ' + str(len(data)))
listbox1.insert(tkinter.END, number)
listbox1.itemconfig(tkinter.END, fg='green', bg="#f0f0ff")
listbox1.insert(tkinter.END, '【群发】')
listbox1.itemconfig(tkinter.END, fg='green')
for i in range(len(data)):
listbox1.insert(tkinter.END, (data[i]))
listbox1.itemconfig(tkinter.END, fg='green')
except:
data = data.split(':;')
data1 = data[0].strip() # 消息
data2 = data[1] # 发送信息的用户名
data3 = data[2] # 聊天对象
markk = data1.split(':')[1]
# 判断是不是图片
pic = markk.split('#')
# 判断是不是表情
# 如果字典里有则贴图
if (markk in dic) or pic[0] == '``':
data4 = '\n' + data2 + ':' # 例:名字-> \n名字:
if data3 == '【群发】':
if data2 == user: # 如果是自己则将则字体变为蓝色
listbox.insert(tkinter.END, data4, 'blue')
else:
listbox.insert(tkinter.END, data4, 'green') # END将信息加在最后一行
elif data2 == user or data3 == user: # 显示私聊
listbox.insert(tkinter.END, data4, 'red') # END将信息加在最后一行
listbox.image_create(tkinter.END, image=dic[markk])
else:
data1 = '\n' + data1
if data3 == '【群发】':
if data2 == user: # 如果是自己则将则字体变为蓝色
listbox.insert(tkinter.END, data1, 'blue')
else:
listbox.insert(tkinter.END, data1, 'green') # END将信息加在最后一行
if len(data) == 4:
listbox.insert(tkinter.END, '\n' + data[3], 'pink')
elif data2 == user or data3 == user: # 显示私聊
listbox.insert(tkinter.END, data1, 'red') # END将信息加在最后一行
listbox.see(tkinter.END) # 显示在最后
3.2.8 主函数
r = threading.Thread(target=recv)
r.start() # 开始线程接收信息
root.mainloop()
s.close() # 关闭图形界面后关闭TCP连接
四、实验结果
4.1 用户登录
用户使用默认的服务器端口地址,输入昵称,接着点击‘登录’按钮,即可登录聊天系统,以张三为例,如图4.1.1所示:
4.2 群发消息
用户点击‘用户列表’,然后点击‘群发’,输入信息后点击‘发送’按钮即可群发消息,以张三群发消息为例,如图4.2.1所示
4.3 一对一聊天
用户点击‘用户列表’,然后点击发送的对象,此时聊天窗口的标题变为,当前用户指向目标用户,如图4.3.1所示,此时输入信息后点击‘发送’按钮即可一对一聊天,以张三群发消息给李四为例,如图4.3.2所示
4.4 发送表情
用户点击‘表情’按钮,如图4.4.1所示,此时点击表情即可发送,以张三群发送表情给李四为例,如图4.4.2所示
五、总结和展望
5.1项目总结
本项目是一个在线的聊天室项目,主要运用了python的tkinter图形库、threading多线程库,以及soket库实现网络聊天室,主要实现了群聊和私聊功能,还有发送表情包功能。项目分为服务器端和客户端,采用Tcp协议进行网络数据传输。服务端主要用于存放用户连接信息包括用户的ip地址和端口和用户信息,客户端发送消息时经过服务器转发给其他用户。
5.2 项目展望
本项目实现了基本的聊天功能,后期将会加入的功能有,语音聊天、视屏聊天、文件共享等
参考文献:
[1] 菜鸟教程 python3 教程 https://www.runoob.com/python3/python3-tutorial.html
本文地址:https://blog.csdn.net/qq_38951154/article/details/107270408