基于nio的socket编程

io编程中存在两个问题,io是阻塞的,而且保持多个连接的时候需要加入多线程来保持socket连接。这种方式比较浪费资源,因为每个连接都需要一个线程来保持,这在连接比较多的时候是一个浪费的资源是相当严重的。而两个问题在nio编程中就很好的解决这个问题,但是相对来说,对于编程的复杂度也相对有点提高。

这里不去深入的讨论nio编程的原理,只是基于实用的角度上做一些简单的介绍。在nio编程中,主要有三个组件比较重要:channel、buffer、selector。

channel:

通道,它和io中流有点相似,所以的源数据或者目标数据都是直接和channel打交道的。java nio中channel有以下几种实现,这些通道涵盖了TCP和UDP网络io:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

buffer:

缓冲区 , 他是直接与channel交互的,我们如果要读取channle中的数据时,都是先把数据从channle中读入到buffer中;同样的,如果我们要向channel中写数据的时候,也要先把数据写入到buffer中。java nio中有也有多个实现,他们不要为了针对不同的数据类型:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

注意:这里要介绍一个buffer中三个比较重要的方法,这三个方法在buffer读写的过程中都很有用。这三个方法都跟buffer中三个标记有关系:position、limit、capacity。position相当于指针,表示当前正在读写的位置;limit指的是界限,表示读写的时候界限;capacity表示容量,也就是buffer缓冲区的大小。为了弄明白这些方法,了解这三个标记是前提,说了这么多,这三个方法是:clear(); rewind(); flip()。这三个方法也就是调整这些标记位置,下面是这三个方法的源码,相信很好理解:

  1. public final Buffer clear() {   
  2.          position = 0; //设置当前下标为0  
  3.          limit = capacity; //设置写越界位置与和Buffer容量相同  
  4.          mark = -1; //取消标记  
  5.          return this;   
  1. public final Buffer rewind() {   
  2.         position = 0;   
  3.         mark = -1;    //取消标记 
  4.         return this;   
  5. }  
  1. public final Buffer flip() {   
  2.         limit = position;   
  3.         position = 0;   
  4.         mark = -1;    //取消标记 
  5.         return this;   
  6.  } 

java nio缓冲区中标志mark标记,使缓冲区能够记住一个位置并在之后将其返回。缓冲区的标记在mark( )函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset( )函数将位置设为当前的标记值。如果标记值未定义,调用reset( )将导致InvalidMarkException异常。一些缓冲区函数会抛弃已经设定的标记(rewind( ),clear( ),以及flip( )总是抛弃标记)。如果新设定的值比当前的标记小,调用limit( )或position( )带有索引参数的版本会抛弃标记。如调用mark( )来设定mark = postion。调用reset( )设定position = mark。标记在设定前是未定义的(undefined)。这四个属性之间总是遵循以下关系: 0 <= mark <= position <= limit <= capacity

selector:

选择器,这个就是nio为了解决io中多连接必须要多线程来处理的问题。selector允许单线程处理多个channel,你打开多个连接就会有多个channel,而selector可以管理多个channel。值得注意的是,selector必须要和非阻塞的通道配合使用,也就是说selector不能和fileChannel一起使用,因为fileChannel不能配置非阻塞的模式。

java io和java nio的主要区别:

1、io是面向流的,nio是面向缓冲区的。这就意味着io只能从流中读入一个或者或者多个字节。而nio因为是面向缓冲区的,数据都是先读入到缓冲区,也就是上面buffer中,而且在需要的时候,还可以在缓冲区中前后移动来获取不同位置的数据。但是也有个麻烦的地方,就是要检查缓冲区中是否含有自己要处理的数据。

2、io是阻塞的,nio是非阻塞的。io在数据读入或者写入的时候是阻塞的,直到数据处理完成为止。而nio则是非阻塞的,我们从buffer中读取数据的时候是立即返回的,但是这里有个问题就是我们无法保证缓冲区中是有数据的。但是好处就是,我们读取数据的时候不会阻塞,可以干别的事情。

下图可以说明这个问题(左边的是nio,右边的是io):

下面就是一个基于NIO的socket编程的例子:

public class NioSocketServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel server = ServerSocketChannel.open(); //新建NIO通道
            server.configureBlocking(false); //配置通道为非阻塞的
            ServerSocket socket = server.socket(); //创建socket服务
            socket.bind(new InetSocketAddress(9123)); //绑定端口号
            Selector selector = Selector.open(); //创建选择器
            server.register(selector, SelectionKey.OP_ACCEPT); //将服务注册到选择器上,并且配置关注的事件
            //循环获取通道内是否有关注的事件
            while(true) {
                //判断通道中事发后有关注的事件。如果 >1 则表示通道中有关注的事件
                int num = selector.select();
                if(num < 1) {
                    continue;
                }
                Set selectKeys = selector.selectedKeys();//获得事件的key值
                //遍历所有的事件
                Iterator iterator = selectKeys.iterator(); //获得迭代器
                while(iterator.hasNext()) {
                    SelectionKey key = (SelectionKey) iterator.next();
                    //移走此次事件
                    iterator.remove();
                    //判断该事件是否是请求连接的事件
                    if(key.isAcceptable()) {
                        //获得相应的socket channel
                        SocketChannel client = server.accept();
                        System.out.println("接受来自" + client + "的请求!");
                        client.configureBlocking(false); //将通道配置成非阻塞的
                        ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[1024]); //创建缓冲区
                        //在此通道上注册写事件
                        SelectionKey keyClient = client.register(selector, SelectionKey.OP_WRITE);
                        //通道执行事件
                        keyClient.attach(byteBuffer);
                    }else if(key.isWritable()) {
                        //获得此通道上的socket channel
                        SocketChannel client = (SocketChannel) key.channel(); //获得当前事件下的socketChannel
                        ByteBuffer outByteBuffer = (ByteBuffer)key.attachment(); //获得通道上绑定的byteBuffer
                        //如果缓冲区存在数据,清除掉,防止数据粘连,重置一下
                        if(outByteBuffer.hasRemaining()) {
                            outByteBuffer.clear();
                        }
                        //往此通道上写数据,先写入byteBuffer中
                        outByteBuffer.put("hello world".getBytes());
                        outByteBuffer.flip();
                        client.write(outByteBuffer);
                    }
                    key.channel().close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

猜你喜欢

转载自my.oschina.net/u/1757225/blog/758869
今日推荐