学习NIO小结

来源:互联网 发布:本体网络 编辑:程序博客网 时间:2024/06/15 19:24

NIO(同步非阻塞)在jdk1.4时推出,和传统的IO(同步阻塞)比较有着新的思想,在网上学习和整理知识点时知道学习NIO可分为3个部分

1.channel:通道,数据的读取和写入都可以通过它来完成。

2.selector :选择器,用于选择注册在通道中的已发生的事件。

3.ByteBuffer:一个新的缓冲区,通过这个类我们可以在通道中做写操作和读操作;主要是通过position、limit、和capacity指针的位置来进行操作。

对于以上3部分我就不做过多的解释了,感兴趣的同学可以找找其他文章,他们解释已经很详细了。


下面是我自己参照网上资料做的小练习,有比较详细的注释:

NIO的服务端,大概分一下几个步骤:

a.创建一个选择器selector,打开通道channel。

b.设置此通道为非阻塞的,并绑定监听端口。

c.因为一开始需要和客服端连接才能做后面的操作,我们可以只注册accept事件,而这个事件在客服端只在连接时触发,所以可以在accept的处理方法中再注册其他的事件。

d.使用selector的select方法,这个方法会选择那些数据已经准备好的事件,(如果没有任何事件中的数据准备好那他其实是阻塞的),之后就是处理对应的事件的逻辑了

e.关闭通道。

这里需要注意的是通道中事件的更换,这里有一个问题,如果通道中你没有注册任何事件,那么会造成死循环。剩下的注释在代码中可参照。

