Python 基础学习16:网络编程

Python 网络编程,了解 socket 和 SocketServer 的同步和异步处理。

Python 中有很多库实现了常见的网络协议,可以专注于程序逻辑而不必关心底层线路传输的问题。

常用的模块

标准库中有很多的网络模块,常用的有 scocket 和 SocketServer 相关的模块。

socket

网络编程中的一个基本组件是套接字(socket)。套接字本质上是一个信息通道,两端各有一个程序。

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

客户端套接字处理起来比服务器套接字要容易,它只需连接,完成任务后断开连接就可以了,服务器套接字需要随时准备处理客户端连接,有时还须处理多个连接。

套接子是模块 socket 中的 socket 类的实例。
_class_ socket.socket(_family=AF_INET_, _type=SOCK_STREAM_, _proto=0_, _fileno=None_)

实例化套接字时最多可以指定三个参数:

  • 地址族,默认为 socket.AF_INET (ipv4) socket.AF_INET6 是 ipv6
  • 流套接字(默认)(socket.SOCK_STREAM)还是数据包套接字(socket.SOCK_DGRAM)
  • 协议号通常为零并且可以省略
    创建普通套接字时,不用提供任何参数。

服务器套接字先调用 bind 方法,再调用 listen 方法来监听特定的地址。
随后客户端套接字调用 connect 方法,指定服务器套接字 bind 方法指定的地址。
方法 listen 接受一个参数,用于指定最多可以有多少个连接在队列中等待接纳,到达这个数量后将开始拒绝。

服务器套接字开始监听后,就可以接受客户端的连接了,通过调用 accept 方法来完成。这个方法将阻断到客户端连接到来为止,然后返回一个格式为 (client, address) 的元组,其中 client 是一个客户端套接字, address 是客户端的地址。这种模式称为同步网络编程。

服务器处理客户端的连接,然后再次调用 accept 以接着等待新连接的到来,通常是在一个无限循环中完成的。

为了传输数据,套接子提供了两个方法 send 和 recv 。
要发送数据,可调用方法 send 并提供一个 byte 类型的数据。要接收数据,可调用 recv 方法,并指定最多接受多少个字节的数据,1024 是个不错的选择。

服务器套接字示例:

import socket

HOST, PORT = '', 1024

with socket.socket() as s:
    s.bind((HOST, PORT))
    s.listen(5)

    while True:
        c, addr = s.accept()
        print('Client: {}, Addr: {}'.format(c, addr))
        data = c.recv(1024)

        c.send(data.upper())
        c.close

客户端套接字示例:

import sys, socket

HOST, PORT = "localhost", 1024

with socket.socket() as s:
    data = "".join(sys.argv[1:])

    s.connect((HOST, PORT))
    s.send(data.encode())
    received = s.recv(1024)

    print(str(received))

模块 urllib 和 urllib2

这两个模块能够通过网络访问文件,urllib2 相比较 urllib而言能实现 HTTP 身份验证或 Cookie ,或者编写扩展来处理自己的协议,对于简单的下载 urllib 足以。

打开远程文件

使用模块 urllib.request 中的 urlopen 方法,将 URL 作为参数,将访问网页文件对象。
urlopen 返回的类似于文件的对象支持 close, read, readline 和 readlines 方法,还支持迭代。

示例:

from urllib.request import urlopen
import re

data = urlopen('http://www.imxcai.com')

text = data.read().decode("utf-8")

m = re.search('<a href="([^"]+)" .*?>Python</a>', text, re.IGNORECASE)

print(m.group(1))

下载远程文件

要下载文件可以使用 urllib 中的 urlretrieve 方法。该函数不返回类似于文件的对象,而是返回一个格式为 (filename, headers) 的元组,filename 是本地文件的名称,自动创建的,headers 包含远程文件的信息。

urlretrieve 第一个参数指定下载的资源URL,第二个参数可以指定下载的数据存放位置,如果不指定将存放在临时位置,可以调用 urlcleanup 且不提供任何参数就可以帮助清空数据。
示例:

>>> from urllib.request import urlretrieve
>>> data = urlretrieve('http://www.imxcai.com')
>>> data
('/tmp/tmp969pi230', <http.client.HTTPMessage object at 0x7f1dec0c30e0>)

其它模块

Python 标准库中的一些与网络相关的模块:

