最近在看《Python基础教程》,本书从第二十章开始后面都是小的项目实践。
首先就看了第五个项目"虚拟茶话会",该项目主要实现基本的聊天室(即黑窗口式的客户端交互通信),实现多人实时交互聊天功能,有登录房间、注销登录、聊天、查看聊天室里的人等功能。项目涉及到基本的python网络编程知识以及客户端异步通信I/O(即服务器同时处理多客户端交互套接字服务),主要使用Python标准库内asyncore和asynchat模块,项目采用了原型式开发方法,用OO思想实现,程序比较简单。
饭要一口一口吃,网络上相关内容看得眼花缭乱。这里主要试图理一理Python网络编程方面涉及到的主要问题及解决思路,并自己整理一些参考过的资料,参考相关资料。
背景:
Python内有很多针对常见网络协议的库,在库顶部可以获得抽象层,这样就可以集中精力在程序的逻辑处理上,而不是停留在网络实现的细节中。Python在处理字节流的各种模式方面很擅长。
Python中常见的网络设计模块:
(1)socket模块
套接字模块是一个非常简单的基于对象的接口,它提供对低层BSD套接字样式网络的访问。使用该模块可以实现客户机和服务器套接字。要在python 中建立具有TCP和流套接字的简单服务器,需要使用socket模块。利用该模块包含的函数和类定义,可生成通过网络通信的程序。
socket常用方法:
函数 | 描述 |
服务器端套接字函数 | |
s.bind() | 绑定地址(主机,端口号对)到套接字 |
s.listen() | 开始TCP 监听 |
s.accept() | 被动接受TCP 客户的连接,(阻塞式)等待连接的到来 |
客户端套接字函数 | |
s.connect() | 主动初始化TCP 服务器连接 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛异常 |
公共用途的套接字函数 | |
s.recv() | 接收TCP 数据 |
s.send() | 发送TCP 数据 |
s.sendall() | 完整发送TCP 数据 |
s.recvfrom() | 接收UDP 数据 |
s.sendto() | 发送UDP 数据 |
s.getpeername() | 连接到当前套接字的远端的地址 |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回指定套接字的参数 |
s.setsockopt() | 设置指定套接字的参数 |
s.close() | 关闭套接字 |
(2)urllib和urllib2模块
它们均能通过网络访问文件,几乎可以把任何URL指向的东西作为程序的输入。
在涉及到HTTP验证或cookie时,urllib2更合适。(没具体实验)
(3)asyncore和asynchat模块
使用asyncore框架,程序可以处理同时连接的多个用户。asyncore框架位于标准库内,基于一些底层的机制(Select模块中的select函数),这些机制允许服务器逐个地对连接上的用户进行服务。在处理到下一个用户前,它并不读取当前用户的所有可用数据,而是只读取一部分。 另外,服务器只从需要读取数据的套接字中读取,程序一遍遍地循环(轮询)。
asyncore和asychat提供了一个可以处理所有细节的有用框架。
(4)SocketServer模块
SocketServer是一个同步的网络服务器基类,使用它很容易编写服务器。它是标准库中很多服务器框架的基础,包括BaseHTTPServer,SimpleHTTPServer,CGIHTTPServer,SimpleXMLRPCServer和DocXMLRPCServer,这些服务器框架为基础服务期增加了特定的功能。
SocketServer里面包含4个基本的类,针对TCP套接字刘的TCPServer;针对UDP数据报套接字的UDPServer;针对性不强的UnixStreamServer和UnixDatagramServer。后三个不常用。
主要问题:
一个完整的聊天会话需要同时处理多个连接。实现这个问题有三种方法,分叉(forking)、线程(threading)、异步I/O。
分叉:当分叉一个进程时,基本上是复制了它。并且分叉后的两个进程都从当前的执行点继续运行,并且每个进程都有自己的内存副本(变量等)。原来的进程为父进程,新产生的进程为子进程。
在一个使用分叉的服务器中,每一个客户端连接都利用分叉创建一个子进程,父进程继续监听新的进程,子进程处理客户端。当客户端请求结束,子进程退出。父进程和子进程并行运行。
线程:是轻量级的进程或子进程,所有的线程都存在于相同的进程中,共享内存。
异步I/O:基本的实现机制是select模块中的select函数,存在更高层次处理异步I/O的框架即asyncore和asynchat模块。
避免线程和分叉的另一种方法是转换到Stackless Python,这是一个为了能够在不同的上下文之间快速、方便切换而设计的Python版本,它支持一个叫做微线程的类线程的并行形式。
通过对SocketServer服务器使用混入类,派生进程和线程比较容易,但分叉因为每个子进程需要内存所以耗费资源,线程因为共享内存所以必须确保变量不会冲突访问因此产生同步问题。(尽管如此,对于合理数量的客户端考虑到硬件性能提升,分叉在现代操作Unix和Linux系统中比较高效。windows不支持分叉。)
程序实现:
标准库
18.3 asyncore-Asynchronous socket handler
"If your operating system supports the select() system call in its I/O library (and nearly all do), then you can use it to juggle multiple communication channels at once; doing other work while your I/O is taking place in the “background.” Although this strategy can seem strange and complex, especially at first, it is in many ways easier to understand and control than multi-threaded programming. The asyncore module solves many of the difficult problems for you, making the task of building sophisticated high-performance network servers and clients a snap. For “conversational” applications and protocols the companionasynchat module is invaluable."
异步I/O便于建立先进的高性能网络服务器和客户端。
"The basic idea behind both modules is to create one or more network channels, instances of class asyncore.dispatcher and asynchat.async_chat. Creating the channels adds them to a global map, used by the loop() function if you do not provide it with your own map.
Once the initial channel(s) is(are) created, calling the loop() function activates channel service, which continues until the last channel (including any that have been added to the map during asynchronous service) is closed."
这两个模块基本思想都是创建一个或多个网络信道。通过调用loop()函数激活信道服务,服务会一直运行直到最后一个信道被关闭。
"The dispatcher class is a thin wrapper around a low-level socket object. To make it more useful, it has a few methods for event-handling which are called from the asynchronous loop. Otherwise, it can be treated as a normal non-blocking socket object."
"The firing of low-level events at certain times or in certain connection states tells the asynchronous loop that certain higher-level events have taken place. For example, if we have asked for a socket to connect to another host, we know that the connection has been made when the socket becomes writable for the first time (at this point you know that you may write to it with the expectation of success). The implied higher-level events are:
Event | Description |
---|---|
handle_connect() | Implied by the first read or write event |
handle_close() | Implied by a read event with no data available |
handle_accepted() | Implied by a read event on a listening socket |
During asynchronous processing, each mapped channel’s readable() and writable() methods are used to determine whether the channel’s socket should be added to the list of channels select()ed or poll()ed for read and write events."
通过继承asyncore.dispatcher实现基本的异步I/O服务。
18.4 asynchat-Asynchronous socket command/response handler
"This module builds on the asyncore infrastructure, simplifying asynchronous clients and servers and making it easier to handle protocols whose elements are terminated by arbitrary strings, or are of variable length. asynchat defines the abstract class async_chat that you subclass, providing implementations of the collect_incoming_data() and found_terminator() methods. It uses the same asynchronous loop as asyncore, and the two types of channel,asyncore.dispatcher and asynchat.async_chat, can freely be mixed in the channel map. Typically an asyncore.dispatcher server channel generates new asynchat.async_chat channel objects as it receives incoming connection requests."
asynchat模块基于asyncore莫魁岸构建,简化了异步客户端和服务器,使处理以任意字符串和长度结尾的协议更容易。
"This class is an abstract subclass of asyncore.dispatcher. To make practical use of the code you must subclass async_chat, providing meaningfulcollect_incoming_data() and found_terminator() methods. The asyncore.dispatcher methods can be used, although not all make sense in a message/response context.
Like asyncore.dispatcher, async_chat defines a set of events that are generated by an analysis of socket conditions after a select() call. Once the polling loop has been started the async_chat object’s methods are called by the event-processing framework with no action on the part of the programmer."
asynchat.asyn_chat是asyncore.dispatcher的子类,通过继承asynchat.async_chat类,实现collect_incoming_data() and found_terminator()方法。跟asyncore.dispatcher类似,asynchat.asyn_chat定义的一系列事件也是基于对select()函数和poll()函数的分析。
类图:
可用的命令有:login name,logout,say statement,look,who
其中:
ChatServer:__init__()里创建记录登录用户的字典,主聊天室。handle_accept()处理每一个连接到服务器上的客户端套接字,为每一个客户端新建一个ChatSession
ChatSession:__init__()里记录会话的用户名,设置结束符,进入LoginRoom。enter()为从LoginRoom到ChatRoom和从ChatRoom到LogoutRoom的切换。collect_incoming_data()为接收客户端发送数据。found_terminator()为对用户接收到的数据行进行处理,如果数据内容为空则不作处理。hand_close()为关闭套接字并进入LogoutRoom。
Room:__init__创建记录会话的列表,对应于相同的服务器。add()将会话添加到房间。remove()会话离开房间。broadcast()为向房间中所有会话广播数据。do_logout()相应logout命令。
LoginRoom:add()欢迎新进入房间的会话。unknown()命令不存在时提示重新输入。do_login()检查输入的会话用户名是否合法是否已存在,如果合法且不重复则允许进入主聊天室。
ChatRoom:add()向所有会话提示新用户进入,并将新用户加入服务器上的用户字典。remove()向所有会话提示用户离开,并将用户从服务器上的用户字典删除。do_say()向所有会话广播用户发言。do_look()查看哪些用户在聊天室内。do_who()查看登录用户。
LogoutRoom:add()将当前会话从服务器用户字典中删除。
CommandHandler: unknown()响应未知命令。handle()处理从会话中接收到的数据行,根据数据行提取出响应命令 并 执行对应的方法。
问题总结:
(1)dispatcher和async_chat对象类似基本上都是套接字对象,处理跟套接字相关的操作。不同的是前者侧重于接受客户端连接,后者侧重于响应处理消息。
(2)在调试代码的过程中,发现在客户端与服务器通信过程中发送的数据类型为bytes,而在类里面处理发送的数据时需要处理字符串,这就涉及字符串与bytes对象的转换。注意区别b''.join()与''.join()。
# str to bytes
bytes(s, encoding = "utf8")
# bytes to str
str(b, encoding = "utf-8")
(3)getattr()查看对象是否具有相应属性,通过异常实现。
改进方向:
(1)多个聊天室。此时do_who()和do_look()功能不同。
(2)更多操作命令,回复更人性化。
(3)创建GUI客户端。此时,GUI响应的时间循环和服务器通信的时间循环需要协调工作,涉及到线程处理。