套接字是计算机网络的数据结构,它体现了“通行端点”的概念。在任何类型的通信开始之前,网络应用程序都要创建套接字。有两种类型的套接字:基于文件的和基于网络的。
UNIX套接字拥有一个“家族名字” AF_UNIX(又名AF_LOCAL),它代表地址家族(address family):UNIX。因为两个进程运行在同一个计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持他们的底层基础结构。因为文件系统是一个运行在同一台主机上的多个进程之间的共享常量。
另一种类型的套接字是基于网络的,它也有自己的家族名字 :AF_INET,或者地址家族:因特网。另一个地址家族 AF_INET6用于IPv6。
总的来说,Python 支持 AF_UNIX、AF_NETLINK、AF_TIPC 和 AF_INET 家族。
有效的端口号范围为 0~65535(尽管小于 1024 的端口号预留给了系统),如果使用的是 POSIX 兼容的系统,可以在 /etc/services 文件中找到预留的端口号(以及服务器/协议和套接字类型)。
无论你使用哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这种类型的通信称为虚拟电路或流套接字。
面向连接的通信提供序列化的(拆分为多个片段)、可靠的和不重复的数据交付,而没有记录边界。为了实现 TCP 套接字,必须使用 SOCK_STREAM 作为套接字类型。
数据报类型的套接字。在数据传输过程中无法保证它的顺序性、可靠性或重复性。整个消息是整体发送的(不拆分),即保证了数据的边界记录。
实现这种连接类型的主要协议是用户数据报协议(UDP),为了创建 UDP 套接字,必须使用 SOCK_DGRAM 作为套接字类型。
要创建套接字,必须使用socket.socket()函数,它的一般语法如下:
socket(socket_family,socket_type,protocol=0)
其中 socket_family 是 AF_UNIX 或 AF_INET;socket_type 是 SOCK_STREAM 或 SOCK_DGRAM;protocol通常省略,默认为0。
所以,为了创建 TCP/IP 套接字,可以使用下面的方式调用 socket.socket():
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
所以,为了创建 UDP/IP 套接字,可以使用下面的方式调用 socket.socket():
udpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
可以把 socket 模块导入命名空间,这样可以缩短代码:
from socket import *
tcpSock = socket(AF_INET,SOCK_STREAM)
方法 | 说明 |
---|---|
服务器套接字方法 | |
s.bind() | 将地址(主机名、端口号对)绑定到套接字上 |
s.listen() | 设置并启动TCP监听器 |
s.accept() | 被动接受TCP客户端连接,一直等待直到连接到达(阻塞) |
客户端套接字方法 | |
s.connect() | 主动发起TCP服务器连接 |
s.connect_ex() | connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常 |
普通的套接字方法 | |
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.getdocketname() | 当前套接字地址 |
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服务器的一般伪代码:
ss = socket() #创建套接字对象
ss.bind() #套接字与地址绑定
ss.listen() #监听连接
inf_loop: #服务器无限循环
cs = ss.accept() #接受客户端连接
comm_loop: #通信循环
cs.recv()/cs.send() #对话(接受/发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
#第一次发数据要用encode函数,第一次收数据要用decode函数,指定编码
所有的套接字都是通过 socket.socket() 函数创建的。因为服务器需要占用一个端口并等待客户端的请求,所以他们必须绑定到一个本地地址。特别的,TCP 服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始他的无限循环。
调用 accept() 函数之后,就开启了一个简单的(单线程)服务器,它会一直等待客户端的连接。默认情况下,accpet()是阻塞的,这意味着执行将被暂停,直到一个连接到达。
一旦服务器接受了一个连接,就会返回(利用 accept() )一个独立的客户端套接字,用来与即将到来的消息进行交换。这将能够空出原始服务器套接字,以便可以继续等待新的请求。同样的,当一个传入的请求到达时,服务器会创建一个新的通信端口直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接。
举例来说:我们创建一个 TCP 服务器,它接受客户端发送的数据字符串,并将其打上时间戳并返回给客户端:
#tsTeser.py文件 IP:10.0.0.246 OS:Linux
#!/usr/bin/python3
from socket import *
from time import ctime
HOST = '' #HOST 变量是空白的,这是对 bind()方法的标识,表示它可以使用任何可用的地址。
PORT = 21567
BUFSIZ = 1024 #缓冲区
ADDR = (HOST,PORT)
tcpSerSock = socket(AF_INET,SOCK_STREAM)
tcpSerSock.bind(ADDR)
tcpSerSock.listen(5) #传入连接请求的最大数
while True:
print("等待客户端的连接...")
tcpCliSock,addr = tcpSerSock.accept()
print("...连接来自:",addr)
while True:
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
tcpCliSock.send((ctime() + "\t").encode('utf-8') + data) #将字符串作为一个 ASCII字节“字符串”发送,而非 Uniocde 编码
tcpCliSock.close()
tcpSerSock.close()
创建TCP客户端的伪代码
cs = socket() #创建客户端套接字
cs.connect() #尝试连接服务器
comm_loop: #通信循环
cs.send()/cs.recv() #对话(发送/接收)
cs.close() #关闭客户端套接字
所有套接字都是利用 socket.socket() 创建的。一旦客户端拥有了一个套接字,它就可以利用套接字 connect() 方法直接创建一个到服务器的连接。当连接建立后,它就可以参与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。
#tsTclnt.py IP:10.0.0.100 OS:Windows
from socket import *
HOST = '10.0.0.246' # 'li' #指定服务器 IP 或主机名
PORT = 21567
BUFSIZ = 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('utf-8'))
data = tcpCliSock.recv(BUFSIZ) #从服务器接受的数据
if not data:
break
print(data.decode('utf-8'))
tcpCliSock.close()
运行结果:
#服务器端
[root@li python]# ./tsTestser.py
等待客户端的连接...
...连接来自: ('10.0.0.100', 60322)
等待客户端的连接...
#客户端
hi
Mon Aug 3 09:13:52 2020 hi
hello
Mon Aug 3 09:13:56 2020 hello
创建UDP服务器的伪代码
ss = socket() #创建服务器套接字
ss.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
cs = ss.recvfrom()/ss.sendto() #关闭(接收/发送)
ss.close() #关闭服务器套接字
除了普通的创建套接字并绑定到本地地址外,并没有额外的工作。
#tsUserv.py OS:Linux
#!/usr/bin/python3
from socket import *
from time import ctime
host = ''
port = 21567
bufsiz = 1024
addr = (host,port)
udpSerSock = socket(AF_INET,SOCK_DGRAM)
udpSerSock.bind(addr)
while True:
print('等待数据报...')
data,addr = udpSerSock.recvfrom(bufsiz)
udpSerSock.sendto(((ctime() + "\t ").encode('utf-8') + data), addr)
print('...收到的数据来自:',addr)
创建UDP客户端伪代码
cs = socket() #创建客户端套接字
comm_loop: #通信循环
cs.sendto()/cs.recvfrom() #对话(发送/接收)
cs.close() #关闭客户端套接字
#tsUclnt.py OS:Windows
from socket import *
host = '10.0.0.246'
port = 21567
bufsiz = 1024
addr = (host,port)
udpCliSock = socket(AF_INET,SOCK_DGRAM)
while True:
data = input('> ')
if not data:
break
udpCliSock.sendto(data.encode('utf-8'),addr)
data,addr = udpCliSock.recvfrom(bufsiz)
if not data:
break
print(data.decode('utf-8'))
运行结果:
#服务器端
[root@li python]# ./tsUserv.py
等待数据报...
...收到的数据来自: ('10.0.0.100', 64916)
等待数据报...
...收到的数据来自: ('10.0.0.100', 64916)
等待数据报...
#客户端
> hi
Mon Aug 3 09:37:09 2020 hi
> hello
Mon Aug 3 09:37:19 2020 hello
属性名称 | 说明 |
---|---|
数据属性 | |
AF_UNIX、AF_INET、AF_INET6、AF_NETLINK、AF_TIPC | Pyhton中支持的套接字地址家族 |
SO_STREAM、SO_DGRAM | 套接字类型(TCP=流,UDP=数据报) |
has_ipv6 | 指示是否支持ipv6的布尔标识 |
异常 | |
error | 套接字相关错误 |
herror | 主机与地址相关错误 |
gaierror | 地址相关错误 |
timeout | 超时 |
函数 | |
socket() | 以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字 |
socketpair() | 以给定的地址家族、套接字类型和协议类型(可选)创建一对套接字 |
create_connection() | 常规函数,它接收一个地址(主机名,端口号)对,返回套接字对象 |
fromfd() | 以一个打开的文件描述符创建一个套接字对象 |
ssl() | 通过套接字启动一个安全套接字层连接;不执行证书验证 |
getaddrinfo() | 获取一个五元组序列形式的地址信息 |
getnameinfo() | 给定一个套接字地址,返回(主机名,端口号)二元组 |
getfqdn() | 返回完整的域名 |
gethostname() | 返回当前主机名 |
gethostbyname() | 将一个主机名映射到它的IP地址 |
gethostbyname_ex() | gethostname()的扩展版本,它返回主机名、别名主机集合和IP地址列表 |
gethostbyaddr() | 将一个IP地址映射到DNS信息;返回与gethostbyname_ex()相同的3元组 |
getprotobyname() | 将一个协议名映射到数字 |
getservbyname()/getservbyport() | 将一个服务名映射到端口号,或者反过来;对于任何一个函数来说,协议名都是可选的 |
ntohl()/ntohs() | 将来自网络的整数转换为主机字节顺序 |
htonl()/htons() | 将来自主机的整数转换为系网络字节顺序 |
inet_aton()/inet_ntoa() | 将IP地址八进制字符串转换为32位的包格式,或者反过来 |
inet_pton()/inet_ntop() | 将IP地址字符串转换成打包的二进制格式,或者反过来 |
getfaulttimeout()/setdefaulttimeout() | 以秒(浮点数)为单位返回默认套接字超时时间;以秒(浮点数)为单位设置默认套接字超时时间 |
socketserver是标准库中的一个高级模块,它的目的是简化很多样板代码。他们是创建网络客户端和服务器所必需的代码。使用类来编写程序,应用程序是事件驱动的,这意味着只有在系统中的事件发生时,它们才会工作。
类 | 说明 |
---|---|
BaseServer | 仅用于推导,这样不会创建这个类的实例,可以用TCPServer和UDPServer创建类的实例 |
TCPServer/UDPServer | 基础的网络同步TCP/UDP服务器 |
UnixStreamServer/UnixDatagramServer | 基于文件同步的TCP/UDP服务器 |
ForkingMixIn/ThreadingMixIn | 核心派出或线程功能,不能直接实例化这个类 |
ForkingTCPServer/ForkingUDPServer | ForkingMixIn和TCPServer/UDPServer的组合 |
ThreadingTCPServer/ThreadingUDPServer | ThreadingMixIn和TCPServer/UDPServer的组合 |
BaseRequestHandler | 包含处理服务请求的核心功能;仅仅用于推导,不能实例化这个类 |
StreamRequestHandler/DatagramRequestHandler | 实现TCP/UDP服务器的服务处理器 |
事件包括消息的发送与接收。类只定义了一个用来接收客户端消息的事件处理程序。在原始的服务器循环中,我们阻塞等待请求,当接收到请求时就对其提供服务,然后继续等待。在此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。
首先导入服务器类,然后定义与之前相同的主机常量。其次是请求处理程序类,最后启动它。
#!/usr/bin/python3
from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime
host = ''
port = 21567
addr = (host,port)
class MyRequestHandler(SRH): #继承 StreamRequestHandler 类
def handle(self) -> None: #重写 handle() 方法
print('...conncected from:',self.client_address)
self.wfile.write(ctime().decode('utf-8'),self.rfile.readline())
tcpServ = TCP(addr,MyRequestHandler)
print("waiting for connection...")
tcpServ.serve_forever()
from socket import *
host = '10.0.0.246'
port = 21567
bufsize = 1024
addr = (host,port)
while True:
tcpCliSock = socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(addr)
data = input()
if not data:
break
tcpCliSock.send(data.encode('utf-8'))
data = tcpCliSock.recv(bufsize)
if not data:
break
print(data.decode('utf-8'))
tcpCliSock.close()