NIO编程中,需要知道的的重要概念 通道(Channel,封装的Socket)、缓冲区(ByteBuffer等类,用于服务端和客户端数据传递)、选择器(Selector,用于处理Channel)。NIO处理时异步非阻塞的IO,NIO(只有Selector的一个线程,不断轮询处理Channel)效率比BIO高很多(BIO是一个请求一个线程)。NIO中常用的框架有Netty和Mina。

基本概念

NIO网络编程中,重要的类有,
ByteBuffer、(缓冲区)
Channel(ServerSocketChannel/SocketChannel) 通道,双向的
Selector(多路复用器,选择器)

1、缓冲区: 在操作系统中缓冲区是为了解决CPU的计算速度和外设输入输出速度不匹配的问题,因为外设太慢了,如果没有缓冲区,那么CPU在外设输入的时候就要一直等着,就会造成CPU处理效率的低下,引入了缓冲之后,外设直接把数据放到缓冲中,当数据传输完成之后,给CPU一个中断信号,通知CPU:“我的数据传完了,你自己从缓冲里面去取吧”。如果是输出也是一样的道理。

2、通道: 那么通道用来做什么呢?其实从他的名字就可以看出,它就是一条通道,您想传递出去的数据被放置在缓冲区中,然后缓冲区中怎么从哪里传输出去呢?或者外设怎么把数据传输到缓冲中呢?这里就要用到通道。它可以进一步的减少CPU的干预,同时更有效率的提高整个系统的资源利用率,例如当CPU要完成一组相关的读操作时,只需要向I/O通道发送一条指令,以给出其要执行的通道程序的首地址和要访问的设备,通道执行通道程序便可以完成CPU指定的I/O任务。

3、选择器: 另外一项创新是选择器,当我们使用通道的时候也许通道没有准备好,或者有了新的请求过来,或者线程遇到了阻塞,而选择器恰恰可以帮助CPU了解到这些信息,但前提是将这个通道注册到了这个选择器。

NIO编程中,采用的是Channel对Socket的封装,解决了TCP三次握手的问题,每一个客户端(SocketChannel)访问服务器,都会注册到Selector中,并以SelectionKey的集合形式存储起来,在轮询中,对SelectionKey的状态进行检查和业务操作。
SelectionKey.OP_CONNECT //连接
SelectionKey.OP_ACCEPT //接受请求
SelectionKey.OP_READ //读操作
SelectionKey.OP_WRITE  //写操作

Java网络编程之NIO(ServerSocketChannel/SocketChannel)-yellowcong_服务器端

案例

这个案例中,我做了两个Client,不同的是一个Client只有些数据的作用,另外一个具有读写等功能

服务器端

服务器端中,需要注意的是selectionKey.isAcceptable()判断后,需要获取到服务器端的ServerSocketChannel ,然后再获取到客户端发过来的SocketChannel ,并注册。

//获取到SelectionKey 里面的Channel
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();

//线程阻塞,获取客户端的SocketChannel,注册到服务器端
// 接受到此通道套接字的连接。  
SocketChannel sc = channel.accept();

//设定非阻塞
sc.configureBlocking(false);

//注册准备读的事件
sc.register(this.selector,SelectionKey.OP_READ);

完整代码

package yellowcong.socket.nio;

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

import org.junit.experimental.theories.Theories;

/**
 * 创建日期:2017年10月7日 <br/>
 * 创建用户:yellowcong <br/>
 * 功能描述:  
 */
public class Server implements Runnable{
    //多路复用器
    private Selector selector ;

    private ByteBuffer byteBuff  = ByteBuffer.allocate(1024);;

