从零开始学习Netty - 学习笔记 - NIO基础 - 网络编程: Selector

4.网络编程

4.1.非阻塞 VS 阻塞

在网络编程中,**阻塞(Blocking)非阻塞(Non-blocking)**是两种不同的编程模型,描述了程序在进行网络通信时的行为方式。

  1. 阻塞(Blocking)
    • 在阻塞模型中,当程序发起一个网络请求时,它会一直等待直到操作完成或者发生错误
    • 在网络通信过程中,如果数据没有到达,或者连接还没有建立,程序会被挂起,直到数据到达或者连接建立完成。
    • 在阻塞模型中,通常一个线程只处理一个连接,因此需要为每个连接创建一个新的线程,这会增加系统开销,尤其在高并发环境下,可能导致资源耗尽和性能下降。
  2. 非阻塞(Non-blocking)
    • 在非阻塞模型中,程序可以在发起网络请求后立即返回,不必等待操作完成。
    • 如果数据没有到达或者连接尚未建立,程序不会被挂起,而是会立即返回一个状态,告诉调用者当前操作尚未完成。
    • 在非阻塞模型中,程序可以不断轮询网络状态,不断尝试进行数据读取或者连接操作,直到操作完成或者发生错误。
    • 通过使用非阻塞模型,一个线程可以同时处理多个连接,避免了为每个连接创建新线程的开销,提高了系统的性能和资源利用率。

在实际的网络编程中,可以根据具体的需求和系统性能要求选择合适的编程模型。阻塞模型通常更加简单直观,适用于连接数较少且并发要求不高的场景;而非阻塞模型更加灵活,适用于需要处理大量并发连接的高性能网络应用。

4.1.1.阻塞

阻塞模式 一个线程,可能会影响别的线程运行

accept 会影响 read , read 也会影响 accept

Server

/**
 *
 * Server 服务端
 * @author 13723
 * @version 1.0
 * 2024/2/20 13:11
 */
public class Server {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
	public static void main(String[] args) throws IOException {
		// 使用NIO来理解阻塞模式 单线程进行处理
		ByteBuffer buffer = ByteBuffer.allocate(16);
		// 1.创建一个ServerSocketChannel  创建一个服务器
		ServerSocketChannel ssc = ServerSocketChannel.open();
		// 2.绑定监听端口
		ssc.bind(new InetSocketAddress(9000));
		// 3.建立一个练级的集合
		List<SocketChannel> socketChannelList = new ArrayList<SocketChannel>();
		while (true){
			logger.error("--------------- connection  start ----------------");
			// 3.accept 建立和客户端之间的连接,说白了就是和客户端之间进行通信
			// 这里会的方法会阻塞,线程会停止运行 (这里会等一个新的连接,如果没有新的连接建立会一直阻塞在这里)
			SocketChannel sc = ssc.accept();
			logger.error("--------------- connection  {} ----------------",sc);
			socketChannelList.add(sc);
			// 5.介绍客户端发送的数据
			for (SocketChannel socketChannel : socketChannelList) {
				logger.error("--------------- before read  ----------------");
				socketChannel.read(buffer);
				// 切换为读模式
				buffer.flip();
				ByteBufferUtil.debugAll(buffer);
				// 切换为写模式 重新接收新的数据
				buffer.clear();
				logger.error("--------------- after read  ----------------");
			}
		}
	}
}

Client

/**
 * 客户端
 * @author 13723
 * @version 1.0
 * 2024/2/20 13:24
 */
public class Client {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	public static void main(String[] args) throws IOException {
		// 1.创建客户端连接
		SocketChannel sc = SocketChannel.open();
		// 2.设置连接信息
		sc.connect(new InetSocketAddress("localhost",9000));
		// 等待
		logger.error("--------------- waiting ---------------");
	}
}

启动的时候,Server正常进行启动,Client 以debug的方式进行启动

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.1.2.非阻塞

server

缺点:很明显,当我们没有数据的时候

​ accept 和 read 还再循环

