Python3练习项目09:在线聊天室(上)

这个练习项目来自《Python基础教程(第2版)》,案例原名为“虚拟茶话会”。

其实,这个项目就是要实现一个简单的在线聊天室。

在完成这个项目之前,我们需要开启Windows系统的Telnet客户端。

在系统的【控制面板】-【程序和功能】的窗口中,点击左侧的【打开或关闭Windows功能】。

在弹出的窗口中,勾选【Telnet客户端】,然后点击确定按钮,等待系统设置完成。

这个Telnet客户端用于模拟用户和我们编写的聊天室服务器进行通信。

这个项目练习分为两个两个阶段。

第一阶段:了解服务器的搭建,实现基本通讯功能;

第二阶段:实现用户登录、发言、查看在线用户、离开聊天室以及向所有登录用户推送系统信息和用户发言等功能。

我们先来完成第一阶段的目标。

在这里,我们需要使用Python的内置模块中的aysncore模块。

使用aysncore模块主要是为了实现多用户的同时连接。

在aysncore模块包含了一个dispatcher类,通过这个类能够创建套接字对象。

除此之外,我们之后会使用到这个类中的一些方法进行事件处理。

所以,在代码中,我们让服务器类继承自dispatcher类。

1、尝试搭建一个支持多用户连接的服务器。

实现这个服务器,代码比较简单,并且在之前的课程中我们也接触过相关内容。

大家通过代码中的注释,基本就能够理解。

示例代码:(服务器)

import asyncorefrom asyncore import dispatcherclass ChatServer(dispatcher): # 定义聊天服务器类 def __init__(self, port): # 重写构造方法 dispatcher.__init__(self) # 重载超类的构造方法 self.create_socket() # 创建套接字对象 self.set_reuse_addr() # 设置地址可重用 self.bind(('', port)) # 绑定本机地址与端口 self.listen(5) # 设置监听连接数 def handle_accept(self): # 重写处理客户端连接的方法 ssl, addr = self.accept() # 获取服务器端的SSL通道和远程客户端的地址 ssl.send('您已成功连接服务器!'.encode()) # 发送欢迎信息 print('连接来自:', addr[0], '端口:', addr[1]) # 显示输出连接的客户端信息port = 6666 # 设置服务器端口号server = ChatServer(port) # 实例化聊天服务器try: asyncore.loop() # 运行异步循环except KeyboardInterrupt: # 捕获键盘中断异常 print('服务器已被关闭!')

注意:代码中的set_reuse_addr()方法能够保证服务器未正常关闭时,再次开启服务器能够重用端口号。因为服务器异常关闭时,可能导致端口依然被占用,新启动的服务器无法使用该端口。

运行上方代码,开启服务器。

这个时候,我们就可以通过telnet命令和服务器进行连接。

telnet命令为:telnet 127.0.0.1 6666或者telnet localhost 6666

当然,我们也可以编写一个客户端进行连接。

示例代码:(客户端)

import socketserver = socket.socket()host = socket.gethostname()port = 6666server.connect((host, port))print(server.recv(1024).decode())

上方的客户端代码运行之后,获取到欢迎信息就自动退出了。

如果是通过Windows系统中的命令行终端启动服务器的话,可以通过快捷键进行关闭(按完之后可能要等一小会儿),这时except语句会捕获KeyboardInterrupt异常,在命令行窗口中显示输出文字信息“服务器已关闭!”。

2、再次实现聊天服务器,添加处理连接会话的功能。

这一次服务器的实现,我们需要使用asynchat模块。

asynchat模块完成了大部分对套接字的读写操作,我们接下来只需要重写模块中的collect_incoming_data()方法和found_terminator()方法。

大家先来看再次实现的服务器代码。

示例代码:(服务器)

import asyncorefrom asyncore import dispatcherfrom asynchat import async_chatclass ChatSession(async_chat): def __init__(self, sock): async_chat.__init__(self, sock) self.set_terminator('\r\n'.encode()) # 设置数据的终止符号 self.data = [] # 创建数据列表 self.push('欢迎进入聊天室!'.encode('GBK')) # 向单个客户端发送欢迎信息 def collect_incoming_data(self, data): # 重写处理客户端发来数据的方法 self.data.append(data.decode()) # 将客户端发来的数据添加到数据列表 def found_terminator(self): # 重写发现数据中终止符号时的处理方法 line = ''.join(self.data) # 将数据列表中的内容整合为一行 self.data = [] # 清空数据列表 print(line) # 显示客户端输出发来的内容class ChatServer(dispatcher): def __init__(self, port): dispatcher.__init__(self) self.create_socket() self.set_reuse_addr() self.bind(('', port)) self.listen(5) self.sessions = [] def handle_accept(self): ssl, addr = self.accept() self.sessions.append(ChatSession(ssl)) # 将新的用户连接会话添加到会话列表port = 6666server = ChatServer(port)try: asyncore.loop()except KeyboardInterrupt: print('服务器已被关闭!')

