BIO和NIO有啥区别?为啥要使用-Netty!一文说清!

早期的 Java 网络相关的 API(java.net包) 使用 Socket(套接字)进行网络通信,不过只支持阻塞函数使用。

要通过互联网进行通信,至少需要一对套接字:

  1. 运行于服务器端的 Server Socket。
  2. 运行于客户机端的 Client Socket

Socket 网络通信过程如下图所示:

Socket 网络通信过程简单来说分为下面 4 步:

  1. 建立服务端并且监听客户端请求
  2. 客户端请求,服务端和客户端建立连接
  3. 两端之间可以传递数据
  4. 关闭资源

对应到服务端和客户端的话,是下面这样的。

服务器端:

  1. 创建 ServerSocket 对象并且绑定地址(ip)和端口号(port): server.bind(new InetSocketAddress(host, port))
  2. 通过 accept()方法监听客户端请求
  3. 连接建立后,通过输入流读取客户端发送的请求信息
  4. 通过输出流向客户端发送响应信息
  5. 关闭相关资源

客户端:

  1. 创建Socket 对象并且连接指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)
  2. 连接建立后,通过输出流向服务器端发送请求信息
  3. 通过输入流获取服务器响应的信息
  4. 关闭相关资源

一个简单的 demo

为了便于理解,我写了一个简单的代码帮助各位小伙伴理解。

服务端:

public class HelloServer {
private static final Logger logger = LoggerFactory.getLogger(HelloServer.class);

public void start(int port) {
//1.创建 ServerSocket 对象并且绑定一个端口
try (ServerSocket server = new ServerSocket(port)😉 {
Socket socket;
//2.通过 accept()方法监听客户端请求, 这个方法会一直阻塞到有一个连接建立
while ((socket = server.accept()) != null) {
logger.info(“client connected”);
try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
//3.通过输入流读取客户端发送的请求信息
Message message = (Message) objectInputStream.readObject();
logger.info(“server receive message:” + message.getContent());
message.setContent(“new content”);
//4.通过输出流向客户端发送响应信息
objectOutputStream.writeObject(message);
objectOutputStream.flush();
} catch (IOException | ClassNotFoundException e) {
logger.error(“occur exception:”, e);
}
}
} catch (IOException e) {
logger.error(“occur IOException:”, e);
}
}

public static void main(String[] args) {
HelloServer helloServer = new HelloServer();
helloServer.start(6666);
}
}

ServerSocketaccept() 方法是阻塞方法,也就是说 ServerSocket 在调用 accept()等待客户端的连接请求时会阻塞,直到收到客户端发送的连接请求才会继续往下执行代码,因此我们需要要为每个 Socket 连接开启一个线程(可以通过线程池来做)。

上述服务端的代码只是为了演示,并没有考虑多个客户端连接并发的情况。

客户端:

/**

  • @author shuang.kou
  • @createTime 2020年05月11日 16:56:00
    */
    public class HelloClient {

private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);

public Object send(Message message, String host, int port) {
//1. 创建Socket对象并且指定服务器的地址和端口号
try (Socket socket = new Socket(host, port)) {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//2.通过输出流向服务器端发送请求信息
objectOutputStream.writeObject(message);
//3.通过输入流获取服务器响应的信息
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
return objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
logger.error(“occur exception:”, e);
}
return null;
}

public static void main(String[] args) {
HelloClient helloClient = new HelloClient();
helloClient.send(new Message(“content from client”), “127.0.0.1”, 6666);
System.out.println(“client receive message:” + message.getContent());
}
}

发送的消息实体类

/**

  • @author shuang.kou
  • @createTime 2020年05月11日 17:02:00
    */
    @Data
    @AllArgsConstructor
    public class Message implements Serializable {

private String content;
}

首先运行服务端,然后再运行客户端,控制台输出如下:

服务端:

[main] INFO github.javaguide.socket.HelloServer - client connected
[main] INFO github.javaguide.socket.HelloServer - server receive message:content from client

客户端:

client receive message:new content

资源消耗严重的问题

很明显,我上面演示的代码片段有一个很严重的问题:只能同时处理一个客户端的连接,如果需要管理多个客户端的话,就需要为我们请求的客户端单独创建一个线程。 如下图所示:

对应的 Java 代码可能是下面这样的:

new Thread(() -> {
// 创建 socket 连接
}).start();

但是,这样会导致一个很严重的问题:资源浪费

我们知道线程是很宝贵的资源,如果我们为每一次连接都用一个线程处理的话,就会导致线程越来越好,最好达到了极限之后,就无法再创建线程处理请求了。处理的不好的话,甚至可能直接就宕机掉了。

很多人就会问了:那有没有改进的方法呢?

线程池虽可以改善,但终究未从根本解决问题

当然有! 比较简单并且实际的改进方法就是使用线程池。线程池还可以让线程的创建和回收成本相对较低,并且我们可以指定线程池的可创建线程的最大数量,这样就不会导致线程创建过多,机器资源被不合理消耗。

ThreadFactory threadFactory = Executors.defaultThreadFactory();
ExecutorService threadPool = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), threadFactory);
threadPool.execute(() -> {
// 创建 socket 连接
});

但是,即使你再怎么优化和改变。也改变不了它的底层仍然是同步阻塞的 BIO 模型的事实,因此无法从根本上解决问题。

为了解决上述的问题,Java 1.4 中引入了 NIO ,一种同步非阻塞的 I/O 模型。

再看 NIO

Netty 实际上就基于 Java NIO 技术封装完善之后得到一个高性能框架,熟悉 NIO 的基本概念对于学习和更好地理解 Netty 还是很有必要的!

初识 NIO

NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。

NIO 中的 N 可以理解为 Non-blocking,已经不在是 New 了(已经出来很长时间了)。

NIO 支持面向缓冲(Buffer)的,基于通道(Channel)的 I/O 操作方法。

NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式:

  1. 阻塞模式 : 基本不会被使用到。使用起来就像传统的网络编程一样,比较简单,但是性能和可靠性都不好。对于低负载、低并发的应用程序,勉强可以用一下以提升开发速率和更好的维护性
  2. 非阻塞模式 : 与阻塞模式正好相反,非阻塞模式对于高负载、高并发的(网络)应用来说非常友好,但是编程麻烦,这个是大部分人诟病的地方。所以, 也就导致了 Netty 的诞生。

NIO 核心组件解读

NIO 包含下面几个核心的组件:

  • Channel
  • Buffer
  • Selector
  • Selection Key

这些组件之间的关系是怎么的呢?

  1. NIO 使用 Channel(通道)和 Buffer(缓冲区)传输数据,数据总是从缓冲区写入通道,并从通道读取到缓冲区。在面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是通过 Buffer(缓冲区)处理的。 Channel 可以看作是 Netty 的网络操作抽象类,对应于 JDK 底层的 Socket
  2. NIO 利用 Selector (选择器)来监视多个通道的对象,如数据到达,连接打开等。因此,单线程可以监视多个通道中的数据。
    区)传输数据,数据总是从缓冲区写入通道,并从通道读取到缓冲区。在面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是通过 Buffer(缓冲区)处理的。 Channel 可以看作是 Netty 的网络操作抽象类,对应于 JDK 底层的 Socket
  3. NIO 利用 Selector (选择器)来监视多个通道的对象,如数据到达,连接打开等。因此,单线程可以监视多个通道中的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值