public class Server {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());


	public static void main(String[] args) throws IOException {
		// 使用NIO来理解阻塞模式 单线程进行处理
		ByteBuffer buffer = ByteBuffer.allocate(16);
		// 1.创建一个ServerSocketChannel  创建一个服务器
		ServerSocketChannel ssc = ServerSocketChannel.open();
		// TODO 设置为非阻塞模式
		ssc.configureBlocking(false);
		// 2.绑定监听端口
		ssc.bind(new InetSocketAddress(9000));
		// 3.建立一个练级的集合
		List<SocketChannel> socketChannelList = new ArrayList<SocketChannel>();
		while (true){
			// logger.error("--------------- connection  start ----------------");
			// 3.accept 建立和客户端之间的连接,说白了就是和客户端之间进行通信
			// 切换成非阻塞模式了,如果没有连接建立 返回的时一个null值
			SocketChannel sc = ssc.accept();
			if (sc != null){
				logger.error("--------------- connection  {} ----------------",sc);
				sc.configureBlocking(false);
				socketChannelList.add(sc);
			}
			// 5.介绍客户端发送的数据
			for (SocketChannel socketChannel : socketChannelList) {
				// logger.error("--------------- before read  ----------------");
				// 编程非阻塞,但是线程仍然会继续运行 如果没有读取到数据 read会返回0
				int read = socketChannel.read(buffer);
				if (read > 0){
					// 切换为读模式
					buffer.flip();
					ByteBufferUtil.debugAll(buffer);
					// 切换为写模式 重新接收新的数据
					buffer.clear();
					logger.error("--------------- after read  ----------------");
				}
			}
		}
	}
}

client

客户端代码 和上面一样没有做额外改动

在这里插入图片描述

4.2.Selector

介绍选择器Selector之前,先介绍一个概念 IO事件

IO事件
  • IO事件表示通道内的某种IO操作已经准备就绪

例如:在Server Scoket通道上发生的一个IO事件,代表一个新的连接已经准备好,这个事件就叫做接收就绪事件。或者说,一个通道内如果有数据可以读取,就会发生一个IO事件,代表该连接数据已经准备好,这个事件就叫做读就绪事件

JavaNIO将NIO事件做了简化,只定义了四个事件,他们用SelectionKey的4个常量来表示

  • SelectionKey.OP_CONNECT
    • 表示连接就绪事件,用于表示客户端连接建立后触发的事件。客户端的 SocketChannel 关注此事件,以便在连接建立后执行相应的操作。
  • SelectionKey.OP_ACCEPT
    • 表示接受连接就绪事件,用于表示服务器端有连接请求时触发的事件。服务器端的 ServerSocketChannel 关注此事件,以便在有新的连接请求时执行相应的操作。
  • SelectionKey.OP_READ
    • 表示读就绪事件,用于表示通道中有数据可以读取的事件。通常由 SocketChannel 关注,以便在通道中有数据可读时执行相应的读取操作。
  • SelectionKey.OP_WRITE
    • 表示写就绪事件,用于表示通道可以写入数据的事件。通常由 SocketChannel 关注,以便在通道可写入数据时执行相应的写入操作

管理多个channel , 可以发现channel是否有事件发生,有事件发生再去执行 防止cpu空转造成系统资源浪费

/**
 *
 * Server 服务端
 * @author 13723
 * @version 1.0
 * 2024/2/20 13:11
 */
