Python 套接字

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多路复用,都是基于回调来编码,这样的代码维护起来十分困难,并且存在以下问题:

  • 回调异常时难以捕获
  • 回调嵌套导致逻辑复杂
  • 回调嵌套出现异常时难以追踪
  • 回调处理公共数据时容易冲突
  • 难以维护回调中的局部变量
  • 可读性差
  • 共享状态管理困难
  • 异常处理困难

使用协程优势:

  • 回调模式编码复杂
  • 同步编程并发性弱
  • 多线程编程需要考虑线程间同步问题
  • 使用协程实现能够以同步的方式编写异步代码
  • 能够基于单线程来切换任务:线程是由操作系统取切换的,而单线程的切换则意味着需要我们自己去调度任务;并且不再需要锁,并发性高,如果单线程内切换函数,性能远高于线程切换,并发性更高

你可能感兴趣的:(Python 套接字)