Python:网络编程

我将通过示例展示如何使用 Python 来编写以各种方式使用网络(如互联网)的程序。Python 提供了强大的网络编程的支持,有很多库实现了常见的网络协议以及基于这些协议的抽象层,让你能够专注与程序的逻辑,而无需关心通过线路来传输比特的问题。另外,对于有些协议格式,可能没有处理它们的现成代码,但编写起来也很容易,因为 Python 很擅长处理字节流中的各种模式(从各种处理文本文件的方式中,你可能领教了这一点)。

鉴于 Python 提供的网络工具众多,这里只能简要介绍它的网络功能。要更深入的了解 Python 网络编程,推荐你阅读 John Goerzen 的著作《Python 网络编程基础》,其中非常详尽地讨论这个主题。

我首先概述 Python 标准库中的一些网络模块。然后讨论 SocketServer 和相关的类,并介绍同时处理多个连接的各种方法。最后,简单地说一说 Twisted,这是一个使用 Python 编写网络程序的框架,功能丰富而成熟。

注意 如果你的计算机上安装了严格的防火墙,每当你开始运行自己编写的网络程序时,它都可能发出警告,并禁止程序连接到网络。你应对防火墙进行配置,让它允许 Python 完成其工作。如果防火墙有交互式接口,只需在询问时允许连接即可。然而,需要注意的是,任何连接到网络的软件都是安全隐患,即便是你自己编写的软件亦如此(或者说尤其如此)。

几个网络模块

标准库中有很多网络模块,其他地方也有不少。有些网络模块明显主要是处理网络的,但还是有几个其实也是与网络相关的如处理各种编码以便通过网络传输的模块。这里精心挑选了几个模块进行介绍。

模块 socket

网络编程中的一个基本组件是套接字(socket)。套接字基本上是一个信息通道,两端各有一个程序。这些程序可能位于(通过网络相连的)不同的计算机上,通过套接字向对方发送信息。在 Python 中,大多数网络编程都隐藏了模块 socket 的基本工作原理,不与套接字直接交互。

套接字分为两类:服务器套接字和客户端套接字。创建服务器套接字后,让它等待连接请求的到来。这样,它将在某个网络地址(由 IP 地址和端口号组成)处监听,直到客户端套接字建立连接。随后,客户端和服务器就能通信了。

客户端套接字处理起来通常比服务器套接字容易些,因为服务器必须准备随时处理客户端的连接,还必须处理多个连接;而客户端只需连接,完成任务后再断开连接即可。我后面将介绍如何使用 SocketServer 等类和 Twisted 框架进行服务器端编程。

套接字是模块 socket 中 socket 类的实例。实例化套接字最多可指定三个参数:一个地址族(默认为 socket.AF_INET);是流套接字(socket.SOCK_STREAM,默认设置)还是数据报套接字(socket.SOCK_DGRAM);协议(使用默认值 0 就好)。创建普通套接字时,不用提供任何参数。

服务器套接字先调用方法 bind,再调用方法 listen 来监听特定的地址。然后,客户端套接字就可连接到服务器了,办法是调用方法 connect 并提供调用方法 bind 时指定的地址(在服务器端,可使用函数 socket.gethostname 获取当前机器的主机名)。这里的地址是一个格式为 (host, port) 的元组,其中 host 是主机名(如 www.example.com),而 port 是端口号(一个整数)。方法 listen 接受一个参数——待办任务清单的长度(即最多有多少个连接在队列中等待接纳,到达这个数量后开始拒绝连接)。

服务器套接字开始监听后,就可接受客户端的连接了这是使用方法 accept 来完成的。这个方法将阻断(等待)到客户端连接到来为止,然后返回一个格式为 (client, address) 的元组,其中 client 是一个客户端套接字,而 address 是前面解释过的地址。服务器能以其认为合适的方式处理客户端连接,然后再次调用 accept 方法以接着等待新连接到来。这通常是在一个无限循环中完成的。

