基于 Python Socket 的群聊聊天室(带图形界面,包含注册、登录、数据入库功能)

代码下载

https://github.com/juno3550/GroupChatRoom

 

实现框架

Chat 包:

  • server.py:服务器端执行代码(TCP 服务器,根据客户端消息调用 mode 包的注册、登录、聊天功能)
  • client.py:客户端执行代码(连接服务器端,进行注册、登录、聊天)
  • client_draw.py:客户端图形界面绘制

mode 包:

  • chat_mode.py:封装服务器端的聊天逻辑
  • login_mode.py:封装服务器端的登录逻辑
  • register_mode.py:封装服务器端的注册逻辑

db 包:

  • user_info_util.py:基于 mysql_util 查询或新增用户信息
  • mysql_util.py:封装 mysql 基础操作

 

实现效果

示例:执行一个 server.py 与多个 client.py

 

代码实现

Chat 包

server.py

 1 from twisted.internet.protocol import Factory
 2 from twisted.protocols.basic import LineReceiver  # 事件处理器
 3 from twisted.internet import reactor
 4 import json
 5 from mode import chat_mode, login_mode, register_mode
 6 
 7 
 8 # 每一个客户端连接都会对应一个不同的Chat对象
 9 class Chat(LineReceiver):
10 
11     def __init__(self, users):
12         # 存储所有连接用户信息的字典
13         self.users = users
14 
15     # 断开连接时候自动触发,从users字典去掉连接对象
16     def connectionLost(self, reason):
17         if self in self.users.keys():
18             # print("%s断开连接" %self.users[self])
19             del self.users[self]
20 
21     # 对客户端的请求内容做处理,只要收到客户端消息,自动触发此方法
22     def dataReceived(self, data):
23         # 将字节数据解码成字符串
24         data = data.decode('utf-8')
25         data_dict = json.loads(data)
26         # 根据type字段的值,进入对应的逻辑
27         # 登录逻辑
28         if data_dict["type"] == "login":
29             login_mode.login(self, data_dict)
30         # 注册逻辑
31         elif data_dict["type"] == "register":
32             register_mode.register(self, data_dict)
33         # 聊天逻辑
34         elif data_dict["type"] == "chat":
35             chat_mode.chat(self, data_dict)
36 
37 
38 # 处理业务的工厂类,只会实例化一次
39 class ChatFactory(Factory):
40 
41     def __init__(self):
42         # 有多个连接的时候,会有多个chat对象
43         # self.users 在内存地址中,只有一份,所有连接对象都只使用同一个实例变量 self.users(等价于一个全局变量)
44         self.users = {}
45         # key: 连接对象本身;value:登录成功的用户昵称
46 
47     # 一个客户端连接会实例化一个新的Chat对象
48     def buildProtocol(self, addr):
49         print(type(addr), addr)
50         # 返回一个处理具体业务请求的对象,参数传递了字典(存有所有连接对象)
51         return Chat(self.users)
52 
53 
54 if __name__ == '__main__':
55     # 设定监听端口和对象
56     # 使用Tcp协议,实例化ChatFactory
57     reactor.listenTCP(1200, ChatFactory())
58 
59     print ("开始进入监听状态...")
60     reactor.run()  # 开始监听

