这个练习项目来自《Python基础教程(第2版)》,案例原名为“虚拟茶话会”。
其实,这个项目就是要实现一个简单的在线聊天室。
在完成这个项目之前,我们需要开启Windows系统的Telnet客户端。
在系统的【控制面板】-【程序和功能】的窗口中,点击左侧的【打开或关闭Windows功能】。
在弹出的窗口中,勾选【Telnet客户端】,然后点击确定按钮,等待系统设置完成。
这个Telnet客户端用于模拟用户和我们编写的聊天室服务器进行通信。
这个项目练习分为两个两个阶段。
第一阶段:了解服务器的搭建,实现基本通讯功能;
第二阶段:实现用户登录、发言、查看在线用户、离开聊天室以及向所有登录用户推送系统信息和用户发言等功能。
我们先来完成第一阶段的目标。
在这里,我们需要使用Python的内置模块中的aysncore模块。
使用aysncore模块主要是为了实现多用户的同时连接。
在aysncore模块包含了一个dispatcher类,通过这个类能够创建套接字对象。
除此之外,我们之后会使用到这个类中的一些方法进行事件处理。
所以,在代码中,我们让服务器类继承自dispatcher类。
1、尝试搭建一个支持多用户连接的服务器。
实现这个服务器,代码比较简单,并且在之前的课程中我们也接触过相关内容。
大家通过代码中的注释,基本就能够理解。
示例代码:(服务器)
import asyncore from asyncore import dispatcher class 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 socket server = socket.socket() host = socket.gethostname() port = 6666 server.connect((host, port)) print(server.recv(1024).decode())
上方的客户端代码运行之后,获取到欢迎信息就自动退出了。
如果是通过Windows系统中的命令行终端启动服务器的话,可以通过快捷键<Ctrl+C>进行关闭(按完之后可能要等一小会儿),这时except语句会捕获KeyboardInterrupt异常,在命令行窗口中显示输出文字信息“服务器已关闭!”。
2、再次实现聊天服务器,添加处理连接会话的功能。
这一次服务器的实现,我们需要使用asynchat模块。
asynchat模块完成了大部分对套接字的读写操作,我们接下来只需要重写模块中的collect_incoming_data()方法和found_terminator()方法。
大家先来看再次实现的服务器代码。
示例代码:(服务器)
import asyncore from asyncore import dispatcher from asynchat import async_chat class 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 = 6666 server = ChatServer(port) try: asyncore.loop() except KeyboardInterrupt: print('服务器已被关闭!')
在上方代码中,当服务器运行后,每一个来自客户端的连接,都会被作为ChatSession类的参数,实例化为一个会话对象。
在会话对象中,来自客户端的数据内容通过collect_incoming_data()方法进行读取、处理和暂存,并通过found_terminator()方法检测数据中是否包含设置的指定终止符号,当发现终止符号时,对所有的暂存数据进行处理(例如,将发言内容推送给聊天室中所有的在线用户)。
注意,代码中的push()方法能够像单个客户端发送数据内容,发送数据内容时注意进行编码,编码格式为“GBK”,因为不进行编码的话,当发送的内容包含中文时,通过telnet连接所收到的内容会变成乱码。
这里,为了便于测试,我们将客户端也进行更新。
同样要注意,接收内容时要进行解码,编码的格式和push()方法中的格式保持一致。
示例代码:(客户端)
import socket server = socket.socket() host = socket.gethostname() port = 6666 server.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_chat from asyncore import dispatcher import asyncore class 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命令连接服务器,这时每个终端都会显示来自服务器的欢迎信息。
当从任意一个终端输入内容并按下回车键发送,所有的终端中都会显示这条内容。
而且,当关闭任何一个命令行终端,其他的终端中都会显示服务器推送的用户离开信息。
到这里,这个练习项目的第一阶段我们就完成了。
本节练习源代码:【点此下载】
转载请注明:魔力Python » 练习项目09:在线聊天室(上)