python 实现简易多人聊天室

原理

实现一个多人聊天室需要两个部分, client 端 和server 端

client 端只和server端进行通信, 不同的client之间是不会直接通信的

实现需要两个核心的库, socket 和 threading

socket 用来实现网络中的多个节点并进行通信

threading 模块用于支持多线程(python有全局解释锁, 所以是伪多线程)

Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
class ChatServer:
    def __init__(self, host, port) -> None:
        self.host = host
        self.port = port
        self.server_socket = None
        # 客户端列表
        self.client_sockets = []
        self.client_usernames = []
        self.lock = threading.Lock()

    def start(self):
        # family=AF_INET - IPv4地址
        # type=SOCK_STREAM - TCP套接字
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # 2.绑定IP地址和端口(端口用于区分不同的服务)
        self.server_socket.bind((self.host, self.port))

        # 3.开启监听 - 监听客户端连接到服务器
        # 参数512可以理解为连接队列的大小
        self.server_socket.listen(5)
        print(f"Server started on {self.host}:{self.port}")

        # 持续监听输入
        while True:
            # 4.通过循环接收客户端的连接并作出相应的处理(提供服务)
            # accept方法是一个阻塞方法如果没有客户端连接到服务器代码不会向下执行
            # client_socket 是客户端对象
            # address 是连接到服务器的客户端的地址(由IP和端口两部分构成)

            # 首先判断是否已经被关闭
            if getattr(self.server_socket, '_closed', True):
                break
            try:
                client_socket, address = self.server_socket.accept()
            except OSError:
                print("close the chat room")
                break
            self.client_sockets.append(client_socket)
            threading.Thread(target=self.handle_client, args=(client_socket,)).start()

    def handle_client(self, client_socket):
        """
        处理客户端发起会话
        """
        # 用户加入
        # recv(bufsize) 接收TCP数据, 返回bytes, 需要decode, bufsize 表示一次接受的最大数据量, 为2的N次方
        username = client_socket.recv(1024).decode()
        self.client_usernames.append(username)
        self.broadcast(f"{username} has joined the chat.")

        while True:
            try:
                message = client_socket.recv(1024).decode()
                if message:
                    self.broadcast(f"{username}: {message}")
                else:
                    self.remove_client(client_socket)
                    break
            except Exception as e:
                print(e)
                self.remove_client(client_socket)
                break

    def remove_client(self, client_socket):
        """
        退出群聊
        """
        index = self.client_sockets.index(client_socket)
        username = self.client_usernames[index]
        self.client_sockets.remove(client_socket)
        self.client_usernames.remove(username)
        client_socket.close()
        self.broadcast(f"{username} has left the chat.")
        # 当聊天室所有成员退出聊天室之后, 关闭聊天室
        if len(self.client_sockets) == 0:
            print("All users have exited the chat room.")
            self.stop()


    def broadcast(self, message):
        """
        广播消息, 聊天室内所有人都可以看到消息
        """
        with self.lock:
            for client_socket in self.client_sockets:
                client_socket.send(message.encode())

    def stop(self):
        """
        终止server
        """
        self.server_socket.close()

Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ChatClient:
    def __init__(self, host, port):
        # 客户端核心内容
        self.host = host
        self.port = port
        self.client_socket = None
        self.username = None

        # 以下为GUI部分(非必须, 只通过命令行也可以实现聊天部分)
        # 主窗体
        self.root = tk.Tk()
        self.root.title("Chat Room")
        self.root.protocol('WM_DELETE_WINDOW', self.disconnect)
        # 消息面板
        self.message_frame = tk.Frame(self.root)
        self.scrollbar = tk.Scrollbar(self.message_frame)
        # 展示聊天消息列表
        self.message_list = tk.Listbox(self.message_frame, height=20, width=50, yscrollcommand=self.scrollbar.set)
        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.message_list.pack(side=tk.LEFT, fill=tk.BOTH)
        self.message_frame.pack()

        # 输入面板
        self.input_frame = tk.Frame(self.root)
        # 输入框用于输入消息
        self.message_entry = tk.Entry(self.input_frame, width=50)
        self.message_entry.pack(side=tk.LEFT)
        # 发送消息按钮
        self.send_button = tk.Button(self.input_frame, text="Send", command=self.send_message)
        self.send_button.pack(side=tk.LEFT)

        self.input_frame.pack()


    def connect(self):
        self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client_socket.connect((self.host, self.port))
        self.username = self.prompt_username()
        self.client_socket.send(self.username.encode())
        threading.Thread(target=self.receive_messages).start()
        self.root.mainloop()

    def prompt_username(self):
        while True:
            # 对话框要求输入用户名
            username = simpledialog.askstring('input', 'input your username')
            if username:
                return username

    def send_message(self):
        message = self.message_entry.get()
        self.client_socket.send(message.encode())
        # 发送消息后, 清空消息输入框
        self.message_entry.delete(0, tk.END)

    def receive_messages(self):
        while True:
            try:
                message = self.client_socket.recv(1024).decode()
                self.message_list.insert(tk.END, message)
                self.message_list.see(tk.END)
            except Exception as e:
                print(e)
                break

    def disconnect(self):
        self.client_socket.close()
        self.root.destroy()