client.py

  1 import tkinter
  2 from tkinter import messagebox
  3 import json
  4 import time
  5 import threading
  6 import select
  7 from socket import *
  8 import traceback
  9 from chat import client_draw
 10 
 11 
 12 class Client:
 13 
 14     # 配置连接
 15     def connect(self):
 16         # 创建socket
 17         self.s = socket(AF_INET, SOCK_STREAM)
 18         # 服务器端和客户端均在同个机器上运行
 19         remote_host = gethostname()
 20         # 设置端口号
 21         port = 1200
 22         # 发起连接
 23         self.s.connect((remote_host, port))
 24         print("从%s成功连接到%s" % (self.s.getsockname(), self.s.getpeername()))
 25         return self.s
 26 
 27     # 监听(接收)消息
 28     def receive(self, s):
 29         # 需要监控的对象列表
 30         self.my = [s]
 31         while 1:
 32             print("监听中...")
 33 
 34             # 实参:
 35             # 第1个实参 self.my:可读的对象,监听服务器端的响应消息
 36             # 第2个实参:可写的对象(本例不用)
 37             # 第3个实参:出现异常的对象(本例不用)
 38             # 这三个参数内容都是被操作系统监控的,即select.select()会执行系统内核代码
 39             # 1)当有事件发生时,立马往下执行代码;否则阻塞监控10秒
 40             # 2)若监控10秒了仍无事件发生,才往下执行
 41             rl, wl, error = select.select(self.my, [], [], 10)
 42             # 返回值:
 43             # rl:监听某个文件描述符是否发生了读的事件(server给client发了数据)
 44             #       rl列表一开始为空,只有当s发生事件了(如客户端与服务器端建立了连接),才会将s加到rl中
 45             # wl:监听某个文件描述符是否发生了写的事件(如client给server发了数据)
 46             # error:监听某个文件描述符是否发生了异常事件
 47             # 如果发生事件的对象是客户端连接对象,则代表收到服务器端数据
 48             if s in rl:
 49                 try:
 50                     data = s.recv(1024).decode("utf-8")
 51                     data_dict = json.loads(data)
 52                     # 根据服务器端返回的type值,执行不同逻辑
 53                     type = data_dict["type"]
 54                     # 登录逻辑
 55                     if type == "login":
 56                         # 登录成功,跳转聊天页面
 57                         if "000" == data_dict["code"]:
 58                             nickname = data_dict["nickname"]
 59                             self.chat_interface(nickname)
 60                         # 登录失败,获取失败信息
 61                         else:
 62                             messagebox.showinfo(title="登录提示", message=data_dict["msg"])
 63                     # 注册逻辑
 64                     elif type == "register":
 65                         # 注册成功,跳转聊天页面
 66                         if "000" == data_dict["code"]:
 67                             nickname = data_dict["nickname"]
 68                             messagebox.showinfo(title="进入聊天室", message=data_dict["msg"])
 69                             self.chat_interface(nickname)
 70                         # 注册失败
 71                         else:
 72                             messagebox.showinfo(title="注册提示", message=data_dict["msg"])
 73                     # 聊天逻辑
 74                     elif type == "chat":
 75                         message = data_dict["message"]
 76                         nickname = data_dict["nickname"]
 77                         isMy = data_dict["isMy"]
 78                         chat_time = " " + nickname + "\t" + time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()) + "\n"
 79                         # 聊天页面,显示发送人及发送时间
 80                         self.txtMsgList.insert(tkinter.END, chat_time, "DimGray")
 81                         # 如果是自己发的消息,字体使用'DarkTurquoise'
 82                         if "yes" == isMy:
 83                             self.txtMsgList.insert(tkinter.END, " " + message + "\n\n", 'DarkTurquoise')
 84                         # 如果是别人发的消息,字体使用'Black'
 85                         else:
 86                             self.txtMsgList.insert(tkinter.END, " " + message + "\n\n", 'Black')
 87                         # 插入消息时,自动滚动到底部
 88                         self.txtMsgList.see(tkinter.END)
 89                 except (ConnectionAbortedError, ConnectionResetError):
 90                     # 将连接对象从监听列表去掉
 91                     self.my.remove(s)
 92                     print("客户端发生连接异常,与服务器端断开连接")
 93                     traceback.print_exc()
 94                     s.close()
 95                 except Exception as e:
 96                     print("客户端发生了其它异常: ")
 97                     traceback.print_exc()
 98                     s.close()
 99 
