├── __init__.py ├── .idea ├── .gitignore ├── misc.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── modules.xml └── 多人聊天室案例.iml ├── README.md ├── LICENSE ├── 聊天室客户端.py └── 聊天室服务器.py /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/多人聊天室案例.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi-person-chatroom(基于Python网络编程的多人聊天室) 2 | ## 多人聊天室 3 | 1. 可以有多个客户端,每个客户端都有自己名字(唯一) 4 | 2. 客户端可以发送一条消息到聊天室,所有人都可以看到刚刚发送的该条消息 5 | 3. 服务器只有一个,服务单独的主线程启动和管理服务器 6 | 4. 在服务器中如果有一个客户端连接成功之后,开启一个新的线程和当前客户端会话 7 | 5. 客户端和服务器端都有界面 8 | 6. 当服务器启动之后,如果客户端连接,则需要创建对应会话线程 9 | 7. 客户端连接服务器 10 | 8. 服务在收到客户端连接之后,需要在文本框中显示提示信息,同还需要通知所有的客户端 11 | 9. 客户端发送信息到聊天室 12 | 10. 客户端断开连接 13 | ## 开发日志 14 | ### 2023-9-6 15 | 1. 开发客户端的界面:使用wxpython开发UI 16 | 2. 开发了服务器端的界面,初始化一些属性 17 | ### 2023-9-12 18 | 在服务器引入了线程池以维护线程,减少开销 19 | ## 寄语 20 | 本项目本人将不断扩展优化,如有见解请提出您的宝贵意见,感谢您的每一个star来支持新人开发者! 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jack Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /聊天室客户端.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import wx 3 | from socket import * 4 | import threading 5 | 6 | # 客户端继承 wx.Frame,就拥有窗口界面 7 | class MsbClient(wx.Frame): 8 | def __init__(self,c_name): #c_name:客户端名字 9 | #调用父类的初始化函数 10 | wx.Frame.__init__(self,None,id=101,title='%s的客户端界面'%c_name,pos=wx.DefaultPosition,size=(400,470)) 11 | pl = wx.Panel(self) # 在窗口中初始化一个面板 12 | #在面板里面会放一些按钮,文本框,文本输入框等,把这些对象统一放入一个盒子里面 13 | box = wx.BoxSizer(wx.VERTICAL) # 在盒子里面垂直方向自动排版 14 | 15 | g1 = wx.FlexGridSizer(wx.HORIZONTAL) #可升缩的网格布局,水平方向 16 | #创建两个按钮 17 | conn_button = wx.Button(pl,size=(200,40),label= "连接") 18 | dis_conn_button = wx.Button(pl,size=(200,40),label= "离开") 19 | g1.Add(conn_button,1, wx.TOP | wx.LEFT) # 连接按钮布局在左边 20 | g1.Add(dis_conn_button,1, wx.TOP | wx.RIGHT) # 断开按钮布局在右边 21 | box.Add(g1,1,wx.ALIGN_CENTER) # ALIGN_CENTER 联合居中 22 | 23 | 24 | #创建聊天内容的文本框,不能写消息 :TE_MULTILINE -->多行 TE_READONLY-->只读 25 | self.text = wx.TextCtrl(pl,size=(400,250),style =wx.TE_MULTILINE | wx.TE_READONLY) 26 | box.Add(self.text,1,wx.ALIGN_CENTER) 27 | 28 | #创建聊天的输入文本框,可以写 29 | self.input_text = wx.TextCtrl(pl, size=(400, 100), style=wx.TE_MULTILINE ) 30 | box.Add(self.input_text, 1, wx.ALIGN_CENTER) 31 | 32 | #最后创建两个按钮,分别是发送和重置 33 | g2 = wx.FlexGridSizer(wx.HORIZONTAL) 34 | clear_button = wx.Button(pl, size=(200, 40), label="重置") 35 | send_button = wx.Button(pl, size=(200, 40), label="发送") 36 | g2.Add(clear_button,1,wx.TOP | wx.LEFT) 37 | g2.Add(send_button,1,wx.TOP | wx.RIGHT) 38 | box.Add(g2,1,wx.ALIGN_CENTER) 39 | 40 | 41 | pl.SetSizer(box) #把盒子放入面板中 42 | 43 | ''' 以上代码完成了客户端界面(窗口) ''' 44 | 45 | '''给所有按钮绑定点击事件''' 46 | self.Bind(wx.EVT_BUTTON,self.connect_to_server,conn_button) 47 | self.Bind(wx.EVT_BUTTON,self.send_to,send_button) 48 | self.Bind(wx.EVT_BUTTON,self.go_out,dis_conn_button) 49 | self.Bind(wx.EVT_BUTTON,self.reset,clear_button) 50 | 51 | '''客户端的属性''' 52 | self.name =c_name 53 | self.isConnected = False #客户端是否已经连上服务器 54 | self.client_socket = None 55 | 56 | 57 | # 连接服务器 58 | def connect_to_server(self,event): 59 | print("客户端%s,开始连接服务器"%self.name) 60 | if not self.isConnected: 61 | server_host_port = ('localhost',8888) 62 | self.client_socket = socket(AF_INET,SOCK_STREAM) 63 | self.client_socket.connect(server_host_port) 64 | # 之前规定,客户端只要连接成功,马上把自己的名字发给服务器 65 | self.client_socket.send(self.name.encode('UTF-8')) 66 | t = threading.Thread(target=self.receive_data) 67 | t.setDaemon(True) # 客户端UI界面如果关闭,当前守护线程也自动关闭 68 | self.isConnected = True 69 | t.start() 70 | 71 | # 接受服务器发送过来的聊天数据 72 | def receive_data(self): 73 | print("客户端准备接收服务器的数据") 74 | while self.isConnected: 75 | data =self.client_socket.recv(1024).decode('UTF-8') 76 | # 从服务器接收到的数据,需要显示 77 | self.text.AppendText('%s\n'%data) 78 | 79 | 80 | #客户端发送信息到聊天室 81 | def send_to(self,event): 82 | if self.isConnected: 83 | info = self.input_text.GetValue() 84 | if info != '': 85 | self.client_socket.send(info.encode('UTF-8')) 86 | #输入框中的数据如果已经发送了,输入框重新为空 87 | self.input_text.SetValue('') 88 | 89 | # 客户端离开聊天 90 | def go_out(self,event): 91 | self.client_socket.send('A^disconnect^B'.encode('UTF-8')) 92 | # 客户端主线程也关闭 93 | self.isConnected = False 94 | 95 | 96 | # 客户端输入框的信息重置 97 | def reset(self,event): 98 | self.input_text.Clear() 99 | 100 | if __name__ == '__main__': 101 | app = wx.App() 102 | name = input("请输入客户端名字:") 103 | MsbClient(name).Show() 104 | app.MainLoop() #循环刷新显示 105 | 106 | -------------------------------------------------------------------------------- /聊天室服务器.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import wx 3 | from socket import * 4 | import threading 5 | import time 6 | from concurrent.futures import ThreadPoolExecutor 7 | class MsbServer(wx.Frame): 8 | 9 | def __init__(self): 10 | '''创建窗口''' 11 | wx.Frame.__init__(self, None, id=102, title='Patrick9313的服务器界面', pos=wx.DefaultPosition, size=(400, 470)) 12 | pl = wx.Panel(self) # 在窗口中初始化一个面板 13 | # 在面板里面会放一些按钮,文本框,文本输入框等,把这些对象统一放入一个盒子里面 14 | box = wx.BoxSizer(wx.VERTICAL) # 在盒子里面垂直方向自动排版 15 | 16 | g1 = wx.FlexGridSizer(wx.HORIZONTAL) # 可升缩的网格布局,水平方向 17 | # 创建三个按钮 18 | start_server_button = wx.Button(pl, size=(133, 40), label="启动") 19 | record_save_button = wx.Button(pl, size=(133, 40), label="聊天记录保存") 20 | stop_server_button = wx.Button(pl, size=(133, 40), label="停止") 21 | g1.Add(start_server_button, 1, wx.TOP ) 22 | g1.Add(record_save_button, 1, wx.TOP ) 23 | g1.Add(stop_server_button, 1, wx.TOP ) 24 | box.Add(g1, 1, wx.ALIGN_CENTER) # ALIGN_CENTER 联合居中 25 | 26 | # 创建只读的文本框,显示聊天记录 27 | self.text = wx.TextCtrl(pl, size=(400, 400), style=wx.TE_MULTILINE | wx.TE_READONLY) 28 | box.Add(self.text, 1, wx.ALIGN_CENTER) 29 | pl.SetSizer(box) 30 | '''以上代码窗口结束 ''' 31 | # 创建一个拥有10个工作线程的线程池 32 | self.executor = ThreadPoolExecutor(max_workers=10) 33 | 34 | '''服务准备执行的一些属性''' 35 | self.isOn = False # 服务器没有启动 36 | self.host_port= ('',8888) 37 | self.server_socket = socket(AF_INET,SOCK_STREAM) # TCP协议的服务器端套接字 38 | self.server_socket.bind(self.host_port) 39 | self.server_socket.listen(5) 40 | self.session_thread_map={} # 存放所有的服务器会话线程,字典:客户端名字为Key,会话线程为Value 41 | 42 | '''给所有的按钮绑定相应的动作''' 43 | self.Bind(wx.EVT_BUTTON,self.start_server,start_server_button) #给启动按钮,绑定一个按钮事件,事件触发的时候会自动调用一个函数 44 | self.Bind(wx.EVT_BUTTON,self.save_record,record_save_button) 45 | 46 | 47 | #服务器开始启动函数 48 | def start_server(self,event): 49 | print('服务器开始启动') 50 | if not self.isOn: 51 | #启动服务器的主线程 52 | self.isOn =True 53 | main_thread = threading.Thread(target=self.do_work) 54 | main_thread.setDaemon(True) #设置为守护线程 55 | main_thread.start() 56 | 57 | #服务运行之后的函数 58 | def do_work(self): 59 | print("服务器开始工作") 60 | while self.isOn: 61 | session_socket,client_addr = self.server_socket.accept() 62 | #服务首先接受客户端发过来的第一条消息,我们规定第一条消息为客户端的名字 63 | username = session_socket.recv(1024).decode('UTF-8') #接受客户端名字 64 | #创建一个会话线程 65 | session_thread = SessionThread(session_socket,username,self) 66 | self.session_thread_map[username] = session_thread 67 | # 使用线程池处理连接 68 | self.executor.submit(session_thread.run) 69 | # 表示有客户端进入到聊天室 70 | self.show_info_and_send_client("服务器通知","欢迎%s进入聊天室!"%username,time.strftime('%Y-%m-%d %H:%M:%S',time.localtime())) 71 | self.server_socket.close() 72 | 73 | 74 | #在文本中显示聊天信息,同时发送消息给所有客户端 ,source:信息源,data就是信息, 75 | def show_info_and_send_client(self,source,data,data_time): 76 | send_data = '%s : %s\n时间:%s\n' %(source,data,data_time) 77 | self.text.AppendText('---------------------------------\n%s' %send_data) #在服务器的文本框显示信息 78 | for client in self.session_thread_map.values(): 79 | if client.isOn: #当前客户端是活动 80 | client.user_socket.send(send_data.encode('UTF-8')) 81 | 82 | #服务保存聊天记录 83 | def save_record(self,event): 84 | record = self.text.GetValue() 85 | with open("record.log","w+") as f: 86 | f.write(record) 87 | 88 | 89 | # 服务器端会话线程的类 90 | class SessionThread(threading.Thread): 91 | def __init__(self,socket,un,server): 92 | threading.Thread.__init__(self) 93 | self.user_socket=socket 94 | self.username =un 95 | self.server =server 96 | self.isOn = True # 会话线程是否启动 97 | 98 | def run(self): # 会话线程的运行 99 | print('客户端%s,已经和服务器连接成功,服务器启动一个会话线程'%self.username) 100 | while self.isOn: 101 | data = self.user_socket.recv(1024).decode('UTF-8') #接受客户端的聊天信息 102 | if data == 'A^disconnect^B': # 如果客户端点击断开按钮,先发一条消息给服务器:消息的内容我们规定:A^disconnect^B 103 | self.isOn = False 104 | # 有用户离开,需要在聊天室通知其他人 105 | self.server.show_info_and_send_client("服务器通知","%s离开聊天室!"%self.username,time.strftime('%Y-%m-%d %H:%M:%S',time.localtime())) 106 | else: 107 | # 其他聊天信息,我们应该显示给所有客户端,包括服务器 108 | self.server.show_info_and_send_client(self.username,data,time.strftime('%Y-%m-%d %H:%M:%S',time.localtime())) 109 | self.user_socket.close() # 保持和客户端会话的socket关掉 110 | 111 | if __name__ == '__main__': 112 | app = wx.App() 113 | MsbServer().Show() 114 | app.MainLoop() # 循环刷新显示 --------------------------------------------------------------------------------