NIO详细理解

文章目录

    • BIO与NIO的区别
    • 什么是缓冲区
    • 什么是通道
    • 自己写代码怎么从通道读写数据
    • 反应堆
      • 阻塞式IO模型
      • 非阻塞IO模型
    • 什么是选择器(事件-channel)
      • SelectionKey源码分析
      • Selector源码分析
    • 自定义代码使用selector
  • NIO源码分析
    • Selector具体方法源码分析
    • ServerSocketChannel
  • 实现一个NIO服务器模型
    • NIO客户端
    • NIO服务端
  • 回调函数
    • 回调函数实现
    • 回调函数的作用
  • epoll模型详解
    • epoll原理
    • epoll总结
    • epoll的两种触发模式
    • epoll反应堆模型

BIO与NIO的区别

NIO详细理解_第1张图片

在BIO中一个线程管理一个连接,在NIO中一个线程管理多个连接

  1. BIO是一种同步阻塞式IO,单向传递数据,面向流,操作字节按字节存取。
  2. NIO是一种同步非阻塞式IO,双向传递数据,面向通道,操作缓冲区按快存取。

什么是缓冲区

缓冲区实质上就是一个数组对象,在NIO中读数据是从通道读到缓冲区,写数据是从通道写入到缓冲区。所有缓冲区类型都继承于抽象Buffer。

  • 缓冲区的四个属性
    capacity表示缓冲区容量,创建时设定,不能修改
    positiion表示指向下一个读写的元素
    limit表示指向第一个不能读写的元素
  • Buffer常见方法
    Put() 写入缓冲区 写一个数据,position自动加1,直到limit
    Get() 读取缓冲区 读一个数据,position自动加1,直到limit
    Flip() 反转缓冲区(写完了读) limit=position position=0
    Rewind()重绕缓冲区(重新读写) position=0 limit=capacity

什么是通道

通道代表各种实体进行I/O操作的连接,绑定了套接字,可以从通道读到缓存区,也可以从通道写入缓存区

通道的各种实现:

  • FileChannel:从文件中读写数据
  • ServerSocketChannel:能通过UDP读写网络中的数据
  • SocketChannel:能通过TCP读写网络中的数据
  • DatagramChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个
public interface Channel extends Closeable{
     public boolean isOpen();
     public void close() throws IOException;
}
## AbstractSelectableChannel 相关源码
public abstract class AbstractSelectableChannel extends  SelectableChannel{
       //这个通道的选择器
       private final SelectorProvider provider;
       //已经注册了通道和选择器的key
       private SelectionKey[] keys = null;
       //增加SelectionKey
       private void addKey(SelectionKey k);
       //通过selector注册channel,返回SelectionKey
       public final SelectionKey register(Selector sel, int ops,
                                       Object att);
}

## SocketChannel相关源码
public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{  
    //给通道绑定套接字
    public abstract SocketChannel bind(SocketAddress local){}
    //返回该通道绑定的套接字
    public abstract Socket socket();
    //连接通道的套接字
    public abstract boolean connect(SocketAddress remote){}

    //通过选择器打开通道
    public static SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel(); //The new channel is created
    }
    //从通道写入缓冲区
    public abstract int write(ByteBuffer src)
    //从通道读到缓存区
    public final long read(ByteBuffer[] dsts)
}

自己写代码怎么从通道读写数据

使用NIO读取数据

  • 从FileInputStream获取Channel
  • 创建buffer
  • 将数据从Channel读取到Buffer中
    使用NIO写入数据
  • 从FileOutputStream获取Channel
  • 创建buffer
  • 将数据从Channel写入到buffer

反应堆

阻塞式IO模型

老IO包中,serverSocket和socket都是阻塞式的,每个访问都会开启一个线程

NIO详细理解_第2张图片

非阻塞IO模型

NIO详细理解_第3张图片

  1. 有一个专门线程处理所有IO事件,并负责分发
  2. 使用事件驱动机制
  3. 线程之间使用wait/nofity进行通信