在上方代码中,当服务器运行后,每一个来自客户端的连接,都会被作为ChatSession类的参数,实例化为一个会话对象。

在会话对象中,来自客户端的数据内容通过collect_incoming_data()方法进行读取、处理和暂存,并通过found_terminator()方法检测数据中是否包含设置的指定终止符号,当发现终止符号时,对所有的暂存数据进行处理(例如,将发言内容推送给聊天室中所有的在线用户)。

注意,代码中的push()方法能够像单个客户端发送数据内容,发送数据内容时注意进行编码,编码格式为“GBK”,因为不进行编码的话,当发送的内容包含中文时,通过telnet连接所收到的内容会变成乱码。

这里,为了便于测试,我们将客户端也进行更新。

同样要注意,接收内容时要进行解码,编码的格式和push()方法中的格式保持一致。

示例代码:(客户端)

import socketserver = socket.socket()host = socket.gethostname()port = 6666server.connect((host, port))print(server.recv(1024).decode('GBK')) # 注意解码以及编码格式while True: name = input('请输入发言内容:') server.send('{}\r\n'.format(name).encode()) # 注意将输入内容加上终止符号

启动服务器,并运行客户端。

此时,客户端会收到来自服务器的欢迎信息。

当在客户端输入内容回车之后,服务端的运行窗口会显示来自客户端的内容。

大家可以启动多个客户端进行测试,每个客户端发出的内容都会显示在服务器的运行窗口中。

3、添加广播以及客户端断开连接的功能

在聊天室中,当一个用户发言时,其他用户都能够看到这条发言。

所以,我们需要在代码中添加广播功能,这也就是我们创建会话列表的原因。

将每一个来自客户端的连接保存为一个会话,添加到会话列表中,当广播内容时,遍历这个会话列表,将广播内容推送到每一个会话的客户端。

当然,当一个用户进入或离开聊天室,也就是打开或关闭会话连接时,我们需要将这个用户的连接会话从会话列表中添加或移除,并向其他会话广播该用户进入或离开的信息。(示例代码中只以离开为例,大家可以自行添加进入的广播代码。)

上面所说的这些功能,大家可以通过示例代码中的注释进行理解。

示例代码:(服务器)

from asynchat import async_chatfrom asyncore import dispatcherimport asyncoreclass ChatSession(async_chat): def __init__(self, server, sock, addr): async_chat.__init__(self, sock) self.server = server self.addr = addr self.set_terminator('\r\n'.encode()) self.data = [] self.push('欢迎进入{}聊天室!\r\n'.format(server.name).encode('GBK')) def collect_incoming_data(self, data): self.data.append(data.decode()) def found_terminator(self): line = ''.join(self.data) self.data = [] self.server.broadcast(line) # 广播当前会话的发言内容到所有会话 def handle_close(self): # 定义客户端断开连接的处理方法 async_chat.handle_close(self) # 重载超类中的方法 self.server.disconnect(self) # 从会话列表中移除当前会话 self.server.broadcast('{}离开聊天室!\r\n'.format(self.addr[0])) # 广播当前会话客户端离开信息class ChatServer(dispatcher): def __init__(self, port, name): dispatcher.__init__(self) self.create_socket() self.bind(('', port)) self.listen(5) self.name = name # 设置服务器名称 self.sessions = [] def disconnect(self, session): # 定义客户端断开连接的方法 self.sessions.remove(session) # 从会话列表移除断开连接的会话 def broadcast(self, line): # 定义广播的方法 for session in self.sessions: # 遍历所有会话 session.push('{}\r\n'.format(line).encode('GBK')) # 向所有会话的客户端推送内容 def handle_accept(self): conn, addr = self.accept() self.sessions.append(ChatSession(self, conn, addr))if __name__ == '__main__': port = 6666 name = 'Python' server = ChatServer(port, name) try: asyncore.loop() except KeyboardInterrupt: print('服务器已关闭!')

运行上方代码启动服务器。

因为需要在客户端显示服务器推送的信息,所以这里我们需要使用Telnet连接服务器。

打开多个命令行终端,每个都通过telnet命令连接服务器,这时每个终端都会显示来自服务器的欢迎信息。

当从任意一个终端输入内容并按下回车键发送,所有的终端中都会显示这条内容。

而且,当关闭任何一个命令行终端,其他的终端中都会显示服务器推送的用户离开信息。

到这里,这个练习项目的第一阶段我们就完成了。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180411G0EG7W00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券