基础知识
大体上,互联网可以有如下几个层次构成:
- 底层的网络层:类似 TCP/IP 机制,处理字节间传送,不关心内容;
- 套接字:连接到网络的编程接口,类似 TCP/IP 运行在物理网络层上,支持灵活的客户端/服务器模型;
- 更高层的协议:结构化互联网通信架构,如 FTP 等协议,运行在套接字上,并定义了消息格式和标准地址;
- 服务器端网络脚本:应用模型,如 CGI,定义了网页浏览器和网络服务器之间的通信协议;
- 高层的框架和工具:如 Django, Google App Engine等,使用套接字和通信协议,但可以处理特定技术或较大的问题领域。
Grail
是由Guido Van Rossum
编写的互联网浏览器。一些流行的工具:
- 网络工具:
socket
,select
,socket server
- 客户端协议工具:
FTP
,Email
,HTTP
,TELNET
等; - 服务器端 CGI 脚本:可以满足简单的网站开发任务;
- 网络框架和云:
Django
,用于快速开发;Google App Engine
,“云计算”框架,提供了企业级别工具应用,允许网站利用谷歌网络基础设施;Turbo Gears
,工具综合包,包括一个JavaScript
库,一个模板系统,网络互动的CherryPy
等;Zope
,开源的网络应用服务器和工具包; - Web 服务(XML-RPC, SOAP):
XML-RPC
是是一种通过网络向部件提供远程程序请求的技术,对于客户端而言,网络服务器似乎是简单的函数,当函数调用发出,把传输的数据编码成XML格式,使用 Web 的HTTP传输机制传输到远程服务器。最终效果就是在客户端上简化连接到网络服务器的界面。Python 的xmlrpc.client
和xmlrpc.server
分别用于客户端和服务器。SOAP
机制也是类似。 - 其他工具:
mod_python
:一个在 Apache 网络服务器上优化 Python 的服务器端脚本执行的系统;Twisted
:一个用 Python 编写的支持异步、事件驱动的网络架构,支持大量的网络协议和通用网络服务器部署的与编码;HTML gen
:一个轻量级工具,可以从 Python 对象树直接生成HTML代码来描述网页。
- 网络工具:
套接字层
- 机器标识符:机器名称,可以使用域名或者IP; 端口号,用于会话时的协商数字标识符;
- 机器名和端口号组合可以唯一标志网络上的任何会话。
协议层
- 标准的因特网协议规定了一个结构化方式来探讨套接字,它们一般规范了消息格式和套接字端口号:消息格式提供了会话期间在套接字上交换的字节结构;端口号是保留的数字标识符,用于底层套接字上的信息交换。
端口号规则:
- 原则上,套接字端口可以在0~65535之间的任何16位二进制码整数值。但是0到1023之间的端口号被保留下来分配给了高层的标准协议。
协议 常见功能 端口号 Python 模块 HTTP Web 网站 80 http.client, http.server NNTP Usenet 新闻组 119 nntplib FTP 数据(默认) 文件传送 20 ftplib FTP 控制 文件传送 21 ftplib SMTP 发送邮件 25 smtplib POP3 获取邮件 110 poplib IMAP4 获取邮件 143 imaplib Finger 信息 79 无 SSH 命令行 22 无 Telnet 命令行 23 telnetlib - 客户端和服务器:通常称永久运行的监听程序为服务器,连接程序为客户端。
- 协议结构:有些协议可以在套接字上定义发送信息的内容;其它的协议也许可以在会话期间指定控制信息交换的顺序。
Python 网络库模块
模块 功能 socket, ssl 网络和IPC通信支持(TCP/IP,UDP等) cgi 服务器端CGI脚本支持:解析输入流,转义HTML文本等 urllib.request 从给定地址获取网页 urllib.parse 解析URL字符串不同的部分,可以转义URL http.client, ftplib, nntplib 网络、文件传送、新闻组 http.cookies, http.cookiejar 根据网站要求将数据存储在客户端,便于利用cookies实现自动登录等 poplib, imaplib, smtplib 邮件相关协议 telnetlib telnet协议模块 html.parser, xml.*
解析网页 xdrlib, socket 对二进制数据进行编码,使其具有可移植性,便于传输 struct, pickle 对Python对象进行编码,使其称为打包二进制数据或序列化的字节字符串,便于传输 email.*
通过标题附件和编码解析和撰写电子邮件 mimetypes 推测类型 uu, binhex, base64, binascii, quopri, email.*
编码和解码以文本传输的二进制数据(在电子邮件包中自动进行) socketserver 针对一般网络服务器的框架 http.server 基本的HTTP服务器部署 套接字编程
socket
模块中包含的高级工具:- 转换字节为标准的网络序列(ntohl, htonl);
- 查询及其名和地址(gethostname, gethostbyname);
- 在文件对象上封装套接字对象(sockobj.makefile);
- 使用套接字调用处于非阻塞状态(sockobj.setblocking);
- 设置套接字超时(sockobj.settimeout)。
ssl.wrap_socket
调用可以加密传输。服务器端程序编写的一般步骤:
server = socket(AF_INET, SOCK_STREAM)
:默认创建TCP套接字对象。其中AF_INET
指定的是IP地址协议(还有另外一种是支持IPv6的),SOCK_STREAM
则意味着TCP传输协议。一般都使用这种方式创建。server.bind(host, port)
:可以把套接字对象和某一个地址绑定起来。bind
需要一个适用于已经创建的套接字类型的元组。server.listen(N)
:开始监听来自客户端的请求,并且允许保留N个挂起的请求。通常来说,处理器足够快,所以设置为5对于一般应用就可以了。conn, addr = server.accept()
:调用会返回一个全新的套接字对象,在这个套接字对象上,数据可以在连接的客户端上相互转移;data = conn.recv(1024)
:用于读取发送来的消息,当客户端关闭套接字的末尾时,会触发EOF标志;conn.send(b'msg')
:发送字节字符串到客户端;某些程序可能需要重新发送未发送的部分或者使用conn.sendall
强制发送所有字节;conn.close()
:关闭特定的客户端连接。
可以使用
pickle
和struct
模块序列化对象,然后进行传输处理,这样也比较方便。但针对普通的字符串,使用encode
和decode
方法也可以满足了。客户端程序编写的一般步骤:
client = socket(AF_INET, SOCK_STREAM)
client.connect
client.send
client.recv(1024)
:获取来自服务器的消息client.close()
:关闭与服务器的连接,发送EOF信号
完整的示例程序参见 echo/server.py 和 echo/client.py
处理多个客户端
派生进程处理
- 主要思路就是使用一个父进程长期运行,并监听来自客户端的请求;每当请求被接收后,都会派生一个新的子进程负责处理请求,并给客户端发送响应。完整的支持派生进程的服务器端代码参见 这里
- 使用派生进程的方式处理多个客户端请求时,需要注意僵尸子进程的处理;否则,它们将会浪费系统进程表记录,浪费系统资源。僵尸进程截图如下:
- 需要注意的是,在完成请求后,子进程需要调用
os._exit(N)
来退出,出现上面图片显示的僵尸进程的现象,也是由于我调用了exit(N)
后导致的。看来,需要特别注意这个问题。os._exit
和sys.exit
调用是一样的,但是前者在会立即退出调用进程,没有清理操作等。通常来说,os._exit
用于子进程退出,而sys.exit
则可以在每个地方使用。关于它们的区别可以看看 Stack overflow上的提问。 - 在 Linux 上使用信号处理程序防止僵尸进程:当子进程停止或退出时,通过操作系统把
SIGCHLD
信号传递到父进程来重新设置信号处理器,从而有可能清理僵尸进程。具体来说,如果在 Python 脚本中为SIGCHLD
信号处理器分配SIG_IGN
(忽略)操作,那么当子进程退出时僵尸进程将会通过操作系统而被自动清除掉;这样,父进程也不需要发送等待调用来清理僵尸进程了。具体的改进代码参见 此处,关键代码signal(signal.SIGCHLD, signal.SIG_IGN)
。 使用能够信号的好处有两点:
- 简单:无需追踪并捕获子进程;
- 准确:客户端请求之间也不暂存僵尸进程。
但上述做法最大的缺点是移植性更差,但在 Linux 工作的很好。子进程退出后就会被立即删除,并不会出现大量”将要消失“的僵尸进程。
- 最后,来看看使用
multiprocessing
模块来实现同样的功能吧,事实上也非常简单!完整示例代码详见 此处,关键代码如下:
def dispatcher(server):
assert isinstance(server, socket)
while True:
conn, addr = server.accept()
print('Request from client:', addr, end=' ')
print('at', datetime.now())
multiprocessing.Process(target=handle_request, args=(conn, )).start()
- 但是使用
multiprocessing
模块在标准的 Windows 环境下却可能出问题,因为socket
无法被pickle
传递,所以会出现异常。不过,也没什么大不了的,基本不用 Window 系统,可以忽略这个问题。
多线程服务器
- 多进程服务器存在着性能、可移植性和复杂性的问题,而线程则是新的替代方案;并且线程服务器更加简单。
- 采用多线程模型的完整示例代码可以参见 这里,以下为关键代码:
def dispatcher(server):
assert isinstance(server, socket)
while True:
conn, addr = server.accept()
print('Request from client:', addr, end=' ')
print('at', datetime.now())
thread.start_new(handle_request, (conn, ))
使用 socketserver
socketserver
模块为我们处理了线程或者进程分支的细节部分,我们主要实现的是请求处理的部分,方便使用。- 完整的示例代码参见 这里,以下给出一般的服务器编写模板:
class RequestHandler(socketserver.BaseRequestHandler):
def handle(self):
while True:
data = self.request.recv(1024)
if not data:
break
self.request.send(data)
self.request.close()
def main():
# 此处可以使用ForkingTCPServer等,socketserver会为你处理进程管理的细节。
server = socketserver.ThreadingTCPServer(('', 2050), RequestHandler)
server.serve_forever()
详细的
socketserver
文档参见 socketserver — A framework for network servers。高级部分:
select
多路复用模块以及关于它的更高级模块asyncore.py
如果需要再学习。- 另外,困惑点:异步、同步、并行等的区别可以参阅 这篇文章。
包装套接字
- 核心函数:
makefile
,调用后,它会为我们返回下面的封装对象;然后就可以像对一个文件那样进行操作了。
>>> s.makefile()
<_io.TextIOWrapper name=7 mode='r' encoding='UTF-8'>
- 使用流重定向技术,我们可以方便地将 GUI 程序通过
socket
与非 GUI 程序通信,并且使用起来更加直观清晰。我们可以使用基本的print
和input
函数完成发送和接收,所以对于客户端来说,套接字连接在很大程度上都是无形的了。 - 要注意的是,标准流通常是可以缓冲的,所以在测试时,有些是需要
sys.stdout.flush
显示刷新的,否则发送的数据会不完整甚至可能导致死锁。 - 有关
makefile
的一个应用示例可以参见 简单的文件下载示例,涉及到的关键代码:
sockfile = sock.makefile('r')
name = sockfile.readline()