游戏服务器架构与优化(txt+pdf+epub+mobi电子书下载)


发布时间:2021-03-30 11:29:26

点击下载

作者:蔡能

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

游戏服务器架构与优化

游戏服务器架构与优化试读:

前言

为什么要写这本书

在人们的观念中,游戏行业是一个既火爆又赚钱的行业,而游戏开发在非游戏开发人员的心里,则是比较神奇的存在。隔行如隔山,游戏究竟是如何开发的?游戏服务器究竟又是怎样工作的?

软件服务器后台开发人员对于游戏服务器的开发充满兴趣,而对于本身就是游戏开发者的人来说,对于更深层次的服务器逻辑,各种类型游戏服务器的区分和编写,各种平台的优化,都需要进行更进一步的了解。

作为游戏开发人员,H5 Game和Flash Game之间通信的差异在哪里?MMORPG的服务器究竟该怎么编写?HTTPS通信和普通HTTPS协议有何区分?如何进行分布式编程?如何提高游戏服务器的高并发量?这些都是需要重点关心的问题。

不仅仅是游戏开发人员,作为普通后台开发人员,配置负载均衡,利用后台程序之间的相互通信进行负载,也是比较关心的问题。现今琳琅满目的开发包、开发工具,让这些事情变得事半功倍,在这样的情况下,如何利用现有的工具来进行配置,提高并发量,或者如何利用分布式计算来提高业务效率、工作效率,尽其所能地发挥工具的最大效率,也成为我们必须面对并解决的问题。

对于运维人员来说,面对的不仅仅是服务于程序员的各种后台配置,也需要集群配置、HTTPS,甚至各种类型数据库的配置。

针对以上提到的所有问题,本书会进行深入挖掘。

本书中99%使用Python语言进行代码或者伪代码的编写和说明,为什么使用Python?这是因为虽然开发游戏服务器的语言多种多样,包括C/C++、Python、Node.JS等,但Python是目前上升热度最快,且用户呈直线增长的一种语言,其简洁的特性,就算是非专业程序员都可以很快掌握,学习成本不高,且能达到较为满意的产出。

从国内的情况来看,关于Python或游戏开发的书籍都很多,但是将两者合二为一进行讲解和说明的却并不多见。笔者在几十年的工作过程当中,经历了各种各样的项目,虽然大部分属于游戏行业,但其他行业也有涉及,比如视频类网站、人工智能、区块链等。在经历这些项目的过程中,我看到很多的团队和开发人员为了某一个项目和业务的功能进行各种细节上的取舍,或者各种没有经验一脚踩进大坑,这让我萌生编写本书的想法。我希望能尽一点微薄之力,将在游戏后台或其他后台开发过程中所经历的各种问题、优化方案及解决方案,进行一个提炼和汇总,如果能具体解决读者的各种问题,那就是本书之幸,本人之福。

读者对象

本书针对游戏服务器、应用服务器后台的优化、搭建以及应用进行讲解和介绍。为了照顾各个层面的读者,本书使用的语言是Python 2.7版本,书中并没有就Python基础规则和语法做详细介绍,因此要求读者具有基本的Python编程知识,比如Python库的导入、pip包安装工具的使用、调用库函数等基础语法知识。事实上,如果你专心花上几个小时的时间自学一下Python,就能具备Python的编程基础。

本书对读者的知识背景没有特定要求,如果你拥有了Python编程基础,看完本书应该对服务器端需要做什么、应该做什么、怎么做,有了然于胸的感觉。具体来说,本书适合以下几类读者阅读:

·游戏服务器程序员。本书的核心命题就是游戏服务器的优化,其中涉及游戏服务器的基础、搭建;Socket知识、HTTPS基础;以及使用Python进行实作和优化。无论你是游戏后台开发人员还是客户端开发人员,都可以从书中获益。

·应用服务器开发程序员。虽说游戏服务器程序员是重点,但是我们的核心议题仍然是服务器后台的开发,所以从这点来说,应用服务也是后台程序,本书在剖析游戏服务器后台编写和优化的同时,也涉及了分布式系统、集群,甚至Web服务器,或许在阅读本书后你会迸发一些灵感。

·运维工程师。事实上,只要是服务器开发,都会涉及运维的工作,而一个好的运维,必须而且一定会涉及各种脚本语言的开发和对现有系统的扩展。当一个团队中的程序员忙得不可开交的时候,一个好的运维能帮助程序员分担40%甚至更多的工作量。本书也介绍和挖掘了后台编码过程中的各种配置和搭建,比如HTTPS的配置、Websocket的Python实现等。如果你是运维工程师,或多或少会从书中汲取一些知识。

·Python程序员。Python能做什么?它也能编写游戏服务器吗?答案是肯定的。只要你做得好,搭配得当,使用Python编写游戏后台不会比其他语言差(比如Java),有时候甚至更方便。如果你是Python程序员,又希望涉猎游戏后台的开发,本书一定会对你有极大帮助。

如何阅读本书

本书内容从逻辑上共分为三大部分。

第一部分的内容包括第1~3章,主要介绍了Python中网络的基础模块、通信加密,以及一部分服务器实作。其中:

·第1章的内容包含Socket套接字的介绍和使用、基础send和recv方法、阻塞和非阻塞方式、urllib和urllib2等Python基础库的使用。

·第2章介绍了通信加密,其中包括基础的异或加密、对称和非对称加密,以及OpenSSL、SSL/TLS通信方式等的详细介绍。

