目录
之前手写了同步阻塞的socket通信聊天室:Java实现ChatRoom_zhangm2020的博客-CSDN博客
NIO是怎么实现的?那跟同步阻塞有什么区别呢?
三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动。Selector选择器用于监听多个通道的事件(连接请求,数据到达等)因此使用单个线程就可以监听多个客户端通道
场景:一个服务端一个客户端(不涉及Selector)

- public class NIOServer {
- public static void main(String[] args) throws IOException, InterruptedException {
- //1. 打开一个服务端通道
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- //2. 绑定对应的端口号
- serverSocketChannel.bind(new InetSocketAddress(9999));
- //3. 通道默认是阻塞的,需要设置为非阻塞
- serverSocketChannel.configureBlocking(false);
- System.out.println("服务端启动成功");
- while (true) {
- //4. 检查是否有客户端连接 有客户端连接会返回对应的通道
- SocketChannel socketChannel = serverSocketChannel.accept();
- if (null == socketChannel) {
- System.out.println("没有客户连接,非阻塞我去干其他事情");
- Thread.sleep(2000);
- continue;
- }
- //5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
- ByteBuffer allocate = ByteBuffer.allocate(1024);
- int read = socketChannel.read(allocate);
- String message = new String(allocate.array(), 0, read);
- System.out.println("读到客户端消息:" + message);
- //6. 给客户端回写数据
- socketChannel.write(ByteBuffer.wrap("我是服务端,我收到了你的消息".getBytes()));
- //7. 释放资源
- socketChannel.close();
- }
- }
- }
注:java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)返回值
//正数: 表示本次读到的有效字节个数. //0 : 表示本次没有读到有效字节. //-1 : 表示读到了末尾
该方法是阻塞的,如果客户端连接后一直没有发数据,那么这个方法会阻塞等待客户端信息,所以会造成服务端资源的浪费,场景2会涉及到Selector选择器,可以避免这个问题
- public class NIOClient {
- public static void main(String[] args) throws IOException {
- //1. 打开通道
- SocketChannel socketChannel = SocketChannel.open();
- //2. 设置连接IP和端口号
- socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
- //3. 写出数据
- socketChannel.write(ByteBuffer.wrap("你好,我是客户端".getBytes()));
- //4. 读取服务器写回的数据
- ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
- int read = socketChannel.read(byteBuffer);
- if (read > 0) {
- System.out.println("获取到服务端消息:" + new String(byteBuffer.array(), 0, read));
- //5. 释放资源
- socketChannel.close();
- }
- }
- }


场景:一个服务端多个客户端

- public class NIOSelectorServer {
- public static void main(String[] args) throws IOException {
- //1. 打开一个服务端通道
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- //2. 绑定对应的端口号
- serverSocketChannel.bind(new InetSocketAddress(9999));
- //3. 通道默认是阻塞的,需要设置为非阻塞
- serverSocketChannel.configureBlocking(false);
- //4. 创建选择器
- Selector selector = Selector.open();
- //5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
- serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
- System.out.println("服务端启动成功");
- while (true) {
- //6. 检查选择器是否有事件
- //返回值:事件个数
- int select = selector.select(2000);
- if (0 == select) {
- System.out.println("没有事件");
- continue;
- }
- //7. 获取事件集合
- Set
selectionKeys = selector.selectedKeys(); - Iterator
iterator = selectionKeys.iterator(); - while (iterator.hasNext()) {
- SelectionKey selectionKey = iterator.next();
- //8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
- if (selectionKey.isAcceptable()) {
- System.out.println("获取到连接继续事件");
- //9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
- SocketChannel socketChannel = serverSocketChannel.accept();
- System.out.println("客户端已连接" + socketChannel);
- //通道必须设置为非阻塞,selector是轮询监听,所以不能被阻塞到某个通道上
- socketChannel.configureBlocking(false);
- socketChannel.register(selector, SelectionKey.OP_READ);
- }
- //10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
- if (selectionKey.isReadable()) {
- System.out.println("获取到读就绪事件");
- //11. 得到客户端通道,读取数据到缓冲区
- //获取selector监听到的通道事件
- SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
- ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
- int read = socketChannel.read(byteBuffer);
- if (read > 0) {
- System.out.println("读到客户端消息:" + new String(byteBuffer.array(), 0, read));
- //12. 给客户端回写数据
- socketChannel.write(ByteBuffer.wrap("我是服务端,我收到了你的消息".getBytes()));
- socketChannel.close();
- }
- }
- //13. 从集合中删除对应的事件, 因为防止二次处理
- iterator.remove();
- }
- }
- }
- }
java.nio.channels.Selector#select(long)返回值:事件个数
客户端代码不变
开启两个客户端:客户端0,客户端1
