转载

Netty杂记2—NIO网络编程

在上篇文章中对BIO网络编程的相关内容进行了讲解,通过我们一步一步的优化,虽然我们通过多线程解决了并发访问的问题,但是BIO本身的一些特性造成的问题却没有得到解决。

BIO是阻塞IO,我们使用线程来进行IO的调度,我们无法确定io是否就绪,但是每个IO操作都会创建线程,这个时候如果IO未就绪,那么创建的线程也会处于阻塞状态。

在之前讲解NIO基本知识的时候我们提到过NIO通过通道选择器可以实现同时对多个通道的管理,实际上就是通过管理多个IO操作,换句话说是单线程处理多线程并发,有效的防止线程因为IO没有就绪而被挂起。

在使用NIO进行网络编程的时候需要用到的就是通道选择器,所以我们先看一下通道选择器的相关内容。

NIO

ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。

//代开ServerSocketChannel 
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。

SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:

//打开一个SocketChannel并连接到互联网上的某台服务器。
socketChannel.connect(new InetSocketAddress("localhost",8888));
//一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
SocketChannel socketChannel = SocketChannel.open();

通道选择器 Selector

为什么使用通道选择器?

在前言中提到过,NIO通过通道选择器可以实现同时对多个通道的管理,其实就是同时对多个IO操作的管理,也就是实现了单线程处理多线程并发问题。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此太多的线程会耗费大量的资源,所以使用通道选择器来对多个通道进行管理。

Selector的使用

1. 创建Selector

//通过调用Selector.open()方法创建一个Selector,如下:
Selector selector = Selector.open();

2.将通道注册到通道选择器中

//为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现
//        创建ServerSocketChanner
        ServerSocketChannel ssc = ServerSocketChannel.open();
//        绑定端口号
        ssc.bind(new InetSocketAddress(8888));
//        设置通道非阻塞
        ssc.configureBlocking(false);
//        创建通道选择器
        Selector selector = Selector.open();
//        将通道注册到通道选择器中 要求:通道都必须是非阻塞的  意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式
//        第二个参数是我们需要通道选择器帮我们管理什么事件类型  注册为接受就绪
        ssc.register(selector, SelectionKey.OP_ACCEPT);

register()方法的第二个参数代表注册的通道事件类型,一个通道触发一个事件就意味着该事件准备就绪了,总共有四种事件:Connect(连接就绪 ) Accept(接受就绪) Read(有数据可读的通道 读就绪) Write(写就绪)。对于选择器而言,可以针对性的找到(监听)的事件就绪的通道,进行相关的操作。

这四种事件用SelectionKey的四个常量来表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

可以用“位或”操作符将常量连接起来

SelectionKey对象

对象中包含很多的属性,譬如:

  • interest集合(事件集合)
  • ready集合
  • Channel
  • Selector
  • 附加的对象(可选)

3.选择器的select ()方法

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

4.选择器的selectedKeys()方法

通过调用selector的selectedKeys()方法,可以得到就绪通道的集合。遍历集合可以找到自己需要的通道进行相关的操作。

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();

            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //处理nio事件
                if(key.isAcceptable()){
//                    获取通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();

//                    注册读事件类型
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);


                }

                if(key.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();

//                    读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int len = -1;
                    while (true){
                        byteBuffer.clear();
                        len = socketChannel.read(byteBuffer);
                        if(len == -1){
                            break;
                        }

                        byteBuffer.flip();
                        while (byteBuffer.hasRemaining()){
                            bos.write(byteBuffer.get());
                        }
                    }
//                    打印读取到的数据
                    System.out.println(bos.toString());
                    //写数据给客户端 注册事件类型 写事件
                    socketChannel.register(selector,SelectionKey.OP_WRITE);
                }
                if(key.isWritable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    String msg = "你好,我是服务器";
                    ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
                    byteBuffer.put(msg.getBytes());
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                    socketChannel.close();
                }
                iterator.remove();
            }