    //port 端口号
    public Server(int port) {
        try {
            this.selector = Selector.open();
            ServerSocketChannel channel = ServerSocketChannel.open();

            //设定是否是线程阻塞的
            channel.configureBlocking(false);

            //绑定端口
            channel.bind(new InetSocketAddress(port));

            //注册channel  到Selecor(多路复用器)中
            channel.register(this.selector, SelectionKey.OP_ACCEPT);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

    public void run() {
        System.out.println("服务器启动");
        //在服务器端,轮询操作
        while(true){
            try {
                //开启多路复用器监听操作
                this.selector.select();

                //获取Selector 里面注册的 SelectionKey(每注册一个Channel ,就会有一个SelectionKey,SelectionKey里面有Channel对象 )
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();

                while(keys.hasNext()){
                    SelectionKey selectionKey = keys.next();

                    //直接从容器中移除就可以了,不然重复处理同一条Channel
                    keys.remove();

                    //可以连接的情况 ,刚开始连接时比较特别的,需要获取到客户端管道,然后再注册到服务器端Selector
                    if(selectionKey.isAcceptable()){

                        //获取到SelectionKey 里面的Channel
                        ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();

                        //线程阻塞,获取客户端的SocketChannel,注册到服务器端
                        // 接受到此通道套接字的连接。  
                        // 此方法返回的套接字通道(如果有)将处于阻塞模式。  
                        SocketChannel sc = channel.accept();

                        //设定非阻塞
                        sc.configureBlocking(false);

                        //注册准备读的事件
                        sc.register(this.selector,SelectionKey.OP_READ);
                    }

                    //可读的情况
                    if(selectionKey.isReadable()){

                        //获取到客户端的SocketChannel
                        SocketChannel sc =(SocketChannel) selectionKey.channel();

                        //晴空数据
                        this.byteBuff.clear();
                        int cnt = sc.read(this.byteBuff);

                        //数据没有的情况
                        if(cnt == -1){
                            selectionKey.channel().close();
                            selectionKey.cancel();
                            return;
                        }
                        //复位 ,将position置为1
                        this.byteBuff.flip();

                        //建立一个数组,大小 为传递过来的  数据长度
                        byte [] data = new byte[this.byteBuff.remaining()];
                        //将buffer数据读取到 数组中
                        this.byteBuff.get(data);

                        //打印传送过来的数据内容
                        System.out.println("服务器端:\t"+new String(data));

                        //进行下一步,注册写数据
                        sc.register(this.selector, SelectionKey.OP_WRITE);
                    }else if(selectionKey.isWritable()){//可写的情况
                        this.write(selectionKey);
                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
    /**
     * 写数据到客户端
     * 创建日期:2017年10月7日<br/>
     * 创建用户:yellowcong<br/>
     * 功能描述:
     * @param selectionKey
     */
    public void write(SelectionKey selectionKey){
        try {
            //获取客户端Channel
            SocketChannel clientChannel = (SocketChannel) selectionKey.channel();

            String sendText = "hello client"; //写道客户端的数据
            this.byteBuff.clear(); //在使用ByteBuff前,清空里面数据
            this.byteBuff.put(sendText.getBytes());
            this.byteBuff.flip();        //写完数据后调用此方法,设定Bytebuff的postion为0
            clientChannel.write(byteBuff);

            //设置Channel的下一步操作时 读取
            clientChannel.register(this.selector, SelectionKey.OP_READ);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Server server = new Server(8080);

        new Thread(server).start();
    }
}

客户端1

这个客户端中,我们可以发现,没有用到Selector(选择器),直接就可以发送请求到服务器端

package yellowcong.socket.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CountDownLatch;

/**
 * 创建日期:2017年10月7日 <br/>
 * 创建用户:yellowcong <br/>
 * 功能描述:  
 */
public class Client {

    private static final String  ADDR = "127.0.0.1";
    private static final Integer PORT = 8080;

    public static void  main(String [] args){
        try {

            //打开Socker Channel
            SocketChannel channel = SocketChannel.open();

            //打开链接
            channel.connect(new InetSocketAddress(ADDR, PORT));


            ByteBuffer buff =ByteBuffer.allocate(1024);

            while(true){
                //不断重控制台读取数据
                byte [] data = new byte[1024];
                System.in.read(data);

//              buff.put("发送文本".getBytes());

                //写数据到buff里面
                buff.put(data);

                //复位
                buff.flip();

                //写道服务器
                channel.write(buff);

                //清空
                buff.clear();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

客户端2

客户端2里面有发送和接收的请求和操作,这个客户端和服务端,在获取连接的地方有些区别if(selectionKey.isConnectable()){}判断的是是否获取到了连接,而服务器段是判断if(selectionKey.isAcceptable()){}

package yellowcong.socket.nio;

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.SocketChannel;
import java.util.Iterator;

/**
 * 创建日期:2017年10月7日 <br/>
 * 创建用户:yellowcong <br/>
 * 功能描述:  
 */
public class Client2  implements Runnable{

    private static final String  ADDR = "127.0.0.1";
    private static final Integer PORT = 8080;

    private Selector selector;
    private ByteBuffer byteBuff =ByteBuffer.allocate(1024);

    public Client2() {
        try {
            this.selector = Selector.open();

            //打开Socker Channel
            SocketChannel channel = SocketChannel.open();

            //设定连接
            channel.configureBlocking(false);

            //打开链接
            channel.connect(new InetSocketAddress(ADDR, PORT));

            // 注册连接服务端socket动作  
            channel.register(this.selector, SelectionKey.OP_CONNECT);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
    public void run() {

        while(true){

            try {
                Thread.sleep(1000);
                //多路复用器 监听开始, 这个地方是阻塞的
                this.selector.select();

                //获取所有的 Channel 信息
                Iterator<SelectionKey>  selectedKeys = this.selector.selectedKeys().iterator();

                //当存在Channel的情况
                while(selectedKeys.hasNext()){
                    //获取SelectionKey后,删掉集合里面的这个SelectionKey
                    SelectionKey selectionKey = selectedKeys.next();
                    selectedKeys.remove();//保证channel只遍历一次

                    if(selectionKey.isConnectable()){
                        //连接
                        SocketChannel channel  = (SocketChannel) selectionKey.channel();
                        // 判断此通道上是否正在进行连接操作。  
                        // 完成套接字通道的连接过程。  
                        if (channel.isConnectionPending()) {  
                            channel.finishConnect();  
                            System.out.println("完成连接!");  
                            byteBuff.clear();  
                            byteBuff.put("Hello,Server".getBytes());  
                            byteBuff.flip();  
                            channel.write(byteBuff);

                        }  
                        channel.register(this.selector, SelectionKey.OP_READ);  
                    }else if(selectionKey.isReadable()){
                        //可读
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        //获取到客户端的SocketChannel
                        SocketChannel sc =(SocketChannel) selectionKey.channel();

                        //晴空数据
                        this.byteBuff.clear();
                        int cnt = sc.read(this.byteBuff);

                        //数据没有的情况
                        if(cnt == -1){
                            selectionKey.channel().close();
                            selectionKey.cancel();
                            return;
                        }
                        //复位 ,将position置为1
                        this.byteBuff.flip();

                        //建立一个数组,大小 为传递过来的  数据长度
                        byte [] data = new byte[this.byteBuff.remaining()];
                        //将buffer数据读取到 数组中
                        this.byteBuff.get(data);
                        System.out.println("接收数据");
                        //打印传送过来的数据内容
                        System.out.println("客户端:\t"+new String(data));

                        //进行下一步,注册写数据
                        sc.register(this.selector, SelectionKey.OP_WRITE);

                    }else if(selectionKey.isWritable()){
                        //可写
                        SocketChannel channel = (SocketChannel) selectionKey.channel();

                        byteBuff.clear();

                        //不断重控制台读取数据
                        byteBuff.put("Hello Server,I'm Client ".getBytes());

                        //复位
                        byteBuff.flip();

                        //写道服务器
                        channel.write(byteBuff);

                        //清空
                        byteBuff.clear();

                        //设定这个Channel的操作是发送到Read
                        channel.register(this.selector, SelectionKey.OP_READ);
                    }
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }
    }

    public static void  main(String [] args){

        Client2 client = new Client2();

        new Thread(client).start();
    }



}