模块描述
asynchat包含补充asyncore的功能
asyncore异步套接字处理程序
cgi基本的CGI支持
CookieCookie对象操作,主要用于服务器
cookielib客户端Cookie支持
email电子邮件(包括MIME)支持
ftplibFTP客户端模块
gopherlibGopher客户端模块
httplibHTTP 客户端模块
imaplibIMAP4客户端模块
mailbox读取多种邮箱格式
mailcap通过mailcap文件访问MIME配置
mhlib访问MH邮箱
nntplibNNTP客户端模块
poplibPOP客户端模块
robotparser解析Web服务器robot文件
SimpleXMLRPCServer一个简单的XML-RPC服务器
smtpdSMTP服务器模块
smtplibSMTP客户端模块
telnetlibTelnet客户端模块
urlparse用于解读URL
xmlrpclibXML-RPC客户端支持

SocketServer 及相关的类

模块 SocketServer 是标准库提供的服务器框架基石,这个框架包括 BaseHTTPServer 、 SimpleHTTPServer 、CGIHTTPServer SimpleXMLRPCServer 和 DocXMLRPCServer 等服务器。它们都在基础服务器的基础功能上添加了各种功能:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

SocketServer 包含四个基本的服务器:

  • TCPServer 支持 TCP 套接字流
  • UDPServer 支持 UDP 数据包套接字
  • UnixServer
  • UnixDatagramServer

基于 SockerServer 的极简服务器示例:

from socketserver import TCPServer, StreamRequestHandler

class Handler(StreamRequestHandler):
    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from {}'.format(addr))
        self.wfile.write(b'Thank you for connecting')


server = TCPServer(('', 1234), Handler)
server.serve_forever()

多个连接

前面的服务器解决方法都是同步的:意味着不能同时处理多个客户端的连接请求,只能处理完一个然后再处理第二个。

处理多个连接的主要方式有三种:分叉(forking)、线程化和异步I/O 。
分叉占用的资源较多,且在客户端很多时可伸缩性不佳,而线程化可能带来同步问题。

分叉和线程是什么?

fork 是一个 UNIX 术语,对进程进行 fork ,基本上是赋值它,这样得到的两个进程都将从当前位置开始继续往下执行,且每个进程都有自己的内存副本。原来的进程为父进程,复制的进程为子进程,进程能够判断它们是原始进程还是子进程,因此能够执行不同的操作。

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

鉴于分叉占用的资源较多,还有另一种解决方法:线程化。
线程是轻量级的进程(子进程),都位于同一个进程中并共享内存。由于线程共享内存,必须确保它们不会彼此干扰或同时修改同一项数据,否则将引起混乱,这属于同步问题。

使用 SocketServer 实现分叉和线程化

使用 SocketServer 创建分叉或线程化服务器非常简单,仅当 handle 需要很长时间才能执行完毕时,分叉和线程化才能提供帮助。

分叉服务器示例:

from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
from time import sleep

class Server(ForkingMixIn, TCPServer): pass

class Handler(StreamRequestHandler):

    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from {}.'.format(addr))
        sleep(1000)
        self.wfile.write(b'Thank you for connecting!')

server = Server(('', 1234), Handler)
server.serve_forever()

多个客户端同时连接,此时系统上的进程可以看到 fork 出了多个子进程:

ps -axf -o pid,ppid,comm | grep 72716
  72716    6765  |   |   |   |   \_ python
  72746   72716  |   |   |   |       \_ python
  72789   72716  |   |   |   |       \_ python
  72843   72716  |   |   |   |       \_ python
  72870   72716  |   |   |   |       \_ python
  72918   72716  |   |   |   |       \_ python
  72945   72716  |   |   |   |       \_ python
  73016   72716  |   |   |   |       \_ python

线程化服务器示例:

from socketserver import TCPServer, StreamRequestHandler, ThreadingMixIn
from time import sleep

class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
    def handle(self):
        addr = self.request.getpeername()
        print('Got connection from {}.'.format(addr))
        sleep(1000)
        self.wfile.write(b'Thank you for connecting!')

server = Server(('', 1234), Handler)
server.serve_forever()

查看线程的信息:

pstree -p 75035
python(75035)─┬─{python}(75131)
              ├─{python}(75183)
              ├─{python}(75210)
              ├─{python}(75246)
              ├─{python}(75273)
              ├─{python}(75314)
              └─{python}(75348)

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

