
第一部分 Part 1 网络和服务器
Chapter 1 第1章 Python网络编程模块
作为游戏和软件开发者,不管你是PC客户端或是服务器端程序员,还是手机、Pad移动端程序员,甚至Web程序员,无时不刻都在和网络编程打交道,而日新月异的网络技术以及呈爆炸式增长的应用速度,对我们的编程和业务能力进行了一轮又一轮的轰炸和挑战,幸好在现今开源和大环境的支援下,无数技术前辈和程序员们,以及硬件厂商和软件公司,对所有业务和技术进行了拓展和分工,使得我们在编程的时候,大部分时间只需要将注意力集中在业务的需求和核心模块的开发上,而不需要关注细枝末节的实现,以及对底层系统的分析和理解上,当然这是在大部分情况下。
本书选择Python作为第一编程语言,一是为了能让读者更好地理解和应对实际编程中的问题;二是CPython在标准C函数库的封装上基本沿用了C的写法和参数,对于程序员理解底层也有相当好的帮助;三是Python作为伪代码,对编程的模型及对事务做分析和解释也是比较清晰的一种语法结构,就算读者没有Python编程经验,在看完本书后也将对Python有一定程度的理解,只要稍作学习,就能够编写高效和适应业务的代码。
我们将在第1章介绍Python网络的编程模块,包含较为底层的Socket模块、使用HTTP的urllib,事件驱动的模型和框架,以及各种针对网络编程的方法。
1.1 Python Socket
在开始之前,先对Python和本书所对应的版本号做一个定义。
对于Python来说,最基础的Python实作解释器是使用C语言编写的,也就是说,在普通人的观念中,Python就等于CPython。事实上,这样的定义虽说是约定俗成,但并不精确,作为语言来说,用任何语言去编写其实作的版本都是可行的,Python除了CPython之外,也有Jython、IronPython、PyPy等其他实作版本,而Jython规避了原生C语言带来的多线程问题,这将在后续章节中进行详细剖析。在本书的所有章节中,除非特殊说明,一般使用CPython作为本书的编码版本,而为了照顾绝大多数程序员,以及兼容以往的代码包,Python的大版本号则定为2.7。
Python的Socket库,是Python网络编程中经常用到的一类模块,而Python则是提供了两个模块,一个是标准的Socket,一个是SocketServer。其中使用SocketServer的人不太多,原因是Socket模块已经足够完成任务,而SocketServer则更像是Ruby语言中所提供的封装好的TCPServer、UDPServer等,让人在编写代码的同时更简单和专注。为了能在本章中深入理解Python语言的具体语法结构以及较为底层的Socket接口结构,我们选择Socket模块作为专门讲解的部分。
1.1.1 Socket套接字
Python不少底层模块的封装基本保持了C原型的参数和组织结构,Socket模块也不例外,我们先来看一看如何引入一个Socket模块。
import socket
这样就完成了Socket模块的引入。
当然按照语法,你也可以这样引入模块:
from socket import *
当Python解释器看到import语句后,将会自动从Python安装目录的lib目录寻找需要被import的模块文件。
接下来我们要创建和销毁一个Socket套接字,在开始之前,我们来看一下Python的Socket模块的函数原型:
socket (family, type[, protocal])
也就是使用给定的地址族、套接字类型和协议编号来创建套接字。其中Socket地址族和Socket类型如表1-1所示。
表1-1 Socket套接字地址族和类型表

