Python 中的Socket编程

Python 中的Socket编程

一、数据传输方式

考虑同步异步,阻塞非阻塞的时候,要从进程线程,用户态内核态的角度考虑

1.1 同步与异步

同步与异步是对于线程之间,区别在于当前调用无返回时,操作系统是否会去运行其他线程

同步调用:调用者需要等待被调用者返回结果,才会进行下一步操作

异步调用:调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果

1.2 阻塞非阻塞

阻塞和非阻塞对于同一个线程来说,区别在于线程等待消息的时候 , 当前进/线程是否挂起

阻塞调用:调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。

非阻塞调用:在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

对比

1.3 IO模型

1.3.1 同步阻塞

图1.1 同步阻塞IO模型

​ 当在用户态调用read操作的时候,如果这时候kernel还没有准备好数据,那么用户态会一直阻塞等待,直到有数据返回。当kernel准备好数据之后,用户态继续等待kernel把数据从内核态拷贝到用户态之后才可以使用。这里会发生两种等待:一个是用户态等待kernel有数据可以读,另外一个是当有数据可读时用户态等待数据从内核态拷贝到用户态

1.3.2 同步非阻塞

无数据时返回异常


图1.2 同步非阻塞IO模型

​ 对比第一张同步阻塞IO的图就会发现,在同步非阻塞模型下第一个阶段是不等待的,无论有没有数据准备好,都是立即返回。第二个阶段仍然是需要等待的,用户态需要等待内核态把数据拷贝过来才能使用。对于同步非阻塞模式的处理,需要每隔一段时间就去询问一下内核数据是不是可以读了,如果内核说可以,那么就开始第二阶段等待

1.3.3 IO多路复用

图1.3 多路复用IO模型

selectpollepoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但selectpollepoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

1.3.4 异步

图1.4 异步IO模型

​ 异步模式下,前面提到的两个阶段都不会等待。使用异步模式,用户态调用read方法的时候,相当于告诉内核数据发送给我之后告诉我一声我先去干别的事情了。在这两个阶段都不会等待,只需要在内核态通知数据准备好之后使用即可。通常情况下使用异步模式都会使用callback,当数据可用之后执行callback函数。

异步阻塞

内核态阻塞,但在用户进程里,是多线程异步的。发送方向接收方请求后,不等待响应,可以继续其他工作。 接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。

异步非阻塞

​ 发送方向接收方请求后,不等待响应,可以继续其他工作。 接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。 当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。

1.3.5 对比

图1.5 IO模型对比

二、Socket API


图2 Socket API 的调用顺序
  1. socket(),的 socket 地址族参数 socket.AF_INET 表示因特网 IPv4 地址族,SOCK_STREAM 表示使用 TCP 的 socket 类型,协议将被用来在网络中传输消息。也可以使用 socket.SOCK_DGRAM 创建 UDP Socket

  2. bind() 用来关联 socket 到指定的网络接口(IP 地址)和端口号,bind() 方法的入参取决于 socket 的地址族

  3. listen() 方法有一个 backlog 参数。它指定在拒绝新的连接之前系统将允许使用的 未接受的连接 数量。从 Python 3.5 开始,这是可选参数。如果不指定,Python 将取一个默认值

    如果你的服务器需要同时接收很多连接请求,增加 backlog 参数的值可以加大等待链接请求队列的长度,最大长度取决于操作系统。

  4. accept() 方法阻塞并等待传入连接。当一个客户端连接时,它将返回一个新的 socket 对象,对象中有表示当前连接的 conn 和一个由主机、端口号组成的 IPv4/v6 连接的元组。必须记住调用 accept() 方法拥有了一个新的 socket 对象,因为将用这个 socket 对象和客户端进行通信。和监听socket 不同的是后者只用来授受新的连接请求。

  5. connect() 方法,客户端调用它来建立与服务器的链接,并开始三次握手。

    实例:(使用 with语句,这样就不用再手动调用 s.close() 来关闭 socket 了)

三、实验代码

3.1 同步阻塞

