Python基础教程书籍案例:在线聊天室(虚拟茶话会)【上】
这个练习项目来自《Python基础教程(第2版)》,案例原名为“虚拟茶话会”。
其实,这个项目就是要实现一个简单的在线聊天室。
在完成这个项目之前,我们需要开启Windows系统的Telnet客户端。
在系统的【控制面板】-【程序和功能】的窗口中,点击左侧的【打开或关闭Windows功能】。
在弹出的窗口中,勾选【Telnet客户端】,然后点击确定按钮,等待系统设置完成。
这个Telnet客户端用于模拟用户和我们编写的聊天室服务器进行通信。
这个项目练习分为两个两个阶段。
第一阶段:了解服务器的搭建,实现基本通讯功能;
第二阶段:实现用户登录、发言、查看在线用户、离开聊天室以及向所有登录用户推送系统信息和用户发言等功能。
我们先来完成第一阶段的目标。
在这里,我们需要使用Python的内置模块中的aysncore模块。
使用aysncore模块主要是为了实现多用户的同时连接。
在aysncore模块包含了一个dispatcher类,通过这个类能够创建套接字对象。
除此之外,我们之后会使用到这个类中的一些方法进行事件处理。
所以,在代码中,我们让服务器类继承自dispatcher类。
1、尝试搭建一个支持多用户连接的服务器。
实现这个服务器,代码比较简单,并且在之前的课程中我们也接触过相关内容。
大家通过代码中的注释,基本就能够理解。
示例代码:(服务器)
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
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
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
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
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
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命令连接服务器,这时每个终端都会显示来自服务器的欢迎信息。
当从任意一个终端输入内容并按下回车键发送,所有的终端中都会显示这条内容。
而且,当关闭任何一个命令行终端,其他的终端中都会显示服务器推送的用户离开信息。
到这里,这个练习项目的第一阶段我们就完成了。