public class MyThreadNIOEchoServer {private Selector selector;private ExecutorService tp = Executors.newCachedThreadPool();//private volatile AtomicInteger count = new AtomicInteger(1);private void startServer() throws Exception {//打开一个选择器selector = Selector.open();//打开一个服务端的socket通道 ServerSocketChannel ssc = ServerSocketChannel.open();//设置为非阻塞ssc.configureBlocking(false);//为服务端绑定端口InetSocketAddress isa = new InetSocketAddress(8000);ssc.socket().bind(isa);// 注册感兴趣的事件,此处对accpet事件感兴趣ssc.register(selector, SelectionKey.OP_ACCEPT);for (;;) {selector.select();//这里会阻塞等到兴趣事件的到达//轮询选择器中的到达的事件Set<SelectionKey> readyKeys = selector.selectedKeys();Iterator<SelectionKey> i = readyKeys.iterator();//已经准备好的事件并处理while (i.hasNext()) {SelectionKey sk = i.next();//注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除i.remove();if (sk.isAcceptable()) {//进入accept事件处理方法doAccept(sk);} else if (sk.isValid() && sk.isReadable()) {doRead(sk);//进入read事件处理方法} else if (sk.isValid() && sk.isWritable()) {doWrite(sk);//进入write事件处理方法}}}}/** * 向通道中写数据 * @param sk */private void doWrite(SelectionKey sk) {SocketChannel channel = (SocketChannel) sk.channel();ByteBuffer bb = (ByteBuffer) sk.attachment();//System.out.println("需要输出的:"+new String(bb.array()));//这里的bb.array()方法会返回缓存中的所有数据,并不是可读部分的数据try {//"这里会把全部的数据返回,所以如果当次的内容小于上一次内容时,最后的数据没有覆盖会依然输出"int len = channel.write(bb);if (len == -1) {disconnect(sk);return;}} catch (Exception e) {e.printStackTrace();disconnect(sk);}sk.interestOps(SelectionKey.OP_READ);//更换到read事件,不然会一直是write事件,造成死循环}/** * 读取通道中的数据 * @param sk */private void doRead(SelectionKey sk) {SocketChannel channel = (SocketChannel) sk.channel();ByteBuffer bb = ByteBuffer.allocate(1024);StringBuffer buf = new StringBuffer();int len = 0;try {while(true){bb.clear();//重置bytebuffer,并写入数据len = channel.read(bb);if(len<=0){break;}else{buf.append(new String(bb.array(),0,len,"UTF-8"));}}System.out.println("客服端说:"+new String(buf.toString().getBytes(),"UTF-8"));if (len < 0) {disconnect(sk);return;}} catch (Exception e) {disconnect(sk);return;}//sk.interestOps(SelectionKey.OP_READ); //}private void disconnect(SelectionKey sk) {try {sk.channel().close();} catch (Exception e) {e.printStackTrace();}}private void doAccept(SelectionKey sk) {//SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。System.out.println("开始接受accept");ServerSocketChannel server = (ServerSocketChannel) sk.channel();SocketChannel clientChannel;try {clientChannel = server.accept();clientChannel.configureBlocking(false);//在有新的客服端链接时注册读的通道,并在注册兴趣通道SelectionKey clientKey = clientChannel.register(selector,SelectionKey.OP_READ);ByteBuffer byteBuf = ByteBuffer.allocate(1024);//这里设置的合理的字节长度,不然内容太多会有BufferOverflowException异常clientKey.attach(byteBuf);//增加附加信息,这样就能方便的识别某个给定的通道InetAddress clientAddress = clientChannel.socket().getInetAddress();byteBuf.put(("Accepted connection from "+ clientAddress.getHostAddress()).getBytes());byteBuf.flip();//改变position、limit指针,表示哪里是数据的可读部分clientChannel.write(byteBuf);clientKey.interestOps(SelectionKey.OP_WRITE);//需要激活dowrite事件selector.wakeup();//通知唤醒selector.selector()方法//如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。tp.execute(new Runnable() {@Overridepublic void run() {while(true){ByteBuffer bb = (ByteBuffer) clientKey.attachment();if(bb!=null){break;}}ByteBuffer bb = (ByteBuffer) clientKey.attachment();bb.put("".getBytes());while(true){try {//System.out.println(Thread.currentThread().getName());Scanner scanner = new Scanner(System.in);String nextLine = scanner.nextLine();bb.clear();//重置position和limit位置,这样就可以写入数据bb.put(nextLine.getBytes());bb.flip();//切换到读取模式(把limit的位置指到数据的最后一位,告诉读取者可以读到这里limit)//如果这里没有马上写入数据,,那么我们需要clientKey.interestOps(SelectionKey.OP_WRITE);//需要激活dowrite事件//((SocketChannel)clientKey.channel()).write(bb);clientKey.interestOps(SelectionKey.OP_WRITE);//需要激活dowrite事件selector.wakeup();//通知唤醒selector.selector()方法} catch (Exception e) {e.printStackTrace();break;}}}});} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {MyThreadNIOEchoServer echoServer = new MyThreadNIOEchoServer();try {echoServer.startServer();} catch (Exception e) {e.printStackTrace();}}}
NIO的客服端,这个类中和上一个服务端是差不多的步骤,下面是代码:这里需要注意的是,通道其实是可以写入的,我们其实不需要进行注册,如果注册了处理不好会造成死循环,像上面的服务端一样,注册了WRITE,如果没有在doWrite方法中注册了READ事件,那么就是会死循环。

public class ClientSocketChannel {public static void main(String[] args) {startChannelConection();}public static void startChannelConection() {try {//创建一个selector(通道管理器)Selector selector = Selector.open();//打开通道SocketChannel open = SocketChannel.open();//设置为非阻塞open.configureBlocking(false);//配置链接地址和端口,在调用open.finishConnect();才能完成链接open.connect(new InetSocketAddress("127.0.0.1",8000));//在open通道上注册感兴趣的事件open.register(selector, SelectionKey.OP_CONNECT);//开始循环检测是否有感兴趣的事件发生for(;;){//这个方法会一直阻塞到有感兴趣的事件发生(使用wateup会唤醒这个方法)selector.select();//System.out.println("有事件发生个数"+selector.selectedKeys().size());//得到感兴趣的事件的iteratorIterator<SelectionKey> iterator = selector.selectedKeys().iterator();//迭代这个iterator找到对应事件的处理方法while(iterator.hasNext()){SelectionKey next = iterator.next();//手动移除当前对象,以免重复处理iterator.remove();//开始匹配对应的事件if(next.isValid()){if(next.isConnectable()){System.out.println("开始连接********");SocketChannel channel = (SocketChannel) next.channel();channel.finishConnect();System.out.println("连接完成********"); //在这里可以给服务端发送信息                      channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));                     new Thread(new Runnable() {@Overridepublic void run() {while(true){try {Scanner scanner = new Scanner(System.in);String nextLine = scanner.nextLine();channel.write(ByteBuffer.wrap(nextLine.getBytes())); //next.interestOps(SelectionKey.OP_READ);selector.wakeup();//通知唤醒selector.selector()方法} catch (Exception e) {break;}}}}).start();                next.interestOps(SelectionKey.OP_READ);}else if(next.isValid()&&next.isReadable()){try {SocketChannel channel2 = (SocketChannel) next.channel();ByteBuffer dsts = ByteBuffer.allocate(1024);//如果allocate的值比较小的话为了避免乱码 可以先把所以byte缓存下来之后再使用String的构造方法一次编码为UTF-8,//但是这样有缓存的多余操作//List<Byte> bytearr = new ArrayList<Byte>(); StringBuffer buffer = new StringBuffer();while(true){dsts.clear();int len = 0;len =channel2.read(dsts);if(len<=0){break;}else{buffer.append(new String(dsts.array(), 0, len,"UTF-8"));}}System.out.println(new String(buffer.toString().getBytes(), "UTF-8"));} catch (Exception e) {e.printStackTrace();return;}//next.interestOps(SelectionKey.OP_READ);}else if(next.isWritable()){//因为我们没有注册WRITE事件,所以不会输出下面的语句,System.out.println("dowrite**********");//next.interestOps(SelectionKey.OP_READ);}}}}} catch (Exception e) {e.printStackTrace();}}}

下面是服务端和客服端运行结果:

先运行服务端,在运行客服端,



这个简单的通信其实存在很大问题,比如是否有粘包,半包等问题,所以写出一个高可用的NIO是非常不易的,但是netty框架似乎已经帮我们解决了这些问题。大家有兴趣可以进行了解。



0 0
原创粉丝点击