套接字:通信端点
16.2.1 什么是套接字
套接字是一种具有之前所说的“通信端点”概念的计算机网络数据结构。网络化的应用程序在开始任何通讯之前都必需要创建套接字。就像电话的插口一样,没有它就完全没办法通信。
套接字起源于20世纪70年代加州大学伯克利分校版本的Unix,即人们所说的BSD Unix。因此,有时人们也把套接字称为“伯克利套接字”或“BSD套接字”。一开始,套接字被设计用在同一台主机上多个应用程序之间的通讯。这也被称作进程间通讯,或IPC。套接字有两种,分别是基于文件型的和基于网络型的。
Unix套接字是我们要介绍的第一个套接字家族。其“家族名”为AF_UNIX(在POSIX1.g标准中也叫AF_LOCAL),表示“地址家族:UNIX”。包括Python在内的大多数流行平台上都使用术语“地址家族”及其缩写“AF”。而老一点的系统中,地址家族被称为“域”或“协议家族”,并使用缩写“PF”而不是“AF”。同样的,AF_LOCAL(在2000-2001年被列为标准)将会代替AF_UNIX。不过,为了向后兼容,很多系统上,两者是等价的。Python自己则仍然使用AF_UNIX。
由于两个进程都运行在同一台机器上,而且这些套接字是基于文件的。所以,它们的底层结构是由文件系统来支持的。这样做相当有道理,因为,同一台电脑上,文件系统的确是不同的进程都能访问的。
另一种套接字是基于网络的,它有自己的家族名字:AF_INET,或叫“地址家族:Internet”。还有一种地址家族AF_INET6被用于网际协议第6版(IPv6)寻址上。还有一些其他的地址家族,不过,它们要么是只用在某个平台上,要么就是已经被废弃,或是很少被使用,或是根本就还没有实现。所有地址家族中,AF_INET是使用最广泛的一个。Python 2.5中加入了一种Linux套接字的支持:AF_NETLINK(无连接(稍后讲解))套接字家族让用户代码与内核代码之间的IPC可以使用标准BSD套接字接口。而且,相对之前那些往操作系统中加入新的系统调用、proc文件系统支持或是“IOCTL”等复杂的方案来说,这种方法显得更为精巧,更为安全。
Python只支持AF_UNIX,AF_NETLINK,和AF_INET家族。由于我们只关心网络编程,所以在本章的大部分时候,我们都只用AF_INET。
6.2.2 套接字地址:主机与端口
如果把套接字比做电话的插口—即通信的最底层结构,那主机与端口就像区号与电话号码的一对组合。有了能打电话的硬件还不够,你还要知道你要打给谁,往哪打。一个因特网地址由网络通信所必需的主机与端口组成。而且不用说,另一端一定要有人在听才可以。否则,你就会听到熟悉的声音“对不起,您拨的是空号,请查询后再拨”。你在上网的时候,可能也见过类似的情况,如“不能连接该服务器。服务器无响应或不可达”。
合法的端口号范围为0~65535。其中,小于1024的端口号为系统保留端口。如果你所使用的是Unix操作系统,那么就可以通过/etc/services文件获得保留的端口号(及其对应的服务/协议和套接字类型)。常用端口号列表可以从下面这个网站获得:
16.2.3 面向连接与无连接
1.面向连接
无论你使用哪一种地址家族,套接字的类型只有两种。一种是面向连接的套接字,即在通信之前一定要建立一条连接,就像跟朋友打电话时那样。这种通信方式也被称为“虚电路”或“流套接字”。面向连接的通信方式提供了顺序的、可靠的、不会重复的数据传输,而且也不会被
加上数据边界。这也意味着,每一个要发送的信息,可能会被拆分成多份,每一份都会不多不少地正确到达目的地。然后被重新按顺序拼装起来,传给正在等待的应用程序。
实现这种连接的主要协议就是传输控制协议(即TCP)。要创建TCP套接字就得在创建的时候指定套接字类型为SOCK_STREAM。TCP套接字采用SOCK_STREAM这个名字,表达了它作为流套接字的特点。由于这些套接字使用网际协议(IP)来查网络中的主机,所以这样形成的整个系统,一般会由这两个协议(TCP和IP)名的组合来描述,即TCP/IP。
2.无连接
与虚电路完全相反的是数据报型的无连接套接字。这意味着,无需建立连接就可以进行通讯。但这时,数据到达的顺序、可靠性及不重复性就无法保证了。数据报会保留数据边界,这就表示,数据是整个发送的,不会像面向连接的协议那样被先拆分成小块。
使用数据报来传输数据就像邮政服务一样。邮件和包裹不一定会按它们发送的顺序到达。事实上,它们还有可能根本到达不了!而且,在网络中报文甚至会重复发送,这也增加了复杂性。
既然数据报有这么多缺点,为什么还要使用它呢?(一定有能胜过流套接字的功能!)由于面向连接套接字要提供一些保证,以及要维持虚电路连接,这都是很重的额外负担。数据报没有这些负担,所以它更“便宜”。通常能提供更好的性能,更适合某些应用场合。
实现这种连接的主要协议就是用户数据报协议(即UDP)。要创建UDP套接字就得在创建的时候指定套接字类型为SOCK_DGRAM。SOCK_DGRAM这个名字,也许你已经猜到了,来自于单词“datagram”(“数据报”)。由于这些套接字使用网际协议来查网络中的主机,这样形成的整个系统,一般会由这两个协议(UDP和IP)名的组合来描述,即UDP/IP。
实现这种连接的主要协议就是用户数据报协议(即UDP)。要创建UDP套接字就得在创建的时候指定套接字类型为SOCK_DGRAM。SOCK_DGRAM这个名字,也许你已经猜到了,来自于单词“datagram”(“数据报”)。由于这些套接字使用网际协议来查网络中的主机,这样形成的整个系统,一般会由这两个协议(UDP和IP)名的组合来描述,即UDP/IP。
16.3 Python中的网络编程
现在,你已经有了足够的客户端/服务器架构、套接字和网络方面的知识。我们现在就开始把这些概念带到Python中来。本节中,我们将主要使用socket模块。模块中的socket()函数被用来创建套接字。套接字也有自己的一套函数来提供基于套接字的网络通信。
16.3.1 socket()模块函数
要使用socket.socket()函数来创建套接字。其语法如下:
socket(socket_family, socket_type, protocol=0) |
如前所述,Socket, family不是AF_VNIX就是AF_INET socket_type可以是SOCK_STREAM或SOCK_DGRAM,这一点前面已说过。protocol一般不填,默认值为0。
创建一个TCP/IP的套接字,你要这样调用socket.socket():
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
同样地,创建一个UDP/IP的套接字,你要这样:
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
由于socket模块中有太多的属性,我们在这里破例使用了'from module import *'语句。使用'from socket import *',我们就把socket模块里的所有属性都带到我们的命名空间里了,这样能大幅减短我们的代码。
tcpSock = socket(AF_INET, SOCK_STREAM) |
当我们创建了套接字对象后,所有的交互都将通过对该套接字对象的方法调用来进行。
16.3.2 套接字对象(内建)方法
表16.1中,我们列出了最常用的套接字对象的方法。在下一个小节中,我们将分别创建TCP和UDP的客户端和服务器,它们都要用到这些方法。虽然我们只关心因特网套接字,但是这些方法在Unix套接字中的也有类似的意义。
表16.1 套接字对象的常用函数
表16.1 套接字对象的常用函数
函 数 | 描 述 |
服务器端套接字函数 | |
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() | 连接到当前套接字的远端的地址(TCP连接) |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回指定套接字的参数 |
s.setsockopt() | 设置指定套接字的参数 |
s.close() | 关闭套接字 |
面向模块的套接字函数 | |
s.setblocking() | 设置套接字的阻塞与非阻塞模式 |
s.settimeout()a | 设置阻塞套接字操作的超时时间 |
s.gettimeout()a | 得到阻塞套接字操作的超时时间 |
面向文件的套接字函数 | |
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建一个与该套接字关连的文件对象 |
a.(Python 2.3版本新加入的函数)。
核心提示:在运行网络应用程序时,最好在不同的电脑上执行服务器和客户端的程序。
在本章的例子中,你将看到大量的代码和输出中提及“localhost”主机和127.0.0.1 IP地址。例子中客户端与服务器运行在同一台电脑上,我们建议读者改掉主机名,并把代码放到不同的电脑上运行。眼看着自己的代码让不同的电脑在网络上进行通讯,这一时刻,你更能体会到开发的乐趣。
16.3.3 创建一个TCP服务器
我们首先将给出一个关于如何创建一个通用的TCP服务器的伪代码,然后解释会发生什么问题。要注意的是,这只是设计服务器的一种方法,当你对服务器的设计有了一定的了解之后,你就能用你所希望的方式来修改这段伪代码:
ss = socket() # 创建服务器套接字 ss.bind() # 把地址绑定到套接字上 ss.listen() # 监听连接 inf_loop: # 服务器无限循环 cs = ss.accept() # socket通信为什么要指定端口接受客户端连接 comm_loop: # 通信循环 cs.recv()/cs.send() # 对话(接收与发送) cs.close() # 关闭客户端套接字 ss.close() # 关闭服务器套接字(可选) |
所有的套接字都用socket.socket()函数来创建。服务器需要“坐在某个端口上”等待请求。所以它们必需要“绑定”到一个本地的地址上。由于TCP是一个面向连接的通信系统,在TCP服务器可以开始工作之前,要先完成一些设置。TCP服务器必须“监听”(进来的)连接,设置完成之后,服务器就可以进入无限循环了。
一个简单的(单线程的)服务器会调用accept()函数等待连接的到来。默认情况下,accept()函数是阻塞式的,即程序在连接到来之前会处于挂起状态。套接字也支持非阻塞模式。请参阅相关文档或操作系统手册以了解为何及如何使用非阻塞套接字。
一旦接收到一个连接,accept()函数就会返回一个单独的客户端套接字用于后续的通信。使用新的客户端套接字就像把客户的电话转给一个客户服务人员。当一个客户打电话进来的时候,总机接了电话,然后把电话转到合适的人那里来处理客户的需求。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论