Java服务端(二)- Java网络编程NIO

Java Nio 基础知识点

   并发:多线程抢占CPU,可能不同时执行,侧重于多个任务交替执行。(一个CPU)
   并行:线程可以不共享CPU,可每个线程一个CPU同时执行多个任务。(多个CPU)

   阻塞:相对于数据而言,需要一直等待数据准备好才能进行下一步操作(BIO,accept) 
   非阻塞:不管数据有没有准备好都可以往下执行下一步逻辑

   同步:相对于IO操作而言,在某个时间点只发生了一件事情,在IO读写操作的过程中不能再做别的事情 
   异步:相对于IO操作而言,在某个时间点发生了很多事情,在IO读写操作的瞬间还可以做别的事情

   BIO:同步阻塞  
   NIO:异步非阻塞

 

简介:

          异步 I/O 调用不但不会阻塞,并且可以注册对特定 I/O 事件诸如数据可读、新连接到来等等。

         同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程

 

核心对象:

 Nio Channel:

 

 Selector:

         Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。

        有了Selector,我们就可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。但是在多CPU的系统上也不能浪费,在这里我们只要知道使用Selector能够处理多个通道。  

 多个Channel注册到一个Selector上:

selector与channel的步骤:

     Seletor用来注册对各种 I/O 事件感兴趣的地方,当哪些事件发生时,selector这个对象就会告诉我们所发生的事件。

    1、 创建一个Selector对象

Selector selector = Selector.open();

    2 、将Channel注册到Selector

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

           通过调用 channel.register()方法来实现注册

            需要把注册的Channel设置为异步模式,这样方可实现工作。

            regsiter( ) 的第二个参数,是注册的Selector对Channel中对哪些事件感兴趣,事件的类型有:

                           Connect 、Accept 、Read 、 Write

          通道触发了一个事件意思是该事件已经 Ready(就绪)。所以,某个Channel成功连接到另一个服务器称为 Connect Ready。一个ServerSocketChannel准备好接收新连接称为 Accept Ready,一个有数据可读的通道可以说是 Read Ready,等待写数据的通道可以说是Write Ready。

          四个事件对应到SelectionKey中的四个常量:SelectionKey.OP_CONNECT 、 SelectionKey.OP_ACCEPT 、 SelectionKey.OP_READ 、 SelectionKey.OP_WRITE(注意:如果对对个事件感兴趣可用 or 进行连接)

    3、将Channel注册到Selector后返回的SelectionKey:

         调用 register( ) 的调用的一个返回值是一个SelectionKey。SelectionKey代表这个通道在此Seletor上的这个注册。当某个Selector通知你某个传入事件时,它是通过提供对应该事件的SelectionKey来进行的。SelectionKey还可以通过取消通道的注册。SelectionKey中包含如下属性:The interest set 、 The ready set 、 The Channel 、 The Selector 、 An attached object (optional)

 

注册后的各种操作:

Interest Set

       我们将Channel注册到Selector来监听感兴趣的事件,intereset set就是我们选择感性去的事件:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   

        我们可以使用And和SelectionKey中的常量做运算,从SelectionKey中找到我们感兴趣的事件。

 Read Set

        ready set 是通道已经准备就绪的操作的集合。在一次选Selection之后,你应该会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合:

int readySet = selectionKey.readyOps();

        可以用像检测interest集合那样的方法,来检测Channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel 和Selector
       我们可以通过SelectionKey获得Selector和注册的Channel:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

Attach 一个对象
       可以将一个对象或者更多信息attach 到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

还可以在用register()方法向Selector注册Channel的时候附加对象。如: 
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

 

Selector选择通道

   select:

       一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“Read Ready”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道:

int select():              // 阻塞到至少有一个通道在你注册的事件上就绪
int select(long timeout):  // select()一样,除了最长会阻塞timeout毫秒(参数)
int selectNow():           //不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

         select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道处于就绪状态。
 

selectedKeys():

        一旦调用了select()方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用selector.selectedKeys()方法返回的SelectionKey集合来获得就绪的Channel。请看演示方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

        当你通过Selector注册一个Channel时,channel.register()方法会返回一个SelectionKey对象,这个对象就代表了你注册的Channel。这些对象可以通过selectedKeys()方法获得。你可以通过迭代这些selected key来获得就绪的Channel,下面是演示代码:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) { 
	SelectionKey key = keyIterator.next();
	if(key.isAcceptable()) {
   		// a connection was accepted by a ServerSocketChannel.
	} else if (key.isConnectable()) {
    	// a connection was established with a remote server.
	} else if (key.isReadable()) {
    	// a channel is ready for reading
	} else if (key.isWritable()) {
    	// a channel is ready for writing
	}
    keyIterator.remove();
}

        这个循环遍历selected key的集合中的每个key,并对每个key做测试来判断哪个Channel已经就绪。

        请注意循环中最后的keyIterator.remove()方法。Selector对象并不会从自己的selected key集合中自动移除SelectionKey实例。我们需要在处理完一个Channel的时候自己去移除。当下一次Channel就绪的时候,Selector会再次把它添加到selected key集合中。

 

 

