前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >虚拟茶话会(1):初次实现

虚拟茶话会(1):初次实现

作者头像
不可言诉的深渊
发布2019-07-26 16:40:10
8100
发布2019-07-26 16:40:10
举报

在这个项目中,我们将做些正式的网络编程工作:编写一个聊天服务器,让人们能够通过网络实时地聊天。使用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类)如图所示。

对于这个新版本,有几点需要说明。

  • 调用方法set_terminator将行结束符设置成了"\r\n",这是网络协议中常用的行结束符。
  • ChatSession对象将已读取的数据存储在字节列表data中。读取更多的数据后,将自动调用collect_incoming_data,而这个方法只是将这些数据附加到列表data末尾。使用字节列表来存储数据、然后使用方法join来合并这些字节是一个常用的成例(在较旧的Python版本中,这种做法的效率比不断将字节相加更高)。在较新的Python版本中,完全可以将+=用于字节。
  • 遇到结束符时将调用方法found_terminator。当前,这个方法的实现通过合并数据项来创建一行,然后将self.data重置为空列表。然而,只是将这行打印出来,而没有使用它做任何有用的事情。
  • ChatServer存储了一个会话列表。
  • ChatServer的方法handle_accept现在创建一个新的ChatSession对象,并将其附加到会话列表末尾。

请尝试运行这个服务器,并通过使用多个客户端连接到它。每当你在客户端中输入一行内容时,这些内容都将在服务器所在的终端打印出来。这意味着服务器能够同时处理多个连接。至此,唯一缺失的功能是让客户端能够看到其他人的发言!

4.3.整合起来

要让原型成为简单而功能完整的聊天服务器,还需添加一项主要功能:让用户所说的内容(他们输入的每一行)广播给其他用户。要实现这种功能,可在服务器使用一个简单的for循环来遍历会话列表,并将内容行写入每个会话。要将数据写入async_chat对象,可使用方法push。

这种广播行为也带来了一个问题:客户端断开连接后,你必须确保将其从会话列表中删除。为此,可重写事件处理方法handle_close。第一个原型的最终代码如图所示。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-08-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python机器学习算法说书人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档