基本知识
- 计算机网络知识,主要了解一下OSI的七层架构,以及TCP/IP的四层架构。
- 网络通信中最基本的概念:套接字,也叫通信端点、socket
- 网络通信地址表示:一般是主机+端口号
- 最常用的两种连接方式:面向连接方式(TCP,SOCK_STREAM)和面向无连接方式(UDP, SOCK_DGRAM)
这些基本的知识,网上有很多相关介绍,不是本章重点,有需要可以在网上查找相关资料。
这里学习一下python语言对底层网络编程的支持,这里只学习最常用的socket编程相关知识,详细说明可以参考官方文档:底层网络接口
socket模块
python有一个专门的socket模块来支持底层网络通信。是对Unix系统调用和套接字库接口的直译。
socket编程基本套路
- TCP服务端
s = socket() # 创建一个服务端socket s.bind() # 绑定socket地址(ip+port) s.listen() # 监听此地址是否有连接 loop: cs = s.accept() # 阻塞等待客户端连接,连接成功返回新的套接字对象,用于在此连接上收发数据 comm_loop: cs.recv()/cs.send() # 通信:接收数据/发送数据 cs.close() # 关闭通信套接字对象 s.close() # 关闭服务端套接字对象
- TCP客户端
cs = socket() # 创建一个客户端socket cs.connect() # 通过服务端地址(ip + port)尝试连接服务端 comm_loop: cs.send()/cs.recv() # 通信:发送数据/接收数据 cs.close() # 关闭客户端套接字对象
- UDP服务端
ss = socket() # 创建一个服务端socket ss.bind() # 绑定服务端地址 comm_loop: cs.recvfrom()/cs.sendto() # 通信:接收数据/发送数据 cs.close() # 关闭客户端套接字对象
说明: UDP无连接,不需要监听。先被动接收数据,会返回客户端的地址,后面就可以根据客户端地址主动发送消息。
- UDP客户端
cs = socket() # 创建一个客户端socket comm_loop: cs.sendto()/cs.recvfrom() # 通信:发送数据/接收数据 cs.close() # 关闭客户端套接字对象
socket通信实例
先从一个TCP实例开始学习,功能比较简单:
- 一个客户端,一个服务器端,两个python程序可以放在两台电脑上运行
- 客户端给服务端发送一个消息;服务器端接收到消息之后,加上一个时间戳再返回给客户端
- 无消息则结束
-
服务端代码
from socket import * from time import ctime HOST = '' PORT = 21567 BUFF = 1024 ADDR = (HOST, PORT) tcpSvrSocket = socket(AF_INET, SOCK_STREAM) tcpSvrSocket.bind(ADDR) tcpSvrSocket.listen(5) while True: print("Waiting for connect...") tcpCliSocket, addr = tcpSvrSocket.accept() print(f"...connected from {addr}") while True: data = tcpCliSocket.recv(BUFF) if not data: break; data = data.decode('utf-8') print(f'recive data from client: {data}') resMsg = f'[{ctime()}] {data}' tcpCliSocket.send(bytes(resMsg, 'utf-8')) tcpCliSocket.close() tcpSvrSocket.close()
-
客户端代码
from socket import * HOST = '' PORT = 21567 BUFF = 1024 ADDR = (HOST, PORT) tcpCliSocket = socket(AF_INET, SOCK_STREAM) tcpCliSocket.connect(ADDR) while True: data = input(">>> ") if not data: break tcpCliSocket.send(bytes(data, 'utf-8')) data = tcpCliSocket.recv(BUFF) if not data: break print(data.decode('utf-8')) tcpCliSocket.close()
说明:socket类实现了
__enter__
和__exit__
,可以使用with
语句自动实现close
操作,也即使用with
语句可以不用主动调用socket.close()
接口
模块说明
- 常量
socket模块定义了很多常量,最常使用的如下表
常量名 | 说明 |
---|---|
socket.AF_INET |
一种用于ipv6的地址族,一个四元组(host, port, flowinfo, scope_id) 表示地址 |
socket.AF_INET6 |
一种socket地址族(用于ipv4,也是目前常用的)使用一个(ip, port) 元组表示地址 |
socket.SOCK_STREAM |
面向连接(TCP)的socket类型 |
socket.SOCK_DGRAM |
面向无连接(UDP)的socket类型 |
- 接口
socket模块中有一些函数可以使用,但是最常用的还是socket类生成socket对象,以及类中的接口。类原型如下:class socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
常用接口信息如下表
接口名 | 连接方式 | 说明 |
---|---|---|
socket.bind(address) |
TCP/UDP | 服务端socket绑定到地址address |
socket.listen([backlog]) |
TCP | 启动一个服务器用于接受连接。backlog 表示系统允许暂未 accept 的连接数,超过后将拒绝新连接。最低为 0(小于 0 会被置为 0),未指定则自动设为合理的默认值。 |
socket.accept() |
TCP | 接受一个连接。此 socket 必须绑定到一个地址上并且监听连接。返回一个 (conn, address) 对:其中 conn 是一个新的套接字对象,用于在此连接上收发数据,address 是连接另一端的套接字所绑定的地址。 |
socket.connect(address) |
TCP/UDP | 连接到 address 处的远程套接字。 |
socket.recv(bufsize[, flags]) |
TCP | 从套接字接收数据。返回值是一个字节对象,表示接收到的数据。bufsize 指定一次接收的最大数据量。可选参数 flags 的含义请参阅 Unix 手册页 recv(2),它默认为零。 |
socket.send(bytes[, flags]) |
TCP | 发送数据给套接字。本套接字必须已连接到远程套接字。可选参数 flags 的含义与上述 recv() 中的相同。 |
socket.recvfrom(bufsize[, flags]) |
UDP | 从套接字接收数据。返回值是一对 (bytes, address) ,其中 bytes 是字节对象,表示接收到的数据,address 是发送端套接字的地址。可选参数 flags 的含义请参阅 Unix 手册页 recv(2),它默认为零。 |
socket.sendto(bytes, address) |
UDP | 发送数据给套接字。由 address 指定目标套接字。可选参数 flags 的含义与上述 recv() 中的相同。 |
- 异常
异常名 | 说明 |
---|---|
socket.error |
一个被弃用的 OSError 的别名。 |
socket.herror |
OSError 的子类,本异常通常表示与地址相关的错误。 |
socket.gaierror |
OSError 的子类,来自 getaddrinfo() 和 getnameinfo() ,表示与地址相关的错误。附带的值是一对 (error, string) ,代表库调用返回的错误。 |
socket.timeout |
OSError 的子类,当套接字发生超时,且事先已调用过 settimeout() (或隐式地通过 setdefaulttimeout() )启用了超时,则会抛出此异常。 |
SocketServer模块
socketserver模块是一个用于网络服务器编写的框架,简化了编写网络服务器的任务。例如上面TCP服务器端代码可以使用此模块改写如下:
import socketserver
class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
# self.rfile is a file-like object created by the handler;
# we can now use e.g. readline() instead of raw recv() calls
self.data = self.rfile.readline().strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# Likewise, self.wfile is a file-like object used to write back
# to the client
self.wfile.write(self.data.upper())
if __name__ == "__main__":
HOST, PORT = "localhost", 21567
# Create the server, binding to localhost on port 9999
with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
server.serve_forever()
模块说明
常用实体服务器类:
- TCPServer类,该类使用互联网 TCP 协议,它可以提供客户端与服务器之间的连续数据流。函数原型如下:
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
- UDPServer类,该类使用数据包,即一系列离散的信息分包,它们可能会无序地到达或在传输中丢失。函数原型如下:
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
请求处理句柄对象:
-
BaseRequestHandler类,这是所有请求处理句柄对象的超类。 它定义了下文列出的接口。 一个实体请求处理句柄子类必须定义新的
handle()
方法,并可重载任何其他方法。 对于每个请求都会创建一个新的子类的实例。接口名 说明 setup()
会在 handle()
方法之前被调用以执行任何必要的初始化操作。 默认实现不执行任何操作。handle()
此函数必须执行为请求提供服务所需的全部操作。 默认实现不执行任何操作。 它有几个可用的实例属性;请求为 self.request
;客户端地址为self.client_address
;服务器实例为self.server
,如果它需要访问特定服务器信息的话。finish()
在 handle()
方法之后调用以执行任何需要的清理操作。 默认实现不执行任何操作。 -
StreamRequestHandler和DatagramRequestHandler类,是
BaseRequestHandler
的子类,重载了setup()
和finish()
方法,并提供了self.rfile
(读取以获取请求数据) 和self.wfile
(写入以将数据返回给客户端) 属性。
当然此框架还提供了更加强大的功能,具体可以参考官方文档
网友评论