·第3章开始使用Python代码编写实际可运行的服务器代码,并介绍了Websocket协议、GIL的优劣,以及Python线程池的使用。

第二部分的内容包括第4章和第5章的内容,介绍了基础内存存储和存储方案。

·第4章主要介绍了内存存储的基础内容,包括常用的几种数据库,内存与IO续写速度,同步内存数据等。

·第5章从存储方案展开,结合实例对几种常用的存储方案进行了介绍,如高并发服务器的存储方案等。

第三部分内容包括第6~12章的内容,深度挖掘了分布式、集群、MMORPG服务器类型的交互、弱连接和长连接、服务器承载方案等。在大部分章节中,都包含了:

·基本内容:有关本章所需要的知识点和内容的介绍。

·代码示例:在介绍完内容后,就会有代码示例,以解决基本内容中所提出的问题。

·示例图(表):对于基本内容或者代码示例介绍仍然不清楚的通过示例图进行更详细剖析。

·结果:运行完代码后可能获得的结果。

除了以上内容外,书中还包含以下内容,目的是对书中涉及的特定内容进行解释和说明:

·本章小结:主要章节的结尾写有“小结”,小结分为两种。

·内容小结:有关本章内容的总结。

·代码小结:对代码进行总结,让读者对所阅读的代码有更深的认识。

·粗体字:对于知识点的重要提示或者读者容易混淆和忽略的地方,使用了粗字体进行了提示。

勘误和支持

由于笔者水平有限且撰稿时间有限,书中难免会出现一些错误或者不准确的地方,恳请读者批评指正。读者可通过以下途径联系并反馈建议或意见:

·即时通信:添加个人QQ(37856)或微信(darkspycyber)反馈问题。

·电子邮件:发送email到darkspycpp@gmail.com。

致谢

在本书的撰写过程中,笔者得到了来自多方的指导、帮助和支持。

首先要感谢的是机械工业出版社华章公司的副总编辑杨福川老师。本书是笔者的处女作,杨老师在起初定内容和目录的时候,不辞辛劳,耐心地给笔者这个完全不明白如何系统写书的人相当多的指点和帮助,为此书的撰写提供了方向和思路指导。

其次要感谢我的良师益友赖永浩,即《编写高质量代码:改善Python程序的91个建议》的作者。他给了我极大的帮助和鼓励,在Python领域他几乎是无敌的(笑),能和他共事并且一路走来非常地荣幸和开心。

再次,要感谢在各个项目和工作中提供宝贵经验和支持的良师益友和工作伙伴们,他们是(排名不分先后):王永梅、吴东源、陈文亚、李明江、金李东、李汉曦、王云根、徐逸峰、常萌、赵云峰、王英全等。

另外,还要感谢全程参与审核、校验等工作的孙海亮老师,以及其他背后默默支持的出版工作者,他们的辛勤付出保证了本书能够顺利面世。

最后感谢我的父母、家人和朋友,有了他们,我才有精力完成本书的全部撰写工作。

谨以此书献给热爱编程、热爱游戏、热爱IT工作并为之奋斗的朋友们,愿大家身体健康、生活美满、事业有成!蔡能(DarkSpy)第一部分网络和服务器第1章 Python网络编程模块第2章 通信加密第3章 服务器实作第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 = 4096s_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,指的是IPV4套接字接口;IPPROTO_IPV6,指的是IPV6套接字接口;IPPROTO_TCP,指的是TCP套接字接口。

第二个参数optname指的是选项名称,这些选项名称对于不同的操作系统也会有些许不同,如果level参数选择了基本套接字接口的话,那么一些常用的选项如表1-2所示。表1-2 optname的常用参数和具体意义

当然在level参数选择了IPPROTO_IP或者其他值的时候,optname的值又会有不同的选项,你可以使用搜索引擎或者UNIX的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_RECVBUF,这就代表了我们将Socket接收和发送内容的缓冲区大小从系统的默认值替换为我们自己定义的值,而最后的getsockopt函数则是将设置的值取回来。小结 在服务器代码中,为了保证端口和地址的绑定操作,我们要使用bind函数来进行操作,如果原始Socket参数不够设置,则应使用setsockopt函数来设置更多的内容。

我们还将在后续章节看到这些内容的使用,以及更多内容和函数的整合,最终形成一套完整的服务器流程。1.3 客户端Socket

之所以要在本节提到客户端的Socket方法是因为在本书接下来的部分中,我们将会提到更多和更高阶层次的Python方法、网络编程和架构的实用技术,从这点来讲,涉及客户端和服务器端的通信知识是必不可少的,所以在本章我们将会提及一些客户端特有的Socket编程知识。

connect方法

在Python Socket客户端编程比在服务器端编程所需要控制的参数和代码简单太多了。最基础最简单的版本,除了初始化Socket句柄之外,就只有connect了,我们来考虑下列代码:import sockets_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 sockets_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则是指定有关消息的其他值,具体可以通过UNIX的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,sysaddr=('',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 urllib2request = 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, urllib2req_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_datarequest = urllib2.Request(full_url)response = urllib2.urlopen(request)print response.read()

我们看到,GET方式就是使用url+"?"的形式将GET参数连接起来,而urlencode的意义就在于将字典编码为网页的字符串格式。以此类推,再来看看POST方法:import urllib, urllib2req_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

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载