Netty: NIO网络编程

本文详细介绍了Java NIO的核心内容,包括NIO的原理,其三大核心部分通道、缓冲区和选择器的作用及工作机制。阐述了Buffer的原理、实现类和使用方法,对比了NIO与BIO。还介绍了Channel和Selector的相关知识,并给出服务端与客户端通信、群聊系统的实战案例。

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

一、NIO介绍

NIO介绍

二、NIO原理

  • NIO有三大核心部分: 通道(Channel)、缓冲区(Buffer)和选择器(Selector)。Channel是对原I/O体系中流的模拟,可以通过Channel完成数据的读/写操作;Buffer则用于存储数据,它是NIO与普通I/O的主要区别之一;而Selector则用于监听多个Channel的状态,以实现多路复用。

  • 在NIO的工作过程中,数据首先从Channel读取到Buffer中,或者从Buffer写入到Channel中。这个过程是异步的,因此不会阻塞线程。同时,Selector会不断地轮询注册的Channel,查看是否有已经就绪的I/O操作(例如读或写),如果有,就通知相应的线程进行处理。

  • Java NIO的非阻塞模式,是一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以知道数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不像之前的阻塞 IO那样,非得分配10000个线程。

  • HTT2.0使用了多路复用的技术,做到同一个链接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

总的来说,NIO通过非阻塞I/O操作和事件驱动机制,提高了系统的并发性能和响应速度,使得处理大量连接和I/O操作变得更加高效。因此,NIO已经被越来越多地应用到大型应用服务器中,成为解决高并发与大量连接、I/O处理问题的有效方式。

三、Buffer

1、Buffer原理介绍

Java NIO的Buffer类是一个用于特定基本数据类型的容器。它是一个对象,包含一些要写入或者要读出的数据。在NIO中,所有的数据都是用Buffer处理的。在数据读写之前,需要先放入Buffer,或者从Buffer中取出。Buffer对象中提供了一组方法,可以轻松的使用内存块,此外缓冲区对象那中内置了一些机制,能够追踪和记录缓冲区的状态变化情况。

以下是Java NIO Buffer的基本使用步骤:

  • 分配Buffer:首先,你需要通过调用allocate()方法(对于ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer等)在JVM的堆中分配一个新的Buffer。例如,以下代码分配一个新的ByteBuffer:
ByteBuffer buffer = ByteBuffer.allocate(48);
  • 写入数据到Buffer:调用Buffer类的put()方法将数据写入Buffer。例如:
buffer.put((byte) 10);  
buffer.put((byte) 20);
  • 切换Buffer的读写模式:在写数据到Buffer后,需要调用flip()方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。换句话说,limit现在表示的是之前写进Buffer的元素的个数。现在Buffer准备好从写模式切换到读模式:
buffer.flip();
  • 从Buffer中读取数据:从Buffer中读取数据,你可以使用get()方法。读取数据直到hasRemaining()返回false。这表示已经到达Buffer的末尾:
while(buffer.hasRemaining()){  
    byte b = buffer.get();  
    // do something with b  
}
  • 清除Buffer:当读完Buffer中的数据后,需要清除它以便下次使用。有两种方式可以清除Buffer:调用clear()或compact()方法。clear()方法会清除整个Buffer。compact()方法只会清除已经读过的数据。任何未读的数据都被移到Buffer的起始处,新写入的数据将放到Buffer的未读数据后面。
buffer.clear(); // 清除整个Buffer

或者

buffer.compact(); // 只清除已读数据

注意,在使用Buffer时,要时刻注意Buffer的容量(capacity),位置(position)和限制(limit)。容量是Buffer能够容纳的数据元素的最大数量;位置是下一个要被读或写的元素的索引;限制是第一个不应该被读或写的元素的索引。这些值可以通过调用Buffer的相应方法来获取或设置。

以上就是Java NIO Buffer的基本使用方法。通过合理地使用Buffer,你可以有效地处理大量的数据读写操作,提高程序的性能。