注:每个线程的处理流程大概都是读取数据,解码,计算处理,编码,发送响应。

什么是选择器(事件-channel)

NIO实现非阻塞IO的核心设计是selector
1. 选择器基于reactor模式的工作方式,注册各种IO事件,当事件发生时,再进行相应的处理。
2. 当有读写事件发生时,从selector获取相应的selectionKey,同时从其中找到发生事件的Channel,再进行相应的读写操作

SelectionKey封装了{通道+选择器}

SelectionKey源码分析

class SelectionKey{
    //关联的通道
    public abstract SelectableChannel channel();
    //关联的选择器
    public abstract Selector selector();
    //设置注册的事件操作,通道支持此操作
    public abstract SelectionKey interestOps(int ops);
    //获得已经准备好的操作集
    public abstract int readyOps();
    //-----------------------------------------------------
    //Operation-set bit for read operations.
    public static final int OP_READ = 1 << 0;
    //Operation-set bit for write operations.
    public static final int OP_WRITE = 1 << 2;
    //Operation-set bit for socket-connect operations.
    public static final int OP_CONNECT = 1 << 3;
    //Operation-set bit for socket-accept operations.
    public static final int OP_ACCEPT = 1 << 4;
}

Selector源码分析

public abstract class Selector implements Closeable {
    //创建一个新得Selector
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }  
    //当通道的IO事件就绪时,选择器选择SelectionKey集合
    public abstract int select() ;
    //返回创建通道的selector
    public abstract SelectorProvider provider();
    //返回Selector注册的SelectionKey集合
    public abstract Set<SelectionKey> keys();
    //返回Selector选择的SelectionKey集合
    public abstract Set<SelectionKey> selectedKeys();
    //让第一个没有选择的操作立即返回
    public abstract Selector wakeup();
}

自定义代码使用selector

  1. 向selector注册感兴趣的事件
  2. 从selector获取被触发的事件
  3. 根据不同事件做相应处理

NIO源码分析

epool模型

