python socket编程

来源:互联网 发布:ipad游戏大厅网络异常 编辑:程序博客网 时间:2024/06/07 11:51

python socket编程

标签: python 网络


如何创建socket

一般来说,当你点击该页面的链接时,你的浏览器做了一些和下面代码类似的事情:

#create an INET, STREAMing sockets = socket.socket(    socket.AF_INET, socket.SOCK_STREAM)#now connect to the web server on port 80# - the normal http ports.connect(("www.mcmillan-inc.com", 80))

当connect完成时,套接字s发送一个对该页面的请求并读取回复信息,随后即被销毁,因为客户端套接字通常情况下只被用来做一次交换(或一小组按序进行的交换)。
在网站服务器端发生的事情有一点复杂,首先,服务器创建一个服务器套接字(server socket):

#create an INET, STREAMing socketserversocket = socket.socket(    socket.AF_INET, socket.SOCK_STREAM)#bind the socket to a public host,# and a well-known portserversocket.bind((socket.gethostname(), 80))#become a server socketserversocket.listen(5)

这里需要注意几点:我们使用socket.gethostname(),因此这个套接字对于外部世界来说是可见的,如果我们用了s.bind((‘localhost’),80)或者s.bind((‘127.0.0.1’,80)),我们只会有一个只在本机可见的服务器套接字,s.bind((”,80))指明该套接字刻可以被绑定于服务器刚好拥有的地址。其次:较小数字的端口号常被用来提供众所周知的一些网络服务(如HTTP,SNMP等等),所以你最好用点大一点的数字(如4位数字)。最后,listen函数的参数用于告知socket库在拒绝外部连接之前可以接受5个(通常情况下的最大数)连接请求,如果其它部分的代码都是正确的,那么5个应该很多了。

现在我们有了一个监听80端口的服务器端socket,下面进入到网站服务器的主循环中:

while 1:#accept connections from outside(clientsocket, address) = serversocket.accept()#now do something with the clientsocket#in this case, we'll pretend this is a threaded serverct = client_thread(clientsocket)ct.run()

事实上这个循环有三个工作方式:调度一个线程来处理clientsocket(客户端套接字),创建一个新的进程来处理clientsocket,或者用非阻塞套接字(non-blocking socket)来重构该应用并用select在我们的服务器套接字和任意活动的clientsocket之间实现多路传输。有一点很重要:以上的工作方式就是服务器端套接字所做的全部,它不发送也不接收任何数据,它只创建“客户端”套接字。每一个被创建的clientsocket用于对通过connect()连接到服务器相应端口的其他“客户端”套接字做出反应。一旦我们创建了那个clientsocket,程序就继续监听更多连接。而这两个“客户端”套接字可以自由通话,它们使用一些动态分配的端口,这些端口也会在会话结束后被回收。

IPC

如果你需要在同一机器上的两个进程间实现快速的进程间通信,你应该看看你所用的平台提供的共享内存的形式,因为一种简单的基于共享内存和锁或信号量的协议是目前最快的技术。
如果你确定要用套接字,把“服务器”套接字绑定到’localhost’,在大多数平台上,这会在几层网络代码中间走一条捷径而且会快很多。

使用socket

首先要记住的是,网页浏览器的“客户端”套接字和网站服务器的“客户端”套接字是同一类型的,这意味着,套接字间的交互是一种“端到端”会话方式,换言之,作为设计者,你需要决定会话之间的规则。一般来说,发起连接的套接字开始整个会话,或是通过发出一个请求,或是发出一个信号,但那是设计者的决定了,并不是套接字的规则。

这里有两套动作用于交流,你可以使用send和recv,或者你可以转换你的客户端套接字从而像使用文件一样使用read和write,后者正是java实现它的套接字的方式,我提这个只是为了说明你需要在套接字中使用flush,这些是缓冲“文件”,有一个常见的错误是刚进行完写操作就读取回复,如果不进行flush,你可能需要一直等待回复,因为请求可能还在你的输出缓冲区里。

现在我们来到了套接字的主要难点--网络缓冲区的send和recv操作,它们不一定处理你交给它们的所有字节(或期望从它们那读取到的),因为它们的主要工作就是处理网络缓冲。总的来说,它们在相关的网络缓冲区被填满(send)或被取空(recv)时返回,接着它们会告诉你它们处理了多少字节,而你的责任就是不断重新调用它们直到你的消息被全部处理了。

当一个recv动作返回0字节时,这意味着另一边已经关闭了(或正在处于关闭的过程)连接,你不会在这个连接上再收到任何数据,也许你还能成功发送数据,而这个以后再谈。

像HTTP这样的协议使用一个套接字仅用于一次数据的传送,比如客户端发送一个请求再读取一个回复,然后套接字就被丢弃了,这意味着客户端可以通过检测是否收到0字节数据来判断是否到达回复的尾部。