public class Server {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());


	public static void main(String[] args) throws IOException {
		// 创建一个Selector对象
		Selector selector = Selector.open();
		ByteBuffer buffer = ByteBuffer.allocate(16);
		// 创建channel
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		// 建立selector和Channel之间的连接(将Channel注册到Selector中)
		// SelectionKey 将来事件发生后,通过它,可以知道哪种事件,是那个Channel发生的事件
		// 0 表示不关注任何事件
		SelectionKey sscKey = ssc.register(selector, 0, null);
		// ** 事件有四种类型
		// ?? accept 会在有连接请求时触发 (SelectionKey关注)
		// ?? connect 客户端连接建立后触发的事件
		// ?? read 可读事件 (SocketChannel关注)
		// ?? write 可写事件(SocketChannel关注)

		// 设置具体的事件 (设置只关注 accept事件);
		sscKey.interestOps(SelectionKey.OP_ACCEPT);


		// 2.绑定监听端口
		ssc.bind(new InetSocketAddress(9000));
		while (true){
			// 3.调用selector的select方法(没有事件发生,那么还是阻塞的)
			// !! 注意Selector在编程时,未处理时,不会阻塞会一直进行执行(要么处理,要么取消 不能不管)
			selector.select();
			// 4.处理事件(selectionKeys 中所有的可用的事件)
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			// 遍历的时候 想要删除 必须使用迭代器遍历
			while (iterator.hasNext()){
				SelectionKey key = iterator.next();
				logger.error("key : {}",key);
				// 拿到对应channel
				ServerSocketChannel channel = (ServerSocketChannel)key.channel();
				// 建立连接
				SocketChannel accept = channel.accept();
				logger.error("accept : {}",accept);
				// 一个处理是accept 还可以进行取消
				// key.cancel();
			}
		}
	}
}

在这里插入图片描述

4.2.1处理read

读取数据 每个channel 里面 针对不同的事件类型 又创建了不同的channel进行维护

在这里插入图片描述

public class Server {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
	public static void main(String[] args) throws IOException {
		// 创建一个Selector对象
		Selector selector = Selector.open();
		// 创建channel
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		// 建立selector和Channel之间的连接(将Channel注册到Selector中)
		// SelectionKey 将来事件发生后,通过它,可以知道哪种事件,是那个Channel发生的事件
		// 0 表示不关注任何事件
		SelectionKey sscKey = ssc.register(selector, 0, null);
		// ** 事件有四种类型
		// ?? accept 会在有连接请求时触发 (SelectionKey关注)
		// ?? connect 客户端连接建立后触发的事件
		// ?? read 可读事件 (SocketChannel关注)
		// ?? write 可写事件(SocketChannel关注)

		// 设置具体的事件 (设置只关注 accept事件);
		sscKey.interestOps(SelectionKey.OP_ACCEPT);


		// 2.绑定监听端口
		ssc.bind(new InetSocketAddress(9000));
		while (true){
			// 3.调用selector的select方法(没有事件发生,那么还是阻塞的)
			// !! 注意Selector在编程时,未处理时,不会阻塞会一直进行执行(要么处理,要么取消 不能不管)
			selector.select();
			// 4.处理事件(selectionKeys 中所有的可用的事件)
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			// 遍历的时候 想要删除 必须使用迭代器遍历
			while (iterator.hasNext()){
				SelectionKey key = iterator.next();
				logger.error("key : {}",key);
				// 5.区分事件类型
				if (key.isAcceptable()) {
					// accept事件
					// 拿到对应channel
					ServerSocketChannel channel = (ServerSocketChannel)key.channel();
					// 建立连接
					SocketChannel sc = channel.accept();
					// 设置channel为非阻塞的
					sc.configureBlocking(false);
					// 将管理权交给selector(负责管理当前处理的channel)
					SelectionKey scKey = sc.register(selector, 0, null);
					// 注意 这里是read
					scKey.interestOps(SelectionKey.OP_READ);
					logger.error("sc : {}",sc);
				}else if (key.isReadable()){
					// 读取数据的事件
					SocketChannel channel = (SocketChannel) key.channel();
					ByteBuffer buffer = ByteBuffer.allocate(16);
					channel.read(buffer);
					// 切换为读模式
					buffer.flip();
					ByteBufferUtil.debugRead(buffer);
				}
			}
		}
	}
}

在这里插入图片描述

4.2.2.用完key之后为什么要remove

重点:SelectedKey 只会往里面添加 key ,但是不会进行删除(也就是事件处理完成后,会标记成处理,但是不会删除)