2、Buffer实现类

  • 在NIO中,Buffer是一个顶层父类,它是一个抽象类,类的层级关系下图:
    在这里插入图片描述

    • ByteBuffer:存储字节数据到缓冲区。
      在这里插入图片描述

    • ShortBuffer:存储字符串数据到缓冲区。

    • CharBuffer:存储字符数据到缓冲区。

    • IntBuffer:存储整数数据到缓冲区。

    • LongBuffer:存储长整型数据到缓冲区。

    • DoubleBuffer:存储小数到缓冲区。

    • FloatBuffer:存储小数到缓冲区。

  • Buffer类定义了所有缓冲区都具有的四个属性,来提供其所包含的数据元素的信息:
    | 属性 |描述 |
    |–|–|
    |capacity | 容量,即可以容纳的最大数据量;在缓冲区创建是被设定并且不能改变 |
    |Limit| 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行操作。且极限是可以修改的|
    |position| 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变,为下次读写做准备|
    |mark| 标记|

  • Buffer类相关的方法:

//JDK1.4,引入的api
public final int capacity(); 	//返回此缓冲区的容量
public final int position();	//返回此缓冲区的位置
public final Buffer position(int newPosition);	//设置此缓冲区的位置
public final int limit();	//返回此缓冲区的限制
public final Buffer limit(int newLimit);	//设置此缓冲区的限制
public final Buffer mark();		//在此缓冲区的位置设置标记
public final Buffer reset();	//将此缓冲区的位置重置为以前标记的位置
public final clear();	//清除此缓冲区,即将各个标记恢复到初始化状态,但是数据并没有真正擦除。
public final Buffer flip();		//反转此缓冲区
public final Buffer rewind();	//重绕此缓冲区
public final int remaining();	//返回当前位置与限制之间的元素数
public final boolean hasRemaining();	//告知当前位置和限制之间是否有元素
public final boolean isReadOnly();		//告知此缓冲区是否为只读缓冲区

//JDK1.6 引入的api
public abstract boolean hasArray();		//告知此缓冲区是否具有访问的底层实现数组
public abstract Object array();			//返回此缓冲区的底层实现数组
public abstract int arrayOffset();		//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abastract boolean isDirect();	//告知此缓冲区是否为直接缓冲区

3、示例

package test.nio;

import java.nio.IntBuffer;
public class BasicBuffer {
    public static void main(String[] args) {
        //创建一个buffer, 容量大小为5,即可以存放5个int
        IntBuffer buffer = IntBuffer.allocate(5);

        //向buffer中存放数据
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(i * 2);
        }

        //从buffer中读取数据
        buffer.flip();
        while(buffer.hasRemaining()) {
            int i = buffer.get();
            System.out.println(i);
        }
    }
}

运行结果:

0
2
4
6
8

Process finished with exit code 0

4、NIO和BIO的比较

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。
  • BIO是阻塞的,NIO则是非阻塞的。
  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓存区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

四、Channel

1、介绍

  • Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
    在这里插入图片描述

  • NIO的通道类似于流,有如下区别:

    • 通道可以同时进行读写,而流只能读或者只能写。
    • 通道可以实现异步读写数据。
    • 通道可以从缓冲读数据,也可以写数据到缓冲:
      在这里插入图片描述
    • BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作。
  • BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。

  • Channel在NIO中是一个接口

public interface Channel extends Closeable
  • 常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel(ServerSocketChannel类似ServerSocket、SocketChannel类似Socket)。
  • FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel

2、FileChannel介绍

  • FileChannel主要用来对本地文件进行IO操作,常见的方法有:

    • public int read(ByteBuffer dst),从通道读取数据并放到缓冲区中。
    • public int write(ByteBuffer src),把缓冲区的数据写到通道中。
    • public long transferForm(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道。
    • public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道。
  • 通过channel向文件中写数据。
    blog.csdnimg.cn/direct/a4b5dc74278c40d88a1f791ab936b1c9.png)

package test.nio;