在下面的章节中,我们将逐步详细地描述有关Python Socket的知识点,以及一些基础知识。
1.1.2 SOCK_STREAM、SOCK_DGRAM
在Python Socket中,有两个最基本的参数类型,socket.SOCK_STREAM和socket. SOCK_DGRAM,这两个参数有什么作用呢?
首先,SOCK_STREAM指定的是数据流Socket,一般指的是TCP/IP,而SOCK_DGRAM的DGRAM英文全称指的是datagrams,也就是数据报的形式,没有保障的面向消息的Socket,一般指的是UDP。
而SOCK_RAW则是指原始套接字编程,它可以接收数据帧或者数据包,可以用来监听网络的流量和进行数据包分析。
好了,介绍完基础的参数后,让我们开始创建一个Socket:
s_handle = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s_handle是Socket模块初始化后返回的对象,初始化的参数是AF_INET和SOCK_STREAM,说明初始化的网络参数是TCP协议,虽然可以这么定义,但是在Socket初始化的第三个参数中,我们可以选择IPPROTO_TCP或IPPROTO_RAW来指定所使用的协议,当然你也可以忽略第三个参数。不过,如果第二个参数填的是SOCK_RAW,在初始化之前,你可以使用getprotobyname函数来得到第三个参数指定所使用的协议。将getprotobyname的值传递给Socket的第三个参数,如下所示:
protocal = socket.getprotobyname(‘imcp') s_handle = socket.socket(socket.AF_INET, socket.SOCK_RAW, protocal)
现在我们来看一下如何销毁socket对象。
s_handle.close()
非常简单直接。
接下来我们要做的,就是在刚才初始化的Socket代码后,编写监听、接收、发送等一系列操作,这样才能称为完整的网络程序。我们这里展开讲述的是如何编写最基础的网络服务器部分的代码,以及设置非阻塞的传输模式。
1.1.3 阻塞和非阻塞模式
在整本书中,我们会不间断地介绍阻塞和非阻塞模式在各种应用和网络编程中所扮演的角色,当然在本节中,我们编写的是非阻塞模式。
通俗地讲,阻塞模式指的是在操作系统进行I/O操作完成前,执行的操作函数和内容一直会等待I/O操作完成而不会立刻返回,该函数的执行线程会阻塞在当前函数;而非阻塞模式则相反,执行函数将会立即返回而不管I/O操作是否完成,该函数线程会继续往下执行命令。
设置非阻塞代码有不少方法,最基础的是将socket handle设置为:
s_handle.setblocking(False)
还有第二种方法,结果是近似的,那就是设置超时时间:
s_handle.settimeout(timeout)
也就是为Socket操作设置一个超时时间,在该超时时间内阻塞等待消息,否则继续接着往下跑代码。
当然,要让服务器端代码跑起来是“非阻塞”的感觉,有不少方案可以选择,比如使用select函数来实现非阻塞,在select函数中设置超时时间。我们来考虑以下代码:
while 1: in, out, err = select.select([s_handle, ], [], [], timeout) if len(in) ! = 0: cli_sock, cli_addr = s_handle.accept() in_cli, out_cli, err_cli = select.select([cli_sock, ], [], [], client_timeout) if len(in_cli) ! = 0: buf = cli_sock.recv(buffer_size) if len(buf) >0: do_something cli_sock.close()
select是一个直接访问底层操作系统的函数,它的作用是监控套接字、文件、管道,等待I/O完成,当I/O有读写或者异常产生的时候,它就会捕捉该信息并返回一些值。
在上述代码中,第一次的select函数在timeout的时间段内等待客户端Socket进入,如果在该时间段内有客户端Socket进入的话(返回值in句柄,也就是返回socket可读),则进行accept操作,accept函数的用途为接收一个Socket连接,接收成功后,返回值为一个Python pair,新的socket object以及一个address地址。
于是,在接收到新的socket object后,继续进行select操作,如果返回的Socket可写,则开始接收从客户端传过来的数据。其他细致的Socket函数我们将在后续章节进行讨论。
小结
对于设置非阻塞模式,我们可以通过setblocking函数、settimeout函数进行前期的非阻塞设置,也可以通过select来对参数进行超时设置从而达到非阻塞的目的,当然我们还可以设置接收和发送的超时操作,具体内容将在后续章节说明。
1.2 服务器端其他Socket方法
前面几个小节阐述了在Python中进行Socket编程的最基础的概念和手段,在接下来的章节中我们将更细致地讲解在Python的Socket编程中所需要用到的知识点和基础逻辑表达代码,这将为本书后续所涉及的知识和描述起到铺垫的作用。
一套完整的Socket服务器端程序,除了初始化和销毁Socket句柄之外,还包括最基本的接收和发送功能,以及其中所需要的逻辑处理部分,当然为了保证程序的健壮性和业务流程的顺畅运作,在代码中使用多线程还是多进程也是需要考量的,接下来,我们将介绍更细致的接收和发送部分。
1.2.1 bind和listen
在Socket服务器程序中,当代码初始化完成并得到Socket句柄后,接下来就是设置阻塞和非阻塞方式,当然通过前面几节的学习,我们知道,在Python中,除了标准的setblocking和settimeout函数外,还可以使用select函数的超时来模拟非阻塞的方式。接下来,我们考虑下面的代码:
host = "" port = 4096 s_handle.bind((host, port)) s_handle.listen(5) print "start..." while 1: do_something
在一段网络服务器代码中,开始运作逻辑之前,必须要保证网络地址和端口的绑定。所谓绑定,就是为了确保Socket和本地的地址及端口关联在一起,否则服务器程序无法得知需要绑定的地址和端口,就更不用谈接收客户端发来的数据了。而客户端就不需要这一步操作,因为客户端本身就是与服务器端连接(connect)在一起的。而Python的bind操作需要填入的参数是(ip, port),如果绑定的地址为0.0.0.0,则绑定本机网卡上所有IP的地址。
所以下面的代码是为了保证地址和端口的绑定。
s_handle.bind((host, port))
而listen是为了保证监听所绑定的地址和端口所传来的数据,将主动连接Socket变为被动连接Socket,也就是让它变为“服务器”。请注意,Python中listen函数的参数为backlog。所谓backlog就是指在操作系统内核中,在进程空间维护的请求队列的大小,这个队列指的是操作系统监视跟踪这些已完成的但程序进程还没有进行处理或正在处理的连接(listen后将之递交给select和accept处理),所以listen必须指定其容纳队列的大小,它的值至少为0(Python 2.7以下版本这个值为1),在内部实现上,这个值为backlog+1,也就是至少允许一个用户接入,而最大值依赖操作系统内部实现,在Python 2.7中通常为5。
所以下面这行代码的意义在于允许处理的(未完成或者正在完成的)队列值为6(Python 2.7以上)。
s_handle.listen(5)
再次回到select的功能,我们在之前的小节中对select功能进行了描述,然而select有许多替代的选择,比如poll和epoll等,所以我们不使用select直接accept也是可行的,使用select是为了检查并保证新的连接句柄资源是否用尽,检测句柄是否归还,连接是否超时等,而直接进行accept并非不可行,但可能出现资源耗尽而没有检测到的问题。
1.2.2 setsockopt
我们常常会在很多开源代码里看到setsockopt函数的使用,这个函数到底是做什么的呢?
在本书的1.1节中,我们介绍了Socket初始化、销毁的方法,以及Socket设置的一些参数,但是当你的Socket参数不够设置了怎么办?这个时候,就需要setsockopt参数登场了。
Python的setsockopt接受三个参数:level、optname、value。
第一个参数level指的是定义的层级,其中包括:SOL_SOCKET,指的是基本套接字接口;IPPROTO_IP,指的是IPⅤ4套接字接口;IPPROTO_IPⅤ6,指的是IPⅤ6套接字接口;IPPROTO_TCP,指的是TCP套接字接口。
第二个参数optname指的是选项名称,这些选项名称对于不同的操作系统也会有些许不同,如果level参数选择了基本套接字接口的话,那么一些常用的选项如表1-2所示。
表1-2 optname的常用参数和具体意义

当然在level参数选择了IPPROTO_IP或者其他值的时候,optname的值又会有不同的选项,你可以使用搜索引擎或者UNIⅩ的manual手册查找具体参数。
最后一个参数是value,功能为设置optname选项的值。
现在我们来考虑以下代码:
s_handle=socket.socket(socket.AF_INET, socket.SOCK_STREAM) s_handle.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
代码中,在初始化完毕后,将Socket的选项设置为SO_REUSEADDR,这说明我们需要Socket句柄关闭后能立刻被重用。
我们再来考虑下列代码:
s_handle.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, SEND_BUF_SIZE) s_handle.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, RECV_BUF_SIZE) current_buf_size = s_handle.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
在上述代码中,我们看到代码设置了两次setsockopt,其中一次为SO_SNDBUF,一次为SO_RECⅤBUF,这就代表了我们将Socket接收和发送内容的缓冲区大小从系统的默认值替换为我们自己定义的值,而最后的getsockopt函数则是将设置的值取回来。
小结
在服务器代码中,为了保证端口和地址的绑定操作,我们要使用bind函数来进行操作,如果原始Socket参数不够设置,则应使用setsockopt函数来设置更多的内容。我们还将在后续章节看到这些内容的使用,以及更多内容和函数的整合,最终形成一套完整的服务器流程。
1.3 客户端Socket
之所以要在本节提到客户端的Socket方法是因为在本书接下来的部分中,我们将会提到更多和更高阶层次的Python方法、网络编程和架构的实用技术,从这点来讲,涉及客户端和服务器端的通信知识是必不可少的,所以在本章我们将会提及一些客户端特有的Socket编程知识。
connect方法
在Python Socket客户端编程比在服务器端编程所需要控制的参数和代码简单太多了。最基础最简单的版本,除了初始化Socket句柄之外,就只有connect了,我们来考虑下列代码:
import socket s_handle = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s_handle.connect(("www.msn.com", 80))
在这段代码中,除了前面几个小节介绍的初始化Socket函数之外,还有一段connect。
connect接收一个tuple参数,分别为地址和端口,如果连接出错,则返回一个Socket error。connect还有一个兄弟版本,名为connect_ex,传入的参数也是接收一个tuple,但是返回值不同,它返回的是一个C层级的返回值,如果成功则返回一个0,而如果失败则会返回一个Socket系列的errno错误号,比如10060,或者它会抛出一个异常,比如host not found,11001异常等。
当你不知道需要连接的服务器的默认端口号是什么的时候,可以通过getservbyname函数来获得,考虑下列代码:
import socket s_handle = socket.socket(socket.AF_INET, socket.SOCK_STREAM) port = socket.getservbyname('http', 'tcp') s_handle.connect(('www.msn.com', port))
它的作用是查询所需要的服务名称和协议获取端口号。
当然,除了connect函数外,Python还提供了不少便于使用的函数供我们查询和使用,比如gethostbyname和gethostbyname_ex。
gethostbyname返回所需要查询的主机地址,而gethostbyname_ex则更加强大,它返回的信息除了所查询主机的IP和名称外,还有主机名列表和主机IP地址列表等信息,我们考虑下面的简单代码:
socket.gethostbyname_ex('www.microsoft.com')
其运行结果看起来可能会是这样:
('e2847.ca.s.tl88.net', ['www.microsoft.com', 'www.microsoft.com-c-2.edgekey. net', 'www.microsoft.com-c-2.edgekey.net.globalredir.akadns.net'], ['104.95.198.193'])
小结
在客户端编程中,connect方法是一定会用到的一个函数,gethostbyname将会返回所需要查询的地址等信息。
在Python编程中,我们应该尽可能地利用Python语言提供的一切便利方法来做好编程的准备工作,之后只要集中精力做好自己的事情就可以了。
1.4 通用的Socket方法
在上面几个小节中,我们不仅介绍了Socket控制函数以及客户端的连接函数,还介绍了阻塞和非阻塞的几种方法,当然这些都只是建立在代码的理论模型上,所有代码都还没有进行实际调试和运行,只有在具体的项目进行运作的时候才知道哪些在应用中会有问题,并该如何调整。
在进入更深层次的介绍和内容之前,我们来看一看通用的Socket方法。所谓通用,就是指客户端和服务器端都会用到的Socket方法是通用的,并非只有一部分能用。
1.4.1 recv和send
接收和发送是每个网络程序几乎必须要做的内容,除非只是连接服务器,那就不需要recv和send。
recv和send两个函数也是C标准函数,是供TCP协议编程时使用的发送和接收函数,UDP部分我们将在后面一个小节介绍。
我们先来看一下这两个函数的Python原型,首先是recv:
recv(bufsize[, flags])
Python的recv函数接收Socket传过来的内容,其中bufsize为字符串缓冲区大小,返回的是字符串,而flag则是指定有关消息的其他值,具体可以通过UNIⅩ的manual手册的recv(2)查询到,其中包含:MSG_NOWAIT、MSG_ERRQUEUE、MSG_OOB、MSG_PEEK等参数。
我们再来看看send:
send(string[, flags])
与recv参数相似,send函数接收一串待发送的字符串,返回被发送后的字节数,根据发送字节数的多少,该字节数有可能小于string字符串数量(没有一次性发送完)。
因此,在这里,Python非常人性化地在标准Socket库的基础上添加了如下的函数:
sendall(string[, flags])
该函数保证一次性将字符串全部传完,如果出错,将抛出一个异常。我们直接看下面的代码片段:
...... cli_handle, cli_addr = s_handle.accept() data = s_handle.recv(max_size) cli_handle.sendall('i am here') ......
这只是一个简单的示例,但是我们可以从这里看到,sendall函数取代了send函数的用法,当然为了保险起见,应该将代码加上异常处理。
1.4.2 recvfrom和sendto
在网络编程中,除了TCP模式的网络传输模式外,还有UDP这样面向无连接的网络编程模型,这时候就需要recvfrom和sendto函数了。当然recvfrom和sendto并不仅仅应用于UDP,它们也可以用于TCP的编程。我们先来看看它们在Python中的函数原型。
recvfrom(bufsize[, flags]) sendto(string, address) sendto(string, flags, address)
我们看到recvfrom的函数参数和recv如出一辙,唯一不同的是返回值,recvfrom返回的有两个参数,string和address, string是接收到的内容,address是发送端Socket的地址。
再来看看sendto, sendto有两个相同名字的重载函数,其中第二个函数中间多了一个flags参数,当我们赋予最后一个address参数以Socket地址的时候,第二个参数flags的内容将和上面一个小节的recv参数的内容相同(比如MSG_NOWAIT、MSG_ERRQUEUE、MSG_OOB、MSG_PEEK,等等),我们同样可以通过查询manual的recv(2)得到flags的内容。
下面我们来看一下代码示例:
import socket, sys addr=('<broadcast>',2233) s_handle=socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s_handle.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST,1) while True: data=s_handle.recvfrom(1024) if data: s_handle.sendto("my message", addr) s_handle.close()
可以看到,在客户端的模式下,我们并不需要进行connect操作,由于是UDP模式的网络套接字,其面向的是无连接的操作,而broadcast则标明这是广播的代码。
由于我们在写TCP连接的时候,recv和send已经知道目标机器的Socket地址,所以不需要sendto写明address,但是若在UDP中使用recv,返回值中并没有address信息,所以在回复的时候,sendto也不知道回复给谁,可见,在UDP编程中使用recvfrom是最合适的选择。
小结
recv和send, recvfrom和sendto是通用的Socket方法,这些方法是组成基础Socket代码所必须使用的,包括客户端和服务器端,在UDP中,由于目标地址并不明确(相对于TCP端口和地址已经绑定而言),所以要选择recvfrom和sendto方法,当然TCP方式也可以用recvfrom和sendto,只是有点多此一举。
1.5 SimpleHTTPServer和BaseHTTPServer
在谈到使用Python编写Web服务的代码之前,很多人都会想到大名鼎鼎的Django、Flask、Pyramid、Tornado,等等,这些框架能编写非常好看、强大和复杂的Web内容,然而在本节里,我们将要谈的是最基础的HTTP服务,即Python自带的HTTP服务的框架,而并非是HTTP服务+Web内容。虽然在Python 3中,已经将BaseHTTPServer和SimpleHTTPServer都合并入了http.server框架,然而在2.7版本中,我们将分别学习这两个框架的内容和实际作用。
我们为什么要学习这两个框架?因为在后续章节中将会谈到简易的、低实时性的游戏服务器,它们都可以使用HTTP服务来完成。而作为最基础的HTTP框架,加上前面几节学到的Socket库的内容,我们将能够快速搭建和完成游戏服务器的内容,甚至可以和微信等移动端接口实现对接。
1.5.1 SimpleHTTPServer
SimpleHTTPServer包含了SimpleHTTPRequestHandle类,该类可以执行GET以及一些HTTP头部的请求。我们可以通过命令行来呼叫SimpleHTTPServer,指定HTTP的侦听接口,来达到建立一台简易HTTP服务器的目的,在运行命令行的当前目录下,如果目录下有index.html文件的话,这个文件就会直接成为默认页面,如果没有这个页面,则会在浏览器中列出当前目录的所有内容。
当我们成功在命令行下运行完下面代码的时候,会看到终端上会显示出这样的字符,如图1-1所示。
python -m SimpleHTTPServer 88

