2022-07-24  阅读(5)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/series/article/1945821696

上篇文章大明哥介绍了 SocketChannel 的核心原理及其源码,这篇文章就来介绍如何使用 ServerSocketChannel,分析单独使用 ServerSocketChannel 存在哪些问题。

阻塞模式

我们先看服务端方法:

public static void main(String[] args) throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(100);
    ServerSocketChannel serverSocket = ServerSocketChannel.open()
    serverSocket.bind(new InetSocketAddress(8080));
    List<SocketChannel> channels = new ArrayList<>();
    while(true) {
        SocketChannel sc = serverSocket.accept();
        channels.add(sc);

        for (SocketChannel asc :channels) {
            asc.read(buffer);
            buffer.flip();
            ByteBufferUtil.debugAll(buffer);
            buffer.clear();
        }
    }
}
  • 首先新建一个 ServerSocketChannel 类,同时绑定 8080 端口
  • 然后 while(true)循环调用 accept()来建立连接,同时将该 SocketChannel 加入到 List 集合中,该集合用来装所有与服务端建立连接的 SocketChannel
  • 最后接受客户端发送来的数据,打印出来

现在我们来运行下(这里就不写客户端程序了,就用 MAC 的 iTerm 来模拟即可)。需要开 2 个客户端。

我们先打开 client-01,然后发送一条 “hi,i am client-01”

服务器运行结果:

服务端准确无误打印出 client-01 发送过来的消息(hi,i am client-01)。这个时候你再发一条消息:“hi,i am client-11”,你会惊奇地发现,服务端竟然不输出客户端发来的消息。这个时候你再启动 client-02,神奇的事情发生了,服务端把 client-01 发送的消息(hi,i am client-11)给打印出来了:

为什么会出现这种神奇的现象?主要原因就是该 ServerSocketChannel 是阻塞模式,相关方法都会导致线程的阻塞,当 client-01 建立连接,第一次发送消息时,服务端正常打印消息(hi,i am client-01),这时服务端又运行到 accept(),注意这个方法是阻塞方法,如果没有客户端来建立连接,它会一直阻塞在这里,哪怕 client-01 再次发送消息(hi,i am client-11),服务端也不会打印。这时 client-02 与服务端建立连接,服务端就不会阻塞,打印 client-01 第二次发来的消息(hi,i am client-11)。

所以,阻塞模式存在如下缺陷

  • 单线程情况下,阻塞方法都会导致线程暂停
    • ServerSocketChannel.accept() 会在没有连接建立时让线程暂停,即使有客户端向服务端发送消息,服务单也接收不到直到有新客户端连接服务端,不再阻塞在accept()方法上。
    • SocketChannel.read() 会在通道中没有数据可读时让线程暂停,即使之后有新客户端向服务端发起连接请求也接受不了,直到读取完毕,不再阻塞在read()方法上

所以在单线程情况,服务端几乎不可能正常工作。那多线程呢?多线程情况下,如果连接数过多,必然会导致 OOM,然后线程的上下文切换也会导致性能低下。

非阻塞模式

上面的阻塞模式几乎导致整个服务端是可能使用的,我们是可以使用非阻塞模式来避免的。如下

public static void main(String[] args) throws Exception {
    ByteBuffer buffer = ByteBuffer.allocate(100);
    ServerSocketChannel serverSocket = ServerSocketChannel.open();

    serverSocket.bind(new InetSocketAddress(8080));
    List<SocketChannel> channels = new ArrayList<>();
    while(true) {
        // 非阻塞模式
        serverSocket.configureBlocking(false);

        SocketChannel sc = serverSocket.accept();
        if (sc != null){
          channels.add(sc);
        }

        for (SocketChannel asc :channels) {
            asc.configureBlocking(false);

            int size = asc.read(buffer);
            if (size > 0) {
                buffer.flip();
                ByteBufferUtil.debugAll(buffer);
                buffer.clear();
            }
        }
    }
}
  • 通过 ServerSocketChannel.configureBlocking(false) 将 serverSocket 设置为非阻塞模式,这样 serverSocket 在调用 accept()方法时就不会阻塞了,如果没有连接,则会返回 null
  • 通过 SocketChannel..configureBlocking(false) 将 asc 设置为非阻塞模式,这 asc 在调用 read() 方法就不会阻塞了,如果没有可读数据,它则会返回 -1。

非阻塞模式虽然不会影响业务的使用,但由于在 while(true) 循环里面,CPU 会一直处理运行状态,占用和浪费 CPU 资源。

所以,采用这种 while(true) 循环的暴力方式根本就不适合业务使用,对于 SocketChannel 而言,我们希望他只担任一个通道,传传数据的角色即可,不需再有额外的角色了,故而我们不能放任他们,需要对其进行统一管理,既要有管理器,有连接来了,我就告诉你该建立连接了,有要读的数据,我就告诉你可以读数据了,这样 SocketChannel 是不是就很爽了。在 NIO 中,这个管理器称之为 Selector。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文