// TODO 删除key 一定要删除
// SelectedKey 只会往里面添加 key ,但是不会进行删除(也就是事件处理完成后,会标记成处理,但是不会删除)
// 不然下次进来还是上一个Key上一个Key是没有事件,所有会报空指针
// 这就是这里要使用迭代器的原因,迭代器可以边遍历边删除,forEach不行
iterator.remove();

在这里插入图片描述

4.2.3.处理客户端断开
else if (key.isReadable()){
    try {
        // 读取数据的事件
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 如果是正常断开。那么read返回的是-1 因为每次断开都会触发一次读事件
        int read = channel.read(buffer);
        if (read == -1){
            // 删除key
            key.cancel();
        }else {
            // 切换为读模式
            buffer.flip();
            ByteBufferUtil.debugRead(buffer);
            // TODO 删除key 一定要删除
            // SelectedKey 只会往里面添加 key ,但是不会进行删除(也就是事件处理完成后,会标记成处理,但是不会删除)
            // 不然下次进来还是上一个Key上一个Key是没有事件,所有会报空指针
            iterator.remove();
        }
    }catch (Exception e){
        // 客户端关闭了,这里需要将key从SelectedKey集合中真正的删除
        e.printStackTrace();
        key.cancel();
    }
}

正常断开

// 客户端 需要手动调用close
public class Client {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

	public static void main(String[] args) throws IOException {
		// 1.创建客户端连接
		SocketChannel sc = SocketChannel.open();
		// 2.设置连接信息
		sc.connect(new InetSocketAddress("localhost",9000));
		// 等待
		logger.error("--------------- waiting ---------------");
		// 正常断开 不写就是异常断开
		sc.close();
	}
}

在这里插入图片描述

异常断开(强制断开)

在这里插入图片描述

4.2.4.处理消息边界

当客户端发动的服务端的中文信息过长时,就可能会出现乱码的情况

在这里插入图片描述

在这里插入图片描述

  • 一种思路是,固定消息的长度,数据包的大小一样,服务器按照预定长度读取,缺点是浪费带宽
  • 另一种思路是按照分隔符拆分,缺点是效率低下
  • TLV格式Type类型Length长度Value数据,类型和长度已知情况下,就可以方便获取消息大小,分配合适的buffer,缺点是buffer需要提前分配,如果内容过大,则会影响server吞吐量
    • HTTP 1.1 是LTV格式
    • HTTP 2.0 是LTV格式

在这里插入图片描述

server

每次将ByteBuffer作为参数进行传递 也就是 通过

​ 服务端 注册
​ ByteBuffer buffer = ByteBuffer.allocate(15);
​ SelectionKey scKey = sc.register(selector, 0, buffer);

​ 客户端 获取 重新设置

​ ByteBuffer buffer = (ByteBuffer) key.attachment();

​ key.attach(newByteBuffer);

在这里插入图片描述

