Android网络编程(十四) 之 Socket与NIO

1 简介

NIO(Non-Blocking I/O或叫New I/O)是一种同步非阻塞的I/O模型,主要用于服务端解决高并发或者大量连接的情况的IO处理。它是JDK1.4中引入的,位于java.nio包中,主要用于弥补原来同步阻塞I/O(Blocking I/O或叫BIO)的不足。在NIO出现之前大多服务端主要使用BIO通过新建线程的方式来解决并发请求,如上一篇博文《Android网络编程(十三) 之 Socket和长连接》中的长连接Demo,在每个客户端请求连接后都会创建一个新的Socket对象并内部创建线程来处理相关连接,这样就很容易因线程瓶颈而造成很多限制。

NIO在处理读写是采用了内存映射文件的方式,它基于通道(Channel)和缓冲区(Buffer)进行操作,数据从通道读取到缓冲区或者从缓冲区写入到通道,再通过选择器(Selector)进行监听多个通道的事件,所以区别于BIO的面向流方式,NIO可更加高效地进行文件的读写操作。

2 NIO的组件

2.1 Buffer(缓冲区)

BIO的操作是面向数据流的读写,而NIO所有的数据都是用Buffer缓冲区处理的,缓冲区其实就是一块连续的内存空间,这块内存空间就像一个数据容器般,可以重复的读取数据和写入数据。

2.2 Channel(通道)

Channel通道跟BIO中的Stream类似,都是用于跟连接的对象进行IO操作。它们区别于,Stream是阻塞的单向操作的,即要么读要么写,比如InputStream和OutputStream;而Channel是非阻塞且是线程安全的双向操作的,通过一个Channel既可以进行读也可进行写操作,其所有数据都是映射到内存中通过Buffer来处理

2.3 Selector(选择器)

在BIO中当一个Server端连接着多个Client端时,Server端会为其创建一个线程来提升并发吞吐量,但是一旦并发量上升就会出现明显的弊端。在这情况Selector的优势就出现了。Selector叫做选择器,或者叫做多路复用器,Selector运行在单个线程中但可同时管理一个或多个Channel。它通过不断地轮询进行Channel的状态的检查处理其连接、读、写等操作。意味着可以使用更少的线程来处理多个Client端的请求,避免了使用线程的开销。

3 Socket与NIO

我们还是用一个简单的Demo来实现一个Socket,不过这次使用了NIO的方式。Demo中服务端在App的Service中进行,而客户端在App的Activity中进行,为了展示出服务端可以同时接收多个客户端,Activity的界面特意做了两套客户端,如下图所示。

 

3.1 服务端代码

TCPServerService.java

public class TCPServerService extends Service {
    public final static int SERVER_PORT = 9527;                     // 跟客户端绝定的端口

    private TCPServer mTCPServer;

    @Override
    public void onCreate() {
        super.onCreate();
        initTcpServer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unInitTcpServer();
    }

    /**
     * 初始化TCP服务
     */
    private void initTcpServer() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                mTCPServer = new TCPServer();
                mTCPServer.init();
            }
        }).start();
    }

    /**
     * 反初始化TCP服务
     */
    private void unInitTcpServer() {
        mTCPServer.close();
    }
}

服务端的实现在TCPServerService中,TCPServerService服务启动后,便创建一个线程来创建一个TCPServer对象并执行初始化。

TCPServer.java

public class TCPServer {
    private final static String TAG = "TCPServer----------";

    private String mSendMsg;

    public final static int SERVER_PORT = 9527;                         // 跟客户端约定的端口

    private Selector mSelector;

