单进程tcp服务器-select 笔记总结

本文详细介绍了在不使用多进程或多线程的情况下,如何利用select、poll及epoll实现高效的IO多路复用。通过示例代码解释了它们的工作原理,并对比了各自的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

select 原理

io多路复用没有使用多进程和多线程的情况下完成多个套接字的使用

select 能够完成一些套接字的检查,从头到尾检查一遍后,标记哪些套接字是否可以收数据,返回的时候,就返回能接收数据的套接字,返回的是列表select是由操作系统提供的,效率要高些,非常快的方式检测哪些套接字可以接收数据。select是跨平台的,在window也可以用。

from socket import *
import select
import sys

server_socket = socket(AF_INET, SOCK_STREAM)

server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

server_socket.bind(("", 9999))

server_socket.listen(5)
# 客户列表
socket_list = [server_socket, sys.stdin]  # 读取的列表

writeable_list = []

try:
	while True:
		print("-----11111----")

		read_list, write_list, error_list = select.select(socket_list, writeable_list, socket_list)
		# print(read_list, write_list, error_list)
		for sock in read_list:
			if sock == server_socket:
				print("sock==server_socket")
				new_socket, new_addr = server_socket.accept()
				# 将new_socket添加到列表、
				socket_list.append(new_socket)  # [server_socket,new_socket]
			elif sock == sys.stdin:
				msg = sys.stdin.readline()
				print("键盘输入的内容:", msg)
			else:
				recv_msg = sock.recv(1024)
				if len(recv_msg) == 0:
					print("客户端断开了")
					socket_list.remove(sock)
				elif len(recv_msg) > 0:
					print("收到消息:", recv_msg.decode("utf-8"))
					# 回声
					sock.send(recv_msg)  # 发送同样的信息
except:
	print("----->有异常了")
finally:
	server_socket.close()

网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。

这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。

 

select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。

select 回显服务器

echo(回显)服务器代码

from socket import AF_INET,socket,SO_REUSEADDR,SOCK_STREAM,SOL_SOCKET
from select import select
def main():
   #创建tcp的socket套接字
   server_socket = socket(AF_INET,SOCK_STREAM)
   server_socket.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
   #绑定端口
   server_socket.bind(("",9999))
   #设置监听
   server_socket.listen(5)
   #客户端列表
   socket_lists = [server_socket]
   try:

      while True:
         #检测列表client_lists那些socket可以接收数据,
         #检测列表[]那些套接字(socket)可否发送数据
         #检测列表[]那些套接字(socket)是否产生了异常
         print("select--111")
         #这个select函数默认是堵塞,当有客户端链接的时候解除阻塞,
         # 当有数据可以接收的时候解除阻塞,当客户端断开的时候解除阻塞
         readable, wirteable,excep = select(socket_lists,[],[])
         # print("select--2222")
         # print(111)
         for sock in readable:
            #接收数据
            if sock == server_socket:
               print("sock == server_socket")
               #有新的客户端链接进来
               new_socket,new_address = sock.accept()
               #新的socket添加到列表中,便于下次socket的时候能检查到
               socket_lists.append(new_socket)
            else:
               # print("sock.recv(1024)....")
               #此时的套接字sock是直接可以取数据的
               recv_data = sock.recv(1024)
               if len(recv_data) > 0:
                  print("从[%s]:%s" % (str(new_address),recv_data))
                  sock.send(recv_data)
               else:
                  print("客户端已经断开")
                  #客户端已经断开,要移除
                  sock.close()
                  socket_lists.remove(sock)



   finally:
      #关闭套接字
      server_socket.close()

if __name__ == "__main__":
   main()

总结:

优点支持跨平台

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点

1)select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048

2)对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低

3)当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

poll解决了套接字有上限的问题

poll解决了套接字有上限的问题,效率和select一样,都是轮询方式。

select -->最多1024个套接字-->采用轮询方式进程检测套接字是否可以接收等

poll -->解决了支持套接字上线问题-->采用轮询方式进程检测

epoll-->解决支持上限问题-->采用的是事件通知

selecpoll都是轮询检测方式,效率比较低, epoll采用的事件通知机制,这个时候epoll效率远高于selectpoll

epoll的优点

1)没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024。File Description

2)效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll

3)epoll采用的事件通知机制

epoll服务器代码
from select import *
from socket import *

server_socket = socket(AF_INET, SOCK_STREAM)

server_socket.bind(("", 9999))

server_socket.listen(5)

# 创建epoll对象
epol = epoll()

# 注册事件
epol.register(server_socket.fileno(), EPOLLIN | EPOLLET)

# 装socket的列表
socket_list = {}

# 装socket的地址
socket_addr = {}

while True:
	print("-------1111111-------")
	epoll_list = epol.poll()  # [(fd,事件),(),(),()]
	print("--------22222------")

	for fd, event in epoll_list:
		# 有新的连接
		if fd == server_socket.fileno():
			new_socket, new_addr = server_socket.accept()
			# \往子典中添加数据
			socket_list[new_socket.fileno()] = new_socket
			socket_addr[new_socket.fileno()] = new_addr
			# 注册事件
			epol.register(new_socket.fileno(), EPOLLIN | EPOLLET)
		elif event == EPOLLIN:
			print("哈哈哈!收到数据了")
			new_socket = socket_list[fd]
			new_addr = socket_addr[fd]

			# 读取数据
			content = new_socket.recv(1024)
			if len(content) > 0:
				print("收到数据是:", content.decode("utf-8"))
			else:
				epol.unregister(fd)  # 取消注册
				new_socket.close()
				print(new_addr, "下线了....")

EPOLLINEPOLLET 说明

EPOLLIN (可读)

EPOLLOUT (可写)

EPOLLET (ET模式)

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式LT模式与ET模式的区别如下:

LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。直到你出来为止。

 

ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。下次将不会在通知。

 

ET要比LT效率高


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值