public class Server {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());


	public static void main(String[] args) throws IOException {
		// 创建一个Selector对象
		Selector selector = Selector.open();
		// 创建channel
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		// 建立selector和Channel之间的连接(将Channel注册到Selector中)
		// SelectionKey 将来事件发生后,通过它,可以知道哪种事件,是那个Channel发生的事件
		// 0 表示不关注任何事件
		SelectionKey sscKey = ssc.register(selector, 0, null);
		// ** 事件有四种类型
		// ?? accept 会在有连接请求时触发 (SelectionKey关注)
		// ?? connect 客户端连接建立后触发的事件
		// ?? read 可读事件 (SocketChannel关注)
		// ?? write 可写事件(SocketChannel关注)

		// 设置具体的事件 (设置只关注 accept事件);
		sscKey.interestOps(SelectionKey.OP_ACCEPT);


		// 2.绑定监听端口
		ssc.bind(new InetSocketAddress(9000));
		while (true){
			// 3.调用selector的select方法(没有事件发生,那么还是阻塞的)
			// !! 注意Selector在编程时,未处理时,不会阻塞会一直进行执行(要么处理,要么取消 不能不管)
			selector.select();
			// 4.处理事件(selectionKeys 中所有的可用的事件)
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			// 遍历的时候 想要删除 必须使用迭代器遍历
			while (iterator.hasNext()){
				SelectionKey key = iterator.next();
				logger.error("key : {}",key);
				// 5.区分事件类型
				if (key.isAcceptable()) {
					// accept事件
					// 拿到对应channel
					ServerSocketChannel channel = (ServerSocketChannel)key.channel();
					// 建立连接
					SocketChannel sc = channel.accept();
					// 设置channel为非阻塞的
					sc.configureBlocking(false);
					// 将管理权交给selector(负责管理当前处理的channel)
					//!! 1.将ByteBuffer 注册到SelectionKey中,这样保证每个人SelectionKey都有一个独有的ByteBuff
					//!! 这种称为附件 attachment
					//!! buffer不在作为局部变量了
					ByteBuffer buffer = ByteBuffer.allocate(15);
					SelectionKey scKey = sc.register(selector, 0, buffer);
					// 注意 这里是read
					scKey.interestOps(SelectionKey.OP_READ);
					logger.error("sc : {}",sc);
					// TODO 删除key 一定要删除
					// SelectedKey 只会往里面添加 key ,但是不会进行删除(也就是事件处理完成后,会标记成处理,但是不会删除)
					// 不然下次进来还是上一个Key上一个Key是没有事件,所有会报空指针
					iterator.remove();
				}else if (key.isReadable()){
					try {
						// 读取数据的事件
						SocketChannel channel = (SocketChannel) key.channel();
						// !!2.从读事件中 拿到附件
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						// 为了保证每个Channel都有一个独有的ByteBuffer
						// 如果是正常断开。那么read返回的是-1 因为每次断开都会触发一次读事件
						int read = channel.read(buffer);
						if (read == -1){
							// 删除key
							key.cancel();
						}else {
							// 切换为读模式
							buffer.flip();
							split(buffer);
							// !!3.判断一次是否读取完全
							// 如果position 和 limit一样 说明没有读取完成,需要扩容
							if (buffer.position() == buffer.limit()){
								ByteBuffer newByteBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
								// 新的bytebuffer 是旧的两倍 将旧的ByteBuffer内容设置的到新的中
								buffer.flip();
								newByteBuffer.put(buffer);
								// 新的buffer替换原来的buffer
								key.attach(newByteBuffer);
							}
							iterator.remove();
						}
					}catch (Exception e){
						// 客户端关闭了,这里需要将key从SelectedKey集合中真正的删除
						e.printStackTrace();
						key.cancel();
					}
				}
			}
		}
	}


	private static void split(ByteBuffer source) {
		// 找到一个完整消息, \n
		for (int i = 0; i < source.limit(); i++) {
			if (source.get(i)  == '\n') {
				// 计算消息的长度 (换行符合 + 1 - 起始索引(就是ByteBuffer的Position))
				int length = i + 1 - source.position();
				// 找到一个完整消息了(get(i)不会移动指针)
				ByteBuffer target = ByteBuffer.allocate(length);
				// 从source读取,向target写
				for (int j = 0; j < length; j++) {
					target.put(source.get());
				}
				// 打印拆出来的信息
				ByteBufferUtil.debugAll(target);
			}
		}
		// 因为可能还有没有读取完成的数据,比如一半的数据,留给下次读取
		source.compact();
	}

}

4.2.5.ByteBuffer大小的分配
  • 每个channel都需要记录可能被切分的消息,因为ByteBuffer不能被多个channel共同使用,因此需要为每个channel维护—个独立的 ByteBuffer
  • ByteBuffer 不能太大,比如一个ByteBuffer 1Mb的话,要支持百万连接就要1Tb内存,因此需要设计大小可变的 ByteBuffer
    • 一种思路是首先分配一个较小的buffer,例如4k,如果发现数据不够,再分配8k的buffer,将4kbuffer内容拷贝至8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现http://tutorials,jenkov.com/java-performance/resizable-array.html
    • 另一种思路是用多个数组组成buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗
4.2.6.写入内容过多问题

服务端一次向客户端写入太大的数据

服务端

public class WriteServer {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());


	public static void main(String[] args) throws IOException {
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);


		Selector selector = Selector.open();
		// 直接关注 accept事件
		ssc.register(selector, SelectionKey.OP_ACCEPT);


		ssc.bind(new InetSocketAddress(9000));

		while (true){
			selector.select();

			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()){
				SelectionKey key = iterator.next();
				iterator.remove();
				if (key.isAcceptable()){
					SocketChannel sc = ssc.accept();
					sc.configureBlocking(false);
					// 向客户端发送大量数据
					StringBuffer sb = new StringBuffer();
					for (int i = 0; i < 3000000; i++) {
						sb.append("a");
					}
					ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
					// 通过Channel写入数据 并不能保证一次将所有数据写入到 客户端
					// 返回值代表实际写入的字节数
					while (buffer.hasRemaining()){
						int write = sc.write(buffer);
						logger.error("实际写入的字节数:{}",write);
					}
				}
			}
		}
	}
}

客户端

public class WriteClient {
	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());


	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost",9000));


		// 3.接收数据
		int count = 0;
		while (true){
			ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
			count += sc.read(buffer);
			logger.error("接收的字节数:{}",count);
			buffer.compact();
		}
	}
}

问题

在这里插入图片描述

改进

思路就是:先尝试写一次 如果一次没写完,那么就在关联一个SelectionKey,继续写,就不用while循环一直在那里尝试写了,注意的是,SelectionKey 是可以 进行相加的,比如 既可以读 也可以 ,通过附件 attach传递没有发送完的数据。

注意 读取完成后 记得把数据释放掉。

while (true){
			selector.select();
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()){
				SelectionKey key = iterator.next();
				iterator.remove();
				if (key.isAcceptable()){
					SocketChannel sc = ssc.accept();
					sc.configureBlocking(false);
					SelectionKey scKey = sc.register(selector, 0, null);
					// !! 这里可能原来的是读取事件
					scKey.interestOps(SelectionKey.OP_READ);
					// 向客户端发送大量数据
					StringBuffer sb = new StringBuffer();
					for (int i = 0; i < 30000000; i++) {
						sb.append("a");
					}
					ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
					// 通过Channel写入数据 并不能保证一次将所有数据写入到 客户端
					// 返回值代表实际写入的字节数
					int write = sc.write(buffer);
					logger.error("实际写入的字节数:{}",write);
					// 先尝试写了一次,然后观察是否还有剩余内容
					if(buffer.hasRemaining()){
						// 关注一个写事件
						// !! 这里又加了一个写事件,为了防止把原先的事件覆盖,所以这里需要加上原来事件
						// 读事件 1  写事件 4  加一起 等于5 说明 又关注读又关注写
						scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
						// 要把未写完的数据 放到SelectionKey中
						scKey.attach(buffer);
					}
				} else if (key.isWritable()) {
					// 把上一次 buffer取出来, 关注的socketChannel拿出来
					ByteBuffer buffer = (ByteBuffer) key.attachment();
					SocketChannel sc = (SocketChannel) key.channel();
					// 继续写(数据量很多 就会反复进入可写事件)
					int write = sc.write(buffer);
					logger.error("实际写入的字节数:{}",write);

					// 写完清理附件
					if (!buffer.hasRemaining()){
						// 内容写完了 清楚buffer 可写事件 也不需要进行关联了
						key.attach(null);
						key.interestOps(key.interestOps() - SelectionKey.OP_WRITE);
					}
				}
			}
		}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/400810.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++】1006 - 打印星号三角形 1007 - 统计大写英文字母的个数 1008 - 字符图形9-数字正三角

