在这个项目中,我们将做些正式的网络编程工作:编写一个聊天服务器,让人们能够通过网络实时地聊天。使用Python创建这种程序的方式有很多,一种简单而自然的方法是使用框架Twisted,其核心是LineReceiver类。在本项目中,我将只使用标准库中的异步网络编程模块。
需要指出的是,Python在这方面好像处在过渡期。一方面,有关模块asyncore和asynchat的文档指出,在标准库中包含它们旨在向后兼容,开发新程序时应使用模块asyncio;另一方面,有关asyncio的文档又指出,在标准库中包含这个模块是权宜之计,未来可能将其删除。我将采取保守的做法,选择使用asyncore和asynchat。如果你愿意,可以尝试使用其他方法(如分叉或线程化),甚至可以使用模块asyncio重写这个项目。
1.问题描述
我们将编写一个相对低级的在线聊天服务器。虽然很多社交媒体和消息服务都提供了这样的功能,但自己动手编写在线聊天服务器对深入学习网络编程大有裨益。假设这个项目需求如下。
其中网络连接和程序异步特征需要使用特殊工具来实现。
2.有用的工具
在这个项目中,需要的新工具只有标准库模块asyncore及其相关的模块asynchat。我将简单的介绍这些模块,有关它们的详细信息,请参阅“Python库参考手册”。网络程序的基本组件是套接字。可通过导入模块socket并使用其中的函数来直接创建套接字。既然如此,需要使用asyncore来做什么呢?
框架asyncore让你能够处理多个同时连接的用户。想象一下没有处理并发的特殊工具的情形。你启动服务器,它等待用户连接。用户连接后,他开始读取来自用户的数据,并通过套接字将结果提供给用户。然而,如果已经有用户连接到服务器,结果将如何呢?要连接的用户必须等待,直到第一个用户断开连接为止。这在有些情况下可行,但编写聊天服务器时,关键就是允许多个用户同时连接,不然用户之间如何聊天呢?
框架asyncore基于的底层机制(模块select中的函数select)让服务器能够依次为连接的所有用户提供服务:不是读取来自一个用户的所有数据后,再读取下一个用户的数据。另外,服务器只读取有数据可读取的套接字。这种操作是在循环中反复进行的。对写入处理与此类似。你可使用模块socket和select来实现这种功能,但asyncore和asynchat提供了一个很有用的框架可替你处理这些细节。
3.准备工作
首先,你必须由一台连接到网络(如互联网)的计算机,否则别人无法连接到你的聊天服务器。(可在自己计算机上连接到聊天服务器,但这样做没多大意思。)要连接到聊天服务器,用户必须知道你的计算机地址(可以是机器名,如foo.bar.baz.com,也可以是IP地址)。另外,用户必须知道聊天服务器使用的端口号。这种端口号可在程序中设置;在代码中,使用的端口号为5005(这里是随便选择的)。
注意 有些端口号受到限制,必须有管理员权限才能使用。一般而言,使用大于1023的端口号就不会有什么问题。
为对聊天服务器进行测试,需要有一个客户端——位于用户端的程序,一个这样的简单程序是telnet(它基本上能够让你连接到任何套接字服务器)。在UNIX中,可从命令行执行这个程序。
$ telnet some.host.name 5005
这个命令连接到机器some.host.name的5005端口。要连接到运行命令telnet的机器,只需使用机器名localhost。(你可能想使用开关-e提供一个转义字符,以确保可轻松的退出telnet。有关这方面的细节,请参阅telnet文档。)
在Windows中,可使用提供了telnet功能的终端模拟器,如PuTTY(要下载这个软件并获取有关它的详细信息,请参阅http://www.chiark.greenend.org.uk/~sgtatham/putty)。然而,既然要安装新软件,不如安装为聊天量身定制的客户端程序。MUD(MUSH、MOO或其他相关缩略语)客户端非常适合用于聊天,一个这样的客户端是TinyFugue(要下载这个软件并获取有关它的详细信息,请参阅http://tinyfugue.sf.net)。它主要用于UNIX中,而且有点老,但这也有其魅力所在。也有一些用于Windows中的客户端,只需网上搜索“MUD客户端”之类的关键字就能找到。
4.初次实现
我们来将程序稍作分解。创建两个主要的类:一个表示聊天服务器,另一个表示聊天会话(连接的用户)。
4.1.ChatServer类
为创建简单的ChatServer类,可继承模块asyncore中的dispatcher类。dispatcher类基本上是一个套接字对象,但还提供了一些事件处理功能,稍后你将用到它们。下图是一个基本聊天服务器程序(真的很小)。
如果运行这个程序,什么都不会发生。要让服务器做点有趣的事情,必须调用其方法create_socket来创建一个套接字,还需调用其方法bind和listen将套接字关联到特定的端口并让套接字监听到来的连接(毕竟这是服务器要做的事情)。另外,还需重写事件处理方法handle_accept,让他在服务器接收客户端连接时做些事情。最终的程序如图所示。
方法handle_accept调用self.accept,以允许客户端连接。self.accept返回一个连接(客户端对应的套接字)和一个地址(有关发起连接的机器的信息)。方法handle_accept没有使用返回连接来做有用的事情,而只是打印一条消息,指出有客户端试图建立连接。addr[0]是客户端的IP地址。
在初始化服务器时,调用了create_socket,并通过传入两个参数指定了要创建的套接字类型。虽然也可使用其他的类型,但通常都是用这里使用的类型。对方法bind的调用将服务器关联到特定的地址(主机名和端口)。这里指定的主机名为空(一个空字符串,意味着localhost,用更专业一点的话说就是“当前机器的所有接口”),而端口号为5005。对方法listen的调用让服务器监听连接;它还将队列中等待的最大连接数指定为5。最后,像前面一样调用asyncore.loop来启动服务器的监听循环。
这个服务器实际上是管用的。请尝试运行它,再使用你选择的客户端连接到它。客户端连接将立即断开,而服务器将打印如下内容:
Connection attempt from 127.0.0.1
如果不是从服务器所在的机器连接到它,IP地址将不同。要停止服务器,只需按下相应的键盘快捷键:在UNIX中为Ctrl+C,而在Windows中为Ctrl+Break。
使用键盘快捷键关闭服务器将显示栈跟踪。为避免出现这种情况,可将循环放在try/except语句中。添加一下清理代码后,这个基本服务器如图所示。
这里调用了set_reuse_addr,让你能够重用原来的地址(具体地说是端口号),即便未妥善关闭服务器亦如此。如果不调用set_reuse_addr,可能需要等待一段时间才能重启服务器,或者在服务器崩溃后使用不同的端口号。因为这个程序可能通知操作系统它不再使用这个端口。
4.2.ChatSession类
基本的ChatServer不是很有用。不应对连接企图置若罔闻而应为每个连接创建一个新的dispatcher对象。然而,这些对象的行为与用作主服务器的对象不同,它们不在端口上监听到来的连接,而是已经连接到特定的客户端。它们的主要任务是收集来自客户端的数据(文本)并作出响应。你可以自己实现这种功能,方法是从dispatcher派生出一个类,并重写各种方法,但所幸有一个模块替你完成了其中很大一部分工作,它就是asynchat。
asynchat有点名不副实,它并非我们要编写的流(连续)式聊天应用程序而专门设计的。【asynchat中的chat指的是聊天式(命令-响应)协议。】模块asynchat中有一个async_chat类,其优点是隐藏了大部分基本的读写操作,因为这些操作实现起来可能有点难。要让async_chat发挥作用,只需重写两个方法——collect_incoming_data和found_terminator。每当从套接字读取一些文本后,都将调用collect_incoming_data;而读取到结束符时将调用found_terminator。在这里,结束符为换行符。(你需要在初始化时调用set_terminator来将结束符告知async_chat对象。)
更新后的程序(包含ChatSession类)如图所示。
对于这个新版本,有几点需要说明。
请尝试运行这个服务器,并通过使用多个客户端连接到它。每当你在客户端中输入一行内容时,这些内容都将在服务器所在的终端打印出来。这意味着服务器能够同时处理多个连接。至此,唯一缺失的功能是让客户端能够看到其他人的发言!
4.3.整合起来
要让原型成为简单而功能完整的聊天服务器,还需添加一项主要功能:让用户所说的内容(他们输入的每一行)广播给其他用户。要实现这种功能,可在服务器使用一个简单的for循环来遍历会话列表,并将内容行写入每个会话。要将数据写入async_chat对象,可使用方法push。
这种广播行为也带来了一个问题:客户端断开连接后,你必须确保将其从会话列表中删除。为此,可重写事件处理方法handle_close。第一个原型的最终代码如图所示。
本文分享自 Python机器学习算法说书人 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!