import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel {
    public static void main(String[] args) {
        String str = "hello world 今天是星期日";
        try {
            //创建一个输出流->channel
            FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

            //通过fileOutputStream获取对应的FileChannel
            //这个fileChannel真实类型是FileChannelImpl
            FileChannel channel = fileOutputStream.getChannel();

            //创建一个缓冲区ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

            //将str放入byteBuffer
            byteBuffer.put(str.getBytes());

            //对byteBuffer进行flip
            byteBuffer.flip();

            //将byteBuffer,数据写入到fileChannel
            channel.write(byteBuffer);
            fileOutputStream.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
}

  • 通过Channel读取本地文件数据。
    在这里插入图片描述
package test.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel01 {
    public static void main(String[] args) throws IOException {
        File file = new File("d:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileChannel channel = fileInputStream.getChannel();

        //创建一个缓冲区ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());

        //将通道的数据读入到Buffer
        int read = channel.read(byteBuffer);

        //将byteBuffer的字节数据转成String
        byteBuffer.flip();
        String str = new String(byteBuffer.array());
        System.out.println(str);
        fileInputStream.close();
    }
}

结果展示:

hello world 今天是星期日

Process finished with exit code 0
  • 使用一个Buffer完成文件的读取
package test.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NIOFileChannel02 {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel channel = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file02.txt");
        FileChannel channel1 = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        while (Boolean.TRUE) {
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            System.out.println("read = " + read);
            if (read == -1) {
                break;
            }

            byteBuffer.flip();
            channel1.write(byteBuffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }
}

  • 拷贝文件transferFrom方法
    要求:使用FileChannel(通道)和方法transferFrom,完成文件的拷贝。
package test.nio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class NIOFileChannel03 {
    public static void main(String[] args) throws IOException {
        //创建相关的流
        FileInputStream fileInputStream = new FileInputStream("d:\\star.png");
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\ab.png");

        //获取各个流对应的fileChannel
        FileChannel channel = fileInputStream.getChannel();
        FileChannel channel1 = fileOutputStream.getChannel();

        //使用transform完成拷贝
        channel1.transferFrom(channel, 0, channel.size());

        //关闭相关通道和流
        channel.close();
        channel1.close();

        fileInputStream.close();
        fileOutputStream.close();
    }
}

3、Buffer和Channel的注意事项

  • ByteBuffer支持类型化的put和get,put放入什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常。
package test.nio;

import java.nio.ByteBuffer;
public class NIOByteBufferPutGet {
    public static void main(String[] args) {
        //创建一个buffer
        ByteBuffer allocate = ByteBuffer.allocate(64);

        //类型化方式放入数据
        allocate.putInt(100);
        allocate.putLong(9);
        allocate.putChar('a');
        allocate.putShort((short)4);

        //取出
        allocate.flip();

        System.out.println(allocate.getInt());
        System.out.println(allocate.getLong());
        System.out.println(allocate.getChar());
        System.out.println(allocate.getLong());
    }
}

  • 可以将一个普通Buffer转成只读Buffer。
package test.nio;

import java.nio.ByteBuffer;

public class ReadOnlyBuffer {
    public static void main(String[] args) {
        //创建一个buffer
        ByteBuffer allocate = ByteBuffer.allocate(64);

        System.out.println("capacity = " + allocate.capacity());
        for (int i = 0; i < allocate.capacity(); i++) {
            allocate.put((byte)i);
        }

        //读取
        allocate.flip();

        //得到一个只读的Buffer
        ByteBuffer readOnlyBuffer = allocate.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        //读取
        while(readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }
    }
}

  • NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。
  • NIO支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gattering。
package test.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

/**
 * Scattering: 将数据写入到buffer时,可以采用buffer数组,依次写入。
 * Gathering:从buffer读取数据时,可以采用buffer数组,依次读。
 **/
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws IOException {

        //使用ServerSocketChannel和SocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);

        //绑定端口到Socket,并启动
        serverSocketChannel.socket().bind(inetSocketAddress);

        //创建buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);

        int messageLength = 8; //从客户端接收8个字节
        //等待客户端连接(telnet)
        SocketChannel socketChannel = serverSocketChannel.accept();

        //循环的读取
        while (Boolean.TRUE) {
            int byteRead = 0;
            while(byteRead < messageLength) {
                long read = socketChannel.read(byteBuffers);
                byteRead += read; //累计读取的字节数
                System.out.println("byteRead = " + byteRead);
                //使用流打印,看看当前的这个buffer的position和limit
                Arrays.asList(byteBuffers).stream().map(buffer -> "position = " + buffer.position() + ", limit = " + buffer.limit()).forEach(System.out::println);
            }

            //将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());

            //将数据读出显示到客户端
            long byteWrite = 0;
            while(byteWrite < messageLength) {
                long l = socketChannel.write(byteBuffers);
                byteWrite += l;
            }

            //将所有的buffer进行clear
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());

            System.out.println("byteRead:= " + byteRead + " byteWrite= " + byteWrite + " messageLenght= " + messageLength);
        }
    }
}

五、Selector

1、介绍

  • Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)

  • Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
    在这里插入图片描述

  • 只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。

  • 避免了多线程之间的上下文切换导致的开销。

2、特点

  • Netty的IO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
  • 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  • 线程通道将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
  • 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起。
  • 一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

3、Selector类相关方法

  • Selector类是一个抽象类,常用的方法如下:
public abstract class Selector implements Closeable{
	public static Selector open();	//得到一个选择器对象
	public int select();	//阻塞
	public int select(long timeout);	//监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
	public Set<SelectionKey> selectedKeys();	//从内部集合中得到所有的SelectionKey
}
  • NIO非阻塞网络编程原理分析图

在这里插入图片描述

  • 当客户端连接时,会通过ServerSocketChannel得到SocketChannel。
  • 将SocketChannel注册到Selector上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel。
  • 注册后返回一个SelectionKey,会和该Selector关联。
  • Selector进行监听select方法,返回有事件发生的通道的个数。
  • 进一步得到各个SelectionKey(有事件发生)。
  • 在通过SelectionKey反向获取SocketChannel channel().
  • 通过得到的channel完成业务处理。

4、常用API

(1)SelectionKey

SelectionKey,表示 Selector 和网络通道的注册关系,共四种:

  • int OP_ACCEPT:有新的网络连接可以 accept,值为 16。
  • int OP_CONNECT:代表连接已经建立,值为 8。
  • int OP_READ:代表读操作,值为 1。
  • int OP_WRITE:代表写操作,值为 4。

(2)ServerSocketChannel

ServerSocketChannel 在服务器端监听新的客户端 Socket 连接,负责监听,不负责实际的读写操作。

相关方法如下:


public static ServerSocketChannel open();	//得到一个ServerSocketChannel通道
public final ServerSocketChannel bind(SocketAddress local);	//设置服务器端口号
public final SelectableChannel configureBlocking(boolean block);	//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
public SocketChannel accept();	//接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops);	//注册一个选择器并设置监听事件

(3)SocketChannel

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

相关方法如下:


public static SocketChannel open();	//得到一个SocketChannel通道
public final SelectableChannel configureBlocking(Boolean block);	//设置阻塞或非阻塞模式,取值false表示采用非阻塞模式
public booleanconnect(SocketAddress remote);	//连接服务器
public boolean finshConnect();	//如果上面的方法连接失败,接下来就要通过该方法完成连接操作。
public int wirte(ByteBuffer src);	//往通道里写数据
public int read(ByteBuffer dts);	//从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att);	注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void clise();	//关闭通道

5、实战

(1)实现服务端和客户端通信

编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)

  • 服务端代码