NIO网络编程实例

服务器编程

基本步骤

  1. 打开ServerSocketChanner
  2. 绑定端口号
  3. 设置通道非阻塞
  4. 打开选择器,把通道注册到选择器中
  5. 使用Selector轮询所有的key
    1. 获取socketChannel
    2. 设置非阻塞 注册读事件
    3. 读取操作
    4. 注册写事件
    5. 写操作
Netty杂记2—NIO网络编程

代码

import java.io.ByteArrayOutputStream;
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 Exception{
//        创建ServerSocketChanner
        ServerSocketChannel ssc = ServerSocketChannel.open();

//        绑定端口号
        ssc.bind(new InetSocketAddress(8888));

//        设置通道非阻塞
        ssc.configureBlocking(false);

//        创建通道选择器
        Selector selector = Selector.open();

//        将通道注册到通道选择器中 要求:通道都必须是非阻塞的
//        第二个参数是我们需要通道选择器帮我们管理什么事件类型  注册为接受就绪
        ssc.register(selector, SelectionKey.OP_ACCEPT);

//        遍历通道选择器
        while (true){
            System.out.println("我在8888等你......");
//            返回准备就绪的通道数量
            int nums = selector.select();
//            如果数量小于1说明没有通道准备就绪 跳过本次循环
            if(nums<1) {continue;}

//            获取所有的keys(通道 事件类型)
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();

            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //处理nio事件
                if(key.isAcceptable()){
//                    获取通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = channel.accept();

//                    注册读事件类型
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                if(key.isReadable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();

//                    读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    int len = -1;
                    while (true){
                        byteBuffer.clear();
                        len = socketChannel.read(byteBuffer);
                        if(len == -1){
                            break;
                        }

                        byteBuffer.flip();
                        while (byteBuffer.hasRemaining()){
                            bos.write(byteBuffer.get());
                        }
                    }
//                    打印读取到的数据
                    System.out.println(bos.toString());
                    //写数据给客户端 注册事件类型 写事件
                    socketChannel.register(selector,SelectionKey.OP_WRITE);
                }
                if(key.isWritable()){
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    String msg = "你好,我是服务器";
                    ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
                    byteBuffer.put(msg.getBytes());
                    byteBuffer.flip();
                    socketChannel.write(byteBuffer);
                    socketChannel.close();
                }
                iterator.remove();
            }
        }
    }
}

客户端编程

基本步骤

  1. 打开SocketChannel
  2. 连接服务器
  3. 写数据给服务器
  4. 读取数据
  5. 关闭SocketChannel

代码

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOCleint {
    public static void main(String[] args) throws IOException {
//        创建sc
        SocketChannel socketChannel = SocketChannel.open();
//        连接服务器
        socketChannel.connect(new InetSocketAddress("localhost",8888));

//        写数据给服务器
        String msg = "你好,我是客户端";
        ByteBuffer byteBuffer = ByteBuffer.allocate(msg.getBytes().length);
        byteBuffer.put(msg.getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
//        关闭输出流
        socketChannel.shutdownOutput();

//        读取数据
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        ByteArrayOutputStream bosread = new ByteArrayOutputStream();
        int len = -1;
        while (true){
            readBuffer.clear();
            len = socketChannel.read(readBuffer);
            if(len == -1){
                break;
            }
            readBuffer.flip();
            while (readBuffer.hasRemaining()){

                bosread.write(readBuffer.get());
            }
        }

        System.out.println("我收到:"+bosread.toString());
        socketChannel.close();
    }
}

我不能保证每一个地方都是对的,但是可以保证每一句话,每一行代码都是经过推敲和斟酌的。希望每一篇文章背后都是自己追求纯粹技术人生的态度。

永远相信美好的事情即将发生。

原文  https://juejin.im/post/5afd4546f265da0b70261e96
正文到此结束
Loading...