blog icon indicating copy to clipboard operation
blog copied to clipboard

Python之旅:第十四章-网络编程

Open kaindy7633 opened this issue 3 years ago • 0 comments

Table of Contents generated with DocToc

  • Python之旅:第十四章 网络编程
    • 几个网络模块
      • 模块socket
      • 模块urllib和urllib2
        • 打开远程文件
        • 获取远程文件
    • SocketServer及相关的类
    • 多个连接
      • 使用SocketServer实现分叉和线程化
      • 使用select和poll实现异步IO

Python之旅:第十四章 网络编程

Python提供了强大的网络编程支持,有很多库实现了常见的网络协议以及基于这些协议的抽象层,让你能够专注于程序的逻辑。

几个网络模块

模块socket

网络编程中的一个基本组件是套接字(socket)。套接字是一个信息通道,两端各有一个程序,这些程序位于通过网络连接的不同的计算机上,通过套接字向对方发送消息。

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

套接字是模块socketsocket类的实例。实例化套接字时可指定三个参数:

  • 地址族,默认为socket.AF_INET
  • 类型,是流套接字(socket.SOCK_STREAM,默认设置)还是数据报套接字(socket.SOCK_DGRAM)
  • 协议,使用默认值0

创建普通套接字时,不用提供任何参数。

服务器套接字先调用方法bind,再调用方法listen来监听特定的地址,然后客户端套接字就可以使用方法connect,并提供服务器监听的地址,来连接服务器了。

服务器套接字开始监听后,就可以接收客户端的连接了,这是使用方法accept来完成的。当连接到来时,这个方法将阻断,然后返回一个格式为(client, address)的元组,最后服务器再次调用accept以等待新的连接到来。

为传输数据,套接字提供了两个方法:sendrecv。要发送数据,可调用方法send并提供一个字符串,要接收数据,可调用recv并制定最多接收多少字节的数据,通常1024就可以了。

# 最简单的服务器
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('Thank you for connecting')
    c.close()
# 最简单的客户端
import socket

s = socket.socket()

host = sockent.gethostname()
port = 1234

s.connect((host, port))
print(s.recv(1024))

模块urllib和urllib2

这两个网络模块能够让你通过网络访问文件。它们的功能很相似,对于简单的下载,使用urllib就足够了,如果要实现HTTP身份验证或Cookie,又或者血药编写扩展来处理自己的协议,就需要urllib2了。

打开远程文件

我们可以使用urllib模块轻松的打开远程文件,但只能使用读取模式,跟打开本地文件使用open方法不一样,打开远程文件需要使用urllib.request中的函数urlopen

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

变量webpage将包含一个类似于文件的对象。这个独享支持方法closereadreadlinereadlines,还支持迭代。

获取远程文件

函数urlopen返回一个类似于文件的对象,可从中读取数据。如果要下载文件,并将其副本存储在一个本地文件中,可使用urlretrieve,这个函数返回一个格式为(filename, headers)的元组。filename是本地文件的名称,而headers包含一些远程文件的信息。如果要给下载的副本指定文件名,可以通过第二个参数来指定。

>>> urlretrieve('http://www.python.org', '/Users/liuzhen/python/python_webpage.html')

SocketServer及相关的类

模块SocketServer是Python标准库提供的服务器框架的基石,这个框架包括BaseHTTPServerSimpleHTTPServerCGIHTTPServerSimpleXMLRPCServerDocXMLRPCServer等服务器。

SocketServer包含4个基本的服务器:TCPServerUDPServer,以及很难懂的UnixStreamServerUnixDatagramServer。后面3个我们基本上用不上。

# 基于SocketServer的极简服务器
from socketserver import TCPServer, StreamRequestHandler

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

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

多个连接

前面创建的网络链接方式都是同步的,也就是不能同时处理多个连接。若想要处理多个连接,主要有三种方式:分叉(forking)、线程化和异步I/O。分叉占用的资源较多,而线程化会带来同步问题。

分叉是一个UNIX术语,对进程进行分叉时,基本上是复制它,而这样得到的两个进程都从当前位置开始继续往下执行,且每个进程都有自己的内存副本。原来的进程为父进程,复制的进程为子进程。在分叉服务器中,对于每个客户端的连接,都将通过分叉创建一个子进程。父进程继续监听新连接,而子进程负责处理客户端请求,客户端请求结束后,子进程直接退出。

分叉占用的资源比较多,还有另一种解决方案:线程化。线程是轻量级进程,都位于同一进程中并共享内存,这减少了资源占用,但也带来了缺点:由于线程共享内存,你必须确保它们不会彼此干扰或同时修改同一项数据,否则将引起混乱,这些问题都属于同步问题。

使用SocketServer实现分叉和线程化

# 分叉服务器
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('Thank you for connecting')

server = Server(('', 1234), handler)
server.serve_forever()
# 线程化服务器
from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler

class Server(ThreadingMinIn, TCPServer): pass

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

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

使用select和poll实现异步IO

异步I/O就是只处理当前正在通信的客户端,无需监听,只需监听后将客户端加入队列即可。这就是框架asyncore/asynchatTwisted采用的方法。这种功能的基础是函数selectpoll,它们都位于模块select中,其中poll可伸缩性更高,但只有UNIX系统支持它。

函数select接收三个必不可少的参数和一个可选参数,其中前三个参数为序列,而第四个参数为超时时间。

# 使用select的简单服务器
import socket, select

s = socket.socket()

host = socket.gethostname()
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('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)

使用pollselect更容易。调用poll时,返回一个轮询对象,你可以使用方法register向这个对象注册文件描述符,注册后可使用方法unregister将它们删除。注册对象后,就可以调用其方法poll

# 使用poll的简单服务器
import socket, 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.opll()
    for fd, event inevents:
        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)

本章节完毕

本系列目录:

kaindy7633 avatar Mar 08 '21 03:03 kaindy7633