100     # 进入注册页面
101     def register_interface(self):
102         client_draw.draw_register(self)
103 
104     # 进入聊天页面
105     def chat_interface(self, nickname):
106         client_draw.draw_chat(self, nickname)
107 
108     # 返回登录页面
109     def return_login_interface(self):
110         # 将不需要的控件先销毁
111         self.label_nickname.destroy()
112         self.input_nickname.destroy()
113         self.label_password.destroy()
114         self.input_password.destroy()
115         client_draw.draw_login(self)
116 
117     # 获取输入框内容,进行注册验证
118     def verify_register(self):
119         username = self.input_account.get()
120         password = self.input_password.get()
121         nickname = self.input_nickname.get()
122         try:
123             register_data = {}
124             register_data["type"] = "register"
125             register_data["username"] = username
126             register_data["password"] = password
127             register_data["nickname"] = nickname
128             # 将dict类型转为json字符串,便于网络传输
129             data = json.dumps(register_data)
130             self.s.send(data.encode("utf-8"))
131         except:
132             traceback.print_exc()
133 
134     # 获取输入框内容,进行登录校验
135     def verify_login(self):
136         account = self.input_account.get()
137         password = self.input_password.get()
138         try:
139             login_data = {}
140             login_data["type"] = "login"
141             login_data["username"] = account
142             login_data["password"] = password
143             data = json.dumps(login_data)
144             self.s.send(data.encode('utf-8'))
145         except:
146             traceback.print_exc()
147 
148     # 获取输入框内容,发送消息
149     def send_msg(self):
150         message = self.txtMsg.get('0.0', tkinter.END).strip()
151         if not message:
152             messagebox.showinfo(title='发送提示', message="发送内容不能为空,请重新输入")
153             return
154         self.txtMsg.delete('0.0', tkinter.END)
155         try:
156           chat_data = {}
157           chat_data["type"] = "chat"
158           chat_data["message"] = message
159           data = json.dumps(chat_data)
160           self.s.send(data.encode('utf-8'))
161         except:
162           traceback.print_exc()
163 
164     # 发送消息事件
165     def send_msg_event(self, event):
166         # 如果捕捉到键盘的回车按键,触发消息发送
167         if event.keysym == 'Return':
168             self.send_msg()
169 
170     # 聊天页面,点击右上角退出时执行
171     def on_closing(self):
172         if messagebox.askokcancel("退出提示", "是否离开聊天室?"):
173             self.window.destroy()
174 
175 
176 def main():
177     chatRoom = Client()
178     client = chatRoom.connect()
179     t = threading.Thread(target=chatRoom.receive, args=(client,))  # 创建一个线程,监听消息
180     t.start()
181     # 创建主窗口,用于容纳其它组件
182     chatRoom.window = tkinter.Tk()
183     # 登录界面控件创建、布局
184     client_draw.draw_login(chatRoom)
185     # 进入事件(消息)循环
186     tkinter.mainloop()
187 
188 if __name__ == "__main__":
189     main()