文章目录 问题一&#xff1a;1006 - 打印星号三角形题目描述&#xff1a;输入&#xff1a;输出&#xff1a;样例&#xff1a;1.分析问题2.定义变量3.输入数据4.数据计算5.输出结果 问题二&#xff1a;1007 - 统计大写英文字母的个数题目描述&#xff1a;输入&#xff1a;输出&a…

iMazing3终极iPhone数据设备管理软件

iMazing是一款功能丰富的iOS设备管理软件&#xff0c;具备多种实用功能&#xff0c;以下是它的主要功能的详细介绍&#xff1a; iMazing3Mac-最新绿色安装包下载如下&#xff1a; https://wm.makeding.com/iclk/?zoneid49816 iMazing3Win-最新绿色安装包下载如下&#xff1…

vulfocus靶场搭建

vulfocus靶场搭建 什么是vulfocus搭建教程靶场配置场景靶场编排靶场优化 什么是vulfocus Vulfocus 是一个漏洞集成平台&#xff0c;将漏洞环境 docker 镜像&#xff0c;放入即可使用&#xff0c;开箱即用&#xff0c;我们可以通过搭建该靶场&#xff0c;简单方便地复现一些框架…

Linux系统——nginx服务介绍

一、Nginx——高性能的Web服务端 Nginx的高并发性能优于httpd服务 1.nginx概述 Nginx是由1994年毕业于俄罗斯国立莫斯科鲍曼科技大学的同学为俄罗斯rambler.ru公司开发的&#xff0c;开发工作最早从2002年开始&#xff0c;第一次公开发布时间是2004年10月4日&#xff0c;版本…

Docker技术仓库

数据卷 为什么用数据卷&#xff1f; 宿主机无法直接访问容器中的文件容器中的文件没有持久化&#xff0c;导致容器删除后&#xff0c;文件数据也随之消失容器之间也无法直接访问互相的文件 为解决这些问题&#xff0c;docker加入了数据卷机制&#xff0c;能很好解决上面问题…

CSS 函数详解url、min、rgb、blur、scale、rotate、translate等

随着技术的不断进步&#xff0c;CSS 已经从简单的样式表发展成为拥有众多内置函数的强大工具。这些函数不仅增强了开发者的设计能力&#xff0c;还使得样式应用更加动态、灵活和响应式。本文将深入探讨 CSS 常见的 66 个函数&#xff0c;逐一剖析它们的功能和用法&#xff0c;一…

船舶维保管理系统|基于springboot船舶维保管理系统设计与实现(源码+数据库+文档)

船舶维保管理系统目录 目录 基于springboot船舶维保管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、船舶列表 2、公告信息管理 3、公告类型管理 4、维保计划管理 5、维保计划类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、…

Java 学习和实践笔记(17):构造方法(构造器 constructor)

构造方法&#xff08;构造器 constructor) 它只是用于指明对象的初始化&#xff0c;而不是创造对象。 在每一个类创建完&#xff0c;编译器都会自动做一个无参的构造方法&#xff08;没有显示出来&#xff09;&#xff0c;因为做了这个&#xff0c;所以new才能自动创建对象。…

AndroidStudio 2024-2-21 Win10/11最新安装配置(Ktlion快速构建配置,gradle镜像源)

AndroidStudio 2024 Win10/11最新安装配置 教程目的&#xff1a; (从安装到卸载) &#xff0c;针对Kotlin开发配置&#xff0c;gradle-8.2-src/bin下载慢&#xff0c;以及Kotlin构建慢的解决 好久没玩AS了,下载发现装个AS很麻烦,就觉得有必要出个教程了(就是记录一下:嘻嘻) 因…

Go 1.22中值得关注的几个变化

美国时间2024年2月6日&#xff0c;正当中国人民洋溢在即将迎来龙年春节的喜庆祥和的气氛中时&#xff0c;Eli Bendersky[1]代表Go团队在Go官博发文“Go 1.22 is released![2]”&#xff0c;正式向世界宣告了Go 1.22版本的发布&#xff01; 注&#xff1a;大家可以从Go官网下载G…