但如果你想重用你的套接字,你要明白套接字中没有“EOT”(End Of Transfer),再重复一遍:如果一个套接字的send或recv动作再处理了0字节数据后返回,这个连接已经断了,如果连接没断,你需要一直等recv,因为套接字不会告诉你没有什么要读的了(至少现在是这样)。现在你再思考一下就会发现一个有关套接字的基本的事实:消息的长度必须是固定的(比较讨厌),或者是在一定范围内(还可以接受),或者指定它们的长度(这样好多了),或者由直接关闭连接来结束。你可以随意选择(但有的方法比其他的好)。

假定你不想结束这个连接,那么最简单的解决方法就是定长的消息:

class mysocket:'''demonstration class only  - coded for clarity, not efficiency'''    def __init__(self, sock=None):        if sock is None:            self.sock = socket.socket(                socket.AF_INET, socket.SOCK_STREAM)        else:            self.sock = sock    def connect(self, host, port):        self.sock.connect((host, port))    def mysend(self, msg):        totalsent = 0        while totalsent < MSGLEN:            sent = self.sock.send(msg[totalsent:])            if sent == 0:                raise RuntimeError("socket connection broken")            totalsent = totalsent + sent    def myreceive(self):        chunks = []        bytes_recd = 0        while bytes_recd < MSGLEN:            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))            if chunk == '':                raise RuntimeError("socket connection broken")            chunks.append(chunk)            bytes_recd = bytes_recd + len(chunk)        return ''.join(chunks)

这里的发送代码几乎适用于任何消息体系--在Python中你发送字符串,而且你可以使用len()来获得它的长度(即使它包含’\0’字符)。接收代码要更复杂。(在C语言中,这也不算很坏,除了你不能使用strlen如果消息中含有’\0’字符)

最简单的增强版本就是让消息的第一个字符用于指示消息类型,并由这类型决定消息长度。现在你有两个recv,第一个用来获取(至少)第一个字符因此你可以得到消息长度,第二个通过循环获取剩下的部分。如果你决定使用确定范围的方法,你会获得一些任意大小的数据块,(4096或8192字节对于网络缓冲区大小通常来说是一个不错的匹配),然后扫描数据从而获得你所定范围的定界符。

有一个难点需要理解一下:如果你的对话协议允许多个消息连续发送(没有回复),而且你传递给recv任意大小的块,你将会读到一连串的消息。你需要将那些放在一边并保存好,直到要用的时候。

在消息前面附加一个长度头(比如五个字符)要更复杂,因为(不管你信不信),你可能不会在一个recv中全部获取到这五个字符,如果只是玩玩的话,你可以不管它,但如果是在高负载的网络环境下,这样的代码很快就会崩溃除非使用两个recv循环,第一个用来检测长度,第二个来获取消息的数据部分。讨厌的是,你会发现send不总是一次性发送完所有数据,读了这些,你最终会弄懂它的……

以空间的代价,构建你自己的字符,(保留我的意见),这些改进措施就作为练习留给读者,我们继续探索其他知识。

二进制数据

使用套接字来发送二进制数据的可能性是相当大的,主要的问题在于不是所有的机器使用同样格式的二进制数据。例如,摩托罗拉芯片会用两个字节00 01来表示一个16位整数1,但在Intel和DEC机子上,顺序是反过来的:01 00,套接字库有函数用于转换16位和32位整数-ntohl,htonl,ntohs,htons,这里的“n”表示网络(network),“h”表示主机(host),“s”表示短整数(short),“l”表示长整数(long),在网络传过来的数据顺序和主机上数据顺序相同的机子上,这些没什么用,但如果哪个机子上的字节顺序是反的,这些函数就能正确地转换字节序。

在现如今的32位机子上,用ASCII码表示的二进制数据大小通常比用二进制表示的小,因为很多情况下,很多长整数的值为0或1,用字符“0”来表示只要2个字节的空间,而二进制法要4个字节。当然,这在定长消息的情况下没那么管用,具体怎么用就需要你自己做决定了。

断掉连接

严格来说,在你关闭一个套接字之前你应该使用shutdown,shutdown向另一端的套接字发出询问,根据你传递的参数的不同,它可以表示“我不再发送任何数据,但我会继续监听”,或“我不再监听了,总算可以歇歇了”。但是绝大多数socket库习惯于程序员忽视这样一个规则,所以一般情况下close与shutdown()相同,因此在绝大多数情况下并不需要shutdown。

在类HTTP的交换中有一个高效使用shutdown的方法,客户端发送一个请求,然后调用shutdown(1),这将告诉服务器“客户端完成了发送,但仍然接受信息”,服务器可以通过接收0字节来检测“EOF”,这样服务器就可以确定它得到了完整的请求,然后服务器发送回复,如果发送成功,就说明客户端确实在接收信息。

Python让自动shutdown又前进了一步,比如说当一个套接字被回收时,它会自动调用close如果需要的话。但依赖于此是很不好的习惯,如果一个套接字突然消失,没有进行close,另一端的套接字会永远运行,以为对面只是很慢而已。所以当一切都完成时请close你的套接字。

当套接字死亡(when sockets die)