client_draw.py

  1 import tkinter
  2 
  3 
  4 # 登录页面
  5 def draw_login(self):
  6     # 设置主窗口标题
  7     self.window.title("聊天室登录界面")
  8     # 设置主窗口大小
  9     self.window.geometry("450x300")
 10     # 创建画布
 11     self.canvas = tkinter.Canvas(self.window, height=200, width=500)
 12     # 创建一个`Label`名为`账 号: `
 13     self.label_account = tkinter.Label(self.window, text='账 号')
 14     # 创建一个`Label`名为`密 码: `
 15     self.label_password = tkinter.Label(self.window, text='密 码')
 16     # 创建一个账号输入框,并设置尺寸
 17     self.input_account = tkinter.Entry(self.window, width=30)
 18     # 创建一个密码输入框,并设置尺寸
 19     self.input_password = tkinter.Entry(self.window, show='*', width=30)
 20     # 登录按钮
 21     self.login_button = tkinter.Button(self.window, command=self.verify_login, text="登 录", width=10)
 22     # 注册按钮
 23     self.register_button = tkinter.Button(self.window, command=self.register_interface, text="注 册", width=10)
 24 
 25     # 登录页面各个控件进行布局
 26     self.label_account.place(x=90, y=70)
 27     self.label_password.place(x=90, y=150)
 28     self.input_account.place(x=135, y=70)
 29     self.input_password.place(x=135, y=150)
 30     self.login_button.place(x=120, y=235)
 31     self.register_button.place(x=250, y=235)
 32 
 33 
 34 # 注册界面
 35 def draw_register(self):
 36     # 登录按钮销毁
 37     self.login_button.destroy()
 38     # 注册按钮销毁
 39     self.register_button.destroy()
 40     self.window.title("聊天室注册界面")
 41     self.window.geometry("450x300")
 42     # 创建画布
 43     self.canvas = tkinter.Canvas(self.window, height=200, width=500)
 44     # 创建一个"Label",名为:"昵 称"
 45     self.label_nickname = tkinter.Label(self.window, text='昵 称')
 46     # 创建一个昵称输入框,并设置尺寸
 47     self.input_nickname = tkinter.Entry(self.window, width=30)
 48     # 创建注册按钮
 49     self.register_submit_button = tkinter.Button(self.window, command=self.verify_register, text="提交注册", width=10)
 50     # 创建注册按钮
 51     self.return_login_button = tkinter.Button(self.window, command=self.return_login_interface, text="返回登录",width=10)
 52 
 53     # 注册界面各个控件进行布局
 54     self.label_account.place(x=90, y=70)
 55     self.label_password.place(x=90, y=130)
 56     self.input_account.place(x=135, y=70)
 57     self.input_password.place(x=135, y=130)
 58     self.label_nickname.place(x=90, y=190)
 59     self.input_nickname.place(x=135, y=190)
 60     self.register_submit_button.place(x=120, y=235)
 61     self.return_login_button.place(x=250, y=235)
 62 
 63 
 64 # 聊天室界面
 65 def draw_chat(self, nickname):
 66     self.window.title("【%s】的聊天室界面" % nickname)
 67     self.window.geometry("520x560")
 68     # 创建frame容器
 69     # 放置聊天记录
 70     self.frmLT = tkinter.Frame(width=500, height=320)
 71     # 放置发送内容输入框
 72     self.frmLC = tkinter.Frame(width=500, height=150)
 73     # 放置发送按钮
 74     self.frmLB = tkinter.Frame(width=500, height=30)
 75 
 76     self.txtMsgList = tkinter.Text(self.frmLT)
 77     # 设置消息时间字体样式
 78     self.txtMsgList.tag_config('DimGray', foreground='#696969', font=("Times", "11"))
 79     # 设置自己的消息字体样式
 80     self.txtMsgList.tag_config('DarkTurquoise', foreground='#00CED1', font=("Message", "13"), spacing2=5)
 81     # 设置其它人的消息字体样式
 82     self.txtMsgList.tag_config('Black', foreground='#000000', font=("Message", "13"), spacing2=5)
 83 
 84     self.txtMsg = tkinter.Text(self.frmLC)
 85     # 触发键盘的回车按键事件,发送消息
 86     self.txtMsg.bind("<KeyPress-Return>", self.send_msg_event)
 87     self.btnSend = tkinter.Button(self.frmLB, text='发送', width=12, command=self.send_msg)
 88     # 创建空的Label在左边占个位置,便于发送按钮靠右
 89     self.labSend = tkinter.Label(self.frmLB, width=55)
 90 
 91     # 窗口布局
 92     self.frmLT.grid(row=0, column=0, columnspan=2, padx=10, pady=10)
 93     self.frmLC.grid(row=1, column=0, columnspan=2, padx=10, pady=10)
 94     self.frmLB.grid(row=2, column=0, columnspan=2, padx=10, pady=10)
 95 
 96     # 固定大小
 97     self.frmLT.grid_propagate(0)
 98     self.frmLC.grid_propagate(0)
 99     self.frmLB.grid_propagate(0)