零基础学习8051单片机(十五)

本次先看书学习&#xff0c;并完成了课后习题&#xff0c;题目出自《单片机原理与接口技术》第五版—李清朝 答: &#xff08;1&#xff09;当 CPU正在处理某件事情的时候&#xff0c;外部发生的某一件事件请求 CPU 迅速去处理&#xff0c;于是&#xff0c;CPU暂时中止当前的工…

Android的ViewModel

前言 在Compose的学习中&#xff0c;我们在可组合函数中使用rememberSaveable​​​​​​​保存应用数据&#xff0c;但这可能意味着将逻辑保留在可组合函数中或附近。随着应用体量不断变大&#xff0c;您应将数据和逻辑从可组合函数中移出。 而在之前的应用架构学习中&…

计算机视觉基础知识(十四)--深度学习开源框架

深度学习框架 Caffetensorflow框架是深度学习的库;编程时需要import 应用优势 框架的出现降低了入门的门槛;不需要从复杂的神经网络开始编写代码;根据需要,使用已有的模型;模型的参数经过训练得到;可以在已有的模型基础上增加自己的layer;在顶端选择自己的分类器和优化算法;…

自然语言处理(NLP)—— 神经网络自然语言处理(2)实际应用

本篇文章的第一部分是关于探索词嵌入&#xff08;word embedding&#xff09;向量空间。词嵌入是一种语言模型和文本表示技术&#xff0c;其中单词或短语从词汇表被映射到向量的高维空间中。通过这种方式&#xff0c;可以通过计算向量之间的距离来捕捉单词之间的语义关系。 1.…

unity-firebase-Analytics分析库对接后数据不显示原因,及最终解决方法

自己记录一下unity对接了 FirebaseAnalytics.unitypackage&#xff08;基于 firebase_unity_sdk_10.3.0 版本&#xff09; 库后&#xff0c;数据不显示的原因及最终显示解决方法&#xff1a; 1. 代码问题&#xff08;有可能是代码写的问题&#xff0c;正确的代码如下&#xff…

Vue3之ref与reactive的基本使用

ref可以创建基本类型、对象类型的响应式数据 reactive只可以创建对象类型的响应式数据 接下来让我为大家介绍一下吧&#xff01; 在Vue3中&#xff0c;我们想让数据变成响应式数据&#xff0c;我们需要借助到ref与reactive 先为大家介绍一下ref如何使用还有什么注意点 我们需…

Elasticsearch:使用 ELSER v2 进行语义搜索

在我之前的文章 “Elasticsearch&#xff1a;使用 ELSER 进行语义搜索”&#xff0c;我们展示了如何使用 ELESR v1 来进行语义搜索。在使用 ELSER 之前&#xff0c;我们必须注意的是&#xff1a; 重要&#xff1a;虽然 ELSER V2 已正式发布&#xff0c;但 ELSER V1 仍处于 [预览…

政安晨:【示例演绎机器学习】(二)—— 神经网络的二分类问题示例 (影评分类)

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 政安晨的机器学习笔记 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让小伙伴们一起学习、交流进步&#xff01; 作者对人工智能…

BTC系列-系统学习铭文(一)-比特币上的NFT

Ordinals协议概况 开源项目: https://github.com/ordinals/ord铭文浏览器: https://Ordinals.com关于Ordinals的BIP: https://github.com/ordinals/ord/blob/master/bip.mediawiki序数理论手册: https://docs.ordinals.com/overview.html 所需的技术积累 Ordinals NFTs 是基…

SQL注入:网鼎杯2018-unfinish

目录 使用dirmap扫描 使用dirsearch扫描 使用acunetix扫描 爆破后端过滤的字符 绕过限制获取数据 这次的进行SQL注入的靶机是&#xff1a;BUUCTF在线评测 进入到主页面后发现是可以进行登录的&#xff0c;那么我们作为一个安全人员&#xff0c;那肯定不会按照常规的方式来…