系统间通信(五)---优化篇(4)

    上一篇博文我们讲了非阻塞模式的IO。以Socket通信为例,总结来说,非阻塞IO就是通过为ServerSocket的accept方法和Socket的read方法设置等待时间,避免应用程序在获取连接和读取数据的时候一直等待。但是这种方法虽然解决了程序级别的阻塞,但操作系统底层的操作还是“同步”的,所以并没有彻底解决“等待”问题。

    本文要介绍的仍旧是一种同步通信模型,多路复用IO。java中对多路复用IO的支持是通过Channel,Selector,Buffer这三个核心因素实现的(这也就是java中的NIO(new IO))。


    1.Channel:应用程序和操作系统交互事件,传递内容的通道(所以多路复用IO模型需要操作系统支持)。应用程序可以通过通道向操作系统写数据,也可以通过通道从操作系统读取数据。不管是读数据还是写数据都是通过Buffer来操作的。(我的个人理解:一般的Socket通信,服务端会先创建ServerSocket,然后调用ServerSocket对象的socket方法获取对应的Socket对象,对应过来就是一个Channel就可以看做是一个盛放ServerSocket和Socket的容器)(每个通道都有一个文件状态描述符。所谓文件描述符,是计算机科学的一个术语,表示指向文件的引用的抽象化概念。文件描述符在表现形式上是一个非负整数。实际上,它是一个索引值。文件描述符适合于Unix,Linux这样的操作系统,一般程序的编写不会涉及)。

    JDK中的channel有以下这些:

    

可以看到Channel是一个接口,它提供了两个方法,一个close方法,用于关闭一个通道;一个isOpen方法,用于返回当前通道是开放或关闭。

    常用到的有ServerSocketChannel(应用服务器程序的监听通道,只有通过这个通道,应用程序才能向操作系统注册支持多路复用IO的端口监听,同时支持TCP和UDP)和SocketChannel(TCP Socket套接字的监听通道),让我们看看这两个Channel的结构。

    

    这里说一下ServerSocketChannel中常见的一些方法。

    accept方法用来获取连接到这个通道的SocketChannel。

    bind方法用来向这个通道绑定一个地址,可以是本地,也可以是某个网络地址。

    socket用于获取这个通道的ServerSocket对象。

  validOps用来获取这个通道支持的事件,比如ServerSocketChannel支持SeletionKey.OP_ACCEPT事件,SocketChannel支持SeletionKey.OP_READ,SeletionKey.OP_WRITE,SeletionKey.OP_CONNECT事件。



    可以看到SocketChannel中的大部分方法都是类似于Socket中的读和写的方法。


2.Selector:选择器,可以理解为通道管理器。我们新建一个通道后,会为通道注册一些监听事件,而这个通道管理器,就是监听通道注册的事件什么时候发生。这样就不需要程序(应用程序)通过阻塞或者非阻塞去询问操作系统,而是这个通道管理器代替程序去询问操作系统关心的事件是否发生。那么如何为通道注册关心的事件呢?如何判断通道管理器管理哪个通道呢?先看下面这幅图。

可以看到SelectableChannel类中提供了一个register(Selector,int)方法。怎么理解呢?看本篇文章中的第一幅图,可以看到常用的通道都是继承自SelectableChannel,所以要为要让一个选择器,也就是通道管理器管理一个通道,并且为通道注册关心的事件可以通过register方法来实现。如下:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

常用的通道关心的事件在SelectionKey这个类中都有定义,比如ServerSocketChannel关心的事件和SocketChannel关心的事件都在SelectionKey中有定义。

注:通道管理器只能管理继承了SelectableChannel的Channel。

还记得多路复用IO通信模型是需要操作系统支持的吗?不同操作系统的支持主要是通过不同的Selector来达到的。看下图:


我在写这篇文章的时候是在windows系统上写的,因为最初安装jdk的时候安装的就是windows版本的,所以在代码中只能看到selector在windows中的实现,即WindowsSelectorImpl这个类。在Linux中的实现类为PollSelectorImpl,EpollSelectorImpl。


3.Buffer:为了方便通过Channel读写数据,java NIO采用Buffer,而且为每一个提供读写数据功能的通道都集成了Buffer,比如SocketChannel。Buffer中有三个比较重要的概念,如下:

    private int position = 0;   //正在操作的数据位于缓冲区的位置
    private int limit;   //缓冲区最大可操作的位置
    private int capacity;   //缓冲区的容量

4.多路复用IO的实现方式:select,poll,epoll,kqueue。

方式 设计模式 操作系统 java支持
select 反应器(Reactor) Windows/Linux Windows对于同步IO的支持都是这种方式的
poll 反应器 Linux Linux下的java NIO采用这种方式
epoll 前摄器(Proactor),反应器 Linux Windows下有IOCP提供真正的异步支持,而Linux下用epoll模拟异步支持
kqueue 前摄器 Linux java还没有采用这种方式

5.Java实例

