聊天服务器:
服务器能接受来自不同用户的多个连接;
允许用户同时(并行)操作;
能解释命令,例如,say或者logout;
容易拓展
套接字和端口:
套接字是一种使用标准UNIX文件描述符(file descriptor)与其他程序通信的方式。套接字可以看作是处于不同主机之间的两个程序的通信连接端点。一方面程序将要传输的信息写入套接字中,而另一方面则通过读取套接字内的数据来获得传输的信息。
![]() |
套接字通信示意图
所示为使用套接字进行通信的示意图。假设存在两台主机A与B,在主机A中存在进程C,主机B中存在进程D,当进程C需要将数据送到进程D时,首先将数据写到套接字中,而进程D可以通过读取套接字来获得进程C发送的信息。
在网络中,不同计算机是通过IP地址来区分的,也就是说,要将数据由主机A发送到主机B,只要知道主机B的IP地址就可以确定数据要发送的目的地。但是,在主机A与B中不可能只有进程C和进程D两个进程。主机B在收到主机A发送来的数据后,如何才能确定该数据是发送给进程D?因此,还需要某种标识信息,用于描述网络通信数据发往的进程。TCP/IP协议提出了协议端口的概念,用于标识通信的进程。
当进程与某个端口绑定后,操作系统会将收到的给该端口的数据送往该进程。与文件描述符类似,每个端口都有被称为端口号的整数类型的标识符,该标识符用于区分不同的端口。不同协议可以使用相同的端口号进行数据传输。例如,TCP使用了344的端口号,UDP同样可以使用344端口号进行数据传输。
端口号为一个16位的无符号整数,其取值范围为0~65535。低于256的端口被作为系统的保留端口号,主要用于系统进程的通信,不在这一范围的端口号被称为自由端口号,可以由进程自由使用。
python asyncore
该模块是事件驱动(event-driver)。主要方法是loop(),用于进行网络事件的循环。而类dispatcher用于进行网络交互事件。不能直接Instance,需要通过继承并在__init__中显式的声明。它封装了网络的读写事件,并通过readable()和writable()来进行事件进行控制。
下面是dispatcher的主要方法,这些都是基于消息的,捕捉到某个事件,就会触发:
(1)handle_read():Called when the asynchronous loop detects that a read() call on the channel’s socket will succeed.
(2)handle_write():Called when the asynchronous loop detects that a writable socket can be written
(3)handle_expt():Called when there is out of band (OOB) data for a socket connection
(4)handle_connect():Called when the active opener’s socket actually makes a connection
(5)handle_close():Called when the socket is closed.
(6)handle_error():Called when an exception is raised and not otherwise handled
(7)handle_accept():Called on listening channels (passive openers) when a connection can be established with a new remote endpoint that has issued a connect() call for the local endpoint.
(8)readable():Called each time around the asynchronous loop to determine whether a channel’s socket should be added to the list on which read events can occur
(10)writable():Called each time around the asynchronous loop to determine whether a channel’s socket should be added to the list on which write events can occur
下面是每个channel的主要方法:
(1)create_socket(family, type)
(2)connect(address)
(3)send(data)
(4)recv(buffer_size)
(5)listen(backlog)
(6)bind(address)
(7)accept()
(8)close()
1. 初次实现
1.1 CharServer类
#可接受连接的服务器 from asyncore import dispatcher import asyncore,socket class ChatServer(dispatcher): def handle_accept(self): conn.addr=self.accept() print 'Connection attempt from', addr[0] #客户端的Ip地址 s=ChatServer() s.create_socket(socket.AF_INET, socket.SOCK_STREAM)#创建套接字 s.bind(('',5005))#将服务器绑定到具体的地址(主机名和端口),空字符串(主机名),意味着本地主机(即本地所有接口),端口号为5005 s.listen(5) #告诉服务器要监听连接,并且指定5个连接的代办的事物 asyncore.loop() #启动服务器,循环监听
#具有清理功能的基本服务器 from asyncore import dispatcher import socket, asyncore PORT=5005 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) def handle_accept(self): conn, addr=self.accept() print 'Connection attempt from', addr[0] if __name__ =="__main__": s=ChatServer(PORT) try: asyncore.loop() except KeyboardInterrupt: pass
1.2 CharSession类
收集来自客服端的数据(文本)并且进行响应;
async_chat类(位于asynchat模块中):隐藏了大多数基本的套接字读写操作,简单起见,覆盖其中的collect_incoming_data方法(每次从套接字中读取一些bit文本时调用),found_terminator方法(读取一个结束符时调用)
1)set_terminator方法用于将行终止对象设为网络协议中通常用作终止符的"\r\n"
2)ChatSession对象会将目前读取的数据作为保存为字符串列表data。但读入更多数据时,collect_incoming_data会自动被调用,将新读入的数据追加到列表中。
3)found_terminiator方法在读到终止对象时被调用
4)ChatServer保存回话列表
5)CharServer的handle_accept方法现在创建了新的ChatSession对象,并将其追加到会话列表中
# 带有ChatSession类的服务器程序 from asyncore import dispatcher from asynchat import async_chat import socket,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=[] #处理该行数据 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
1.3 整合
添加功能:将用户的发言(输入的每一行)广播给其他用户——在服务器端的添加for循环,遍历会话的列表,并且将发言行写到每一个客户端里面。为了能在async_chat对象中写入数据,需要使用push方法。
from asyncore import dispatcher from asynchat import async_chat import socket,asyncore PORT=5005 NAME='TestChat' class ChatSession(async_chat): ''' 处理服务器和一个用户之间连接的类 ''' def __init__(self,server,sock): #标准设置任务 async_chat.__init__(self,server,sock) self.server=server self.set_terminator("\r\n") self.data=[] # 问候用户 self.push('Welcome to %s \r\n' % self.server.name) def collect_incoming_data(self,data): self.data.append(data) 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) 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.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) try: asyncore.loop() except KeyboardInterrupt: print
2. 改进
from asyncore import dispatcher from asynchat import async_chat import socket, asyncore PORT = 5005 NAME = 'TestChat' class EndSession(Exception):pass class CommandHandler: ''' 类似于标准库中cmd.Cmd的简单命令处理程序 ''' def unknown(self, session, cmd): #响应未知命令 session.push('Unknown command: %s\r\n' % cmd) def handle(self, session, line): #处理从给定的会话中接受到的行 if not line.strip(): return #分离命令: parts = line.split(' ',1) cmd = parts[0] try: line = parts[1].strip() except IndexError: line = '' #试着查找处理程序 meth = getattr(self, 'do_'+cmd, None) try: #假定它是可调用的 meth(session, line) except TypeError: #如果不可以被调用,此段代码响应 self.unknown(session,cmd) class Room(CommandHandler): ''' 包括一个或多个用户*会话)的泛型环境,负责基本的命令处理和广播 ''' def __init__(self, server): self.server = server self.sessions = [] def add(self, session): #一个会话(用户)已经进入房间 self.sessions.append(session) def remove(self, session): #一个会话(用户)已经离开房间 self.sessions.remove(session) def broadcast(self, line): #向房间中的所有会话发送一行 for session in self.sessions: session.push(line) def do_logout(self, session, line): #响应logout命令 raise EndSession class LoginRoom(Room): ''' 为刚刚连接上的用户准备的房间 ''' def add(self,session): Room.add(self,session) #当用户进入时,问候他或她 self.broadcast('Welcome to %s\r\n' % self.server.name) def unknown(self, session, cmd): #所有未知命令(除了login或者logout外的一起) #会导致一个警告 session.push('Please log in \nUse "login"\r\n') def do_login(self, session, line): name = line.strip() #确保用户输入了名字 if not name: session.push('Please enter a name\r\n') #确保用户名没有被使用 elif name in self.server.users: session.push('The name "%s" is taken.\r\n' % name) sessoin.push('Please try again.\r\n') else: #名字没问题,所以存储在会话中,并且将用户移动到主聊天室 session.name = name session.enter(self.server.main_room) class ChatRoom(Room): ''' 为多用户相互聊天准备的房间 ''' def add(self, session): #告诉所有人有新用户进入 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) #告诉所有人有用户离开 self.broadcast(session.name + ' has left the room.\r\n') def do_say(self, session, line):
#处理say命令,用于广播一个单行,用发言的用户的名字作为前缀 self.broadcast(session.name + ': ' + line + '\r\n') def do_look(self, session, line): #处理look命令,该命令用于查看谁在房间里 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): #处理who命令,该命令用于查看谁登陆了 session.push('The following are logged in:\r\n') for name in self.server.users: session.push(name + '\r\n') class LogoutRoom(Room): ''' 为单用户准备的简单房间,只用于将用户名从服务器移除 ''' def add(self, session): #当会话(用户)进入要删除的LogoutRoom try: del self.server.users[session.name] except KeyError: pass class ChatSession(async_chat): ''' 单会话,负责和单用户通信 ''' def __init__(self, server, sock): async_chat.__init__(self,sock) self.server = server self.set_terminator('\r\n') self.data = [] self.name = None #所有的会话都开始于单独的LoginRoom self.enter(LoginRoom(server)) def enter(self, room): #从当前房间移除自身(self),并且将自身添加到下一个房间 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): ''' 只有一个房间的聊天服务器 ''' def __init__(self, port, name): dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 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
聊天服务器可用命令: