├── __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 |
4 |
5 |
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() # 循环刷新显示
--------------------------------------------------------------------------------