100 
101     self.labSend.grid(row=0, column=0)
102     # 发送按钮布局
103     self.btnSend.grid(row=0, column=1)
104     self.txtMsgList.grid()
105     self.txtMsg.grid()
106 
107     # WM_DELETE_WINDOW 不能改变,这是捕获命令
108     self.window.protocol('WM_DELETE_WINDOW', self.on_closing)

mode 包

chat_mode.py

 1 import json
 2 
 3 
 4 # 聊天逻辑
 5 def chat(self, data_dict):
 6     """
 7     :param self: 连接对象
 8     :param data_dict: 客户端的请求消息
 9     """
10     message = data_dict["message"].strip()
11     # 遍历所有的连接对象,群发消息
12     for user in self.users.keys():
13         data = {}
14         data["type"] = "chat"
15         # 获取当前发送消息客户端的昵称
16         nickname = self.users[self]
17         data["nickname"] = nickname
18         # "isMy"键默认为no
19         data["isMy"] = "no"
20         # 如果遍历的对象与发消息客户端是同一个,则将isMy字段设为yes, 便于前端用来判断展示不同的字体样式
21         if user == self:
22             data["isMy"] = "yes"
23         data["message"] = message
24         data = json.dumps(data)
25         user.sendLine(data.encode("utf-8"))

login_mode.py

 1 from db.user_info_util import user_util
 2 import json
 3 
 4 
 5 # 登录逻辑
 6 def login(self, data_dict):
 7     """
 8     :param self: 连接对象
 9     :param data_dict: 客户端的请求消息
10     """
11     username = data_dict["username"].strip()
12     password = data_dict["password"].strip()
13     # 服务器端的响应消息
14     data = {}
15     # 账号密码不能为空
16     if username and password:
17         code, msg, nickname = login_check(username, password)
18     elif not username:
19         code = "003"
20         msg = "登录用户名不能为空"
21     elif not password:
22         code = "004"
23         msg = "登录密码不能为空"
24     # 登录成功,将连接对象以及昵称加到users中,便于后续遍历发送消息
25     if code == "000":
26         # 在全局变量users中新增用户信息
27         self.users[self] = nickname
28         data["nickname"] = nickname
29     data["type"] = "login"
30     data["code"] = code
31     data["msg"] = msg
32     data = json.dumps(data)
33     self.sendLine(data.encode("utf-8"))
34 
35 
36 # 登录校验逻辑
37 def login_check(username, password):
38     # 通过用户名到数据库获取用户信息
39     user_info = user_util.user_check(username)
40     # 未查到该用户信息,代表未注册
41     if len(user_info) == 0:
42         data = ("001", "账号【%s】未注册,请先进行注册!" % username, None)
43     # 密码错误
44     elif password != user_info[0][1]:
45         data = ("002", "密码有误,请重新输入!", None)
46     # 正常登录
47     else:
48         # 获取昵称
49         nickname = user_info[0][2]
50         data = ("000", "账号【%s】登录成功!" % username, nickname)
51     return data

register_mode.py

 1 from db.user_info_util import user_util
 2 import json
 3 
 4 
 5 # 注册逻辑
 6 def register(self, data_dict):
 7     """
 8     :param self: 连接对象
 9     :param data_dict: 客户端的请求消息
10     """
11     username = data_dict["username"].strip()
12     password = data_dict["password"].strip()
13     nickname = data_dict["nickname"].strip()
14     # 服务器端的响应消息
15     data = {}
16     # 三者均不为空才能走注册校验
17     if username and password and nickname:
18         code, msg = register_check(username, password, nickname)
19     elif not username:
20         code = "002"
21         msg = "注册账号不能为空"
22     elif not password:
23         code = "003"
24         msg = "注册密码不能为空"
25     elif not nickname:
26         code = "004"
27         msg = "注册昵称不能为空"
28     if code == "000":
29         self.users[self] = nickname
30         data["nickname"] = nickname
31 
32     data["type"] = "register"
33     data["code"] = code
34     data["msg"] = msg
35     data = json.dumps(data)
36     self.sendLine(data.encode("utf-8"))
37 
38 
39 # 注册校验
40 def register_check(username, password, nickname):
41     user_info = user_util.user_check(username)
42     if len(user_info) > 0:
43         data = ("001", "账号【%s】已被注册过" % user_info)
44     else:
45         user_util.user_insert(username, password, nickname)
46         data = ("000", "账号【%s】注册成功,点击'确定'进入聊天页面" % username)
47     return data

