趣谈java网络编程

204 阅读4分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

在进行任何框架学习之前,了解它们的思想是很有必要的,这会使我们走在论证的基础上,并在此基础上发现真正有价值的东西,并将其应用于工程实践当中,发挥它的价值。

本篇的内容来源于Netty的贡献者之一Norman Maurer,Norman Maurer是Netty的核心贡献者之一,Norman Maurer最初发现Netty当然也是基于业务上的需求,不过很快,他就着迷了,在它熟悉Netty之后,他就致力于改进和回馈这个开源社区。

深入的了解Nettty,我们就不得不回到那个落后的年代,让我们把时间倒推到1995年,看看早期的网络编程是什么样的吧。

早期的java网络编程

早期的网络编程人员,需要学习大量的C语言套接字,去处理他们在不同操作系统上遇到的问题,虽然java封装了一些门面库,但对于设计和实现复杂的客户端来说,你不得不写大量的样板代码,我想每个使用过java网络API的人,在开始进行网络编程的时候都写过类似代码:

//创建一个新的serverSocket 用于监听port端口
ServerSocket serverSocket = new ServerSocket(port);
while (true) {
    //等待接收新的连接
    Socket clientSocket = serverSocket.accept();
    //转换流对象
    BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
   //.....进行流处理
    

}

为了便于理解,我们可以用一张图来描述代码执行的整个过程,这样看起来更直观一些:

image.png

ServerSocket上的accept()方法将会一直等待到一个连接的到来,如果此时其它连接也到来了,那只能等着,也就是我们常说的阻塞。

这有点像什么呢?我们可以想象一下我们平时煮面的场景,水没开的时候,你只能等待,如果水开了才能下面,你说那很好办,我们可以为每个请求都创建一个线程来解决上面的问题。

于是,一个很好的想法诞生了💡,就像下面这样:

image.png

很好,我们的服务器确实已经知道怎么处理多个请求了,但新的问题来了,就好比你要为每次煮面都提前备好热水,你以为解决了我们等待的问题,实际上却制造了新的问题,也就是说煮面变得复杂了。

同样,为每个请求都创建一个线程,有可能造成大量的线程处于休眠状态,它们只能等待数据的输入或者数据的准备就绪,这对宝贵的资源来说是一种浪费,不光如此你还要为线程栈分配内存,大小在64kb-64mb之间不等,另外上下文的切换也变得麻烦,怎么看都不是一种更好的选择。

我们需要重新思考我们的设计,使我们的服务器只在没有来自任何输入或输出流的事件需要响应时才等待,并且我们还得使用少量线程来完成这个事。

java NIO

根据上面的需求,由于我们只能使用少量线程来处理我们的网络请求,同时还得只在没有任何时间的时候才进行等待,那我们就得将所有的请求看成一个组合,而不是把关注点放在单个请求上。这就是事件通知机制,于是,我们可以对我们的设计继续进行改进。

image.png

Sun的开发人员使用这个思想作为他们解决阻塞方案的基础,他们也给这个方案起了一个很好听的名字:New IO。

这个设计实现上使用了一个单一的线程处理多个并发请求的连接,可以在任意时间检查任何操作的读写状态,总体来看相比阻塞IO,该设计使用了更少的线程,减少了线程上下文的切换,当然,在没有IO任务时,这些线程也可以做一些其他操作。