JAVA NIO server demo:

socket server端工作标准流程

  • 创建socket: 创建ServerSocketChannel,通过ServerSocketChannel.open()方法。
  • 绑定socket:ServerSocketChannel绑定端口,通过serverSocketChannel.bind()方法。
  • 前置准备: 创建selector对象,通过Selector.open()方法。
  • 前置准备: 注册Channel到selector并绑定事件,通过serverSocketChannel.register()。
  • 监听端口号: 通过listen()方法开始进入监听。
  • 处理事件: while循环中等待select操作返回区分连接还是数据进行不同处理。

NIO java代码实例:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {

    private Selector selector;

    public void initServer(int port) throws IOException {
        // 获得一个ServerSocketChannel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置通道为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 将该通道对应的ServerSocket绑定到port端口     
        serverSocketChannel.bind(new InetSocketAddress(port));
        // 获得一个通道管理器
        this.selector = Selector.open();
        // 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
        // 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void listen() throws IOException {
        System.out.println("服务端启动成功!");
        // 轮询访问selector
        while (true) {
            // 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
            selector.select();
            // 获得selector中选中的项的迭代器,选中的项为注册的事件
            Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey) ite.next();
                // 删除已选的key,以防重复处理
                ite.remove();
                
                if (key.isAcceptable()) {// 客户端请求连接事件
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    // 获得和客户端连接的通道
                    SocketChannel channel = server.accept();
                    // 设置成非阻塞
                    channel.configureBlocking(false);
 
                    // 在这里可以给客户端发送信息哦
                    channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息")
                            .getBytes("utf-8")));
                    // 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                    channel.register(this.selector, SelectionKey.OP_READ);
                    
                } else if (key.isReadable()) {// 获得了可读的事件
                    read(key);
                }
 
            }
 
        }
    }

    public void read(SelectionKey key) throws IOException {
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(512); 
        channel.read(buffer);
        byte[] data = buffer.array();
        String msg = new String(data).trim();
        System.out.println("服务端收到信息:" + msg);
        ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes("utf-8"));
        channel.write(outBuffer);// 将消息回送给客户端
    }

    public static void main(String[] args) throws IOException {
        NIOServer server = new NIOServer();
        server.initServer(8000);
        server.listen();
    }
}

 

 

  核心对象:

 1、ServerSocketChannel:

          简介:

                    ServerSocketChannel 是一个基于通道的socket监听器。它同我们所熟悉的 ServerSocket 执行相同的基本                任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。

          使用  ServerSocketChannel 的静态 open()  工厂方法创建一个新的 ServerSocketChannel对象。

          把  ServerSocketChannel  通道设置为非阻塞。

          给 ServerSocketChannel 对象绑定端口

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(localAddress);

 

          ServerSocketChannel阻塞与非阻塞

       非阻塞:

public class ServerSocketChannelApp {  
    private static final String MSG = "hello, I must be going \n";  
    public static void main(String[] args) throws Exception {  
        ServerSocketChannel ssc = ServerSocketChannel.open();  
        ServerSocket ss = ssc.socket();  
        ss.bind(new InetSocketAddress(8888));  
        ssc.configureBlocking(false);  
        ByteBuffer buffer = ByteBuffer.wrap(MSG.getBytes());  
  
        while (true) {  
            System.out.println("wait for connection ……");  
            SocketChannel sc = ssc.accept();  
            if (sc == null) {  
                // no connections, snooze a while ...  
                Thread.sleep(1000);  
            } else {  
                System.out.println("Incoming connection from " + sc.socket().getRemoteSocketAddress());  
                buffer.rewind();  
                //write msg to client  
                sc.write(buffer);  
                sc.close();  
            }  
        }  
    }  
}  

     阻塞:

public class ServerSocketApp {  
    public static void main(String[] args) throws Exception {  
        ServerSocket ss = new ServerSocket(8989);  
        ss.accept();  
        System.out.println(1);  
    }  
}   

 

问题: 

        1、一个NIO是不是只有一个selector?

            不是,一个系统可以有多个selector。

        2、selector 是不是只能注册一个ServerSocketChannel?

           可以注册多个

 

对比:

     传统SocketIO:

 

       NIO:

         一个线程(服务员)+selector 既要看大门又要提供客人的点菜服务。

        注意:一个线程加一个 selector 才具有先多个客人服务的能力

   

    nettyIO:

           创建多个线程(服务员),服务员A只负责A区域,服务员B只负责B区域等等,以及还有一个服务员负责看大门。

 

 

 

 

 

 

参考博客:

https://www.jianshu.com/p/7b507069debb

https://blog.csdn.net/suifeng3051/article/details/48441629      很好的一篇讲原理的博客

 

https://www.cnblogs.com/wuyida/p/6300966.html

https://segmentfault.com/a/1190000012651999               讲解 ServerSocketChannel的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值