3.1.1 服务端
import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.bind((HOST,PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by',addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

3.1.2 客户端
import socket

HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 (非系统级的端口: 大于 1023)

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.connect((HOST,PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)

print('Received', repr(data))
3.1.3 测试

图3.1 服务端输出

图3.2 客户端输出

3.2 非阻塞


图4.1 accept和recv存在阻塞现象

图4.2 阻塞过程

accept阻塞

在没有新的套接字来之前,不能处理已经建立连接的套接字的请求

recv 阻塞

在没有接受到客户端请求数据之前,不能与其他客户端建立连接


图4.3 非阻塞过程

非阻塞忙询:非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有

3.2.1 服务端
import socket

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.setblocking(False) #设置为非阻塞
    s.bind((HOST,PORT))
    s.listen()
    conns = []
    while True:
        try:
            conn, addr = s.accept()
            print('Connected by', addr)
            conns.append(conn)
            conn.setblocking(False)
        except BlockingIOError as e:
            pass

        tmp_list = [conn for conn in conns]
        for conn in tmp_list:
            try:
                data = conn.recv(1024)  # 接收数据1024字节
                if data:
                    print('收到的数据是{}'.format(data.decode()))
                    conn.send(data)
                else:
                    print('close conn', conn)
                    conn.close()
                    conns.remove(conn)
                    print('还有客户端=>', len(conns))
            except IOError:
                pass
3.2.2 用户端
import socket

HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 (非系统级的端口: 大于 1023)

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.connect((HOST,PORT))
    while True:
        msg = input(">>>")
        if msg != 'q':
            s.send(msg.encode())
            data = s.recv(1024)
            print('收到的数据{}'.format(data.decode()))
        else:
            s.close()
            print('close client socket')
            break
3.2.3 测试

图5.1 服务端输出

图5.2 客户端1

图5.3 客户端2

​ 可以看到在等待client的数据的时候依然可以接收tmp进程的输入(字符串"bb")

​ 非阻塞IO模型缺点:不停地轮询recv,占用较多的CPU资源。 对应BlockingIOError的异常处理也是无效的CPU花费 (非阻塞中无数据时返回异常)

​ 如何解决:多路复用IO

3.3 多路复用IO

​ python中有封装好的类socketServer 内部使用IO多路复用,从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接到服务器时,Socket服务端都会在服务器是创建一个线程或者“进程专门负责处理当前客户端的所有请求。

​ 基于select的多路复用:(epoll效率更高,但Windows下不支持)

3.3.1 服务端
import select,socket,queue

HOST = '127.0.0.1'
PORT = 65432

with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s:
    s.bind((HOST,PORT))
    s.listen()
    s.setblocking(False)
    msg_dic = dict() #定义一个队列字典
    inputs = [s, ]  # 由于设置成非阻塞模式,accept和recive都不阻塞了,没有值就会报错,因此最开始需要最开始需要监控服务端本身,等待客户端连接
    outputs = []
    while True:
        # exceptional表示如果inputs列表中出现异常,会输出到这个exceptional中
        readable, writeable, exceptional = select.select(inputs, outputs, inputs)  # 如果没有任何客户端连接,就会阻塞在这里
        for r in readable:  # 没有个r代表一个socket链接
            if r is s:  # 如果这个socket是server的话,就说明是是新客户端连接了
                conn, addr = r.accept()  # 新连接进来了,接受这个连接,生成这个客户端实例
                print("Connected by", addr)
                inputs.append(conn)  # 为了不阻塞整个程序,我们不会立刻在这里开始接收客户端发来的数据, 把它放到inputs里, 下一次loop时,这个新连接
                # 就会被交给select去监听
                msg_dic[conn] = queue.Queue()  # 初始化一个队列,后面存要返回给这个客户端的数据
            else:  # 如果不是server,就说明是之前建立的客户端来数据了
                data = r.recv(1024)
                print('收到的数据是{}'.format(data.decode()))
                msg_dic[r].put(data)  # 收到的数据先放到queue里,一会返回给客户端
                outputs.append(r)  # 为了不影响处理与其它客户端的连接 , 这里不立刻返回数据给客户端

        for w in writeable:  # 要返回给客户端的链接列表
            data_to_client = msg_dic[w].get()
            w.send(data_to_client)  # 返回给客户端的源数据
            outputs.remove(w)  # 确保下次循环的时候writeable,不返回这个已经处理完的这个连接了

        for e in exceptional:  # 处理异常的连接
            if e in outputs:  # 因为e不一定在outputs,所以先要判断
                outputs.remove(e)
            inputs.remove(e)  # 删除inputs中异常连接
            del msg_dic[e]  # 删除此连接对应的队列
3.3.2 客户端

和上一个一样

3.3.3 测试

和上一次测试一样开了两个客户端,在关闭其中一个客户端之后服务端输出如下


图7 客户端输出

可以看到select一直在主动查询数据

3.4 异步

asyncore,socketserver都是异步socket的包装,下面基于socketserver实现一个异步服务器。

(貌似最好用Twisted暂时没看)

3.4.1 服务端与客户端
import socket
import threading
import socketserver

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
    """
    处理request的类
    self.request代表当前的request对象,self.server代表服务器对象。
    对于TCP链接,self.request是当前request的socket。
    此处实现的是回显,对于客户端发送信息返回线程名和信息
    """
    def handle(self):
        data = self.request.recv(1024)
        cur_thread = threading.current_thread()
        response = f"{cur_thread.name}: {data}"
        self.request.sendall(bytes(response, encoding = "utf8"))

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    """
    混合类ThreadingMixin,产生一个新的线程,提供异步处理的能力
    """
    pass

def client(ip, port, message):
    """
    客户端
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    try:
        sock.sendall(message)
        response = sock.recv(1024)
        print (f'收到的数据是: {response}')
    finally:
        sock.close()


HOST, PORT = '127.0.0.1', 0 #0意味着选择任意未使用的端口

server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
ip, port = server.server_address

# 服务器会启动一个线程
# 以后的每次request会开启新线程
server_thread = threading.Thread(target=server.serve_forever)
# 主程序终止时退出服务器线程
server_thread.daemon = True
server_thread.start()
print ("服务器线程:", server_thread.name)

client(ip, port, b"1")
client(ip, port, b"2")
client(ip, port, b"3")

server.shutdown()
server.server_close()
3.4.2 测试

图8 程序输出

可以看到每个客户端开启一个新线程

3.5 异步非阻塞

3.5.1 服务端
import socket
import threading
import socketserver


HOST = '127.0.0.1'
PORT = 65432

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(f'与{ self.client_address}连接')
        while True:
            data = self.request.recv(1024)
            if not data:
                break
            cur_thread = threading.current_thread()
            response = f"{cur_thread.name}: {data}"
            self.request.sendall(bytes(response, encoding = "utf8"))

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
server.serve_forever()
3.5.2 测试

图9 服务器输出

图10 客户端1输出

图11 客户端2输出
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值