在你使用互联网时,接触到的无非是这两个大功能。第一、上网获取你想要的的信息。动态的进行查询、筛选得到你想要的结果。第二、就是通信。你想要上网联系你的家人、朋友等,这时,就需要网络通信,也就是我们常说的网络编程。
第一个知识,在上篇文章的python操作数据库已经讲了,可前往:教你轻松学会Python操作数据库。 那本篇文章呢,我就来讲讲第二个功能——通信(网络编程)。
主要点:网络编程概念、网络模块(包含通信原理)、SocketServer及相关的类。
一、网络编程概念
虽说是概念,我将用容易让你理解的一句话给你讲:
刚才也讲到,网络编程其实就是我们常用到的通信。你发送信息给对方,对方能收到你的信息。这就是一个很简单的示例。
如果你是个初学者,对这方面的问题很好奇,其实就是你对这些方法不理解,没接触过。接触过之后你就不会很好奇了。
二、网络模块
最常用同时也是必要的网络模块,不得不讲一下socket,以及其他模块。
- socket模块
套接字听说过吗?估计你会有点熟悉。没错,网络编程中的一个基本组件套接字(socket)。这是在通信过程中必不可少的组件。套接字是一个信息通道,在收发信息的两端各有一个程序。所以,在不同的计算机或手机上,就是通过套接字向对方发送信息。
套接字分为两类:一类是服务器套接字,另一类是客户端套接字。
当服务器的套接字创建之后,它将等待连接请求的到来。一直处于监听状态,当然她必须要有一个地址,也就是我们常说的IP地址和端口号。等待客户端套接字与其建立连接,接着两者即可进行通信。
相比于服务器套接字,客户端的套接字处理起来会容易很多,因为服务器必须准备虽是处理客户端连接,而且可能是多个连接。然而客户端只需要连接,完成任务后断开连接即可。
套接字是模块socket中socket类的实例。实例化套接字(socket()函数来创建套接字)时最多可指定三个参数:
socket.socket([family[, type[, proto]]])
一、地址族,默认是socket.AF_INET
二、类型:流套接字(socket.SOCK_STREAM)、数据报套接字(socket.SOCK_DGRAM)
三、协议:一般写默认值0.
参数
family: 套接字家族可以使AF_UNIX或者AF_INET
type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
protocol: 一般不填,默认为0.
服务器套接字的创建:
先调用bind()方法,绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。再调用listen()来监听特定的地址,开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。开始监听之后,可用accept()方法来接收客户端的连接。这个方法将阻断(等待)到客户端连接到来为止,然后返回一个格式为(client,address)的元组。这个意思就是说如果当前有连接客户端的话,服务器端就会阻断其他客户端的连接,也就是等待。等当前客户端连接断开之后,服务端会再次调用accept()等待新的连接。所以,服务端的套接字通常是写在无限循环语句中。
注:服务器有两种网络编程形式。1.阻断(同步)网络编程 2.非阻断(异步)网络编程
上面这种讲的是同步,稍后我会讲异步编程形式。
客户端套接字的创建:
在服务器端创建完套接字,客户端套接字就可以连接到服务器了。首先,调用connect()方法,并提供调用方法bind时指定的地址。此地址可通过函数socket.gethostname()获取当前机器的主机名。s.connect((hostname,port)),地址是一个元组格式。hostname为主机名,port是端口号。
传输数据,套接字函数:
两个最基本的方法send()和recv(),发送数据,可使用send方法并提供一个字符串。接收数据可使用recv方法,可指定接收多少个字节数据。一般使用1024.
其他方法:
名称 描述
s.sendall() 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom() 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
s.sendto() 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile() 创建一个与该套接字相关连的文件
下面,我给出一个服务器端和客户端的编程示例:
服务器端:
import socket # 导入 socket 模块
s = socket.socket() # 创建 socket 对象
host = socket.gethostname() # 获取本地主机名
port = 1234 # 设置端口
s.bind((host, port)) # 绑定端口
s.listen(5) # 等待客户端连接
str = 'This is python network programming instruction'.encode('UTF-8')
#修改编码格式
while True:
c,addr = s.accept() # 建立客户端连接
print('连接地址:', addr)
c.send(str)
c.close() # 关闭连接
客户端:
import socket # 导入 socket 模块
s = socket.socket() # 创建 socket 对象
host = socket.gethostname() # 获取本地主机名
port = 1234 # 设置端口号
s.connect((host, port))
print(s.recv(1024))
s.close()
执行代码的时候先执行服务器端,在执行客户端。
运行结果:
- 其它模块
上面讲的socket只是众多网络模块中的一个,下表列出与网络相关的模块,仅供参考:
模块 描述
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客户端支持
菜鸟教程上给列出的网络模块大家也可参考一下:
协议 功能用处 端口号 Python 模块
HTTP 网页访问 80 httplib, urllib, xmlrpclib
NNTP 阅读和张贴新闻文章,俗称为"帖子"119 nntplib
FTP 文件传输 20 ftplib, urllib
SMTP 发送邮件 25 smtplib
POP3 接收邮件 110 poplib
IMAP4 获取邮件 143 imaplib
Telnet 命令行 23 telnetlib
Gopher 信息查找 70 gopherlib, urllib
三、SocketServer及相关的类
通过上面的讲解,你对基础的网络编程是不是认知了很多。的确,上面讲的是基础,确实很简单。不过那个只是原理,只能一个客户端与服务器端连接,如果有多个客户端发起请求,那么就只能等待了。可试想,假如你做了一个网站,每一次只能一个人访问,那这样的网站还会有人来吗?所以,为解决这个问题,就需要讲接下来的知识——SocketServer。
SocketServer模块是标准库提供的服务器框架的基石,这个框架包含BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer等服务器,它们在基本服务器上添加了各种功能。
SocketServer包含4个基本的服务器:TCPServer(支持TCP套接字流)、UDPServer(支持UDP数据报套接字)以及不常用的UnixStreamServer和UnixDatagramServer。
使用模块SocketServer编写服务器时,大部分代码都位于请求处理其中。每当服务器收到客户端的连接请求时,都将实例化一个请求处理程序,并对其调用各种处理方法来处理请求。
具体调用哪些方法取决于使用的服务器类和请求处理程序类;还可从这些请求处理器类派生出子类,从而让服务器调用一组自定义的处理方法。基本请求处理程序类BaseRequestHandler将所有操作都放在一个方法中——服务器调用方法handle。这个方法可通过属性self.request来访问客户端套接字。如果处理的是流,可使用StreamRequestHandler类,它包含另外两个属性:self.rfile(用于读取)和self.wfile(用于写入)。可使用这两个类似于文件的对象来与客户端通信。
下面给出一个简单服务器的SocketServer代码示例,StreamRequestHandler负责在使用完连接后将其关闭。
from socketserver import TCPServer,StreamRequestHandler
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('连接地址',addr)
self.wfile.write('连接成功!')
server = TCPServer(('',1234),Handler) #''表示运行该服务器的计算机
server.serve_forever()
多个连接:
想要同时处理多个客户端的连接请求,可以有以下几种方法:分叉(forking)、线程化、异步I/O。
分叉和线程可以通过SocketServer轻松实现。异步I/O需要通过select和poll实现。
分叉是一个UNIX术语。分叉占用资源较多,如果客户端数量过多,可伸缩性就降下来了。但如果客户端数量一般,将分叉用于Linux或者UNIX系统中,效率还是挺高。如果要提高效率,可以增加CPU的数量。(Windows不支持分叉)
线程化减少了空间资源,由于线程共享内存,所以必须要确保它们不会彼此干扰或同时修改一项数据,否则会引起混乱。
分叉服务器:
from socketserver import TCPServer,ForkingMixIn,StreamRequestHandler
class Server(ForkingMixIn,TCPServer):pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('连接地址:',addr)
self.wfile.write('连接成功!')
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('连接地址:',addr)
self.wfile.write('连接成功!')
server = Server(('',1234),Handler)
server.serve_forever()
select和poll实现异步I/O:
当服务器与客户端通信时,来自客户端的数据可能时断时续。如果使用了分叉和线程化,就可以实现处理多个连接请求:一个进程(线程)等待数据时,其他进程(线程)可继续处理其客户端。
当然,我们可以使用异步I/O——只处理当前正在通信的客户端。不需要不断监听,只需要监听将客户端加入队列即可。
这就是框架asyncore/asynchat和Twisted采取的方法。这种功能的基石是函数select或poll。这两个函数都位于模块select中,其中poll的可伸缩性更高,但只有UNIX系统支持它。
函数select接收三个必要参数和一个可选参数,前三个参数为序列,第四个参数为超时时间(单位秒)。这些序列包含文件描述符整数,表示正在等待的连接。这三个序列分别为需要输入、输出以及发生异常的连接。如果没有指定超时时间,select将阻断(等待)到有文件描述符准备就绪;如果指定了超时时间,select将最多阻断指定的秒数;如果超时时间为零,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('连接地址:',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)
方法poll使用起来比select容易,在调用poll时,将返回一个轮询对象。可使用方法register向这个对象注册文件描述符。注册后可使用方法unregister将它们删除。注册对象后,可调用其方法poll(可接受一个可选的超时时间参数)。这将返回一个包含(fd,event)元组的列表(可能为空),其中fd为文件描述符,而event是发生的事件。event是一个位掩码,这意味着它是一个整数,其各个位对应于不同的事件。各种事件是用select模块中的常量表示的,如下表。
select模块中的轮询事件常量
事件名 描述
POLLIN 文件描述符中有需要读取的数据
POLLPRI 文件描述符中有需要读取的紧急数据
POLLOUT 文件描述符为写入数据做好了准备
POLLERR 文件描述符出现了错误状态
POLLHUP 挂起。连接已断开
POLLNVAL 无效请求。连接未打开
要检查指定的位是否为1,即是否发生了相应的事件,可使用按位与运算符(&):
if event & select.POLLIN:
...
使用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.poll()
for fd,event in events:
if fd in fdmap:
c,addr = s.accept()
print('连接地址:',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)