Python 基础学习16:网络编程

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

常用的模块

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

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 以接着等待新连接的到来,通常是在一个无限循环中完成的。

为了传输数据,套接子提供了两个方法 sendrecv
要发送数据,可调用方法 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, readlinereadlines 方法,还支持迭代。

示例:

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 是标准库提供的服务器框架基石,这个框架包括 BaseHTTPServerSimpleHTTPServerCGIHTTPServer SimpleXMLRPCServerDocXMLRPCServer 等服务器。它们都在基础服务器的基础功能上添加了各种功能:

+------------+
| 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/asynchatTwisted 采取的方法。这种方法的基石是函数 selectpoll 。两个函数都位于 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来减少垃圾评论。了解我们如何处理您的评论数据

滚动至顶部