当服务器与客户端通信时,来自客户端的数据可能时断时续,可以通过分叉和线程化解决,还有另一种做法是只处理当前正在通信的客户端,无需不断监听,只需监听后将客户端加入队列即可。这是框架 asyncore/asynchat 和 Twisted 采取的方法。这种方法的基石是函数 select 或 poll 。两个函数都位于 select 模块中,其中 poll 可伸缩性高,但只有 UNIX 系统支持。

函数 select 接受三个必不可少的参数和一个可选参数,其中前三个参数为序列,第四个参数为超时时间,单位是秒。
三个序列分别表示需要输入和输出以及发生异常的连接,如果没有指定超时时间,select 将阻断到有文件描述符准备就绪时,如果超时时间为零,将不断轮询。

select 返回三个序列,也就是一个长度为 3 的元组,每个序列都包含相应参数中处于活动状态的文件描述符。返回的第一个序列包含有数据需要读取的所有输入文件描述符。

使用 select 的服务器示例:

import socket, select

s = socket.socket()

host = ''
port = 1234
s.bind((host, port))
s.listen(5)

inputs = [s]
while True:
    rs, ws, es = select.select(inputs, [], [])
    for r in rs:
        if r is s:
            c, addr = s.accept()
            print(b'Got connection from', addr)
            inputs.append(c)
    else:
        try:
            data = r.recv(1024)
            disconnected = not data
        except socket.error:
            disconnected = True

        if disconnected:
            print(r.getpeername(), 'disconnected')
            inputs.remove(r)
        else:
            print(data)

测试客户端:

import sys, socket
from time import sleep

HOST, PORT = "localhost", 1234

with socket.socket() as s:
    data = "".join(sys.argv[1:])

    s.connect((HOST, PORT))

    while True:
        sleep(3)
        s.send(data.encode())
    received = s.recv(1024)

    print(str(received))

输出效果:

b'Got connection from' ('127.0.0.1', 59308)
b'client_A'
b'client_A'
b'Got connection from' ('127.0.0.1', 51866)
b'client_A'
b'Client_B'
b'client_A'
b'Client_B'
b'client_A'
b'Client_B'

方法 poll 使用起来比 select 容易。
调用 poll 时,会返回一个轮询对象,可使用 register 方法项这个对象注册文件描述符,注册后可以通过 unregister 将其删除。

注册对象后,可调用其方法 poll,将返回一个包含 (fd, event) 元组的列表。fd 为文件描述符,event 是发生的事件,它是一个位掩码,是一个整数,各个位对应不同的事件。
各个事件是用 select 模块中的常量表示的,要检查指定的位是否为1 ,可以使用按位与运算符:
if event & select.POLLIN: ...

select 模块中轮询事件常量:

事件名描述
POLLIN文件描述符中有需要读取的数据
POLLPRI文件描述符中有需要读取的紧急数据
POLLOUT文件描述符为写入数据做好了准备
POLLERR文件描述符出现了错误状态
POLLHUP挂起。连接已断开
POLLNVAL无效请求。连接未打开

使用 poll 的简单服务器示例:

import socket, select

s = socket.socket()

host, 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 connectin from {}.'.format(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)

Twisted

Twisted 是由 Twisted Matrix Laboratories 开发的,是一个事件驱动的 Python 网络框架。
Twisted 是一个功能及其丰富的框架,支持 Web 服务器和客户端、SSH2、
SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP、DNS等。

编写 Twisted 服务器

Twisted 采用的是基于事件的方法,只需实现处理如下情形的事件处理程序:

  • 客户端发起连接
  • 有数据到来
  • 客户端断开连接

专用类可在基本类的基础上定义更细致的事件。

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

使用 Twisted 创建的简单服务器示例:

from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory, connectionDone
from twisted.python.failure import Failure

class SimpleLogger(Protocol):
    def connectionLost(self, reason: Failure = ...) -> None:
        return super().connectionLost(reason)

    def connectionMade(self):
        print('Got a connection')
        return super().connectionMade()

    def dataReceived(self, data: bytes) -> None:
        print(data)
        return super().dataReceived(data)


factory = Factory()
factory.protocol = SimpleLogger

reactor.listenTCP(1234, factory)
reactor.run()

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理。

滚动至顶部