注意 这里讨论的服务器编程形式称为阻断(同步)网络编程。在后面,你将看到非阻断(异步)网络编程示例,以及如何使用线程来同时处理多个客户端。

为传输数据,套接字提供了两个方法:send 和 recv(表示 receive)。要发送数据,可调用方法 send 并提供一个字节流;要接收数据,可调用 recv 并指定最多接收多少个字节的数据。如果不确定该指定什么数字,1024 是个不错的选择。

下面的两段代码展示了最简单的客户端程序和最简单的服务器程序。如果在同一台机器上运行它们(先运行服务器程序),服务器程序将打印一条连接请求的消息,然后客户端程序将打印它从服务器那里收到的消息。在服务器还在运行时,可运行多个客户端。在客户端程序中,通过将 gethostname 调用替换为服务器机器的主机名,可分别在两台通过网络连接的机器上运行这两个程序。

注意 可使用的端口号通常受到限制,在 Linux 或 UNIX 系统中,需要有管理员权限才能使用 1024 以下的端口号。这些编号较小的端口是供标准服务使用的。例如,端口 80 供 Web 服务器使用。另外,使用 Ctrl+C 停止服务器后,可能需要等待一段时间才能使用该服务器原来使用的端口(否则,可能出现“地址已被占用”错误消息)。

最简单的服务器

import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
s.listen(5)
while True:
    c, addr = s.accept()
    print('Got connection from', addr)
    c.send(b'Thank you for connecting')
    c.close()

最简单的客户端

import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.connect((host, port))
print(s.recv(1024))