    public void init() {
        ServerSocketChannel serverSocketChannel = null;
        try {
            serverSocketChannel = ServerSocketChannel.open();
            // 设置非阻塞
            serverSocketChannel.configureBlocking(false);
            // 获取与此Channel关联的ServerSocket并绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(SERVER_PORT));
            // 注册到Selector,等待连接
            mSelector = Selector.open();
            serverSocketChannel.register(mSelector, SelectionKey.OP_ACCEPT);
            while (mSelector != null && mSelector.isOpen()) {
                // 选择一组对应Channel已准备好进行I/O的Key
                int select = mSelector.select();
                if (select <=0) {
                    continue;
                }
                // 获得Selector已选择的Keys
                Set<SelectionKey> selectionKeys = mSelector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();

                    // 移除当前的key
                    iterator.remove();

                    if (selectionKey.isValid() && selectionKey.isAcceptable()) {
                        handleAccept(selectionKey);
                    }
                    if (selectionKey.isValid() && selectionKey.isReadable()) {
                        handleRead(selectionKey);
                    }
                    if (selectionKey.isValid() && selectionKey.isWritable()) {
                        handleWrite(selectionKey);
                    }

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (mSelector != null) {
                    mSelector.close();
                    mSelector = null;
                }
                if (serverSocketChannel != null) {
                    serverSocketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleAccept(SelectionKey selectionKey) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        // 注册读就绪事件
        client.register(mSelector, SelectionKey.OP_READ);
        Log.d(TAG, "服务端 已经跟 客户端(" + client.getRemoteAddress() + ") 连接上");
    }

    private void handleRead(SelectionKey selectionKey) throws IOException {
        SocketChannel client = (SocketChannel) selectionKey.channel();

        //读取服务器发送来的数据到缓冲区中
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int bytesRead = client.read(byteBuffer);
        if (bytesRead > 0) {
            String inMsg = new String(byteBuffer.array(), 0, bytesRead);
            // 处理数据
            processMsg(selectionKey, inMsg);
        }
        else {
            Log.d(TAG, "服务端 收到 客户端(" + client.getRemoteAddress() + ") 断开请求");
            client.close();
        }
    }

    private void handleWrite(SelectionKey selectionKey) throws IOException {
        if (TextUtils.isEmpty(mSendMsg)) {
            return;
        }
        SocketChannel client = (SocketChannel) selectionKey.channel();

        ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
        sendBuffer.put(mSendMsg.getBytes());
        sendBuffer.flip();

        client.write(sendBuffer);
        mSendMsg = null;

        client.register(mSelector, SelectionKey.OP_READ);
    }

    /**
     * 处理数据
     *
     * @param selectionKey
     * @param inMsg
     * @throws IOException
     */
    private void processMsg(SelectionKey selectionKey, String inMsg) throws IOException {
        SocketChannel client = (SocketChannel) selectionKey.channel();
        Log.d(TAG, "服务端 收到 客户端(" + client.getRemoteAddress() + ") 数据:" + inMsg);

        // 估计1亿的AI代码
        String outMsg = inMsg;
        outMsg = outMsg.replace("吗", "");
        outMsg = outMsg.replace("?", "!");
        outMsg = outMsg.replace("?", "!");
        sendMsg(selectionKey, outMsg);
    }

    /**
     * 发送数据
     *
     * @param selectionKey
     * @param msg
     * @throws IOException
     */
    public void sendMsg(SelectionKey selectionKey, String msg) throws IOException {
        mSendMsg = msg;
        SocketChannel client = (SocketChannel) selectionKey.channel();
        client.register(mSelector, SelectionKey.OP_WRITE);
        Log.d(TAG, "服务端 回复 客户端(" + client.getRemoteAddress() + ") 发送数据:" + msg);
    }

    /**
     * 断开连接
     */
    public void close() {
        try {
            Log.d(TAG, "服务端中断所有连接");
            mSelector.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPServer类核心代码就是init方法,可见方法内存在ServerSocketChannel和Selector,它们便是我们上面介绍的通道和选择器。除此外还有一个SelectionKey,它是用于维护Channel和Selector的对应关系。

SelectionKey里头有四个常量:SelectionKey.OP_CONNECT、SelectionKey.OP_ACCEPT、SelectionKey.OP_READ、SelectionKey.OP_WRITE,它们表示Channel注册到Selectort感兴趣的事件。对应selectionKey.isConnectable()、selectionKey.isAcceptable()、selectionKey.isReadable()、selectionKey.isWritable()方法会返回true,所以可以理解成,主要注册了相应的事件,上述循环中便会执行相应返回true的动作。

3.2 客户端代码

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private TCPClient mTcpClient1;
    private TCPClient mTcpClient2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent service = new Intent(this, TCPServerService.class);
        startService(service);

        mTcpClient1 = new TCPClient("客户端A");
        mTcpClient2 = new TCPClient("客户端B");

        Button btnConnection1 = findViewById(R.id.btn_connection1);
        btnConnection1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.connectServer();
            }
        });
        Button btnSend1 = findViewById(R.id.btn_send1);
        btnSend1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.sendMsg("你好吗?");
            }
        });
        Button btnDisconnect1 = findViewById(R.id.btn_disconnect1);
        btnDisconnect1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.disconnectService();
            }
        });


        Button btnConnection2 = findViewById(R.id.btn_connection2);
        btnConnection2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.connectServer();
            }
        });
        Button btnSend2 = findViewById(R.id.btn_send2);
        btnSend2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.sendMsg("吃饭了吗?");
            }
        });
        Button btnDisconnect2 = findViewById(R.id.btn_disconnect2);
        btnDisconnect2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.disconnectService();
            }
        });
    }
}