db 包

user_info_util.py

 1 from db import mysql_util
 2 
 3 class UserUtil:
 4 
 5     def __init__(self, host, port, db, user, passwd, charset="utf8"):
 6         self.mysql = mysql_util.MysqlTool(host, port, db, user, passwd, charset)
 7 
 8     def user_check(self, username):
 9         check_sql = "SELECT username,password,nickname FROM user WHERE username = '%s'" % username
10         self.mysql.connect()
11         user_info = self.mysql.get_all(check_sql)
12         self.mysql.close()
13         return user_info
14 
15     def user_insert(self, username, passwd, nickname):
16         insert_sql = "INSERT INTO user(username,password,nickname) VALUES('%s','%s','%s')" % (username, passwd, nickname)
17         self.mysql.connect()
18         self.mysql.insert(insert_sql)
19         self.mysql.close()
20 
21 user_util = UserUtil("localhost", 3306, "test", "root", "admin")
22 
23 if __name__ == "__main__":
24     print(user_util.user_check("username_test"))
25     user_util.user_insert("username_test2", "pwd", "nickname")
26     print(user_util.user_check("username_test2"))

mysql_util.py

 1 import pymysql
 2 
 3 
 4 class MysqlTool:
 5 
 6     def __init__(self, host, port, db, user, passwd, charset="utf8"):
 7         self.host = host
 8         self.port = port
 9         self.db = db
10         self.user = user
11         self.passwd = passwd
12         self.charset = charset
13 
14     # 创建数据库连接与执行对象
15     def connect(self):
16         try:
17             self.conn = pymysql.connect(host=self.host, port=self.port,
18                                         db=self.db, user=self.user, passwd=self.passwd, charset=self.charset)
19             self.cursor = self.conn.cursor()
20         except Exception as e:
21             print(e)
22 
23     # 关闭数据库连接与执行对象
24     def close(self):
25         try:
26             self.cursor.close()
27             self.conn.close()
28         except Exception as e:
29             print(e)
30 
31     # 获取一行数据
32     def get_one(self, sql):
33         try:
34             self.cursor.execute(sql)
35             result = self.cursor.fetchone()
36         except Exception as e:
37             print(e)
38         else:
39             return result
40 
41     # 获取全部行的数据
42     def get_all(self, sql):
43         try:
44             self.cursor.execute(sql)
45             result = self.cursor.fetchall()
46         except Exception as e:
47             print(e)
48         else:
49             return result
50 
51     # 增删改查的私有方法
52     def __edit(self, sql):
53         try:
54             execute_count = self.cursor.execute(sql)
55             self.conn.commit()
56         except Exception as e:
57             print(e)
58         else:
59             return execute_count
60 
61     # 插入数据
62     def insert(self, sql):
63         return self.__edit(sql)
64 
65     # 删除数据
66     def delete(self, sql):
67         return self.__edit(sql)
68 
69 
70 if __name__ == "__main__":
71     mysql = MysqlTool("localhost", 3306, "test", "root", "admin")
72     mysql.connect()
73     mysql.insert('insert into user(username, password, nickname) values("username_test1", "pwd_test1", "nick_test1");')
74     print(mysql.get_all("select * from user;"))
75     mysql.delete('delete from user where username="%s"' % "username_test1")
76     print(mysql.get_all("select * from user;"))

 

 

 

posted @ 2021-01-26 20:14  Juno3550  阅读(1677)  评论(3编辑  收藏  举报