socket介绍
socket是我们进行网络通信编程的基础,socket协议不属于应用层的任何一个协议,但底层能够直接调用传输层的TCP/UDP协议,从而定义我们自己的应用层协议,实现通信功能。也就是说socket让我们能够使用TCP/UDP来进行通信,python中提供了socket
模块可以实现socket编程
C/S模型
服务端
1.定义socket协议类型:socket.socket(family, type)
注:
family中分有:
AF.INET IPV4(默认)
AF.INET6 IPV6
AF.UNIX 本地
type中分有:
SOCK_STREAM 流式socket,对TCP(默认)
SOCK_DGRAM 数据报式socket,对UDP
SOCK_RAW 原始socket,可以处理ICMP、IGMP报文(一般socket不行)
2.绑定监听端口:server.bind(ip, port)
3.监听端口:listen()
,里面可以设置最大监听客户端数
4.等待链接:accept()
,当没有客户端连接时,服务端一直停留在这
5.接收:socket.recv()
,里面单位是字节,建议不超过8192
6.发送:socket.send(message)
参考:https://www.cnblogs.com/JenningsMao/p/9487465.html
客户端
1.定义socket协议类型:socket.TCP/IP
2.连接远程机器:connect(a.ip, a.port)
(也可以使用connect_ex
,使用方式一样,但是对于连接失败的处理机制不同:connect
连接失败时会抛出异常,而connect_ex
只有在连接成功时才返回一个状态码0)
3.发送:socket.send(message)
,发送信息,需要为字节类型,所以如果只是英文之类的话引号前加个b就行,如b'hello'
,如果是中文字符之类的就用encode
,然后服务端decode
就行了
4.接收:socket.recv()
,里面单位是字节
5.关闭:socket.close()
TCP通信
TCP通信是有连接的,因此双方通信前需要先建立连接后才能进行消息的收发
简单示例
- 服务端:
import socket
server = socket.socket()
server.bind(('localhost',8000)) # 绑定监听端口
server.listen(5) # 监听,并且设置最大监听数为5个客户端
print("start...")
conn, addr = server.accept()
# conn绑定的是服务端和客户端之间的连接,addr是对应的连接IP和端口
print(conn,addr)
data = conn.recv(1024)
print(data)
conn.send(data.upper()) # 将接收的信息转大写
server.close()
- 客户端:
import socket
client = socket.socket() # 声明socket类型并生成socket连接对象
client.connect(('localhost',8000)) # 连接服务端
client.send(b"abc") # 连接成功,发送字节流信息
data = client.recv(1024) # 总共接收1024个字节
print(data) #输出ABC
client.close()
客户端发命令,服务端执行并将结果返回(SSH)示例
- 服务端:
import socket
import os
server = socket.socket()
server.bind(('localhost',6969)) #绑定监听端口
server.listen(5) #监听
print("start...")
conn, addr = server.accept() #等待,当获取到时分别记录该连接实例和ip地址,以备后面标记
print(conn, addr)
wrong = "command is wrong"
while True:
data = conn.recv(1024)
content = os.popen(data.decode()).read()
#一定要在这里就.read(),因为popen执行指令后返回的是类似文件类型
#如果.read()一次,文件指针就到末尾了,那第二次及以后.read()内容就是空的
if not content:
#因为客户端每次都会recv等待接收信息,如果信息为空就会一直卡在那
#所以当这边命令结果为空时我们也得发个错误信息过去,让他结束这次的接收
print(wrong)
conn.send(wrong.encode('utf-8'))
continue
conn.send(content.encode('utf-8'))
#发送信息时都是一次性全发过去,如果超过接收的最大长度时会保存到缓冲区,等待下次接收的时候再接收
print("success")
server.close()
- 客户端:
import socket
client = socket.socket() #声明socket类型并生成socket连接对象
client.connect(('localhost',6969))
while True:
msg = input()
if len(msg) == 0:continue
client.send(msg.encode('utf-8')) #发送字节流信息
while True: #循环接收报文
data = client.recv(1024) #一次接收1024个字节
if len(data) < 1024: #判断当接收的字节数小于1024时说明是最后一段内容了,就停止接收
print(data.decode())
break
print(data.decode())
print("received")
client.close()
实现http请求服务示例
import socket
def get(url):
path = "/"
if "/" in url:
url, path = url.split("/")
if not path:
path = "/"
host, port = url.split(":")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, int(port)))
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf-8"))
data = b""
while True:
buf = client.recv(1024)
if not buf:
break
data += buf
data = data.decode("utf-8")
print(data)
client.close()
get("127.0.0.1:5000/")
web服务端实现
基于socket实现
import socket
import time
server = socket.socket()
server.bind(("127.0.0.1", 5000))
server.listen()
while True:
conn, addr = server.accept()
msg = conn.recv(1024)
print(msg)
data = b"HTTP/1.1 200 ok\r\n\r\n"
with open("test.html", "rb") as f:
data += f.read()
conn.send(data)
# 在http请求当中,每次通信结束的标志就是请求断开
# 所以在close之前,前台都会认为后台还会有数据发送过来,因此会一直等待
# 也因此在close之前,服务端可以不断地往前台send数据
for i in range(10):
conn.send(b"data...
")
time.sleep(2)
# 在这2s内因为没有close,因此也会一直处于等待状态
conn.send(b"end")
conn.close()
由于http连接是每次通信都重新建立请求,每次通信完就直接断开的,因此在没断开之前,浏览器都会认为服务端数据没发完,从而一直处于等待状态(即一直转圈圈)
注:
http响应请求中时,第一个空行是切分响应头和响应体的标识,因此需要发送两个换行,即\n\n
。而使用\r\n\r\n
是因为需要兼容各种平台,比如windows
下\r\n
才是换行,而不是\n
基于wsgi实现
import time
from threading import Thread
from wsgiref.simple_server import make_server
def html(conn):
data = b"hello
"
conn.send(data)
conn.close()
def application(environ, response):
response("200 OK", [("k1", "v1"), ("k2", "v2")])
path = environ["PATH_INFO"]
data = f"Hello! {path}
"
return [bytes(data, "utf-8")]
server = make_server("127.0.0.1", 8000, application)
print("start...")
server.serve_forever()
注意点
在python中进行TCP通信时,如果建立连接以后,只要其中一端断开,那么另一端将会不停地收到空的字节数据,举例:
- 服务端:
import socket
server = socket.socket()
server.bind(('localhost',8000))
server.listen()
print("start...")
conn, addr = server.accept()
server.close()
- 客户端:
import socket
import time
client = socket.socket()
client.connect(('localhost',8000))
while 1:
print(client.recv(1024))
time.sleep(1)
client.close()
# b''
# b''
# ...
结果会发现服务端建立连接并断开以后,客户端会不停地收到空的字节数据,所以我们对这种情况进行相应的处理-当收到的数据为空,说明对方断开连接,需要进行相应的处理
UDP通信
UDP无需建立连接,直接进行消息的收发,因此速度相比于TCP更快,但无法得知对方是否成功收到消息
简单示例
- 服务端:
import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 8000))
msg = server.recv(1024)
# 直接等待消息到达,无需连接
print(msg)
# b'over'
- 客户端:
import socket
client = socket.socket(type=socket.SOCK_DGRAM)
client.sendto(b"over", ("127.0.0.1", 8000))
# 使用sendto直接将数据发送到指定位置,无需建立连接
client.close()
UDP通信示例
- 服务端:
import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 8000))
while True:
msg, addr = server.recvfrom(1024)
print(f"from:{addr}, msg:{msg}")
server.sendto(msg, addr)
- 客户端:
import socket
import time
client = socket.socket(type=socket.SOCK_DGRAM)
server = ("127.0.0.1", 8000)
while True:
msg = input(">>")
client.sendto(msg.encode("utf-8"), server)
reply = client.recv(1024)
if reply == b"":
client.close()
break
print(reply)
粘包问题
只出现在TCP协议里,由于多条消息没有边界,并且一堆的优化算法,假如在短间隔内发送端发送了多条消息,那么这些消息默认会一起放在一个缓冲区里,因此多个报文内容就可能连在一起无法分辨,当接收端接收时,就会看到多条消息堆在了一起,举例:
- 服务端:
import socket
server = socket.socket()
server.bind(("127.0.0.1", 8000))
server.listen()
conn, addr = server.accept()
msg = conn.recv(1024)
print(f"from:{addr}, msg:{msg}")
conn.send(msg)
conn.send(msg)
conn.send(msg)
conn.send(msg)
conn.send(msg)
- 客户端:
import socket
import time
client = socket.socket()
client.connect(("127.0.0.1", 8000))
msg = input(">>")
client.send(msg.encode("utf-8"))
time.sleep(1)
reply = client.recv(1024)
print(reply)
# >>aaa
# b'aaaaaaaaaaaaaaa'
可以看出服务端发送了5条信息,但客户端由于睡了1秒,导致5条信息被直接堆到了一条信息里
注:实际上TCP是面向流数据传输的,没有包这个概念,所以黏包的说法其实并不准确,说是黏包,实际上是因为没有很好地划分数据流而导致的问题,也可以理解成上层的业务包的拆分问题
粘包解决
sleep延迟发送
可以在发送的多条数据间通过sleep
等待来造成缓冲区超时,从而隔开消息,一般时间弄0.5以上比较有效,但这种方法在实时要求高的情况下弊端很大
收发间隔进行
可以在每发一条消息后都要等待对方回应接收成功后才继续发,举例:
- 服务端:
import socket
import os
import time
server = socket.socket()
server.bind(('localhost',6969))
server.listen(5)
print("start...")
conn, addr = server.accept()
wrong = "command is wrong"
while True:
data = conn.recv(1024)
content = os.popen(data.decode()).read()
if not content:
print(wrong)
conn.send(wrong.encode('utf-8'))
continue
conn.send(str(len(content.encode('utf-8'))).encode('utf-8'))
conn.recv(1024) #在两条信息之间隔开一次接收
conn.send(content.encode('utf-8'))
print("success")
server.close()
- 客户端:
import socket
client = socket.socket()
client.connect(('localhost',6969))
while True:
msg = input()
if len(msg) == 0:continue
client.send(msg.encode('utf-8'))
date_size = client.recv(1024)
client.send("第一个报文接收完毕".encode('utf-8'))
#第一条接收结束后发送一个确认信息,配合服务端隔开两次发送的信息
print(date_size)
while True:
data = client.recv(1024)
if len(data) < 1024:
print(data.decode())
break
print(data.decode())
print("received")
client.close()
发送数据报长度
我们可以再每次发送数据前,先发送一个数据报告知数据长度,然后再发送数据,其中可以通过struct.pack
方法获取发送数据的长度,并转成固定的四个字节;然后接收端先接收4字节数据,并通过struct.unpack
方法获取字节的长度,然后根据该长度获取指定的数据,举例:
- 发送端:
import socket
import struct
server = socket.socket()
server.bind(("127.0.0.1", 8000))
server.listen()
conn, addr = server.accept()
msg = conn.recv(1024)
print(f"from:{addr}, msg:{msg}")
for i in range(5):
l = struct.pack("i", len(msg))
# 将数据长度转成固定长度字节
conn.send(l)
# 先发送长度
conn.send(msg)
# 再发送数据
server.close()
- 接收端:
import socket
import time
import struct
client = socket.socket()
client.connect(("127.0.0.1", 8000))
msg = input(">>")
client.send(msg.encode("utf-8"))
time.sleep(1)
while True:
l = client.recv(4)
# 先获取长度数据
if not l:
break
l = struct.unpack('i', l)[0]
reply = client.recv(l)
# 根据长度获取数据
print(reply)
client.close()
# >>aaa
# b'aaa'
# b'aaa'
# b'aaa'
# b'aaa'
# b'aaa'
可以看到数据就没有黏在一起了
更多参考
http://www.cnblogs.com/alex3714/articles/5830365.html
IP/端口可复用
由于TCP连接中,先断开的一方将会保留资源等待一定时间,例如下面的代码启动后,访问对应地址,然后手动关闭服务端,此时再打开就会提示端口被占用无法启动:
import socket
import os
def handle(conn):
request = conn.recv(1024)
print(request)
response = "HTTP/1.1 200 OK\r\n\r\n" + "aaa"
conn.send(response.encode("utf-8"))
conn.close()
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('localhost',5000))
server.listen(5)
while True:
conn, addr = server.accept()
handle(conn)
server.close()
if __name__ == "__main__":
main()
由于一个端口只能绑定一个程序,而此时端口资源还未被释放,所以无法启动服务。因此可以通过setsockopt
方法配置允许地址端口复用,举例:
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 在绑定前配置允许复用地址和端口
server.bind(('localhost',5000))
server.listen(5)
...
基于socket的协议解析工具实现
import socket
import struct
import json
class BaseParser:
"""协议解析基类"""
# 协议头起始位置
HEADER_OFFSET = 0
# 协议头长度
HEADER_LENGTH = 0
@classmethod
def parse_head(cls, header):
"""解析协议头方法"""
raise NotImplementedError("must implement parse_head")
@classmethod
def parse(cls, packet):
"""协议解析方法"""
header = packet[cls.HEADER_OFFSET: cls.HEADER_OFFSET + cls.HEADER_LENGTH]
body = packet[cls.HEADER_OFFSET + cls.HEADER_LENGTH:]
res_header = cls.parse_head(header)
res_body = cls.parse_body(body)
return {"header": res_header, "body": res_body}
@classmethod
def parse_body(cls, body):
"""协议数据解析方法"""
l = len(body)
# 将字节逐个解析
data = struct.unpack(l * "B", body)
string = ""
for c in data:
# 无法用ascii展示的用.来表示
if c >= 127 or c < 32:
string += "."
else:
string += chr(c)
return string
class IPParser(BaseParser):
"""IP协议解析"""
# 没有扩展选项,默认IP头长度为20
HEADER_LENGTH = 20
@classmethod
def parse_head(cls, header):
"""解析IP头
- 格式:
line1 版本(4) 首位长度(4) 服务类型(8) 总长度(16)
line2 标识(16) 标志(3) 片偏移(13)
line3 TTL(8) 协议(8) 首部校验和(16)
line4 源IP(32)
line5 目的IP(32)
... 选项(可选)
"""
line1 = struct.unpack(">BBH", header[:4])
ip_version = line1[0] >> 4
iph_length = line1[0] & 15
service_type = line1[1]
pkg_length = line1[2]
line3 = struct.unpack(">BBH", header[8:12])
TTL = line3[0]
protocol = line3[1]
iph_checksum = line3[2]
line4 = struct.unpack(">4s", header[12:16])
# 32位字节转IP地址
src_ip = socket.inet_ntoa(line4[0])
line5 = struct.unpack(">4s", header[16:20])
dst_ip = socket.inet_ntoa(line5[0])
return {
"ip_version":ip_version,
"iph_length":iph_length,
"service_type":service_type,
"pkg_length":pkg_length,
"TTL":TTL,
"protocol":protocol,
"iph_checksum":iph_checksum,
"src_ip":src_ip,
"dst_ip":dst_ip,
}
class TransParser(BaseParser):
"""传输层协议头解析基类"""
HEADER_OFFSET = 20
class UDPParser(TransParser):
"""UDP协议解析"""
HEADER_LENGTH = 8
@classmethod
def parse_head(cls, header):
"""解析IP头
- 格式:
line1 源端口(16) 目的端口(16)
line2 UDP长度(16) 校验和(16)
"""
res = struct.unpack(">HHHH", header)
[src_port, dst_port, udp_length, udp_checksum] = res
return {
"src_port":src_port,
"dst_port":dst_port,
"udp_length":udp_length,
"udp_checksum":udp_checksum,
}
class TCPParser(TransParser):
"""TCP协议解析"""
HEADER_LENGTH = 20
@classmethod
def parse_head(cls, header):
"""解析IP头
- 格式:
line1 源端口(16) 目的端口(16)
line2 序号(32)
line3 确认号(32)
line4 数据偏移(4) 保留字段(6) TCP标记(6) 窗口(16)
line5 校验和(16) 紧急指针(16)
... 选项(可选)
"""
line1 = struct.unpack(">HH", header[:4])
src_port = line1[0]
dst_port = line1[1]
line2 = struct.unpack(">L", header[4:8])
seq_num = line2[0]
line3 = struct.unpack(">L", header[8:12])
ack_num = line3[0]
line4 = struct.unpack(">BBH", header[12:16])
data_offset = line4[0] >> 4
flags = line4[1] & int('00111111', 2)
FIN = flags & 1
SYN = (flags >> 1) & 1
RST = (flags >> 2) & 1
PSH = (flags >> 3) & 1
ACK = (flags >> 4) & 1
URG = (flags >> 5) & 1
window_size = line4[2]
line5 = struct.unpack(">HH", header[16:20])
tcp_checksum = line5[0]
urg_point = line5[1]
return {
"src_port":src_port,
"dst_port":dst_port,
"seq_num":seq_num,
"ack_num":ack_num,
"data_offset":data_offset,
"flag": {
"FIN":FIN,
"SYN":SYN,
"RST":RST,
"PSH":PSH,
"ACK":ACK,
"URG":URG,
},
"window_size":window_size,
"tcp_checksum":tcp_checksum,
"urg_point":urg_point,
}
class Server:
"""数据包监听器"""
def __init__(self):
# 基于IPV4,获取原始字节流
self.sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
self.ip = "0.0.0.0"
self.port = 80
self.sock.setblocking(False)
self.sock.bind((self.ip, self.port))
# 设置混合模式
# self.sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
def loop(self):
"""循环监听数据包"""
while True:
try:
packet, addr = self.sock.recvfrom(65535)
ip_res = IPParser.parse(packet)
res = {"ip": ip_res}
ip_header = ip_res["header"]
# IP协议头的protocol为17代表UDP协议
if ip_header["protocol"] == 17:
res["udp"] = UDPParser.parse(packet)
# IP协议头的protocol为6代表TCP协议
elif ip_header["protocol"] == 6:
res["tcp"] = TCPParser.parse(packet)
print(json.dumps(res, indent=4))
except BlockingIOError:
pass
except Exception as e:
print(e)
if __name__ == "__main__":
Server().loop()
socketserver
基于socket模块封装的支持并发的服务端模块,举例:
- 服务端:
import socketserver
class Server(socketserver.BaseRequestHandler):
def handle(self):
"""所有的请求都会进入该方法处理"""
conn = self.request
try:
content = conn.recv(1024).decode("utf-8")
print(f"recv from {self.client_address}")
conn.send(b"reply")
conn.close()
except ConnectionResetError:
pass
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8000), Server)
server.serve_forever()
- 客户端:
import socket
import time
client = socket.socket()
client.connect(("127.0.0.1", 8000))
msg = input(">>")
client.send(msg.encode("utf-8"))
reply = client.recv(1024)
print(reply)
client.close()
异步IO
并发、并行
- 并发:一个时间段内有多个任务在同一个CPU上运行,但任意时刻只有一个任务在CPU上运行,只是CPU切换速度很快,看起来就跟一起运行一样
- 并行:任意时刻上有多个任务运行在多个CPU上
因为CPU数量有限,因此不太可能高并行,但是高并发是可以实现的
同步、异步、阻塞、非阻塞
只有和IO操作相关时才有同步、异步、阻塞、非阻塞概念
- 同步:代码在调用IO操作时,必须等待IO操作完成才返回的调用方式
- 异步:代码在调用IO操作时,不必等待IO操作完成就可以返回的调用方式
- 阻塞:调用函数的时候,当前线程被挂起
- 非阻塞:调用函数的时候,当前线程不会被挂起,而是立即返回
- 同步、异步:可以看做消息通信的一种机制
- 阻塞、非阻塞:函数调用的一种机制
unix下五种IO模型
- 阻塞式IO
- 非阻塞式IO
- IO复用
- 信号驱动式IO
- 异步IO
阻塞式IO
举例:
import socket
def get(url):
path = "/"
if "/" in url:
url, path = url.split("/")
if not path:
path = "/"
host, port = url.split(":")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 此处连接阻塞,直到连接成功才往后执行
client.connect((host, int(port)))
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf-8"))
data = b""
while True:
buf = client.recv(1024)
if not buf:
break
data += buf
data = data.decode("utf-8")
print(data)
client.close()
get("127.0.0.1:5000/")
connect连接成功之前,会阻塞等待,此时不耗费CPU
- 特点:使用简单,但是大量时间在等待IO,CPU长期处于空闲,资源利用率低
非阻塞式IO
不用等待连接成功,立即返回,举例:
import socket
def get(url):
path = "/"
if "/" in url:
url, path = url.split("/")
if not path:
path = "/"
host, port = url.split(":")
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(False)
# 非阻塞设置
try:
client.connect((host, int(port)))
# 阻塞不会消耗IO
except BlockingIOError:
pass
# 不停询问连接是否建立完成,此时也可以做一些额外的任务
while True:
try:
client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf-8"))
break
except OSError:
pass
data = b""
# 不停地监听是否有消息发来,此时可以做一些额外的任务
while True:
try:
buf = client.recv(1024)
except BlockingIOError:
continue
if not buf:
break
data += buf
data = data.decode("utf-8")
print(data)
client.close()
get("127.0.0.1:5000/")
但后面的代码如果需要连接后才能执行的,如send,就需要while循环监听是否连接完成,此时十分耗费CPU。但如果后面不是需要连接才能执行的代码,就可以趁机执行,所以说虽然非阻塞了,但一直在循环等待连接完成才能执行的操作,并没有真正实现并发(当然并发还是可以实现的)
参考:https://blog.csdn.net/weixin_42089175/article/details/81607558
IO复用
- IO多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),就能够通知程序进行相应的读写操作
- select时虽然在没有数据报准备好时也会一直阻塞等待监听,但和阻塞式不同的是他可以监听多个,只要有一个socket连接成功,就会通知
- 能够同时监听多个socket就是其最大的优势,但有个问题就是其也无法省去将数据从内核复制到用户空间(读写事件)的时间
- 目前大部分高并发都是基于IO复用
信号驱动式IO
异步IO
真正意义上的异步IO,但由于编码难度相比IO复用要大很多,而且效果提升并不会很明显,所以目前还是以IO复用为主
IO多路复用的几种机制
select
最早的一种IO多路复用的机制,也是支持最广泛的一种,基本上几乎所有平台都支持,是其的一大优点。但其在单个进程中能够监视的文件描述符存在最大限制,在Linux上一般为1024,虽然有办法可以提升限制,但同时也会造成效率降低
poll
该机制随着监视的描述符数量增加,效率也会线性下降
epoll
是前两种机制的增强版,相比之下,该机制更加的灵活,没有描述符数量限制,其查询使用了红黑树来提高性能,nginx就是使用该机制,但该机制只支持linux,不支持windows
注:
select/poll/epoll本质都是同步IO,因为他们都需要在读写事件就绪后自己负责读写,因此读写的过程是阻塞的,而异步IO则无需自己进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间
epoll/select对比
- 在高并发的情况下,如果连接活跃度不高,那么
epoll
优于select
- 在并发性不高,且连接活跃的情况下,则
select
优于epoll
比如游戏中并发不高,而且连接以后很少断开,一般是不停地进行数据传输,这时候用select
效率就更高;但在web上,epoll
就更好
基于select实现HTTP服务
可以使用select模块下的select方法,但更推荐selectors模块下的DefaultSelector类,在windows平台下,其底层就是基于select,举例:
import selectors
import socket
class Server:
"""基于Selector实现的HTTP服务端"""
def __init__(self, ip="localhost", port=5000):
"""启动TCP服务,注册selector监听事件"""
self.selector = selectors.DefaultSelector()
self.server = socket.socket()
self.server.bind((ip, port))
self.server.listen(5)
self.server.setblocking(False)
# 注册建立连接时的回调
self.selector.register(self.server, selectors.EVENT_READ, self.accept)
def accept(self, sock, event):
"""建立新连接回调"""
conn, addr = self.server.accept()
conn.setblocking(False)
print('accepted new connection:', conn, 'from', addr)
# 注册允许读取数据时回调
self.selector.register(conn, selectors.EVENT_READ, self.read)
def read(self, conn, event):
"""读取数据回调"""
try:
header = self.get_header(conn)
print("headers:", header)
# 返回响应头
conn.send(b"""HTTP/1.1 200 OK\r\n\r\n{"status": 1}""")
except Exception as e:
print('connect wrong', conn, e)
finally:
self.selector.unregister(conn)
conn.close()
@staticmethod
def get_header(conn):
"""读取请求头"""
headers = []
conn_rfile = conn.makefile("rb")
while True:
row = conn_rfile.readline(65535)
if row in [b"", b"\r\n", b"\n", b"\r"]:
break
headers.append(row.replace(b"\r\n", b"").decode("iso-8859-1"))
return ", ".join(headers)
def loop(self):
"""selector监听回调"""
print("start listening...")
while True:
events = self.selector.select()
for key, event in events:
callback = key.data
callback(key.fileobj, event)
if __name__ == "__main__":
Server().loop()
基于select实现HTTP请求
和上面同理,举例:
import socket
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
selector = DefaultSelector()
class Get:
def connected(self, key):
"""定义写事件执行的回调函数"""
selector.unregister(key.fd)
# 一个事件只能注册一次,因此需要注销后再注册新的事件
self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf-8"))
selector.register(self.client.fileno(), EVENT_READ, self.readable)
# 注册读事件就绪时执行的回调
def readable(self, key):
"""定义读事件的回调函数"""
data = b""
while True:
try:
d = self.client.recv(1024)
except BlockingIOError:
continue
if not d:
break
data += d
data = data.decode("utf8")
print(data)
selector.unregister(key.fd)
self.client.close()
def get(self, url):
"""主方法"""
self.path = "/"
if "/" in url:
url, self.path = url.split("/")
if not self.path:
self.path = "/"
self.host, self.port = url.split(":")
self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client.setblocking(False)
try:
self.client.connect((self.host, int(self.port)))
except BlockingIOError:
pass
selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
# 注册写事件就绪时执行的回调
def loop():
# 事件循环,不断请求socket状态,并调用对应的回调函数
while True:
try:
ready = selector.select()
# 当存在事件就绪,则执行对应回调
for key, mask in ready:
call_back = key.data
call_back(key)
except OSError:
break
get = Get()
get.get("127.0.0.1:5000/")
loop()
编程模式:
select + 回调 + 事件循环
很多都是通过先注册事件回调+事件循环来实现多路复用,如asyncio、tornado等,该模式优点:
- 并发性高
- 基于单线程
缺点:基于回调方式编写代码,代码步骤混乱,调试困难
使用协程实现异步IO原因
基于select的IO多路复用,都是基于回调来编码,这样的代码维护起来十分困难,并且存在以下问题:
- 回调异常时难以捕获
- 回调嵌套导致逻辑复杂
- 回调嵌套出现异常时难以追踪
- 回调处理公共数据时容易冲突
- 难以维护回调中的局部变量
- 可读性差
- 共享状态管理困难
- 异常处理困难
使用协程优势:
- 回调模式编码复杂
- 同步编程并发性弱
- 多线程编程需要考虑线程间同步问题
- 使用协程实现能够以同步的方式编写异步代码
- 能够基于单线程来切换任务:线程是由操作系统取切换的,而单线程的切换则意味着需要我们自己去调度任务;并且不再需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高