network programming - QLGQ/learning-python GitHub Wiki

Client /Server Architecture

Introduction

服务器是一个软件或硬件,用于向一个或多个客户端(客户)提供所需要的“服务”。服务器存在的唯一目的就是等待客户的请求,给这些客户服务,然后再等待其他的请求。

Hardware Client/Server Architecture

  1. 打印(机)服务器是一个硬件服务器的例子。
  2. 文件服务器就是另一个硬件服务器的例子。

Software Client/Server Architecture

软件服务器也是运行在某个硬件上的。但不像硬件服务器那样,有专门的设备,如打印机、磁盘等。软件服务器提供的服务主要是程序的运行、数据的发送与接收、合并、升级或其他的程序或数据的操作。

  1. Web服务器
    如今,最常用的软件服务器是Web服务器。一台机器里放一些网页或Web应用程序,然后启动服务。这样的服务器的任务就是接受客户端的请求,把网页发给客户端(如用户计算机上的浏览器),然后等待下一个客户端请求。这些服务启动后的目标就是“永远运行下去”。虽然它们不可能实现这样的目标,但只要没有关机或硬件出错等外力干扰,它们就能够运行非常长的一段时间。
  2. 数据库服务器
    数据库服务器是另一种软件服务器。它们接受客户端的保存或读取请求,完成请求,然后再等待其他的请求。它们也被设计为要能“永远”运行。
  3. 窗口服务器
    这些服务器几乎可以被认为是硬件服务器了。它们运行于一个有显示器的机器上。窗口客户端实际上是那些在运行时需要窗口环境的程序,它们一般叫做图形用户界面(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

  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

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模块的所有属性。
**6
13行:**HOST变量为空,表示bind()函数可以绑定在所有有效的地址上。我们还选用了一个随机生成的未被占用的端口号。在程序中,我们把缓冲区的大小设定为1K。你可以根据网络情况和应用的需要来修改这个大小。listen()函数的参数只是表示最多允许多少个连接同时连进来,而后来的连接就会被拒绝掉。TCP服务器的套接字(tcpSerSock)在第11行被生成。随后把套接字绑定到服务器的地址上,然后开始TCP监听。
15~28行: 在进入到服务器的无限循环后,我们(被动地)等待连接的到来。当有连接时,我们进入对话循环,等待客户端发送数据。如果消息为空,表示客户端已经退出,那就再去等待下一个客户端连接。得到客户端消息后,我们在消息前加一个时间戳然后返回。最后一行不会被执行到,放在这里用于提醒读者,在服务器要退出的时候,要记得调用close()函数。