可能在使用阻塞式套接字时最糟糕的事就是另一端突然变慢时(没有close),你的套接字可能要一直等待了。SCOKSTREAM是一个可靠的协议,它在断开一个连接前会等待很长时间,如果你在使用线程,那整个线程就没有用了,对此你也做不了什么。只要你没有做一些傻事,比如在进行阻塞阅读时加锁,那么线程是不会消耗很多资源的。不要试着去杀死线程,线程比进程更快的一个原因是线程避免了间接自动回收资源的开销,换句话说,如果你杀死线程,那么进程可能就像醉了一样。

非阻塞套接字

如果你已经理解了前面的内容,那么你已经知道了绝大部分你需要知道来使用套接字的内容,你只需要以相同的方式调用相同的函数,你的应用程序就可以运行良好。

在Python中,你可以使用socket.setblocking(0)来使用非阻塞的套接字,在C中,这更加复杂(举例来说,你需要在BSD风格的O_NONBLOCK和难以分辨的Posix风格的O_NDELAY以及完全不同的TCP_BODELAY中做出选择),但思路都是一样的,你在创建套接字之后,使用它之前做这件事(事实上,你可以在它们之间来回切换)。

和之前最主要的不同是send、recv、connect和accept不需要做任何事就可以返回了,你(当然)有多种选择,你可以检查返回的内容以及错误内容,一般情况下你会疯掉的,如果你不信的话,可以试一试……你的应用程序会变得更大,bug更多并堵住CPU,所以我们跳过这些死脑筋的方法并做正确的事。

使用select。

在C语言中,写select相当复杂,在Python中,这是小菜一碟,但Python中的版本和C语言中的很接近,所以如果你理解了Python中的select,那么C语言中的也不会有什么问题:

ready_to_read, ready_to_write, in_error = \           select.select(              potential_readers,              potential_writers,              potential_errs,              timeout)

你向select中传递三个列表:第一个包括所有你想要读取的套接字,第二个包括所有你想要写入的套接字,最后一个(一般是空的)包括你想要检查是否有错误的套接字。你要知道一个套接字可以出现在不止一个列表中,select调用是阻塞式的,但你可以给它设置一个超时,这通常是明智的,给它一个合适的较长的超时时间(比如一分钟)除非你有什么其它更重要的要做。

作为返回,你会得到三个列表,它们包括了真正可读,可写和错误的套接字,这些列表都是你传进去的相关列表的子集(可能是空的)。

如果一个套接字在输出的可读列表里,你可以百分之九十的肯定那个套接字上的recv会返回一些东西,可写列表也是这样,你可以发送一些东西,也许不都是你想要的东西,但有总比没有好(事实上,任何合理的套接字都会返回可写对象-这只是意味着对外的网络缓冲区(outbound network buffer)是有空间的)。

如果你有一个服务器端套接字,把它放在potential_readers列表里,如果它在可读列表中返回,你的accept将可以(几乎肯定)正常工作。如果你创建了一个新的套接字来和其它的连接,那么把它放在potential_writers列表里,如果它出现在返回的可读列表中,你可以基本确定它连上了。

select有一个非常讨厌的问题:如果在输入列表中有一个崩溃了的套接字,select就会崩溃,你就需要通过循环来找出那个崩溃了的套接字然后使用select([sock],[],[],0),这里的timeout为0表示不花多少时间,但这样很不好。

事实上,select也可以方便地处理阻塞式套接字,有一种方法可以确定套接字是否会阻塞-当缓冲区中有东西时套接字返回可读对象,但是,这并不能帮助确定对面的套接字是否关闭还是正忙。

可移植性提醒:在Unix系统中,select既可以在套接字上工作,也可以在文见上工作,但不要在Windows上这样尝试,因为在Windows上,select只在套接字上工作,同样记住在C语言中,很多高级的套接字操作在Unix系统和Windows系统上是不同的。事实上,在Windows上我经常对套接字使用线程(工作地相当好),必须直面的是,Windows上的实现与Unix上的大不相同。

性能

毫无疑问,最快的套接字代码是同时使用非阻塞式和select来加速自身的,你可以把占用网络资源而不占用CPU资源的代码放到一起,问题是这样写的app不能做其他事了,它需要时刻准备交换数据。

假定你的app确实要做更多事,线程化是一个可选的解决方法(而且使用非阻塞式比使用阻塞式的快),可惜的是,线程支持在不同的Unix中API和质量都不同,所以常见的方法是fork出子进程来处理每个连接,但同时这个开销也很可观(不要在Windows上这样做--进程创建的开销太大了),这同时也意味着除非每个子进程都是完全独立的,那么你就需要使用其它形式的IPC,比如管道、共享内存和信号量在父进程和子进程间通信。

最后,记住即使阻塞式套接字要比非阻塞式慢点,但在很多情况下它们才是“正确”的解决方案。总之,如果你的app是数据驱动型的,而数据来源于套接字,那么就没有多少意义研究复杂的逻辑,这样你的app只要等待select而不是recv。

1 0