package test.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //得到一个selector对象
        Selector selector = Selector.open();

        //绑定一个端口6666,在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));

        //设置为非阻塞
        serverSocketChannel.configureBlocking(Boolean.FALSE);

        //把 serverSocketChannel注册到selector,关心事件为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //循环等待客户端连接
        while(Boolean.TRUE) {
            //这里我们等待1秒,如果没有事件发生,返回
            if (selector.select(1000) == 0) {
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }

            //如果返回的>0,就获取到相关的selectorkey集合
            //1.如果返回的>0,表示已经获取到关注的事件
            //2.selector.selectedKeys()返回关注事件的集合
            //3.通过selectionKeys反向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            //遍历Set<SelectionKey>,使用迭代器遍历
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                //获取到SelectionKey
                SelectionKey key = iterator.next();

                //根据key对应的通道发生的事件做不同的处理
                if (key.isAcceptable()) { //如果是OP_ACCEPT, 有新的客户端连接
                    //该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(Boolean.FALSE);
                    //将SocketChannel注册到selector, 关注事件为OP_READ,同时给socketChannel关联一个Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));


                }
                if (key.isReadable()) { //发生了OP_READ事件
                    //通过key 反向获取到对应的channel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //获取到读channel关联的Buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客户端 " + new String(buffer.array()));
                }

                //手动从集合中移除当前的selectionKey,防止重复操作
                iterator.remove();
            }
        }
    }
}

  • 对操作系统有一定了解的同学,就会大概知道这里监听的是一个Accept通道。这个通道的
    作用就是监听,实际建立连接了还会有一个通道。

  • 简单说一下为什么。因为客户端发请求的时候,服务器这边是肯定要先有一个监听通道,
    监听某个端口是否有客户端要建立链接,如果有客户端想要建立链接,那么会再创建一个和
    客户端真正通信的通道。

  • 如果有其它客户端还想要建立链接,这个Accept监听端口监听到了,就会再创建几个真正
    的通信通道。

  • 也就是Server的一个端口可以建立多个TCP连接,因为IP层协议通过
    目标地址+端口+源地址+源端口四个信息识别一个上下文。

  • 客户端代码

