5. Python脚本学习实战笔记五 茶话会
本篇名言:“得得失失平常事;是是非非任自由;恩恩怨怨心不愧;冷冷暖暖我自知;坎坎坷坷人生路;曲曲折折事业梯;凡事不必太在意;愿你一生好运气!别驻足,梦想要不停追逐;别认输,遨过黑夜才有日出;要记住,成功就在下一步;路很苦,汗水是最美的书;尽情欢呼,相约巅峰共舞!人生短暂,不必计较太多得失!成功会被时间掩住光彩;失败会在岁月中淡化!人生最珍贵的不是“得不到”和“已失去”而是现在能把握的幸福!平淡是真!”
上篇中的NNTP协议大家一定会感觉太老,不过本篇的项目就有亲切感了。我们来实现一个聊天服务器。可以使用TWISTED框架,当时这不是本篇重点,本篇使用标准块内的asyncore和asynchat模块。
1. 需求
学习网络编程知识。
….
没了,需求这是这个简单明了,好吧~~
2. 工具和准备
asyncore和asynchat模块,asyncore框架可以处理同时连接的多个用户。
此外服务端还需要一个端口来提供给客户端连接。这里只要取一个大于1023的端口即可,因为小于1023的话是系统保留的,最好别用。
3. 初次实现
废话不多,直接开工了这次。
需要两个主类,一个作为聊天服务器,一个用于表示每个聊天会话。
3.1 ChatServer类
ChatServer类继承于asyncore的dispatcher以创建基础的ChatServer类。
一个简单服务程序如下:
fromasyncore import dispatcher
importasyncore
class ChatServer(dispatcher):pass
s = ChatServer()
asyncore.loop()
这个什么都不能做。能接受链接的话修改如下:
fromasyncore import dispatcher
importsocket, asyncore
class ChatServer(dispatcher):
def handle_accept(self):
conn, addr = self.accept()
print'Connection attempt from', addr[0]
s = ChatServer()
s.create_socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('', 5005))
s.listen(5)
asyncore.loop()
测试:
直接执行:telnet 127.0.0.1 5005
然后服务端出现如下:
Connectionattempt from 127.0.0.1
telnet命令没有,通过如下步骤设置:
1、 打开控制面板
2、 程序和功能
3、 打开或关闭WINDOWS功能
4、 CHECK TELNET 客户端。
TELNET输入如果出现乱码:
按下 CTRL+] 即可正常输入。
3.2 ChatSession类
基本的ChatSession类用处不大,在代码实现中为每个连接创建一个dispatcher对象。主要任务是收集来自客户端的数据进行响应,可以使用asynchat模块。
为了让asynchat起作用,只要覆盖两个方法即可:collect_incoming_data和found_terminator。
collect_incoming_data:在从套接字中读取一些bit文本时调用。
found_terminator:在读取一个结束符时调用。结束符通过set_terminiator方法设置,一般设置为”\r\n”.
带ChatSession类的如下:
fromasyncore import dispatcher
fromasynchat import async_chat
importsocket, asyncore
PORT = 5005
class ChatSession(async_chat):
def __init__(self,sock):
async_chat.__init__(self, sock)
self.set_terminator("\r\n")
self.data = []
def collect_incoming_data(self,data):
self.data.append(data)
def found_terminator(self):
line = ''.join(self.data)
self.data = []
#Do something with the line...
print line
class ChatServer(dispatcher):
def __init__(self,port):
dispatcher.__init__(self)
self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(('', port))
self.listen(5)
self.sessions = []
def handle_accept(self):
conn, addr = self.accept()
self.sessions.append(ChatSession(conn))
if__name__ == '__main__':
s = ChatServer(PORT)
try:asyncore.loop()
except KeyboardInterrupt: print
3.3 整合
还需要将用户的发言广播给其他的用户。
可以通过在服务器端遍历回话的列表,将发言行写到每一个客户端里面。
PS:必须保证在客户单断开连接后,将它从会话列表中移除。通过重写事件处理方法handle_close 来实现这个功能。
代码实现如下:
fromasyncore import dispatcher
fromasynchat import async_chat
importsocket, asyncore
PORT = 5005
NAME = 'TestChat'
class ChatSession(async_chat):
"""
A class that takes care of a connectionbetween the server
and a single user.
"""
def __init__(self,server, sock):
#Standard setup tasks:
async_chat.__init__(self, sock)
self.server = server
self.set_terminator("\r\n")
self.data = []
#Greet the user:
self.push('Welcome to %s\r\n' % self.server.name)
def collect_incoming_data(self,data):
self.data.append(data)
def found_terminator(self):
"""
If a terminator is found, that meansthat a full
line has been read. Broadcast it toeveryone.
"""
line = ''.join(self.data)
self.data = []
self.server.broadcast(line)
def handle_close(self):
async_chat.handle_close(self)
self.server.disconnect(self)
class ChatServer(dispatcher):
"""
A class that receives connections andspawns individual
sessions. It also handles broadcasts tothese sessions.
"""
def __init__(self,port, name):
#Standard setup tasks
dispatcher.__init__(self)
self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
self.set_reuse_addr()
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(line + '\r\n')
def handle_accept(self):
conn, addr = self.accept()
self.sessions.append(ChatSession(self,conn))
if__name__ == '__main__':
s = ChatServer(PORT, NAME)
try:asyncore.loop()
except KeyboardInterrupt: print
基本实现了聊天功能,不过还存在一些问题和功能的局限性。
4. 重构
针对初次实现,无法判断是谁在讲话,而且缺少命令解释的支持。
4.1 基础命令解释
例如输入 say hello
调用如下方法:
do_say(‘hello’)
下面是解释未知命令的方法:
class CommandHandler:
"""
Simple command handler similar to cmd.Cmdfrom the standard
library.
"""
def unknown(self,session, cmd):
'Respondto an unknown command'
session.push('Unknown command: %s\r\n' % cmd)
def handle(self,session, line):
'Handlea received line from a given session'
ifnot line.strip():return
#Split off the command:
parts = line.split(' ', 1)
cmd = parts[0]
try: line = parts[1].strip()
except IndexError: line = ''
#Try to find a handler:
meth = getattr(self, 'do_'+cmd, None)
try:
#Assume it's callable:
meth(session, line)
except TypeError:
#If it isn't, respond to the unknown command:
self.unknown(session, cmd)
4.2 房间
每个房间都是一个拥有自定义专名命令的CommandHandler对象。
此外,还能记录哪个用户目前位于聊天室内。
class Room(CommandHandler):
"""
A generic environment that may contain oneor more users
(sessions). It takes care of basic commandhandling and
broadcasting.
"""
def __init__(self,server):
self.server = server
self.sessions = []
def add(self,session):
'Asession (user) has entered the room'
self.sessions.append(session)
def remove(self,session):
'Asession (user) has left the room'
self.sessions.remove(session)
def broadcast(self,line):
'Senda line to all sessions in the room'
for session in self.sessions:
session.push(line)
def do_logout(self,session, line):
'Respondto the logout command'
raise EndSession
除了基本的add和remove方法之外,broadcast方法会对房间内的所有用户调用push方法。还定义了do_logout方法,引发一个较高级别的处理操作的异常。
4.3 登陆和退出房间
Room子类还应该能表示其他状态。例如登陆和退出房间。
登陆时候打印一个欢迎语,退出时在服务器端删除用户名。
4.4 主聊天室
主聊天室覆盖add和remove方法。Add方法会广播用户进入的消息,并将用户名添加到服务端的users字典内。Remove方法广播用户离开的消息。
此外还有3个命令
l Say命令:广播一个单行,用发言的用户的名字作为前缀
l Look:查看房间内有哪些用户
l Who:告诉用户哪些用户登录到了当前的服务器。当扩展到多个房间的时候look和who的命令就会不同了。
4.5 新的服务器
相比第一个实现,
l ChatSession有个enter方法,用于进入新房间
l ChatSession构造函数使用了LoginRoom
l Handle_close方法使用了LogoutRoom
l ChatServer构造函数增加了users字典,并且将名为main_room的ChatRoom增加为自己的特性。
5. 交付
最后实现代码如下:
和C比起来,短短200行代码(还包括空行的)就实现了一个聊天室的程序,是不是很给力?
fromasyncore import dispatcher
fromasynchat import async_chat
importsocket, asyncore
PORT = 5005
NAME = 'TestChat'
class EndSession(Exception):pass
class CommandHandler:
"""
Simple command handler similar to cmd.Cmdfrom the standard
library.
"""
def unknown(self,session, cmd):
'Respondto an unknown command'
session.push('Unknown command: %s\r\n' % cmd)
def handle(self,session, line):
'Handlea received line from a given session'
ifnot line.strip():return
#Split off the command:
parts = line.split(' ', 1)
cmd = parts[0]
try: line = parts[1].strip()
except IndexError: line = ''
#Try to find a handler:
meth = getattr(self, 'do_'+cmd, None)
try:
#Assume it's callable:
meth(session, line)
except TypeError:
#If it isn't, respond to the unknown command:
self.unknown(session, cmd)
class Room(CommandHandler):
"""
A generic environment that may contain oneor more users
(sessions). It takes care of basic commandhandling and
broadcasting.
"""
def __init__(self,server):
self.server = server
self.sessions = []
def add(self,session):
'Asession (user) has entered the room'
self.sessions.append(session)
def remove(self,session):
'Asession (user) has left the room'
self.sessions.remove(session)
def broadcast(self,line):
'Senda line to all sessions in the room'
for session in self.sessions:
session.push(line)
def do_logout(self,session, line):
'Respondto the logout command'
raise EndSession
class LoginRoom(Room):
"""
A room meant for a single person who hasjust connected.
"""
def add(self,session):
Room.add(self, session)
#When a user enters, greet him/her:
self.broadcast('Welcome to %s\r\n' % self.server.name)
def unknown(self,session, cmd):
#All unknown commands (anything except login or logout)
#results in a prodding:
session.push('Please log in\nUse "login <nick>"\r\n')
def do_login(self,session, line):
name = line.strip()
#Make sure the user has entered a name:
ifnot name:
session.push('Please enter a name\r\n')
#Make sure that the name isn't in use:
elif name in self.server.users:
session.push('The name "%s" is taken.\r\n' %name)
session.push('Please try again.\r\n')
else:
#The name is OK, so it is stored in the session, and
#the user is moved into the main room.
session.name = name
session.enter(self.server.main_room)
class ChatRoom(Room):
"""
A room meant for multiple users who canchat with the others in
the room.
"""
def add(self,session):
#Notify everyone that a new user has entered:
self.broadcast(session.name + ' has entered the room.\r\n')
self.server.users[session.name]= session
Room.add(self, session)
def remove(self,session):
Room.remove(self, session)
#Notify everyone that a user has left:
self.broadcast(session.name + ' has left the room.\r\n')
def do_say(self,session, line):
self.broadcast(session.name+': '+line+'\r\n')
def do_look(self,session, line):
'Handlesthe look command, used to see who is in a room'
session.push('The following are in this room:\r\n')
for other in self.sessions:
session.push(other.name + '\r\n')
def do_who(self,session, line):
'Handlesthe who command, used to see who is logged in'
session.push('The following are logged in:\r\n')
for name in self.server.users:
session.push(name + '\r\n')
class LogoutRoom(Room):
"""
A simple room for a single user. Its solepurpose is to remove
the user's name from the server.
"""
def add(self,session):
#When a session (user) enters the LogoutRoom it is deleted
try: del self.server.users[session.name]
except KeyError: pass
class ChatSession(async_chat):
"""
A single session, which takes care of thecommunication with a
single user.
"""
def __init__(self,server, sock):
async_chat.__init__(self, sock)
self.server = server
self.set_terminator("\r\n")
self.data = []
self.name = None
#All sessions begin in a separate LoginRoom:
self.enter(LoginRoom(server))
def enter(self,room):
#Remove self from current room and add self to
#next room...
try: cur = self.room
except AttributeError: pass
else: cur.remove(self)
self.room = room
room.add(self)
def collect_incoming_data(self,data):
self.data.append(data)
def found_terminator(self):
line = ''.join(self.data)
self.data = []
try: self.room.handle(self, line)
except EndSession:
self.handle_close()
def handle_close(self):
async_chat.handle_close(self)
self.enter(LogoutRoom(self.server))
class ChatServer(dispatcher):
"""
A chat server with a single room.
"""
def __init__(self,port, name):
dispatcher.__init__(self)
self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind(('', port))
self.listen(5)
self.name = name
self.users = {}
self.main_room = ChatRoom(self)
def handle_accept(self):
conn, addr = self.accept()
ChatSession(self, conn)
if__name__ == '__main__':
s = ChatServer(PORT, NAME)
try:asyncore.loop()
except KeyboardInterrupt: print
聊天服务器可用的命令
命令 | 可用于 | 描述 |
Login name | 登陆房间 | 登陆服务器 |
Logout | 所有房间 | 退出服务器 |
Say statement | 聊天室 | 发言 |
Look | 聊天室 | 查看同一个房间内的人 |
who | 聊天室 | 查看登陆到服务器的人 |
5.1 关于测试
执行Python脚本后,用telnet登陆即可。