有关模块 socket 的更详细信息,请参阅“Python 库参考手册”以及 Gordon McMillanc 撰写的文章“Socket Programming HOWTO”(http://docs.python.org/dev/howto/socket.html)。

模块 urllib 和 urllib3

在可供使用的网络库中,urllib 和 urllib3 可能是投入产出比最高的两个。它们能让你通过网络访问文件,就像这些文件位于你的计算机中一样。只需一个简单的函数调用,就几乎可将统一资源定位符(URL)可指向的任何动作作为程序的输入。想想这种功能与模块 re 结合起来使用都能做什么吧!你可下载网页、从中提取信息并自动生成研究报告。

模块 urllib 和 urllib3 的功能差不多,但 urllib3 更好一些。对于简单的下载,urllib 绰绰有余。如果要实现 HTTP 身份验证或 Cookie,抑或编写扩展来处理自己的协议,urllib3 可能是更好的选择。

打开远程文件

几乎可以像打开本地文件一样打开远程文件,差别是只能使用读取模式,以及使用模块 urllib.request 中的函数 urlopen,而不是 open(或 file)。

>>> from urllib.request import urlopen
>>> webpage = urlopen('http://www.python.org')

如果连接到了网络,变量 webpage 将包含一个类似于文件的对象,这个对象与网页 http://www.python.org 相关联。

注意 要在没有联网的情况下尝试使用模块 urllib,可使用以 file: 打头的 URL 访问本地文件,如 file:c:\text\somefile.txt(别忘了对反斜杠进行转义)。

urlopen 返回的类似于文件的对象支持方法 close、read、readline 和 readlines,还支持迭代等。

假设要提取刚才所打开网页中链接 About 的相对 URL,可使用正则表达式。

>>> import re
>>> text = webpage.read()
>>> m = re.search(b'<a href="([^"]+)" .*?>about</a>', text, re.IGNORECASE)
>>> m.group(1)
b'/about/'

注意 当然,如果这个网页发生了变化,你可能需要修改使用的正则表达式。

获取远程文件

函数 urlopen 返回一个类似于文件的对象,可从中读取数据。如果要让 urllib 替你下载文件,可使用 urlretrieve。这个函数不返回一个类似于文件的对象,而返回一个格式为 (filename, headers) 的元组,其中 filename 是本地文件的名称(由 urllib 自动创建),而 headers 包含一些有关远程文件的信息(这里不会介绍 headers,如果想要更深入的了解它,请在有关 urllib 的标准库文档中查找 urlretrieve)。如果要给下载的副本指定文件名,可通过第二个参数来提供。

urlretrieve('http://www.python.org', 'C:\\python_webpage.html')

这将获取 python 官网的主页,并将其存储到文件 C:\python_webpage.html 中。但使用完毕后,你可能想将其删除,以免占用磁盘空间。要清空这样的临时文件,可调用函数 urlcleanup 且不提供任何参数,它将负责替你完成清空工作。

一些实用的函数

除了通过 URL 读取和下载文件外,urllib 还提供了一些用于操作 URL 的函数,如下所示(这里假设你对 URL 和 CGI 略知一二)。

  • quote(string[, safe]):返回一个字符串,其中所有的特殊字符(在 URL 中有特殊意义的字符)都已替换为对 URL 友好的版本(如将 ~ 替换为 %7E)。如果要将包含特殊字符的字符串用作 URL,这很有用。参数 safe 是一个字符串(默认为 '/'),包含不应该像这样对其进行编码的字符。
  • quote_plus(string[, safe]):类似于 quote,但也将空格替换为加号。
  • unquote(string):与 quote 相反。
  • unquote_plus(string):与 quote_plus 相反。

urlencode(query[, doseq]):将映射(如字典)或由包含两个元素的元组(形如 (key, value))组成的序列转换为“使用 URL 编码的”字符串。这样的字符串可用于 CGI 查询中(详细信息请参阅 Python 文档)。

其他模块

前面说过,除了这里讨论的模块外,Python 库等地方还包含很多与网络相关的模块。下表列出了 Python 标准库中的一些与网络相关的模块。

模块

描述

asynchat

包含补充 asyncore 的功能

asyncore

异步套接字处理程序

cgi

基本的 CGI 支持

Cookie

Cookie 对象操作,主要用于服务器

cookielib‍‍

客户端 Cookie 支持

email

电子邮件(包括 MIME)支持

ftplib

FTP 客户端模块

gopherlib

Gopher 客户端模块

httplib

HTTP 客户端模块

imaplib

IMAP4 客户端模块

mailbox

读取多种邮箱格式

mailcap

通过 mailcap 访问 MIME 配置

mhlib

访问 MH 邮箱

nntplib

NNTP 客户端模块

poplib

POP 客户端模块

robotparser

解析 Web 服务器 robot 文件

SimpleXMLRPCServer

一个简单的 XML-RPC 服务器

smtpd

SMTP 服务器模块

smtplib

SMTP 客户端模块

telnetlib

Telnet 客户端模块

urlparse

用于解读 URL

xmlrpclib

XML-RPC 客户端支持

SocketServer 及相关的类

从上面可知,编写简单的套接字服务器。然而,如果要创建并非简单的服务器,还是求助于服务器模块吧。模块 SocketServer 是标准库提供的服务器框架的基石,这个框架包括 BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer 和 DocXMLRPCServer 等服务器,它们在基本的服务器的基础上添加了各种功能。

SocketServer 包含 4 个基本的服务器:TCPServer(支持 TCP 套接字流)、UDPServer(支持 UDP 数据报套接字)以及更难懂的 UnixStreamServer 和 UnixDatagramServer。后面 3 个你可能不会用到。

使用模块 SocketServer 编写服务器时,大部分的代码都位于请求处理器中。每当服务器收到客户端的连接请求时,都将实例化一个请求处理程序,并对其调用各种处理方法来处理请求。具体调用哪些方法取决于使用的服务器类和请求处理程序类;还可以从这些请求处理类派生出子类,从而让服务器调用一组自定义的处理方法。基本请求处理程序类 BaseRequestHandler 将所有操作都放在一个方法中——服务器调用方法 handle。这个方法可通过属性 self.request 来访问客户端套接字。如果处理的是流(使用 TCPServer 时很可能如此),可使用 StreamRequestHandler 类,它包含另外两个属性:self.rfile(用于读取)和 self.wfile(用于写入)。你可使用这两个类似于文件的对象来与客户端通信。

模块 SocketServer 还包含很多其他的类,它们为 HTTP 服务器提供了基本的支持(如运行 CGI 脚本),以及 XML-RPC 的支持。

下面的代码是前面所示最简单的服务器的 SocketServer 版本,可与前面最简单的客户端协同工作。请注意,StreamRequestServer 负责在使用完连接后将其关闭。另外,主机名 '' 表示运行该服务器的计算机。

基于 SocketServer 的最简单的服务器。

from socketserver import TCPServer, StreamRequestHandler


class Handler(StreamRequestHandler):
    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from', addr)
        self.wfile.write(b'Thank you for connecting.')
server = TCPServer(('', 1234), Handler)
server.serve_forever()

有关模块 SocketServer 的详细信息,请参阅“Python 库参考手册”以及 John Goerzen 的著作《Python 网络编程基础》。

多个连接

前面讨论的服务器解决方案都是同步的:不能同时处理多个客户端的连接请求。如果连接持续时间较长,比如完整的聊天会话,就需要能够同时处理多个连接。

处理多个连接的主要方式有三种:分叉(forking)、线程化和异步 I/O。通过结合使用 SocketServer 中的混合类和服务器类,很容易实现分叉和线程化。即便不使用这些类,这两种方式也很容易实现。然而,它们确实存在缺点。分叉占用的资源较多,且在客户端很多时可伸缩性不佳(但只要客户端数量适中,分叉在现代 UNIX 和 Linux 系统中的效率很高。如果系统有多个 CPU,效率就更高了);而线程化可能会带来同步问题。这里不深入讨论这些问题,只演示如何使用这些方式。

分叉和线程是什么

你可能不知道分叉和线程是什么,这里简单的说说。分叉是一个 UNIX 术语。对进程(运行的程序)进行分叉时,基本上是复制它,而这样得到的两个进程都将从当前位置开始继续往下执行,且每个进程都有自己的内存副本(变量等)。原来的进程为父进程,复制的进程为子进程。如果你是科幻小说迷,可将它们视为并行的宇宙:分叉操作在时间轴上创建一个分支,最终得到两个独立存在的宇宙(进程)。所幸进程能够判断它们是原始进程还是子进程(通常查看函数 fork 的返回值),因此能够执行不同的操作。(如果不能,两个进程将做同样的事情,这除了让计算机陷入停顿外还有什么意义?)

在分叉服务器中,对于每个客户端的连接,都将通过分叉创建一个子进程。父进程继续监听新连接,而子进程负责处理客户端请求。客户端请求结束后,子进程直接退出。由于分叉出来的进程并行的运行,因此客户端无需等待。

鉴于分叉占用的资源较多(每个分叉出来的进程都必须有自己的内存),还有一种解决方案:线程化。线程是轻量级进程(子进程),都位于同一个进程中并共享内存。这减少了占用的资源,但也带来了一个缺点:由于线程共享内存,你必须确保它们不会彼此干扰或同时修改同一项数据,否则将引起混乱。这些问题都属于同步问题。在现代操作系统(不支持分叉的 Windows 除外)中,分叉的速度其实非常快,较新的硬件能够更好地应付资源的消耗。如果你不想处理麻烦的同步问题,分叉可能是不错的选择。

然而,如果能够完全杜绝并行性,就再好不过了。在这里,将介绍基于函数 select 的其他解决方案。另一种避免线程和分叉的办法是使用 Stackless Python(http://stackless.com),它是一个能够快速而轻松地在不同上下文之间切换的 Python 版本。它支持一种类似于线程的并行方式,名为微线程,其可伸缩性比真正的线程高得多。例如,“星战前夜在线”(EVEOnline,http://www.eve-online.com)用 Stackless Python 微线程为数以千计的用户提供服务。

在较低的层次实现异步 I/O 要难一些,其基本机制是模块 select 中的函数 select,使用起来非常棘手。幸运的是,有用于实现异步 I/O 的高级框架,让你能够通过简单而抽象的接口使用可伸缩的强大机制。标准库提供了一个这样的基本框架,由模块 asyncore 和 asynchat 组成。后面讨论的 Twisted 是一个非常强大的异步网络编程框架。

使用 SocketServer 实现分叉和线程化

使用框架 SocketServer 创建分叉或线程化服务器非常简单,几乎不需要任何解释。下面的两段代码分别演示了如何实现分叉和线程化。仅当 handle 方法需要很长时间才能执行完毕时,分叉和线程化才能提供帮助。请注意,Windows 不支持分叉。

分叉服务器

from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler


class Server(ForkingMixIn, TCPServer):
    pass
class Handler(StreamRequestHandler):
    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from', addr)
        self.wfile.write(b'Thank you for connecting')
server = Server(('', 1234), Handler)
server.serve_forever()

线程化服务器

from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler


class Server(ThreadingMixIn, TCPServer):
    pass
class Handler(StreamRequestHandler):
    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from', addr)
        self.wfile.write(b'Thank you for connecting')
server = Server(('', 1234), Handler)
server.serve_forever()

使用 select 和 poll 实现异步 I/O

当服务器与客户端通信时,来自客户端的数据可能时断时续。如果使用了分叉和线程化,这就不是问题:因为一个进程(线程)等待数据时,其他进程(线程)可继续处理其客户端。然而,另一种做法是只处理当前正在通信的客户端。你甚至无需不断监听,只需监听后将客户端加入队列即可。

这就是框架 asyncore/asynchat 和 Twisted 采取的方法。这两种功能的基石是函数 select 和 poll(如果系统支持)。这两个函数都位于模块 select 中,其中 poll 的可伸缩性更高,但只有 UNIX 系统支持它(Windows 不支持)。

函数 select 接受三个必不可少的参数和一个可选参数,其中前三个参数为序列,而第四个参数为超时时间(单位为秒)。这些序列包含文件描述符整数(也可以是这样的对象:包含文件描述符整数的方法 fileno),表示我们正在等待的连接。这三个序列分别表示需要输入和输出以及发生异常(错误等)的连接。如果没有指定超时时间,select 将阻断(即等待)到有文件描述符准备就绪;如果指定了超时时间,select 将最多阻断指定的秒数;如果超时时间为零,select 将不断轮询(即不阻断)。select 返回三个序列(即一个长度为 3 的元组),其中每个序列都包含相应参数中处于活动状态的文件描述符。例如,返回的第一个序列包含有数据需要读取的所有输入文件描述符。

这些序列也可包含文件对象(Windows 不支持)或套接字。如下所示的服务器代码使用 select 来为多个连接提供服务。(请注意,将服务器套接字传递给了 select,让 select 能够在有新连接到来时发出信号。)这个服务器是一个简单的日志程序,将来自客户端的数据都打印出来。要进行测试,可使用 telnet 连接到它,也可通过编写一个基于套接字的简单客户端来向它发送数据。尝试使用 telnet 建立多个到该服务器的连接,核实它能够处理多个客户端(虽然这样输出的日志中将混杂多个客户端的输入)。

使用 select 的简单服务器

import socket
import select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
s.listen(5)
inputs = [s]
a = None
while True:
    rs, ws, es = select.select(inputs, [], [])
    for r in rs:
        if r is s:
            c, addr = s.accept()
            print('Got connection from', addr)
            inputs.append(c)
        else:
            try:
                data = r.recv(1024)
                a = data
                disconnected = not data
            except socket.error:
                disconnected = True
            if disconnected:
                print(r.getpeername(), 'disconnected')
                inputs.remove(r)
            else:
                print(a)

方法 poll 使用起来比 select 容易。调用 poll 时,将返回一个轮询对象。你可使用方法 register 向这个对象注册文件描述符(或包含方法 fileno 的对象)。注册后可使用方法 unregister 将它们删除。注册对象(如套接字)后,可调用其方法 poll(它接受一个可选的超时时间参数)。这将返回一个包含 (fd, event) 元组的列表(可能为空),其中 fd 为文件描述符,而 event 是发生的事件。event 是一个位掩码,这意味着它是一个整数,其各个位对应于不同的事件。各种事件是用 select 模块中的常量表示的,如下表所示。要检查指定位是否为 1(即是否发生了相应的事件),可以像下面这样使用按位与运算符(&):

if event & select.POLLIN: ...

事件名

描述

POLLIN

文件描述符中有需要读取的数据

POLLPRI

文件描述符中有需要读取的紧急数据

POLLOUT

文件描述符为写入数据做好了准备

POLLERR

文件描述符出现了错误状态

POLLHUP‍‍

挂起。连接已断开。

POLLNVAL

无效请求。连接未打开

下面的代码是使用 poll 的简单服务器。请注意,我添加了一个从文件描述符(int)到套接字对象的映射(fdmap)。

import socket
import select
s = socket.socket()
host = socket.gethostname()
port = 1234
s.bind((host, port))
fdmap = {s.fileno(): s}
s.listen(5)
p = select.poll()
p.register(s)
while True:
    events = p.poll()
    for fd, event in events:
        if fd in fdmap:
            c, addr = s.accept()
            print('Got connection from', addr)
            p.register(c)
            fdmap[c.fileno()] = c
        elif event & select.POLLIN:
            data = fdmap[fd].recv(1024)
            if not data:  # 没有数据--连接已关闭
                print(fdmap[fd].getpeername(), 'disconnected')
                p.unregister(fd)
                del fdmap[fd]
            else:
                print(data)

有关 select 和 poll 的详细信息,请参阅“Python 库参考手册”(http://python.org/doc/lib/module-select.html)。另外,阅读标准库模块 asyncore 和 asynchat 的源代码(位于安装的 Python 中的文件 asyncore.py 和 asynchat.py 中)也能获得启迪。

Twisted

Twisted 是由 Twisted Matrix Laboratories (http://twistedmatrix.com)开发的,这是一个事件驱动的 Python 网络框架,最初是为编写网络游戏开发的,但现在被各种网络软件使用。在 Twisted 中,你能实现事件处理程序,就像在 GUI 工具包中一样。实际上,Twisted 与多个常用的 GUI 工具包(Tk、GTK、Qt 和 wxWidgets)配合的天衣无缝。

这里介绍一些基本概念,并演示如何使用 Twisted 完成一些简单的网络编程任务。掌握这些基本概念后,你就可参考 Twisted 文档(可在 Twisted 网站找到,这个网站还有很多其他的信息)来完成更复杂的网络编程。Twisted 是一个功能极其丰富的框架,支持 Web 服务器和客户端、SSH2、SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP、DNS 等!

下载并安装 Twisted

Twisted 安装起来非常容易。直接用 pip 安装即可。

安装完成后就应该能够使用 Twisted 了。

编写 Twisted 服务器

前面编写的简单套接字服务器非常清晰,其中有些包含显式的事件循环,用于查找新连接和新数据。基于 SocketServer 的服务器有一个隐式的循环,用于查找连接并为每个连接创建处理程序,但处理程序必须显示的读取数据。Twisted 采用的是基于事件的方法。要编写简单的服务器,只需实现处理如下情形的事件处理程序:客户端发起连接,有数据到来,客户端断开连接(以及众多其他的事件)。专用类可在基本类的基础上定义更细致的事件,如包装“数据到来”事件,收集换行符之前的所有数据再分派“数据行到来”事件。

注意 有一个 Twisted 特有的概念我没有介绍,那就是延迟对象(deferred)和延迟执行(deferred execution)。有关这方面的详细信息,请参阅 Twisted 文档(如阅读教程“Deferreds are beautiful”,这可在 Twisted 文档中的 HOWTO 页面中找到)。

事件处理程序是在协议中定义的。你还需要一个工厂,它能够在新连接到来时创建这样的协议对象。如果你只想创建自定义协议类的实例,可使用 Twisted 自带的工厂——模块 twisted.internet.protocol 中的 Factory 类。编写自定义协议时,将模块 twisted.internet.protocol 中的 Protocol 作为超类。有新连接到来时,将调用事件处理程序 connectionMade;连接中断时,将调用 connectionLost。来自客户端的数据是通过处理程序 dataReceived 接收的。当然,你不能使用事件处理策略来向客户端发送数据。这种工作是使用对象 self.transport 完成的,它包含一个 write 方法。这个对象还有一个 client 属性,其中包含客户端的地址(主机名和端口)。

下面的代码是 Twisted 服务器。在这段代码中,包含一些设置工作:需要实例化 Factory,并设置其属性 protocol,让它知道该使用哪种协议(这里是一个自定义协议)与客户端通信。

接下来,开始监听指定的端口,让工厂通过实例化协议对象来处理连接。为此,调用了模块 reactor 中的函数 listenTCP。最后,通过调用模块 reactor 中的函数 run 启动这个服务器。

使用 Twisted 创建的简单服务器

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory, connectionDone


class SimpleLogger(Protocol):
    def connectionMade(self):
        print('Got connection from', self.transport.client)
    def connectionLost(self, reason=connectionDone):
        print(self.transport.client, 'disconnected')
    def dataReceived(self, data):
        print(data)
factory = Factory()
factory.protocol = SimpleLogger
reactor.listenTCP(1234, factory)
reactor.run()

如果使用 telnet 连接到这个服务器以便测试它,每行输出可能只有一个字符,是否如此取决于缓冲等因素。你可使用 sys.stdout.write 而不是 print,但在很多情况下,你可能希望每次得到一行,而不是得到随意的数据。为此,可编写一个自定义协议,尽管这很容易,但实际上有一个提供这种功能的现成类。模块 twisted.protocols.basic 包含几个预定义的协议,其中一个就是 LineReceiver。它实现了 dataReceived,并在收到一整行后调用事件处理程序 lineReceived。

提示 要在收到数据后做些除调用 lineReceived(它依赖实现了 dataReceived 的 LineReceiver)外的其他事情,可使用 LineReceiver 定义的事件处理程序 rawDataReceived。

切换到协议 LineReceiver 需要做的工作很少,如下面的代码所示。如果查看运行这个服务器得到的输出,将发现换行符被删除了。换而言之,使用 print 不能再生成两个换行符。

使用协议 LineReceiver 改进后的日志服务器

from twisted.internet import reactor
from twisted.internet.protocol import Factory, connectionDone
from twisted.protocols.basic import LineReceiver


class SimpleLogger(LineReceiver):
    def connectionMade(self):
        print('Got connection from', self.transport.client)
    def connectionLost(self, reason=connectionDone):
        print(self.transport.client, 'disconnected')
    def lineReceived(self, line):
        print(line)
    def rawDataReceived(self, data):
        pass
factory = Factory()
factory.protocol = SimpleLogger
reactor.listenTCP(1234, factory)
reactor.run()

前面说过,Twisted 框架的功能比这里介绍的要多得多。如果你要更深入的了解,可参阅 Twisted 网站(http://twistedmatrix.com)的在线文档。

小结

这里简要地介绍了多种 Python 网络编程方法,选择哪种方法取决于具体需求和你的偏好。选择一种方法后,你很可能需要更深入的学习。下面是这里介绍的一些主题。

  • 套接字和模块 socket:套接字是让程序(进程)能够通信的信息通道,这种通信可能需要通过网络进行。模块 socket 让你能够在较低的层面访问客户端套接字和服务器套接字。服务器套接字在指定的地址处监听客户端连接,而客户端套接字直接连接到服务器。
  • urllib 和 urllib3:这些模块让你能够从各种服务器读取和下载数据,为此你只需提供指向数据源的 URL 即可。模块 urllib 是一种比较简单的实现,而 urllib3 功能强大、可扩展性极强。这两个模块都通过诸如 urlopen 等函数来完成工作。
  • 框架 SocketServer:这个框架位于标准库中,包含一系列同步服务器基类,让你能够轻松的编写服务器。它还支持使用 CGI 的简单 Web(HTTP)服务器。如果要同时处理多个连接,必须使用支持分叉线程化的混合类。
  • select 和 poll:这两个函数让你能够在一组连接中找出为读取和写入准备就绪的连接。这意味着你能够以循环的方式依次为多个连接提供服务,从而营造出同时处理多个连接的假象。另外,相比于线程化或分叉,虽然使用这两个函数编写的代码要复杂些,但解决方案的可伸缩性和效率要高得多。
  • Twisted:这是 Twisted Matrix Laboratories 开发的一个框架,功能丰富而复杂,支持大多数主要的网络协议。虽然这个框架很大且其中使用的一些成例看起来宛如天书,但其基本用法简单而直观。框架 Twisted 也是异步的,因此效率和可伸缩性都非常高。对很多自定义的网络应用程序来说,使用 Twisted 来开发很可能是最佳的选择。

本文分享自微信公众号 - 小陈学Python(gh_a29b1ed16571),作者:小陈学Python

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 虚拟茶话会(1):初次实现

    在这个项目中,我们将做些正式的网络编程工作:编写一个聊天服务器,让人们能够通过网络实时地聊天。使用Python创建这种程序的方式有很多,一种简单而自然的方法是使...

    不可言诉的深渊
  • Python 文件分类

    文件分类应该是大家每天都在做的事情。假设现在有一大批文件(这一大批文件类型不定,也就是说有些是文本文件有些是二进制文件)都被放在了一个文件夹中,需要对它们进行一...

    不可言诉的深渊
  • Nginx 之访问认证

    我们都知道,Web 服务器程序部署成功并启动之后,都是可以公开访问的,要想控制成只有部分人可以访问必然需要配置一下访问认证,实现访问认证的方法有很多,主要有两种...

    不可言诉的深渊
  • SOCKET,TCP/UDP,HTTP,FTP

    (一)TCP/UDP,SOCKET,HTTP,FTP简析 TCP/IP是个协议组,可分为三个层次:网络层、传输层和应用层: 网络层:IP协议、ICMP协议、A...

    苦咖啡
  • js获取元素样式之getComputedStyle方法

    习惯了jquery的同学应该都知道获取元素样式的方式可以直接写成(obj).css(style);更方便的获取高度宽度等一些样式可以直接使用(obj).heig...

    无邪Z
  • String,StringBuffrer,StringBuilder 三兄弟

    String这个类是Java中使用得最频繁的类之一,并且又是各大公司面试喜欢问到的地方,今天就来和大家一起学习一下String、StringBuilder和St...

    爱敲代码的猫
  • Android studio 下的SDK Manager只显示已安装包的情况

    听着music睡
  • T-SQL日期和时间函数

    SQL Server 2008 R2 使用 GetSystemTimeAsFileTime() Windows API 来获取日期和时间值。精确程度取决于运行 ...

    fireWang
  • Pandas缺失值处理 | 轻松玩转Pandas(3)

    禹都一只猫olei
  • SQL Server 数据表复制

    有数据表 A, 希望将他的内容导出到另一张表 B,此时 B 并未创建,希望在导出的同时创建表 B。

    tonglei0429

扫码关注云+社区

领取腾讯云代金券