Selector具体方法源码分析

  1. Selector.open()
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    public static SelectorProvider provider() {
        synchronized (lock) {
            //保证只有一个provider对象实例
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

默认的WindowsSelectorProvider实现类

  WindowsSelectorImpl(SelectorProvider var1) throws IOException {
        super(var1);
        //获取文件描述符
        this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
        SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
        var2.sc.socket().setTcpNoDelay(true);
        //获取文件描述符
        //把唤醒端的文件描述符(wakeupSourceFd)放到pollWrapper里;
        this.wakeupSinkFd = var2.getFDVal();
        this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
    }

实际上就是new 了一个 WindowsSelectorImpl对象实例。
以及建立了Pipe,并把pipe的wakeupSourceFd放入pollArray中,这个pollArray是Selector的枢纽。

ServerSocketChannel

实现一个NIO服务器模型

NIO客户端

/**
 * @author yujie.wang
 *	SocketChannel 客户端代码测试
 */
public class SocketChannel_Client {
	
	private final static String DEFAULT_HOST = "127.0.0.1";
	
	private final static int DEFAULT_PORT = 4567;
	
	private SocketChannel channel;
	
	private Socket socket;
	
	//分配一个大小为50字节的缓冲区 用于客户端通道的读写
	private ByteBuffer buffer = ByteBuffer.allocate(50);
	
	public SocketChannel_Client(){
		this(DEFAULT_HOST, DEFAULT_PORT);
	}
	
	public SocketChannel_Client(String host, int port){
		init(host,port);
	}

	
	/**
	 * 打开通道并设置对等的客户端socket对象
	 * 建立与服务端通道的连接
	 * @param host
	 * @param port
	 */
	public void init(String host, int port){
		try {
			//打开一个客户端通道,同时当前通道并没有与服务端通道建立连接
			channel = SocketChannel.open();
			//获得客户端socket
			socket = channel.socket();
			//配置客户端socket的ip:port
			setSocket();
			//将通道设置为非阻塞工作方式
			channel.configureBlocking(false);
			//异步连接通道注册的套接字,发起连接之后就立即返回
			channel.connect(new InetSocketAddress(host,port));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
	    //初始化NIO客户端:打开通道,获取套接字,设置非阻塞,连接套接字
		SocketChannel_Client client = new SocketChannel_Client();
		//不断的判断是否连接
		client.finishConnect();
		System.out.println("connect success");
		//向服务器写数据
		client.write("Hello World");
		System.out.println("client write end");
		//从服务器读取数据
		client.read();
		sleep(15000);
		System.out.println("client exit");
 
	}
	
	/**
	 * 验证连接是否建立
	 */
	public void finishConnect(){
		try {
			while(!channel.finishConnect()){
				// nothing to do,wait connect
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 验证当前连接是否可用
	 */
	public void isConnected(){
		try {
			if(channel == null || !channel.isConnected())
				throw new IOException("channel is broken");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 配置客户端通道对等的Socket
	 */
	public void setSocket(){
		try {
			if(socket != null ){
				//设置socket 读取的超时时间5秒
				//socket.setSoTimeout(5000);
				//设置小数据包不再组合成大包发送,也不再等待前一个数据包返回确认消息
				socket.setTcpNoDelay(true);
				//设置如果客户端Socket关闭了,未发送的包直接丢弃
				socket.setSoLinger(true, 0);
			}
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
	
	public void write(String data) {
		buffer.clear();
		buffer.put(data.getBytes());
		buffer.flip();
		try {
			// write并不一定能一次将buffer中的数据都写入 所以这里要多次写入
			// 当多个线程同时调用同一个通道的写方法时,只有一个线程能工作,其他现在则会阻塞
			while(buffer.hasRemaining()){
				channel.write(buffer);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void read(){
		try {
			buffer.clear();
			// read方法并不阻塞,如果有数据读入返回读入的字节数,没有数据读入返回0 ,遇到流的末尾返回-1
			// 当然这里和Socket和ServerSocket通信一样 也会存在消息无边界的问题 我们这里就采取简单的读取一次作为示例
			System.out.println("read begin");
			channel.read(buffer);
		/*	while(buffer.hasRemaining() && channel.read(buffer) != -1){
				printBuffer(buffer);
			}*/
			buffer.flip();
			printBuffer(buffer);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 输出buffer中的数据
	 * @param buffer
	 */
	public void printBuffer(ByteBuffer buffer){
		while(buffer.hasRemaining()){
			System.out.print((char)buffer.get());
		}
		System.out.println("");
		System.out.println("****** Read end ******");
	}
	
	/**
	 * 判断通道是否打开
	 * @return
	 */
	public boolean isChannelOpen(){
		try {
			return channel.finishConnect() ? true : false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return false;
	}
	
	/**
	 * 关闭通道
	 */
	public void closeChannel(){
		if(channel != null){
			try {
				channel.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	public static void sleep(long time){
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

NIO服务端

/**
 * @author yujie.wang
 * ServerSocketChannel 测试用例
 */
public class ServerSocketChannel_Server {
 
	private final static int DEFAULT_PORT = 4567;
	
	private ServerSocketChannel channel;
	
	private Selector selector;
	
	private ServerSocket serverSocket;
	
	public ServerSocketChannel_Server(){
		this(DEFAULT_PORT);
	}
	
	public ServerSocketChannel_Server(int port){
		init(port);
	}
	
	public static void main(String[] args) {
	    //打开服务器通道,绑定IP设置非阻塞;打开选择器,注册通道
		ServerSocketChannel_Server server = new ServerSocketChannel_Server();
		server.selector();
		System.out.println("server exit");
	}
	
	public void init(int port){
		try {
			//打开一个服务端通道
			channel = ServerSocketChannel.open();
			//获得对等的ServerSocket对象
			serverSocket = channel.socket();
			//将服务端ServerSocket绑定到指定端口
			serverSocket.bind(new InetSocketAddress(port));
			System.out.println("Server listening on port: "+ port);
			//将通道设置为非阻塞模式
			channel.configureBlocking(false);
			//打开一个选择器
			selector = Selector.open();
			//将通道注册到打开的选择器上
			channel.register(selector, SelectionKey.OP_ACCEPT);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public void selector(){
		try {
			while(true){
				System.out.println("begin to select");
				//select()方法会阻塞,直到有准备就绪的通道有准备好的操作;或者当前线程中断该方法也会返回
				//这里的返回值不是选择器中已选择键集合中键的数量,而是从上一次select()方法调用到这次调用期间进入就绪状态通道的数量
				int readyKeyCount = selector.select();
				if(readyKeyCount <= 0){
					continue;
				}
				System.out.println("ok select readyCount: "+ readyKeyCount);
				//获得已选择键的集合这个集合中包含了 新准备就绪的通道和上次调用select()方法已经存在的就绪通道
				Set<SelectionKey> set = selector.selectedKeys();
				Iterator<SelectionKey> iterator = set.iterator();
				while(iterator.hasNext()){
					SelectionKey key = iterator.next();
					//通过调用remove将这个键key从已选择键的集合中删除
					iterator.remove();
					if(key.isAcceptable()){
						handleAccept(key);
					}else if(key.isReadable()){
						handleRead(key);
					}else if(key.isWritable()){
						handleWrite(key,"Hello World");
					}
				}
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 处理客户端连接事件
	 * @param key
	 * @param selector
	 */
	public void handleAccept(SelectionKey key){
		try {
			//因为能注册SelectionKey.OP_ACCEPT事件的只有 ServerSocketChannel通道,
			//所以这里可以直接转换成ServerSocketChannel
			ServerSocketChannel channel = (ServerSocketChannel)key.channel();
			//获得客户端的SocketChannel对象 ,accept这里不会阻塞,如果没有连接到来,这里会返回null
			SocketChannel client = channel.accept();
			System.out.println("Accepted Connected from: "+ client);
			//将客户端socketChannel设置为非阻塞模式
			client.configureBlocking(false);
			//为该客户端socket分配一个ByteBuffer
			ByteBuffer buffer = ByteBuffer.allocate(50);
			client.register(selector, SelectionKey.OP_READ, buffer);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * 处理读取数据事件
	 * @param key
	 */
	public void handleRead(SelectionKey key){
		try {
			//从键中获得相应的客户端socketChannel
			SocketChannel channel = (SocketChannel)key.channel();
			//获得与客户端socketChannel关联的buffer
			ByteBuffer buffer = (ByteBuffer)key.attachment();
			buffer.clear();
			//将数据读取到buffer中,这里read方法不会阻塞
			//有数据返回读取的字节数,没有数据返回0,遇到流的末尾则返回-1
			//这里为了避免消息的无边界 性 ,所以只读取一次数据
			int count = channel.read(buffer);
			System.out.println("read count:"+ count);
			buffer.flip();
			//输出数据
			printBuffer(buffer);
			buffer.clear();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	/**
	 * 处理写入数据的时间
	 * @param key
	 * @param data
	 */
	public void handleWrite(SelectionKey key,String data){
		try {
			SocketChannel channel = (SocketChannel)key.channel();
			ByteBuffer buffer = (ByteBuffer)key.attachment();
			buffer.clear();
			buffer.put(data.getBytes());
			buffer.flip();
			while(buffer.hasRemaining()){
				channel.write(buffer);
			}
			buffer.clear();
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
 
	public static void printBuffer(ByteBuffer buffer){
		while(buffer.hasRemaining()){
			System.out.println("positon: "  + buffer.position()+ " limit:"+ buffer.limit());
			System.out.print((char)buffer.get());
		}
		System.out.println("");
		System.out.println("****** Read end ******");
		System.out.println("");
	}
	
}

回调函数

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数将函数的调用者和被调用者分离,通过将回调函数传入调用者,实现当调用者进行调用时回调Callback。而且可以将相关信息当作事件封装到参数中进行消息传递。

回调函数实现

⑴定义一个回调函数;
⑵提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者
⑶当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。

回调函数的作用

回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法

epoll模型详解

https://blog.csdn.net/daaikuaichuan/article/details/83862311

epoll原理

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包)也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?
select模型:进程每次询问系统是否有发生事件的TCP连接时,首先把这100万个连接告诉操作系统,然后通过查询选择出发生事件的连接。这会涉及到用户到内核态内存的大量复制。
epoll模型:在Linux内核申请了一个简易文件系统,然后通过三步进行选择就绪的事件。
1. 调用epoll_create建立一个epoll对象(在epoll文件系统中给这个句柄分配资源);
2. 调用epoll_ctl向epoll对象中添加这100万个连接的套接字;
3. 调用epoll_wait收集发生事件的连接。

struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存准备就绪的事件列表*/
  struct list_head rdllist;
  ...
};
    我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
    所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。
   在epoll中对于每一个事件都会建立一个epitem结构体,如下所示:
struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

NIO详细理解_第4张图片

epoll总结

一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。

  1. 执行epoll_create()时,创建了红黑树和就绪链表;
  2. 执行epoll_ctl()时,向红黑树增加socket句柄,首先检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上。然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
  3. 执行epoll_wait()时立刻返回准备就绪链表里的数据即可。
    NIO详细理解_第5张图片

epoll的两种触发模式

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
NIO详细理解_第6张图片
LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。

ET模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回;

epoll反应堆模型

epoll原来的流程

  1. 通过epoll_create() 创建一个epoll对象,建立红黑树和就绪链表
  2. 通过epoll_ctl() 注册监听的套接字的fd
  3. 通过epoll_wait()监听发生事件的fd,然后返回就绪fd数组
    epoll反应堆模型流程
    有客户端连接上来—>lfd调用acceptconn()—>将cfd挂载到红黑树上监听其读事件—>
    epoll_wait()返回cfd—>cfd回调recvdata()—>将cfd摘下来监听写事件—>
    epoll_wait()返回cfd—>cfd回调senddata()—>将cfd摘下来监听读事件—>…—>

Unix 中所有的东西是文件!因此,与 Internet 上别的程序通讯的时候,要通过文件描述符。利用系统调用 socket()得到网络通讯的文件描述符
NIO详细理解_第7张图片
回调函数将函数的调用者和被调用者分离,通过将回调函数传入调用者,实现当调用者进行调用时回调Callback。而且可以将相关信息当作事件封装到参数中进行消息传递。

  1. 描述就绪文件描述符的相关信息
struct myevent_s
{
    int fd;             //要监听的文件描述符
    int events;         //对应的监听事件,EPOLLIN和EPLLOUT
    void *arg;          //指向自己结构体指针
    void (*call_back)(int fd,int events,void *arg); //回调函数
    int status;         //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
    char buf[BUFLEN];   
    int len;
    long last_active;   //记录每次加入红黑树 g_efd 的时间值
};
  1. 封装一个自定义事件,包括fd,这个fd的回调函数,还有一个额外的参数项
    注意:在封装这个事件的时候,为这个事件指明了回调函数,一般来说一个fd只对一个特定的事件感兴趣,当这个事件发生的时候,就调用这个回调函数
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int fd,int events,void *arg), void *arg)
{
    ev->fd = fd;
    ev->call_back = call_back;
    ev->events = 0;
    ev->arg = arg;
    ev->status = 0;
    if(ev->len <= 0)
    {
        memset(ev->buf, 0, sizeof(ev->buf));
        ev->len = 0;
    }
    ev->last_active = time(NULL); //调用eventset函数的时间
    return;
}

你可能感兴趣的:(面试)