package test.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
    public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(Boolean.FALSE);
        //提供服务器端的IP和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他的工作");
            }
        }
        //如果连接成功,就发送数据
        String str = "hello world";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        // 发送数据,将buffer数据写入channel
        socketChannel.write(buffer);
        System.in.read();
    }
}

(2)群聊系统

实例要求:

  • 编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)。
  • 实现多人群聊。
  • 服务器端:可以监测用户上线,离线,并实现消息转发功能。
  • 客户端:通过 Channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)。

六、Selector、Channel和Buffer关系

  • 每个channel都会对应一个Buffer。
  • Selector对应一个线程,一个线程对应多个channel(连接)。
  • 程序切换到那个Channel是由事件决定的,Event就是一个总要的概念。
  • Selector会根据不同的事件,在各个通道上切换。
  • Buffer就是一个内存块,底层是一个数组。
  • 数据的读取写入是通过Buffer。BIO中要么是输入流要么是输出流,不能双向。但是NIO的Buffer是可以读也可以写的,需要调用flip方法切换。
  • channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。
    在这里插入图片描述
package test.nio;

import java.nio.IntBuffer;
public class BasicBuffer {
    public static void main(String[] args) {
        //创建一个buffer, 容量大小为5,即可以存放5个int
        IntBuffer buffer = IntBuffer.allocate(5);

       for (int j = 0; j < 5; j ++) {
           //向buffer中存放数据
           for (int i = 0; i < buffer.capacity(); i++) {
               buffer.put(i * 2);
           }

           //从buffer中读取数据
           buffer.flip();
           while(buffer.hasRemaining()) {
               int i = buffer.get();
               System.out.println(i);
           }
           System.out.println("==============================================================================");
           buffer.flip();
       }
    }
}

输出结果:

0
2
4
6
8
==============================================================================
0
2
4
6
8
==============================================================================
0
2
4
6
8
==============================================================================
0
2
4
6
8
==============================================================================
0
2
4
6
8
==============================================================================

Process finished with exit code 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

玉成226

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值