图1-1 Python命令行启动简易HTTP服务
如果运行的目录下没有默认页面文件index.html的话,则会显示当前目录下的所有目录和文件,如图1-2所示。

图1-2 SimpleHTTPServer在浏览器中的目录内容列表
当然我们可以更进一步地编写Python代码,这将在后续的章节介绍。
1.5.2 BaseHTTPServer
BaseHTTPServer提供了Web服务(HTTPServer)和处理器的类(BaseHTTPRequestHandler)。其中HTTPServer是SocketServer.TCPServer的子类。我们来看一下Python文档中关于BaseHTTPServer的一个示例:
def run(server_class=BaseHTTPServer.HTTPServer, handler_class=BaseHTTPServer.BaseHTTPRequestHandler): server_address = ('', 8000) httpd = server_class(server_address, handler_class) httpd.serve_forever() run()
该示例定义了一个默认服务的类和句柄类,都继承自BaseHTTPServer本身,我们可以看到打开的端口是8000,然而由于没有任何实现代码,所以当运行这段代码的时候,浏览器会告诉你错误代码为501,该服务器不支持GET操作。
现在让我们将SimpleHTTPServer和BaseHTTPServer结合起来,对上述的run函数调用方法稍作修改,用SimpleHTTPServer替代handle_class,在不改变原有代码的基础上来看以下代码:
run(BaseHTTPServer.HTTPServer, SimpleHTTPServer.SimpleHTTPRequestHandler)
当我们将handle_class替换为标准的SimpleHTTPServer之后,获得的结果就是如图1-2所示的列表,因为我们没有在自己的继承类中做任何事情,所以这个简单的HTTPServer只能当作一个demo服务来使用。
那我们应该怎么做呢?
首先尝试重写一份BaseHTTPRequestHandler子类的do_GET方法,我们将在随后贴上的示例代码中看到对run函数的一些修改,以达到能顺利浏览内容的目的。
class SampleGet(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): contents = "Hello World" enc="UTF-8" contents=contents.encode(enc) self.send_response(200) self.send_header("Content-type", "text/html; charset=UTF-8") self.send_header("Content-Length", str(len(contents))) self.end_headers() self.wfile.write(contents) run(handler_class=SampleGet)
仔细看一下这个SimpleGet类,该类继承自BaseHTTPRequestHandler,并重写了do_GET方法,Python将会在运行到GET方法的时候自动呼叫这个函数。contents是返回客户端浏览器的数据,当然在返回数据之前,我们需要设置一系列东西,包括编码方式、数值为200的返回值,以及HTML的头部信息。在最后,我们看到往self.wfile写入contents, wfile是在BaseHTTPRequestHandler中定义的一个文件对象,该对象是发送给浏览器的file内容,当然与之对应的有rfile,即从浏览器接收回来的内容。于是在最后写入contents的时候,浏览器接收到成功的code 200以及一系列HTML头信息后,就开始显示contents的内容了,当然如果这个时候你读取一个纯粹的HTML文件写入wfile,效果就是显示一段网页了。
还有do_POST函数,让我们可以处理POST方法,等等,我们可以重写函数来定义自己需要的内容。
小结
BaseHTTPServer和SimpleHTTPServer是最基础的Python框架,我们可以借助其所提供的内容来编写Web服务。
我们专门抛出这一章来讲解SimpleHTTPServer和BaseHTTPServer,是为后面的内容做好铺垫,本书后面会讲到HTTP形式的服务器框架(当然Java程序员可能会使用Tomcat,而Python程序员则需要这样的框架和逻辑),以便对客户端建立连接,这样HTTP形式的弱连接服务器也就可以顺理成章地编写下去了。
1.6 urllib和urllib2
这一节我们花一点小篇幅来介绍urllib和urllib2,这两个库在Python 3.x中已经被合并为urllib,但在我们所讲解的2.7版本中,它们还是两个独立的库。介绍这两个库的目的是为了我们能更好地编写上一节所提到的HTTP框架。用到HTTP框架,几乎一定会用到urllib和urllib2,分析url、提交HTTP请求等都离不开这些库。
这两个库的侧重点不同,urllib做的是请求URL相关的操作和内容,最主要是进行HTTP的URL的操作,然而urllib只能接收一个URL,可以进行urlencode方法(urlencode可以GET查询的字符串),而urllib2可以接受Request的对象,然后设置URL的头。
我们先来讲解urllib库,urllib中有许多有用的方法,我们会拣几种常用的库来进行讲解。
1.6.1 urllib.urlopen和urllib2.urlopen
urlopen用于操作远程获取到的url内容,它的原型是:
urllib.urlopen(url, data=None, proxies=None)
该函数的返回值为一个对象,返回的对象可以进行类文件的操作,比如read、readline、readlines、fileno、close、info、getcode和geturl这些操作。
然而urllib2也有一个相同的urlopen函数,该函数是这么定义的:
urllib2.urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT)
urllib2的urlopen函数将urllib的urlopen中的proxies修改为连接超时的参数。这个设置对于服务器代码来说是非常有用的,当我们的服务器逻辑需要访问目标机器获取页面数据的时候(比如JS数据的url请求),如果在指定时间内获取不到,就会有超时的错误出现,节省CPU的时间,让服务器的逻辑更稳定地运行下去。当然urllib2也可以自己设置proxy。不管是urllib还是urllib2,除了第一个参数外,后面这两个参数使用的频率都不太高。
我们来看下urllib2的urlopen和一系列函数的用法:
import urllib2 request = urllib2.Request(uri) request.add_header('User-Agent', 'mozilla') response = urllib2.urlopen(request, timeout=10) page = response.read()
在urlopen之前,将请求加上HTTP头信息,同时第二个参数设置了timeout值,这样就可以对Web服务器发送请求得到需要的内容,现在许多服务器都会走微信或者微博的JS接口,通过这种做法,使用urllib2做转发和接收结果是非常方便的。
我们来看下urllib2是如何设置proxy参数的:
def sample(uri, enable_p): proxy_handler = urllib2.ProxyHandler({"http" : 'http://127.0.0.1:8087'}) no_proxy_handler = urllib2.ProxyHandler({}) if enable_p: opener = urllib2.build_opener(proxy_handler) else: opener = urllib2.build_opener(no_proxy_handler) urllib2.install_opener(opener) request = urllib2.Request(uri) request.add_header('User-Agent', 'mozilla') response = urllib2.urlopen(request) print response.read() sample('http://www.google.com', True)
这是一段完整的通过代理服务器获取Google网站的代码,我们可以看到,urllib2可以设置ProxyHandler来定制proxy服务,代码很容易理解,设置完proxy之后,就可以使用build_opener和install_opener来设置urllib2的全局环境变量。
我们将在下一小节讲述更重要的urllib2.Request函数。
1.6.2 urllib2中的GET和POST方法
如果服务器需要经过某种登录的网页,则需要用到基础的GET或者POST方式,在网络编程中,这种方式便于和远程网页服务器交互,包括微信的JS服务以及游戏中的登录模块。urllib和urllib2配合字典的请求,就可以组成浏览器的GET和POST请求,然后使用Request给远程服务器,我们先来看一下GET方式:
import urllib, urllib2 req_data={} req_data['username'] = "myaccount@163.com" req_data['password']="mypassword" url_data = urllib.urlencode(req_data) url = "http://www.some_domain.com/login" full_url = url + "? "+url_data request = urllib2.Request(full_url) response = urllib2.urlopen(request) print response.read()
我们看到,GET方式就是使用url+"? "的形式将GET参数连接起来,而urlencode的意义就在于将字典编码为网页的字符串格式。以此类推,再来看看POST方法:
import urllib, urllib2 req_data={} req_data['username'] = "myaccount@163.com" req_data['password']="mypassword" url_data = urllib.urlencode(req_data) url = "http://www.some_domain.com/login" request = urllib2.Request(full_url, url_data) response = urllib2.urlopen(request) print response.read()
POST形式的代码直接将字典编码后的字符串填入Request函数的第二个参数,我们来看一下Request函数的原型:
Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False)
后续几个参数可以直接填入HTTP头信息等,我们可以用这些参数方便灵活地使用Request函数。
在游戏服务器中,特别是现今的手机游戏中,我们将会有很大可能用到微信、微博等第三方登录接口,而urllib和urllib2则是不可或缺的获取url内容的标准配备,常备无患。
小结
通过1.5节和1.6节的学习可以看到,结合urllib、urllib2,以及BaseHTTPServer、SimpleHTTPServer,我们组成了完整的HTTP服务器和HTTP请求服务器,这对于做一些HTTP之间的数据交互是非常方便的,下面的小节,我们将着重介绍一个重量级的框架Twisted,它不仅能提供Web服务,更能显示Web内容以及提交Web请求。
1.7 事件驱动框架Twisted
本节我们将花一定的篇幅来讲解Twisted框架,为什么要讲解这个框架?因为Twisted是一个基于事件驱动的,集各种服务应用于一体的引擎框架。
Twisted支持多种传输和应用层的协议,包括我们之前介绍过的TCP、UDP、HTTP、SSL/TLS、IMAP、SSH、IRC和FTP。
就像Python语言,Twisted也拥有“batteries included”(拎包入住,拿来即用)的特征,它的所有协议都有客户端和服务器端的实现,也有命令行工具,可以方便地配置和部署。
我们可以在http://twistedmatrix.com下载Twisted的最新版本,当然要使用Twisted,还需要安装Zope依赖库。
所谓事件驱动(event-driven),指的是每发生一次事件(event),就呼叫一次行为(handler),而中间如何匹配每一个行为,就需要通过消息或者逻辑关系来控制事件所对应的handler。最浅显易懂的就是用户界面编程,所有用户界面的逻辑交互和反馈都需要通过某个事件来驱动,比如鼠标点击事件,点击了某个菜单后,界面从消息队列里获取具体事件消息,发现是鼠标点击,则呼叫鼠标事件所对应的handler,当然这个handler可以是用户自定义的,这就给事件驱动编程带来了灵活性和扩展性。
Twisted框架是基于编程模式中的Reactor模式来构建的,要搞清楚Twisted,就必须明白什么是Reactor。
1.7.1 Reactor模式
Reactor模式简单地说,是拥有一个或者多个输入的源头,拥有一个服务控制器(Service Handler)和多个请求控制器(Request Handler),服务控制器会将输入的事件请求按照具体事件消息或者规则分发给请求控制器。
我们可以用生活中的场景描述Reactor模式,举个例子,一个办理某卡片的窗口,客户在办理窗口前填写表格(注册输入源),填写完毕后,排队交给窗口(提交给Service Handler),由窗口将内容和表格提交给制作卡片的办公室(Request Handler),并通知客户内容已经提交。当Request Handler制作完卡片后,通知Service Handler,由Service Handler通过呼叫客户排队号码通知客户(输入源)得到结果(卡片)。
现在,我们来看一个Twisted的示例程序:
from twisted.internet.protocol import Factory from twisted.protocols.basic import LineReceiver from twisted.internet import reactor class Chat(LineReceiver): def __init__(self, users): self.users = users self.name = None self.state = "GETNAME" def connectionMade(self): self.sendLine("What's your name? ") def connectionLost(self, reason): if self.name in self.users: del self.users[self.name] def lineReceived(self, line): if self.state == "GETNAME": self.handle_GETNAME(line) else: self.handle_CHAT(line) def handle_GETNAME(self, name): if name in self.users: self.sendLine("Name taken, please choose another.") return self.sendLine("Welcome, %s! " % (name, )) self.name = name self.users[name] = self self.state = "CHAT" def handle_CHAT(self, message): message = "<%s> %s" % (self.name, message) for name, protocol in self.users.iteritems(): if protocol ! = self: protocol.sendLine(message) class ChatFactory(Factory): def __init__(self): self.users = {} # maps user names to Chat instances def buildProtocol(self, addr): return Chat(self.users) reactor.listenTCP(8123, ChatFactory()) reactor.run()
这是使用Twisted编写的TCP服务器代码,这段程序可以通过一个telnet与该程序进行TCP服务的连接。
连接成功后,服务器会发送一段“What's your name”的问题,当客户端输入名字后,服务器会发送一段“Welcome,你的名字!”的回复。
从代码上看,我们看到了Twisted最基础的用法,以及Reactor模式的好处。首先我们几乎不需要关心循环以及各种复杂网络操作,相对于前面几节的网络程序编写流程,Twisted框架直接就进入了核心内容,当我们把注意力集中在逻辑上的时候,只需要关注几个状态点就可以了,这些状态就是事件模式中Reactor所对应的机制。其次,可以看到,在lineReceived函数中,随着状态的不同,选择不同的执行函数,这就是事件编程的核心基础,当然读者朋友可能会说,难道事件编程就是简单的if else判断吗?当然不仅如此,事件编程不仅仅是逻辑判断,还有事件处理函数,消息或者信号机制等,这一切组成了事件编程。
有很多人将事件编程和单线程编程、多线程编程并列摆放,等量齐观,认为这是第三种编程方式,我并不同意这样的观点,严格地说,事件编程更像是一种编程的表达方式(pattern)和解决思维,而单线程、多线程是直接与底层相关的硬性的编码方式,虽然这确实是一种解决思路,但和事件编程不同的是,事件编程可以是单线程,也可以是多线程的,而单线程、多线程本身则没有任何选择。
1.7.2 run、stop和callLater
Twisted的reactor是使用reactor.run开始呼叫的,如果你需要切换不同的reactor版本(默认版本是使用select函数的,select函数的作用在前面已经详细阐述过),你可以在import默认reactor之前安装其他版本,比如下面的示例:
from twited.internet import pollreactor pollreactor.install() from twisted.internet import reactor reactor.run()
这就将备选的poll方式安装到了代码当中。
由于Twisted的reactor是单线程的,所以在定义事件函数(callback function)的时候,我们要做到函数做完事情就立刻返回,否则就会阻塞。
下面看一个将run、stop、callLater函数结合在一起运用的例子:
class Countdown(object): counter = 5 def count(self): if self.counter == 0: reactor.stop() else: print self.counter, '...' self.counter -= 1 reactor.callLater(1, self.count) from twisted.internet import reactor reactor.callWhenRunning(Countdown().count) print 'Start! ' reactor.run() print 'Stop! '
在这段代码里,callWhenRunning和callLater函数将会向reactor注册一个事件函数(callback function),这个事件函数就是class Countdown中的count,后面的第一个参数是延迟时间,也就是延迟一秒,每隔一秒运行一次count函数,直到counter变为0,最后stop。有兴趣的读者可以试试,同样可以在官方的资料找到原始代码。
1.7.3 Transports、Protocols、Protocol Factoies以及Deferred
Transports代表着一个收发字节的单个连接,Transports也是网络连接层的一个抽象概念,比如一条TCP连接,UDP连接的抽象。
Protocols属于网络应用协议的一种抽象,而所对应的协议,可以是FTP、IMAP等。Protocols协议使用的连接就是Transports的每一个实例。
如果我们需要自己定义Protocols和Transports连接起来,那么就需要用到Protocol Factories方法了,这种方法让我们可以将网络连接和自己定义的Protocols合并起来,我们在1.7.1节的实例程序已经看到了这样的用法。
关于Deferred, Twisted使用Deferred来管理事件函数的序列,解决了单线程阻塞的问题(事实上,Deferred也是使用select等方式来模拟单线程多任务系统,所以在编程当中,程序员最好不要将阻塞代码写入事件回调函数),程序将一串函数添加到Deferred,这其中有两串函数序列被填入,第一串是callback,第二串是errback,(也就是出错时候的事件函数,当出现错误的时候,会调用对应序列的errback)。
from twisted.web.client import getPage from twisted.internet import reactor def errorHandler(error): print "An error has occurred: <%s>" % str(error) reactor.stop() def printContents(contents): print contents reactor.stop() deferred = getPage('http://twistedmatrix.com/does-not-exist') deferred.addCallback(printContents) deferred.addErrback(errorHandler) reactor.run()
可以看到在上述代码的运行过程中,当页面抓取不到的时候,将会抛出一个Failure异常,这是一个Twisted内部的异常类,当错误发生的时候,将会调用Deferred中注册的errback事件函数。
我们还看到,相比前面几小节,Twisted获取一个网页的内容根本不需要urllib等繁琐的操作,直接getPage就可以了。
Twisted是一套非常庞大和高效的框架,读者如果有兴趣,可以在其官网看到它的文档,以及不少使用Twisted编写的服务和应用,都可以拿来做源码研究。
小结
在本章中,我们学习了Python中原生的网络编程,也学习了Twisted框架的用法,在后续的章节中,我们会讲解加密、数据存储和实战级别的服务器编写,本章中的知识会在后续章节中得到进一步的应用。