服务端代码:

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
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 MyTest {


    public static void main(String[] args) throws Exception {
        
        //创建通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        
        //切换成非阻塞模式
        serverSocketChannel.configureBlocking(false);
        
        //获取ServerSocket
        ServerSocket serverSocket = serverSocketChannel.socket();
        
        serverSocket.setReuseAddress(true);
        
        //绑定端口
        serverSocket.bind(new InetSocketAddress(8999));
        
        //创建选择器
        Selector selector = Selector.open();
        
        //为通道注册选择器和监听事件,ServerSocketChannel只监听accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        
        //
        while(true){
            
            if(selector.select(5000)==0){
                //3秒之内没有客户端连接请求
                //可以利用CPU先处理其他事情
                System.out.println("没有客户端请求");
                continue;
            }
            
            //表示有客户端请求
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            /***
             * selector.keys()和selector.selectedKeys()是有区别的:
             * selector.selectedKeys().iterator()得到的iterator在下面remove的时候不会报错,
             * 而selector.keys().iterator()得到的iterator在remove的时候会报错,UnsupportedOperationException
             * 这个地方涉及一个知识点:java集合权限,感兴趣的同学可以在百度UnsupportedOperationException或者Collections.unmodifiableList(List list)
             * Collections.unmodifiableList(List list)是修改集合的权限
             */
            try{
            while(iterator.hasNext()){
                //遍历每一个客户端请求
                SelectionKey selectionKey = iterator.next();
                //将这个客户端请求从集合中移除掉,不然这一批客户端请求处理完之后,下一批客户端请求过来的时候,还会处理一遍这批的客户端请求
                iterator.remove();
                //获取当前处理的这个客户端和服务端的channel
                SelectableChannel selectableChannel = selectionKey.channel();
                if(selectionKey.isValid()&&selectionKey.isAcceptable()){
                    //表示客户端连接请求已经收到
                    //获取该客户端和服务端连接的ServerSocketChannel,可以理解为socket通信中的ServerSocket
                    ServerSocketChannel serverSocketChannelNow = (ServerSocketChannel) selectableChannel;
                    //获取该客户端和服务端连接的SocketChannel,可以理解为socket通信中的Socket
                    SocketChannel socketChannel = serverSocketChannelNow.accept();
                    //为通道注册选择器和监听事件,SocketChannel监听read事件(也可以监听write事件和connect事件)
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("当前通道注册read事件成功");
                }else if(selectionKey.isValid()&&selectionKey.isConnectable()){
                    //表示与客户端的连接已经建立
                    System.out.println("客户端与服务端连接已建立");
                }else if(selectionKey.isValid()&&selectionKey.isReadable()){
                    //表示可以读取客户端传输的数据
                    //获取当前的socket通道
                    SocketChannel socketChannel = (SocketChannel) selectableChannel;
                    //获取客户端地址
                    InetSocketAddress inetSocketAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
                    //获取客户端端口
                    int port = inetSocketAddress.getPort();
                    //拿到SocketChannel的缓冲区,准备读取数据
                    ByteBuffer contextBytes = (ByteBuffer) selectionKey.attachment();
                    int length = -1;
                    try {
                        length = socketChannel.read(contextBytes);
                    } catch (Exception e) {
                        //发生异常表示客户端因为某种原因停止运行了,这时要关闭channel
                        socketChannel.close();
                        return ;
                    }
                    if(length==-1){
                        //表示缓冲区没有数据
                        return;
                    }
                    byte[] bytes = contextBytes.array();
                    String msgEncode = new String(bytes,"UTF-8");
                    String msg = URLDecoder.decode(msgEncode, "UTF-8");
                    System.out.println("客户端传输的内容为:"+msg);
                    //如果收到客户端发送“over”,则清空缓冲区,并且回传客户端处理结果
                    if(msg.indexOf("over")!=-1){
                        contextBytes.clear();
                        
                        //******************************
                        //      真正的处理客户端请求的过程
                        //******************************
                        
                        String sendMsg = URLEncoder.encode("返回处理结果over", "UTF-8");
                        ByteBuffer sendBuffer = ByteBuffer.wrap(sendMsg.getBytes());
                        socketChannel.write(sendBuffer);
                        socketChannel.close();
                    }else{
                        contextBytes.position(length);
                        contextBytes.limit(contextBytes.capacity());
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        }
    }
}

客户端代码:

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;

public class Client {

    public static void main(String[] args) {
        Socket socket = null;
        InputStream is = null;
        OutputStream os = null;
        try {
            //客户端请求建立连接
            socket = new Socket("localhost", 8997);
            is = socket.getInputStream();
            os = socket.getOutputStream();
            byte[] buffer = new byte[1024];
            String msg = "来自客户端的请求over";
            buffer = msg.getBytes();
            os.write(buffer);
            os.flush();
            
            byte[] receiveMsg = new byte[1024];
            String str = "";
            while(is.read()!=-1){
                is.read(receiveMsg);
                str += new String(receiveMsg);
                if(str.indexOf("over")!=-1){
                    break;
                }
            }
            str = URLDecoder.decode(str, "UTF-8");
            System.out.println("服务器端返回的信息为:"+str);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally {
            try {
                is.close();
                os.close();
                socket.close();
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }        
    }
}
6.总结:多路复用IO还是同步IO模型,但是它在操作系统级别进行了优化,也解决了阻塞。而且一个端口可以处理多种协议。

猜你喜欢

转载自blog.csdn.net/Dream_Ryoma/article/details/80493657