java IO 之网络 NIO

日常中我们最常用的网络通信有UDP和TCP,其中TCP是最为常用的。TCP连接使用Socket来实现,一般最为常用的Socket为 BIO,既阻塞流,模型如下图。

即每当有新的连接连入后都开启一个线程来处理这次的连接,这样当连接量过大时会导致线程数越来越多,而线程是非常耗费资源的。自JDK1.4后引入了NIO我们便可以摆脱这种情况。

NIO的连接模型如下

接下来看一看NIO的使用

首先是服务端代码

1.启动类

public class Server {
    private static int DEFAULT_PORT = 8090;  
    private static TaskHandle taskHandle;  
    public static void start(){  
        start(DEFAULT_PORT);  
    }  
    public static synchronized void start(int port){  
        if(taskHandle!=null)  
            taskHandle.stop();  
        taskHandle = new TaskHandle(port);  
        new Thread(taskHandle,"Server").start();  
    }  
    public static void main(String[] args){  
        start();  
    }  
}

2.任务处理类

package nio.socketChennel;

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

public class TaskHandle implements Runnable {
    private Selector selector;
    private ServerSocketChannel serverChannel;
    private volatile boolean started;

    /**
     * 构造方法
     * 
     * @param port
     *            指定要监听的端口号
     */
    public TaskHandle(int port) {
        try {
            // 创建选择器
            selector = Selector.open();
            // 打开监听通道
            serverChannel = ServerSocketChannel.open();
            // 如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
            serverChannel.configureBlocking(false);// 开启非阻塞模式
            /*
             * 绑定端口 backlog设为1024 backlog参数的含义是等待队列的长度,既当前连接正在处理时,
             * 最多能有backlog个连接排队等候,当队列满后,再请求的连接会被拒绝掉
             */
            serverChannel.socket().bind(new InetSocketAddress(port), 1024);
            // 监听客户端连接请求
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 标记服务器已开启
            started = true;
            System.out.println("服务器已启动,端口号:" + port);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        started = false;
    }

    @Override
    public void run() {
        // 循环遍历selector
        while (started) {
            try {
                // 无论是否有读写事件发生,selector每隔1s被唤醒一次
                selector.select(1000);
                // 阻塞,只有当至少一个注册的事件发生的时候才会继续.
                // selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
        // selector关闭后会自动释放里面管理的资源
        if (selector != null)
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            // 处理新接入的请求消息
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                // 通过ServerSocketChannel的accept创建SocketChannel实例
                // 完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
                SocketChannel sc = ssc.accept();
                // 设置为非阻塞的
                sc.configureBlocking(false);
                // 注册为读
                sc.register(selector, SelectionKey.OP_READ);
            }
            // 读消息
            if (key.isReadable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                // 创建ByteBuffer,并开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // 读取请求码流,返回读取到的字节数
                int readBytes = sc.read(buffer);
                // 读取到字节,对字节进行编解码
                if (readBytes > 0) {
                    // 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
                    /*
                     * 将缓存字节数组的指针设置为数组的开始序列即数组下标0。这样就可以从buffer开头,对该buffer进行遍历(读取
                     * )了。
                     */
                    buffer.flip();
                    // 根据缓冲区可读字节数创建字节数组
                    /*
                     * ByteBuffer.remaining(),此方法返回剩余的可用长度,此长度为实际读取的数据长度,即缓冲区可读字节数长度
                     * ByteBuffer.get(byte[]),从ByteBuffer中读取byte[]
                     */
                    byte[] bytes = new byte[buffer.remaining()];
                    // 将缓冲区可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String expression = new String(bytes, "UTF-8");
                    System.out.println("服务器收到消息:" + expression);
                    // 处理数据
                    String result = null;
                    try {
                        // do something .....
                        result = "OK I get it!";
                    } catch (Exception e) {
                        result = "计算错误:" + e.getMessage();
                    }
                    // 发送应答消息
                    doWrite(sc, result);
                }
                // 没有读取到字节 忽略
                // else if(readBytes==0);
                // 链路已经关闭,释放资源
                else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    // 异步发送应答消息
    private void doWrite(SocketChannel channel, String response) throws IOException {
        // 将消息编码为字节数组
        byte[] bytes = response.getBytes();
        // 根据数组容量创建ByteBuffer
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
        // 将字节数组复制到缓冲区
        writeBuffer.put(bytes);
        // flip操作
        writeBuffer.flip();
        // 发送缓冲区的字节数组
        channel.write(writeBuffer);
        // ****此处不含处理“写半包”的代码
    }
}

客户端代码

1.启动类

扫描二维码关注公众号,回复: 1422220 查看本文章
public class Client {

    private static String DEFAULT_HOST = "127.0.0.1";  
    private static int DEFAULT_PORT = 8090;  
    private static ClientHandle clientHandle;  
    private static int sign = 1;
    public static void start(){  
        start(DEFAULT_HOST,DEFAULT_PORT);  
    }  
    public static synchronized void start(String ip,int port){  
        if(clientHandle!=null)  
            clientHandle.stop();  
        clientHandle = new ClientHandle(ip,port,sign);  
        sign = sign + 1;
        new Thread(clientHandle,"Server").start();  
    }  
    //向服务器发送消息  
    public static boolean sendMsg(String msg) throws Exception{  
        if(msg.equals("q")) return false;  
        ClientHandle clientHandle = new ClientHandle(DEFAULT_HOST,DEFAULT_PORT,sign);
        new Thread(clientHandle,"Server").start();  
        Thread.sleep(1000);
        clientHandle.sendMsg(msg);  
        sign = sign + 1;
        return true;  
    }  
    public static void main(String[] args){  
        start();  
    }  
}

2.连接线程类

package nio.socketChennel;

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

public class ClientHandle implements Runnable {
    private String host;  
    private int port;  
    private Selector selector;  
    private SocketChannel socketChannel;  
    private volatile boolean started;  
    private int sign;
  
    public ClientHandle(String ip,int port, int sign) {  
        this.host = ip;  
        this.port = port;  
        this.sign = sign;
        try{  
            //创建选择器  
            selector = Selector.open();  
            //打开监听通道  
            socketChannel = SocketChannel.open();  
            //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式  
            socketChannel.configureBlocking(false);//开启非阻塞模式  
            started = true;  
        }catch(IOException e){  
            e.printStackTrace();  
            System.exit(1);  
        }  
    }  
    
    
    public void stop(){  
        started = false;  
    }  
    
    
    @Override  
    public void run() {  
        try{  
            doConnect();  
        }catch(IOException e){  
            e.printStackTrace();  
            System.exit(1);  
        }  
        //循环遍历selector  
        while(started){  
            try{  
                //无论是否有读写事件发生,selector每隔1s被唤醒一次  
                selector.select(1000);  
                //阻塞,只有当至少一个注册的事件发生的时候才会继续.  
//              selector.select();  
                Set<SelectionKey> keys = selector.selectedKeys();  
                Iterator<SelectionKey> it = keys.iterator();  
                SelectionKey key = null;  
                while(it.hasNext()){  
                    key = it.next();  
                    it.remove();  
                    try{  
                        handleInput(key);  
                    }catch(Exception e){  
                        if(key != null){  
                            key.cancel();  
                            if(key.channel() != null){  
                                key.channel().close();  
                            }  
                        }  
                    }  
                }  
            }catch(Exception e){  
                e.printStackTrace();  
                System.exit(1);  
            }  
        }  
        
      //selector关闭后会自动释放里面管理的资源  
        if(selector != null)  
            try{  
                selector.close();  
            }catch (Exception e) {  
                e.printStackTrace();  
            }  
    }  
    private void handleInput(SelectionKey key) throws IOException{  
        if(key.isValid()){  
            SocketChannel sc = (SocketChannel) key.channel();  
            if(key.isConnectable()){  
                if(sc.finishConnect());  
                else System.exit(1);  
            }  
            //读消息  
            if(key.isReadable()){  
                //创建ByteBuffer,并开辟一个1M的缓冲区  
                ByteBuffer buffer = ByteBuffer.allocate(1024);  
                //读取请求码流,返回读取到的字节数  
                int readBytes = sc.read(buffer);  
                //读取到字节,对字节进行编解码  
                if(readBytes>0){  
                    //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作  
                    buffer.flip();  
                    //根据缓冲区可读字节数创建字节数组  
                    byte[] bytes = new byte[buffer.remaining()];  
                    //将缓冲区可读字节数组复制到新建的数组中  
                    buffer.get(bytes);  
                    String result = new String(bytes,"UTF-8");  
                    System.out.println("客户端收到消息:" + result);  
                }  
                //没有读取到字节 忽略  
//              else if(readBytes==0);  
                //链路已经关闭,释放资源  
                else if(readBytes<0){  
                    key.cancel();  
                    sc.close();  
                }  
                
            }  
        }  
    }  
    //异步发送消息  
    private void doWrite(SocketChannel channel,String request) throws IOException{  
        //将消息编码为字节数组  
        byte[] bytes = request.getBytes();  
        //根据数组容量创建ByteBuffer  
        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);  
        //将字节数组复制到缓冲区  
        writeBuffer.put(bytes);  
        //flip操作  
        writeBuffer.flip();  
        //发送缓冲区的字节数组  
        channel.write(writeBuffer);  
        //****此处不含处理“写半包”的代码  
    }  
    private void doConnect() throws IOException{  
        if(socketChannel.connect(new InetSocketAddress(host,port)));  
        else socketChannel.register(selector, SelectionKey.OP_CONNECT);  
    }  
    public void sendMsg(String msg) throws Exception{  
        socketChannel.register(selector, SelectionKey.OP_READ);  
        doWrite(socketChannel, msg);  
        System.out.println("第" + sign + "个客户端发出信息");
    }  
}

测试代码

import java.util.Scanner;

public class Test {
    //测试主方法  
    @SuppressWarnings("resource")  
    public static void main(String[] args) throws Exception{  
        //运行服务器  
        Server.start();  
        //避免客户端先于服务器启动前执行代码  
        Thread.sleep(100);  
        //运行客户端   
      //  Client.start();  
        Thread.sleep(1000);  
        while(true){
            Client.sendMsg("test message");
            //Thread.sleep(1000);  
        }
    }  
}

经过使用,个人认为。NIO解决了之前BIO在请求连接客户端数量庞大时带来的服务端线程数量过多的问题。使得服务端线程不必与客户端连接数量1:1,而是1:N。但是它也有相应的一些问题无法解决,使用时所有的客户端连接都在一个线程中管理,相应的任务也都在这个线程中类执行。如果是执行比较耗时的操作那么对于客户端的响应会变慢,因为要一个一个连接轮流处理,这样每个连接轮流等待处理的情况我觉得就是另一种意义上的阻塞。但如果为每个连接开线程的话就会又沦落为BIO的情况了。所以网上的其他相关资料也说NIO适合那些客户端链接量大但每个连接操作不太耗时的情景,如聊天室。BIO适合客户端连接数小的情况。

猜你喜欢

转载自www.cnblogs.com/codefeng/p/9127372.html