欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

BIO阻塞在了哪里?
(last modified Sep 21, 2023, 8:12 PM )
by
侧边栏壁纸
  • 累计撰写 178 篇文章
  • 累计创建 62 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

BIO阻塞在了哪里?

橙序员
2023-08-09 / 0 评论 / 0 点赞 / 300 阅读 / 1,201 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

客户端与服务端之间的交互流程如下:
1. 创建服务端socket,绑定系统端口并监听此端口的客户端连接。
2. 创建客户端连接,指定ip和端口地址。
3. 客户端发起连接请求,服务端接收并受理连接。
4. 连接建立成功,客户端与服务端开始数据交互。
5. 服务端处理客户端请求的流程如下:
- 通过走读源码可以了解到交互的具体流程。
- 关键类的定位:ServerSocket、Socket、SocketImpl等。
- SocketImpl是所有socket实现类(如PlainSocketImpl)的父类,提供了socket相关的方法定义。
- PlainSocketImpl是默认的socket实现类,实现了具体的服务端和客户端操作逻辑。
- DualStackPlainSocketImpl是PlainSocketImpl的拓展类,实现了具体的服务端和客户端操作的相关系统交互。

BIO的C/S架构中,每个客户端连接对应一个线程,服务端与客户端并发访问数呈1:1的关系。这种架构缺乏弹性伸缩能力,当并发访问量过大时,系统可能会产生线程堆栈溢出、创建新线程失败的问题,导致宕机或僵死,无法对外提供服务。

概念

一个客户端(连接)对应一个线程,只有一个服务线程为每个连接分配处理线程

image-20230807174215606

缺点:缺乏弹性伸缩能力,服务端与客户端并发访问数呈1:1的正比关系,并发访问量过大,系统会产生线程堆栈溢出,创建新线程失败的问题,导致宕机或者僵死,不能对外提供服务。

源码解析

抛个问题~

首先我们请思考一个把大象装冰箱的问题,网络编程实际的交互流程应该是什么样的?

  1. 创建服务端socket
  2. 绑定系统端口到服务端socket
  3. 监听此端口的客户端连接
  4. 创建客户端
  5. 连接指定ip,端口地址
  6. 接收连接,服务端受理
  7. 连接成功,客户端建立

源码细节

服务端处理请求的流程如下:

image-20230807195846818

走读源码,我们可以看到如下交互流程:

bio-code

源码中几个关键类的定位如下:

继承关系 类变量 备注
ServerSocket implements java.io.Closeable private SocketImpl impl; 服务端的socket
Socket implements java.io.Closeable SocketImpl impl; 客户端的socket
SocketImpl implements SocketOptions Socket socket = null; ServerSocket serverSocket = null; 所有socket实现类的父类,提供了socket相关的方法定义,包括服务端操作和客户端操作
PlainSocketImpl extends AbstractPlainSocketImpl private AbstractPlainSocketImpl impl; 委托类,由于服务端和客户端创建时默认会创建PlainSocketImpl的拓展类SocksSocketImpl,而SocketImpl的真正实现只能通过委托类指定
AbstractPlainSocketImpl extends SocketImpl private SocketInputStream socketInputStream = null; private SocketOutputStream socketOutputStream = null; 默认的socket实现类,实现了具体的服务端和客户端操作逻辑
DualStackPlainSocketImpl extends AbstractPlainSocketImpl 具体的服务端和客户端操作的相关系统交互

系统交互

BIO的C/S架构中,服务端和客户端的交互如下:

image-20230808153124117

结合源码分析,我们JVM虚拟机其实是对用户缓存进行的操作,所以上述流程在系统层面交互为:

image-20230808152850859

阻塞的原因

BIO慢的核心在于我们创建ServerSocket的accept方法时,accept方法对于客户端连接的请求是单线程阻塞的:

    protected final Object fdLock = new Object();

    //加锁获取文本描述符
    FileDescriptor acquireFD() {
        synchronized (fdLock) {
            fdUseCount++;
            return fd;
        }
    }
    //加锁释放文本描述符
    void releaseFD() {
        synchronized (fdLock) {
            fdUseCount--;
            if (fdUseCount == -1) {
                if (fd != null) {
                    try {
                        socketClose();
                    } catch (IOException e) {
                    } finally {
                        fd = null;
                    }
                }
            }
        }
    }

	//服务端接收连接请求
    protected void accept(SocketImpl s) throws IOException {
        acquireFD();
        try {
            //获取文本描述符成功,则更新
            socketAccept(s);
        } finally {
            releaseFD();
        }
    }

服务端的请求受理默认情况下耗时是可以忽略不计的,但是如果我们想模拟高并发的场景,可以通过继承ServerSocket,增加受理连接时的耗时来模拟连接过多导致服务端瓶颈的情况:

public class SleepSocket extends ServerSocket {
 public SleepSocket(int port) throws IOException {
     super(port);
 }

 @Override
 public Socket accept() throws IOException {
     try {
         Thread.sleep(1000);
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     }
     return super.accept();
 }
}

这样我们在同时有100个线程请求服务端时会发现:

the timeServer is start in Address:0.0.0.0/0.0.0.0:8080
24Read timed out
4Read timed out
34Read timed out
... 读超时 ...
8Read timed out
2Read timed out
16Read timed out
9Read timed out
2023-08-07T18:41:56.100服务端接收消息:client id: 5
68Connection refused: connect
62Connection refused: connect
69Connection refused: connect
...此处有多个连接拒绝的请求...
90Connection refused: connect
92Connection refused: connect
2023-08-07T18:41:57.081服务端接收消息:client id: 7
....省略正常日志.....
java.net.SocketTimeoutException: Accept timed out
	at java.net.DualStackPlainSocketImpl.waitForNewConnection(Native Method)
	at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:135)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
	at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
	at java.net.ServerSocket.implAccept(ServerSocket.java:560)
	at java.net.ServerSocket.accept(ServerSocket.java:528)
	at Bio.SleepSocket.accept(SleepSocket.java:19)
	at Bio.TimeServer.run(TimeServer.java:25)
	at java.lang.Thread.run(Thread.java:748)
The Time server close

进程已结束,退出代码为 0

默认客户端创建连接时,超时时间为0,所以客户端连接服务端会有如下场景:

  1. 连接创建成功,此时会发送请求到服务端并尝试在约定的读超时时间内读取服务端返回结果,如果服务端未返回则会提示“java.net.SocketTimeoutException: Read timed out”
  2. 连接创建失败,即此时服务端正在处理并建立和其他客户端的连接中,无法处理当前客户端建立连接的请求。此时会提示“java.net.ConnectException: Connection refused: connect”
  3. 全部客户端连接处理完成后,等待指定时间后无新客户端连接请求服务端,则服务接收客户端超时,此时会提示“java.net.SocketTimeoutException: Accept timed out“
0

评论区