Python中的网络编程
. 本节中使用的主要模块是socket模块,在模块中可以找到socket()函数,该函数用于创建套接字对象。套接字也有自己的方法集,这些方法可以实现基于套接字的网络通信。
socket()模块函数
. 创建套接字要使用socket.socket()函数,其一般语法如下:
socket(socket_family,socket_type,protocol = 0)
. 其中socket_family是AF_UNIX或AF_INET,socket_type是SOCK_STREAM或SOCK_DGRAM,protocol通常省略,默认为0。因此创建TCP或UDP套接字的形式如下:
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
udpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
. 在有了套接字对象后,就可以使用套接字对象的方法来做进一步交互。对Python来说,可以使用“from socket import * ”将socket属性引入命名空间中,这样能够大大缩短代码:
tcpSock = socket.socket(AF_INET,SOCK_STREAM)
套接字对象(内置)方法
. 以下的表列举了一些常见的套接字对象的方法和属性:
名称 | 描述 |
---|---|
服务器套接字方法 | |
s.bind() | 将地址(主机号、端口号对)绑定到套接字上 |
s.listen() | 设置并启动TCP监听器 |
s.accept() | 被动接受TCP客户端连接,一直等待知道连接到达(阻塞),每个连接进来的客户端,都会通过accept函数返回一个不同的客户端的socket对象和属于客户端的套接字 |
客户端套接字方法 | |
s.connect() | 主动发起TCP服务器连接 |
s.connect_ex() | 上个方法的升级版,会以错误码的形式返回问题,而不是抛出异常 |
普通的套接字方法 | |
s.recv() | 接受TCP消息 |
s.recv_into() | 接受TCP消息到指定的缓冲区 |
s.send() | 发送TCP消息 |
s.sendall() | 完整的发送TCP消息 |
s.recvfrom() | 接受UDP消息 |
s.recvfrom_into() | 接受UDP消息到指定的缓冲区 |
s.sendto() | 发送UDP消息 |
s.getpeername() | 连接到套接字(TCP)的远程地址 |
s.getsockname() | 当前套接字的地址 |
s.getsockopt() | 返回给定套接字选项的值 |
s.setsockopt() | 设置给定套接字选项的值 |
s.shutdown() | 关闭连接 |
s.close() | 关闭套接字 |
s.detach() | 在未关闭文件描述符的情况下关闭套接字,返回文件描述符 |
s.ioctl() | 控制套接字的模式(仅支持 Windows) |
面向阻塞的套接字方法 | |
s.setblocking() | 设置套接字的阻塞或非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 获取阻塞套接字操作的超时时间 |
面向文件的套接字方法 | |
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建与套接字关联的文件对象 |
数据属性 | |
s.family | 套接字家族 |
s.type | 套接字类型 |
s.proto | 套接字协议 |
创建TCP服务器
. 首先看看创建TCP服务器的一般伪代码:
ss = socket() #创建服务器套接字
ss.bind() #套接字与地址绑定
ss.listen() #监听连接
inf_loop: #服务器无限连接
cs = ss.accept() #接受客户端连接
comm_loop: #通信循环
cs.recv()/cs.send() #对话(接受/发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
. 所有的套接字都通过socket()函数来创建对象,因为服务器需要占用一个端口等待客户端的请求,所以需要绑定一个本地地址,特别的TCP服务器必须监听(传入的)连接,一旦这个过程完了之后,服务器就可以开始它的无限循环。
调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达(套接字也确实支持非阻塞模式)。
一旦服务器接收了一个连接,就会返回(利用accept())一个独立的客户端套接字,用来与即将到来的消息进行交换。也就是说,当一个传入的请求到达时,服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口(原始服务器套接字),以使其能够接受新的客户端连接;而通过使用这个新的套接字,客户端和服务器之间就可以开始参与发送和接受的对话中,直到连接终止(当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接)。
接下来创建一个TCP服务器,它接受来自客户端的消息,并返回加了时间戳的相同内容的消息:
from socket import *
from time import ctime
Host = ''
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
tcpSerSock = socket(AF_INET,SOCK_STREAM)
tcpSerSock.bind(Addr)
tcpSerSock.listen(5)
while True:
print('waiting for connection...')
tcpCliSock,addr = tcpSerSock.accept()
print('...connected from:',addr)
while True:
data = tcpCliSock.recv(Bufsize)
if not data:
break
senddata = '[%s] %s.' % (bytes(ctime(),'utf-8'),data)
tcpCliSock.send(senddata.encode())
tcpCliSock.close()
tcpSerSock.close()
创建TCP客户端
. 创建客户端相对要简单一些,首先还是看看伪码:
cs = socket() # 创建客户端套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通信循环
cs.send()/cs.recv() # 对话(发送 / 接收)
cs.close() # 关闭客户端套接字
. 一旦客户端拥有了一个套接字,它就可以利用套接字的connect()方法直接创建一个到服务器的连接,当连接建立之后就可以参与到与服务器的对话中,最后,如果客户端完成了它的事务,它就可以关闭套接字,终止此次连接。
以下是TCP客户端的代码示例:
from socket import *
Host = 'localhost'
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
tcpCliSock = socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(Addr)
while True:
data = input('> ')
if not data:
break
tcpCliSock.send(data.encode())
data = tcpCliSock.recv(Bufsize)
if not data:
break
print(data.decode('utf-8'))
tcpCliSock.close()
. 客户端也有一个无限循环,但是不会意味着它会永远运行下去,客户端循环在以下两种情况将会跳出:用户没有输入;或者服务器终止且对recv()方法的调用失败。
执行TCP服务器和客户端
. 首先要运行的应该是服务器,因为如果先运行客户端,那么将无法进行任何连接。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。
创建UDP服务器
. UDP服务器不需要TCP服务器那么多设置,因为它不是面向连接的,除了等待传入的连接外,几乎不需要做任何其他工作。它的伪码如下:
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 关闭(接收 / 发送)
ss.close() # 关闭服务器套接字(可选)
. 从上面的伪代码中可以看出,除了普通的创建套接字并将其绑定到本地地址(主机名/端口号对)外,并没有额外的工作。UDP 和 TCP 服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使客户端连接到一个独立的套接字“转换“的操作,以下是UDP加时间戳的服务器代码示例:
from socket import *
from time import ctime
Host = ''
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
udpSerSock = socket(AF_INET,SOCK_DGRAM)
udpSerSock.bind(Addr)
while True:
print('waiting for message...')
data,addr = udpSerSock.recvfrom(Bufsize)
senddata = '[%s] %s.' % (bytes(ctime(),'utf-8'),data)
udpSerSock.sendto(senddata.encode(),addr)
print('...received from and returned to :',addr)
udpSerSock.close()
创建UDP客户端
. UDP的客户端代码是最短的,它的伪代码如下:
cs = socket() # 创建客户端套接字
comm_loop: # 通信循环
cs.sendto()/cs.recvfrom() # 对话(发送 / 接收)
cs.close() # 关闭客户端套接字
. 一旦创建了套接字对象,就进入对话循环中与服务器进行信息交互,最后当通信结束的时候会关闭套接字。以下是UDP的时间戳客户端代码示例:
from socket import *
Host = 'localhost'
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
udpCliSock = socket(AF_INET,SOCK_DGRAM)
while True:
data = input('> ')
if not data:
break
udpCliSock.sendto(data.encode(),Addr)
data,Addr = udpCliSock.recvfrom(Bufsize)
if not data:
break
print(data)
udpCliSock.close()
socket模块属性
. 除了现在熟悉的 socket.socket()函数之外,socket 模块还提供了更多用于网络应用开发的属性,下标列出了常用的属性:
属性名 | 描述 |
---|---|
数据属性 | |
AF_UNIX、AF_INET、AF_INET6 、AF_NETLINK 、AF_TIPC | Python 中支持的套接字地址家族 |
SO_STREAM、SO_DGRAM | 套接字类型(TCP=流,UDP=数据报) |
has_ipv6 | 指示是否支持 IPv6 的布尔标记 |
异常 | |
error | 套接字相关错误 |
herror | 主机和地址相关错误 |
gaierror | 地址相关错误 |
timeout | 超时时间 |
函数 | |
socket() | 以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字对象 |
socketpair() | 以给定的地址家族、套接字类型和协议类型(可选)创建一对套接字对象,函数用于创建一对无名的、相互连接的套接字。 如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中 |
create_connection() | 常规函数,它接收一个地址(主机名,端口号)对,返回套接字对象 |
fromfd() | 以一个打开的文件描述符创建一个套接字对象 |
ssl() | 通过套接字启动一个安全套接字层连接;不执行证书验证 |
getaddrinfo() | 获取一个五元组序列形式的地址信息 |
getnameinfo() | 给定一个套接字地址,返回(主机名,端口号)二元组 |
getfqdn() | 返回完整的域名 |
gethostname() | 返回当前主机名 |
gethostbyname() | 将一个主机名映射到它的 IP 地址 |
gethostbyname_ex() | gethostbyname()的扩展版本,它返回主机名、别名主机集合和 IP 地址列表 |
gethostbyaddr() | 将一个 IP 地址映射到 DNS 信息;返回与 gethostbyname_ex()相同的 3 元组 |
getprotobyname() | 将一个协议名(如‘tcp’)映射到一个数字 |
getservbyname()/getservbyport() | 将一个服务名映射到一个端口号,或者反过来;对于任何一个函数来说,协议名都是可选的 |
ntohl()/ntohs() | 将来自网络的整数转换为主机字节顺序 |
htonl()/htons() | 将来自主机的整数转换为网络字节顺序 |
inet_aton()/inet_ntoa() | 将 IP 地址八进制字符串转换成 32 位的包格式,或者反过来(仅用于 IPv4 地址) |
inet_pton()/inet_ntop() | 将IP 地址字符串转换成打包的二进制格式,或者反过来(同时适用于 IPv4 和IPv6 地址) |
getdefaulttimeout()/setdefaulttimeout() | 以秒(浮点数)为单位返回默认套接字超时时间;以秒(浮点数)为单位设置默认套接字超时时间 |