客户端的实现在MainActivity中,MainActivity主要是创建了两个TCPClient对象,然后对应界面中的按钮作相应的逻辑。

TCPClient.java

public class TCPClient {
    private static final String TAG = "TCPClient**********";

    private String mSendMsg;

    private Selector mSelector;
    private SocketChannel mSocketChannel;
    private String mClientName;                                                 // 客户端命名

    public TCPClient(String clientName) {
        mClientName = clientName;
    }

    /**
     * 连接服务端
     */
    public void connectServer() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                init();
            }
        }).start();
    }

    public void init() {
        try {
            mSocketChannel = SocketChannel.open();
            // 设置为非阻塞方式
            mSocketChannel.configureBlocking(false);
            // 连接服务端地址和端口
            mSocketChannel.connect(new InetSocketAddress("127.0.0.1", TCPServerService.SERVER_PORT));
            // 注册到Selector,请求连接
            mSelector = Selector.open();
            mSocketChannel.register(mSelector, SelectionKey.OP_CONNECT);
            while (mSelector != null && mSelector.isOpen()) {
                // 选择一组对应Channel已准备好进行I/O的Key
                int select = mSelector.select();
                if (select <=0) {
                    continue;
                }
                Set<SelectionKey> selectionKeys = mSelector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();

                    // 移除当前的key
                    iterator.remove();

                    if (selectionKey.isValid() && selectionKey.isConnectable()) {
                        handleConnect();
                    }
                    if (selectionKey.isValid() && selectionKey.isReadable()) {
                        handleRead();
                    }
                    if (selectionKey.isValid() && selectionKey.isWritable()) {
                        handleWrite();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (mSelector != null) {
                    mSelector.close();
                    mSelector = null;
                }
                if (mSocketChannel != null) {
                    mSocketChannel.close();
                    mSocketChannel = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleConnect() throws IOException {
        // 判断此通道上是否正在进行连接操作。
        if (mSocketChannel.isConnectionPending()) {
            mSocketChannel.finishConnect();
            mSocketChannel.register(mSelector, SelectionKey.OP_READ);
            Log.d(TAG, mClientName + " 已经跟服务端连接上");
        }
    }

    private void handleRead() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int bytesRead = mSocketChannel.read(byteBuffer);
        if (bytesRead > 0) {
            String inMsg = new String(byteBuffer.array(), 0, bytesRead);
            Log.d(TAG, mClientName + " 收到 服务端 数据: " + inMsg);
        } else {
            mSocketChannel.close();
        }

    }

    private void handleWrite() throws IOException {
        if (TextUtils.isEmpty(mSendMsg)) {
            return;
        }
        ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
        sendBuffer.put(mSendMsg.getBytes());
        sendBuffer.flip();

        mSocketChannel.write(sendBuffer);

        Log.d(TAG, "--------------------------------------");
        Log.d(TAG, mClientName + " 发送数据: " + mSendMsg);

        mSendMsg = null;
        mSocketChannel.register(mSelector, SelectionKey.OP_READ);
    }

    /**
     * 发送数据
     *
     * @param msg
     * @throws IOException
     */
    public void sendMsg(String msg) {
        if (mSelector == null || !mSelector.isOpen() || mSocketChannel == null || !mSocketChannel.isOpen()) {
            return;
        }
        try {
            mSendMsg = msg;
            mSocketChannel.register(mSelector, SelectionKey.OP_WRITE);
            // 进行呼醒,因为在int select = mSelector.select();中阻塞住了
            mSelector.wakeup();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 断开连接
     */
    public void disconnectService() {
        if (mSelector == null || !mSelector.isOpen() || mSocketChannel == null || !mSocketChannel.isOpen()) {
            return;
        }
        try {
            Log.d(TAG, "--------------------------------------");
            Log.d(TAG, mClientName + " 主动断开跟 服务端 连接");
            if (mSelector != null) {
                mSelector.close();
                mSelector = null;
            }
            if (mSocketChannel != null) {
                mSocketChannel.close();
                mSocketChannel = null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPClient类对外就是对应两种按钮事件:连接服务端、断开连接,基本上跟服务端TCPServer类的逻辑很像。

3.3 输出日志

运行程序后,相应执行连接和断开按钮会能输出以下日志:

2020-03-03 17:45:15.967 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:15.968 30533-30533/com.zyx.myapplication I/Choreographer: Skipped 4 frames!  The application may be doing too much work on its main thread.
2020-03-03 17:45:15.974 30533-30601/com.zyx.myapplication D/FlymeTrafficTracking: tag  (69) com.zyx.myapplication Thread-4 uid 10238
2020-03-03 17:45:15.976 30533-30601/com.zyx.myapplication D/TCPClient**********: 客户端A 已经跟服务端连接上
2020-03-03 17:45:15.976 30533-30580/com.zyx.myapplication D/TCPServer----------: 服务端 已经跟 客户端(/127.0.0.1:41996) 连接上
2020-03-03 17:45:17.816 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:17.819 30533-30601/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-03-03 17:45:17.819 30533-30601/com.zyx.myapplication D/TCPClient**********: 客户端A 发送数据: 你好吗?
2020-03-03 17:45:17.820 30533-30580/com.zyx.myapplication D/TCPServer----------: 服务端 收到 客户端(/127.0.0.1:41996) 数据:你好吗?
2020-03-03 17:45:17.820 30533-30580/com.zyx.myapplication D/TCPServer----------: 服务端 回复 客户端(/127.0.0.1:41996) 发送数据:你好!
2020-03-03 17:45:17.822 30533-30601/com.zyx.myapplication D/TCPClient**********: 客户端A 收到 服务端 数据: 你好!
2020-03-03 17:45:20.020 30533-30533/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-03-03 17:45:20.020 30533-30533/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-03-03 17:45:20.021 30533-30533/com.zyx.myapplication D/TCPClient**********: 客户端A 主动断开跟 服务端 连接
2020-03-03 17:45:20.021 30533-30601/com.zyx.myapplication D/FlymeTrafficTracking: untag(69) com.zyx.myapplication Thread-4 uid 10238 4047ms
2020-03-03 17:45:20.023 30533-30580/com.zyx.myapplication D/TCPServer----------: 服务端 收到 客户端(/127.0.0.1:41996) 断开请求

4 总结

好了,到此Socket的使用包括长连接、NIO都已通过上篇和本篇博文介绍完毕,有兴趣的朋友可以将两篇文章中的两个Demo结合来搭建一个属于自己长连接框架。点击下载Demo

 

发布了106 篇原创文章 · 获赞 37 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/104062815