network programming - QLGQ/learning-python GitHub Wiki
Client /Server Architecture
Introduction
服务器是一个软件或硬件,用于向一个或多个客户端(客户)提供所需要的“服务”。服务器存在的唯一目的就是等待客户的请求,给这些客户服务,然后再等待其他的请求。
Hardware Client/Server Architecture
- 打印(机)服务器是一个硬件服务器的例子。
- 文件服务器就是另一个硬件服务器的例子。
Software Client/Server Architecture
软件服务器也是运行在某个硬件上的。但不像硬件服务器那样,有专门的设备,如打印机、磁盘等。软件服务器提供的服务主要是程序的运行、数据的发送与接收、合并、升级或其他的程序或数据的操作。
- Web服务器
如今,最常用的软件服务器是Web服务器。一台机器里放一些网页或Web应用程序,然后启动服务。这样的服务器的任务就是接受客户端的请求,把网页发给客户端(如用户计算机上的浏览器),然后等待下一个客户端请求。这些服务启动后的目标就是“永远运行下去”。虽然它们不可能实现这样的目标,但只要没有关机或硬件出错等外力干扰,它们就能够运行非常长的一段时间。 - 数据库服务器
数据库服务器是另一种软件服务器。它们接受客户端的保存或读取请求,完成请求,然后再等待其他的请求。它们也被设计为要能“永远”运行。 - 窗口服务器
这些服务器几乎可以被认为是硬件服务器了。它们运行于一个有显示器的机器上。窗口客户端实际上是那些在运行时需要窗口环境的程序,它们一般叫做图形用户界面(GUI)程序。这些程序如果在一个DOS窗口或Unix的shell等没有窗口服务器的纯文本环境中运行,将无法启动。一旦窗口服务器可以使用时,那一切就正常了。
Client/Server Network Programming
在完成服务之前,服务器必需要先完成一些设置,先要创建一个通信端点,让服务器能“监听”请求。所有的客户只要创建一个通信端点,建立到服务器的连接,然后客户端就可以提出请求了。请求中,也可以包含必要的数据交互。一旦请求处理完成,客户端收到了结果,通信就结束了。
Sockets: Communication Endpoints
套接字是一种具有之前所说的“通信端点”概念的计算机网络数据结构。网络化的应用程序在开始任何通讯之前都必需要创建套接字。就像电话的插口一样,没有它就完全没办法通信。
套接字有两种,分别是基于文件型的和基于网络型的。eg:AF_UNIX、AF_INET。
Socket Addresses: Hot-Pot Pairs
如果把套接字比做电话的插口——即通信的最底层结构,那主机与端口就像区号与电话号码的一对组合。一个因特网地址由网络通信所必需的主机和端口组成。合法的端口号范围为0~65535
,其中,小于1024的端口号为系统保留端口。如果你所使用的是Unix操作系统,那么就可以通过/etc/services文件获得保留的端口号(及其对应的服务/协议和套接字类型)。常用端口号列表可以从下面这个网站获得:http://www.iana.org/assignments/port-numbers 。
Connection-Oriented Socket vs. Connectionless
- 面向连接
无论你使用哪一种地址家族,套接字的类型只有两种。一种是面向连接的套接字,即在通信之前一定要建立一条连接,就像跟朋友打电话时那样。这种通信方式也被称为“虚电路”或“流套接字”。面向连接的通信方式提供了顺序的、可靠的、不会重复的数据传输,而且也不会被加上数据边界。这也意味着,每一个要发送的信息,可能会被拆分成多份,每一份都会不多不少地正确到达目的地。然后被重新按顺序拼装起来,传给正在等待的应用程序。
实现这种连接的主要协议就是传输控制协议(即TCP)。要创建TCP套接字就得在创建的时候指定套接字类型为SOCK_STREAM。TCP套接字采用SOCK_STREAM这个名字,表达了它作为流套接字的特点。由于这些套接字使用网际协议(IP)来查找网络中的主机,所以这样形成的整个系统,一般会由这两个协议(TCP和IP)名的组合来描述,即TCP/IP。 - 无连接
与虚电路完全相反的是数据报型的无连接套接字。这意味着,无需建立连接就可以进行通讯。但这时,数据到达的顺序、可靠性及不重复性就无法保证了。数据报会保留数据边界,这就表示,数据是整个发送的,不会像面向连接的协议那样被先拆分成小块。由于面向连接套接字要提供一些保证,以及要维持虚电路连接,这都是很重的额外负担。数据报没有这些负担,所以它更“便宜”。通常能提供更好的性能,更适合某些应用场合。
实现这种连接的主要协议就是用户数据报协议(即UDP)。要创建UDP套接字就得在创建的时候指定套接字类型为SOCK_DGRAM。 SOCK_DGRAM这个名字,也许你已经猜到了,来自于单词“datagram”(“数据报”)。由于这些套接字使用网际协议来查找网络中的主机,这样形成的整个系统,一般会由这两个协议(UDP和IP)名的组合来描述,即UDP/IP。
socket() Module Function
要使用socket.socket()函数来创建套接字,其语法如下:
socket(socket_family, socket_type, protocol=0)
如前所述,socket_family不是AF_UNIX就是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)
当我们创建了套接字对象后,所有的交互都将通过对该套接字对象的方法调用来进行。
Socket Object (Built-In) Methods
套接字对象的常用函数
函数 | 描述 |
---|---|
服务器端套接字函数 | |
s.bind() | 绑定地址(主机名,端口号对)到套接字 |
s.listen() | 创建并开始TCP监听 |
s.accept() | 被动接受TCP客户端连接,(阻塞式)等待连接的到来 |
客户端套接字函数 | |
s.connect() | 主动初始化TCP服务器端连接 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错代码,而不是抛出异常 |
公共用途的套接字函数 | |
s.recv() | 接收TCP数据 |
s.recv_into() | 接收TCP数据到特定的缓冲区 |
s.send() | 发送TCP数据 |
s.sendall() | 完整发送TCP数据 |
s.recvfrom() | 接收UDP数据 |
s.recvfrom_into() | 接收UDP数据到特定的缓冲区 |
s.sendto() | 发送UDP数据 |
s.getpeername() | 连接到当前套接字的远端的地址(TCP连接) |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回指定套接字的参数 |
s.setsockopt() | 设置指定套接字的参数 |
s.shutdown() | 关闭连接 |
s.close() | 关闭套接字 |
s.detach() | 返回文档描述符并关闭套接字 |
s.ioctl() | 控制套接字的模式(仅适用于Windows) |
面向模块的套接字函数 | |
s.setblocking() | 设置套接字的阻塞或非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 获得阻塞套接字操作的超时时间 |
面向文件的套接字函数 | |
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建一个与该套接字相关联的文件对象 |
数据属性 | |
s.family | 套接字家族 |
s.type | 套接字类型 |
s.proto | 套接字协议 |
Create a TCP Server
ss = socket() # 创建服务器套接字
ss.bind() # 绑定套接字到地址上
ss.listen() # 监听连接
inf_loop: # 服务器无限循环
cs = ss.accept() # 接受客户端连接
comm_loop: # 通信循环
cs.recv()/cs.send() # 对话(接收与发送)
cs.close() # 关闭客户端套接字
ss.close() # 关闭服务器套接字 (可选)
所有的套接字都用socket.socket()函数来创建。
服务器需要“坐在某个端口上”等待请求,所以它们必须要“绑定”到一个本地的地址上。由于TCP是一个面向连接的通信系统,在TCP服务器可以开始工作之前,要先完成一些设置。TCP服务器必须“监听”(进来的)连接,设置完成之后,服务器就可以进入无限循环了。
一个简单的(单线程的)服务器会调用accept()函数等待连接的到来。默认情况下,accept()函数是阻塞式的,即程序在连接到来之前会处于挂起状态。套接字也支持非阻塞模式。
一旦接收到一个连接,accept()函数就会返回一个单独的客户端套接字用于后续的通信。使用
新的客户端套接字就像把客户的电话转给一个客户服务人员。当一个客户打电话进来的时候,总机接了电话,然后把电话转到合适的人那里来处理客户的需求。
这样就可以空出总机,也就是最初的那个服务器套接字,于是,话务员就可以等待下一个电
话(客户端请求),与此同时,前一个客户与对应的客户服务人员在另一条线路上进行着他
们之间的对话。同样的,当一个请求到来时,要创建一个新的端口,然后直接在那个端口上与客户对话,这样就可以空出主端口来接受其他客户的连接。
Instance:
#!/usr/bin/env python
from socket import *
from time import ctime
HOST = ''
PORT = 21567
BUFSIZ = 1024
ADDR = (HOST, PORT)
tcpSerSock = socket(AF_INET, SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5)
while True:
print 'waiting for connection...'
tcpCliSock, addr = tcpSerSock.accept()
print '...connected from:', addr
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
tcpCliSock.send('[%s] %s' % (
ctime(), data))
tcpCliSock.close()
tcpSerSock.close()
**14行:**第1行是Unix的启动信息行,随后我们导入了time.ctime()函数和socket模块的所有属性。13行:**HOST变量为空,表示bind()函数可以绑定在所有有效的地址上。我们还选用了一个随机生成的未被占用的端口号。在程序中,我们把缓冲区的大小设定为1K。你可以根据网络情况和应用的需要来修改这个大小。listen()函数的参数只是表示最多允许多少个连接同时连进来,而后来的连接就会被拒绝掉。TCP服务器的套接字(tcpSerSock)在第11行被生成。随后把套接字绑定到服务器的地址上,然后开始TCP监听。
**6
15~28行: 在进入到服务器的无限循环后,我们(被动地)等待连接的到来。当有连接时,我们进入对话循环,等待客户端发送数据。如果消息为空,表示客户端已经退出,那就再去等待下一个客户端连接。得到客户端消息后,我们在消息前加一个时间戳然后返回。最后一行不会被执行到,放在这里用于提醒读者,在服务器要退出的时候,要记得调用close()函数。