文章目录
BIO、NIO到Netty
一、BIO
- BIO(Blocking IO),即阻塞IO,在 Linux IO模型 中描述过BIO的特点,它在等待数据的过程中线程是会阻塞的,直到数据到来,因此一个连接就需要一个线程去处理。
1.1 代码
1.1.1 客户端
- 客户端代码启动后,将连接的socket对象传到两个线程中,一个写线程负责写数据到服务端,一个读线程负责接收服务端的数据,客户端写入over 则会停止通信。
public class BioClient {
private static final int PORT = Utils.PORT;
private static final String IP = Utils.IP;
static PrintWriter printWriter = null;
public static void main(String[] args) throws IOException {
Socket socket = new Socket(IP, PORT);
System.out.println("Input your info:");
//新启动一个写线程用于发送消息到服务端
//新启动一个读线程用于接收服务端的响应消息
new WriteThread(socket).start();
new ReadThread(socket).start();
}
static class WriteThread extends Thread {
static Socket socket;
WriteThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
String line;
while (true) {
line = new Scanner(System.in).next();
printWriter.println(line);
if ("over".equalsIgnoreCase(line)) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("write thread end ...");
}
}
}
static class ReadThread extends Thread {
static Socket socket;
ReadThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while (true) {
while ((line = bufferedReader.readLine()) != null) {
if ("over".equalsIgnoreCase(line)) {
return;
}
System.out.printf("收到数据: %s", line);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("read thread end ...");
clear(socket);
}
}
}
private static void clear(Socket socket) {
if (socket != null) {
try {
System.out.println("client close...");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1.1.2 服务端代码
- 服务端循环等待客户端的连接,收到连接后交给一个线程去处理,处理的逻辑很简单,如果收到的是over就结束,不是的话就返回收到的内容。
public class BioServer {
private static final int PORT = Utils.PORT;
private static ServerSocket serverSocket;
/**
* 执行任务的线程池
*/
private static ExecutorService executorService = new ThreadPoolExecutor(
5,
10,
10L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(20),
new NamedThreadFactory()
);
private static void start() throws Exception {
try {
//1.绑定端口
serverSocket = new ServerSocket(PORT);
System.out.println("Bio server start , the port listening is : " + PORT);
while (true) {
//1.服务端等待socket上客户端的连接,注意这个方法会一直阻塞直到有连接位置
Socket accept = serverSocket.accept();
System.out.println("New connection established ...,ready to execute the task... ");
//2.将建立的连接交给线程处理,因此一个连接需要一个线程处理
executorService.execute(new BioServerHandler(accept));
}
} finally {
if (serverSocket != null) {
serverSocket.close();
}
}
}
public static void main(String[] args) throws Exception {
start();
}
}
/**
* 处理逻辑很简单,如果客户端输入的是“over”,那么就断开此次连接,反之则给与一个回复表示自己收到了。
* @Date 2019/4/22 12:43
*/
public class BioServerHandler implements Runnable {
private Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
System.out.println("服务端处理端口:" + socket.getPort());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
String line;
String result;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("Server received :" + line);
if ("over".equalsIgnoreCase(line)) {
printWriter.println("over");
break;
}
printWriter.println("收到了!len:" + line.length());
}
} catch (Exception e) {
//如果客户端关闭,则提示连接关闭
if (e instanceof SocketException && "Connection reset".equalsIgnoreCase(e.getMessage())) {
System.out.println("Connection closed ... ");
} else {
e.printStackTrace();
}
} finally {
clear();
}
}
private void clear() {
if (socket != null) {
try {
System.out.println("server close...");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
下面是服务端打印:
Bio server start , the port listening is : 12345
New connection established ...,ready to execute the task...
服务端处理端口:53930
New connection established ...,ready to execute the task...
服务端处理端口:53937
New connection established ...,ready to execute the task...
服务端处理端口:53944
Server received :client1
Server received :client2
Server received :client3
三个客户端分表输入client1、client2、client3:
Input your info:
client1
Input your info:
client2
Input your info:
client3
- 可以看到,服务端监听12345端口,但是每次接收到新的连接,通过 Socket accept = serverSocket.accept() 就会得到一个Socket对象,该Socket对象会使用一个新的空闲端口和客户端建立连接,并交给一个线程处理。(这里容易误认为通信所使用的端口就是监听的端口)
1.2 BIO分析
- BIO的优点是简单,每接受一个新的客户端连接之后,交给一个新的线程去处理业务,服务端监听在一个固定的端口,每当接收一个新的连接之后,将使用一个新的空闲端口和客户端进行通信。
- 缺点:不能支持大量的连接,一个连接需要一个线程,因此需要消耗一个线程的资源,操作系统中线程是宝贵的资源,因此当并发的客户端数量增多时,服务端的资源消耗很容易达到瓶颈。
- 代码上,我们重点可以只看服务端,服务端使用 ServerSocket 绑定一个端口,ServerSocket 在死循环中不断的监听客户端,每收到一个客户端连接之后通过 accept 方法返回一个 Socket 代表与客户端的一个 TCP连接,然后由一个线程去处理这个连接,处理线程通过Socket对象可以拿到服务端处理所使用的端口,客户端的地址端口,以及输入输出流等对象,完成读写操作。
二、NIO
2.1 NIO核心三剑客
- Selector:选择器,实现一个线程就能监听多个 Channel 的状态;可以把 Selector 理解为一个管家(可以由一个线程或者线程池负责),它可以管理很多的通道,不再像BIO那样一个连接需要一个线程了
1.Selector多路选择器可以管理多个channel,在不同平台对应的抽象不一样,在Linux中channel对应文件描述符,在Windows中对应句柄。如此完成一个线程对多个连接的管理,
因此真正处理数据的线程就不需要阻塞了。等到真正某个channel有数据需要读写的时候,通知到对应的事件处理器来处理这个连接,这个处理的过程是阻塞的,在处理的过程中
也是对缓冲区的处理,而不需要对流进程处理。
- Channel:Channel 通道代表一个连接,这个连接不仅仅是与网络设备,也可能是文件。通道有打开和关闭状态,打开状态下可以执行IO操作。在NIO中比较核心的Channel是
ServerSocketChannel 和 SocketChannel,二者分别代表服务端监听等待连接的Channel和客户端发起连接的Channel,有点类似于 ServerSocket 和 Socket 之间的关系。
不过这两个都是抽象类。不过这里是讲 NIO,Netty就是基于NIO的,但是在Netty中又重新定义了Channel接口以及 ServerSocketChannel 等继承体系。
NIO中Channel和BIO中流的区别:
普通IO流中,中要么是输入流要么是输出流,不可能同时为输入和输出流,Cahnnel是双向的,NIO中通过flip既可以读也可以写
普通IO流中直接对流进行读写,NIO中是通过buffer操作,永远不能直接操作Channel,
- Buffer:缓冲区,用于和 Channel交互,从Channel中读取数据到Buffer,从Buffer将数据写入到Channel,缓存区的引入让线程无需等待IO,数据到达时可以先写入buffer,再由buffer交给线程,也让读写更加灵活。Buffer本质是一块内存区域,通过buffer和Channel通道打交道,不能直接去操作Channel,读写一块内存区域显然是灵活的,可以通过指针来灵活处理
2.2 代码
- NIO 代码部分主要看服务端的代码,看服务端如何通过一个线程来管理多个连接(Channel)
public class NioServerTest {
private static final int[] PORTS = new int[]{12345, 12346, 12347};
public static void main(String[] args) throws Exception {
//1.创建一个Selector
Selector selector = Selector.open();
for (int port : PORTS) {
//2.创建 serverSocketChannel,注册到 selector 选择器 , 设置非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//3.端口绑定,通过 ServerSocketChannel 关联的 ServerSocket 绑定
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(port));
}
while (true) {
//4.select 是阻塞方法,有事件就返回
int num = selector.select();
//5.获取事件,可能多个通道有事件,因此返回的是一个集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//可接受事件
if (selectionKey.isAcceptable()) {
//拿到channel对象
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//得到 SocketChannel ,代表 TCP 连接对象
SocketChannel socketChannel = channel.accept();
System.out.println("服务端处理端口:" + socketChannel.socket().getPort());
//配置非阻塞,由此可以看到客户端 socketChannel 也可以是非阻塞的,
// configureBlocking 方法实际上定义在父类,因此客户端服务端都是非阻塞的
socketChannel.configureBlocking(false);
//也把连接对象注册到selector,连接对象关心的应该是读写事件
socketChannel.register(selector, SelectionKey.OP_READ);
//移除非常关键,因此这个连接事件已经处理了,不移除的话会多次处理
iterator.remove();
System.out.println("获取到客户端的连接: " + socketChannel);
} else if (selectionKey.isReadable()) {
//拿到channel对象
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
int readBytes = socketChannel.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("服务端收到消息 : " + body);
//将消息写回客户端
byte[] resp = body.getBytes();
ByteBuffer write = ByteBuffer.allocate(body.getBytes().length);
write.put(resp).flip();
socketChannel.write(write);
}
iterator.remove();
}
}
}
}
}
- 服务端监听3个端口,多个客户端分别从三个端口连接服务端,打印如下:
//服务端打印
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:54844]
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12346 remote=/127.0.0.1:54856]
获取到客户端的连接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12347 remote=/127.0.0.1:54866]
服务端收到消息 : client1
服务端收到消息 : client2
服务端收到消息 : client3
- 可以看到,服务端可以监听多个端口,每个端口都是由一个 ServerSocketChannel 监听,(实际上是ServerSocket监听),而这些ServerSocketChannel都可以交给一个 Selector 来管理;与此同时,监听端口的 ServerSocketChannel 每次收到一个连接请求之后产生一个可连接事件,并通过accept方法会创建一个SocketChannel对象,这个SocketChannel也可以交给Selector来管理,处理后续的 Readable 可读事件。由此我们看到 Selector 可以管理多个 ServerSocketChannel 和多个 SocketChannel,前者用于监听客户端的连接,连接到来然后accept之后就会产生一个SocketChannel,大致的流程图如下:
2.3 NIO小结
- NIO 实现了非阻塞的网络通信,它通过 Selector 组件来管理很多连接(Channel代表一个连接,其实就是管理很多Channel),一个 Channel 没有事件触发时,就会去看下一个Channel是否有事件发生,因为大量的Channel其实需要处理IO的工作量是很少的,大量的时间都无事可做,因此一个线程通过Selctor就可以处理大量的连接,由此实现了一个线程去应对高并发。
- 值得注意的是NIO中 Selector 管理的 Channel 大致有两类,一类是 ServerSocketChannel ,此类是监听端口的Channel,它等待客户端连接;另一类是 SocketChannel,在ServerSocketChannel 调用 accept 接受客户端连接时会得到一个SocketChannel,它是处理与客户端的读写事件的 Channel,这两类 Channel 大致和 ServerSocket 和 Socket 的关系类似,关于两组Socket和Channel更多内容在后续Channel的文章中介绍。
三、BIO和NIO
3.1 对比
- Java中BIO和NIO有一些区别,如下:
对比项 | BIO | NIO |
---|---|---|
读写 | 基于流( Stream ) | 基于缓冲区( Buffer ) |
模式 | 阻塞 IO | 非阻塞 IO |
核心组件 | 无 | 选择器或者多路复用器(Selector) |
优点 | 大数据量传输或者并发不高时,性能更好,响应更及时 | 较少数量的线程就可以管理较多的连接,支持更高并发 |
缺点 | 一个线程处理一个请求,难以支持并发数高的场景 | 响应有微弱延时 |
应用场景 | 并发不高场景下的基于线程的服务模式 | 基于事件的服务模式 |
3.2 关于流和buffer
- BIO 是面向流的,字节流或者字符流,流的特点是:单向且顺序;要么输入流,要么输出流,不能为双向,另一个就是顺序读写,我们不能回溯或者跳转到某个位置重新操作。
- Buffer是一块内存区域,NIO下是通过buffer和Channel通道打交道的,不能直接去操作Channel,读写一块内存区域显然是灵活的,可以通过指针来灵活处理。
3.3 关于阻塞
-
BIO在读取数据时会一直阻塞,如果此时没有数据或者数据尚未准备好,线程会一直阻塞直到数据准备好,显然这个阻塞等待的过程线程不能做其他的事情是一种很大的资源浪费。这种模式下,对于一个客户端的连接,服务端就需要一个线程来处理,即使客户端这边一直不发数据,服务端也要阻塞在那里等着。
-
NIO模式下如果没有数据,那么线程会立即返回,返回后可以对下一个连接进行读取操作,这样不阻塞就可以让一个线程处理多个连接。而在网络连接中,读取数据往往对时间的占比不多,比如即时通信,很多连接只会在其一小部分时间里面发送少量数据,此种情况用NIO就很有优势,如果用BIO就需要很多线程且每个线程有大量的时间是被阻塞的。
四、Netty
4.1 了解Netty
-
Netty是基于Java NIO的一个异步事件驱动的网络通信框架,其底层是依赖于NIO相关的API的。和NIO一样,Netty的核心特点是异步和事件驱动;
-
异步:Netty 中Api基本是异步的,调用Api之后会立刻返回,但是并不确定结果是否成功,需要通过回调来判断结果。异步方式比同步方式复杂很多,但是性能更好;
-
事件驱动:Netty本身实现了很多协议,将事件映射到一个个回调方法上,对应事件发生后,对应的方法就会被调用。
-
参照 Netty官网 的部分描述
设计:各种传输类型(阻塞/非阻塞)下统一的API、可定制化的线程模型、灵活的事件模型
性能:更高的吞吐,更低的延迟、更少的资源消耗、减少不必要的内存拷贝(零拷贝)
安全:SSL支持
- 下面是Netty的模块划分:
Core :核心部分,底层的网络通用抽象和部分实现。
Extensible Event Model :可拓展的事件模型。Netty 是基于事件模型的网络应用框架。
Universal Communication API :通用的通信 API 层。Netty 定义了一套抽象的通用通信层的 API,比如NIO和BIO的切换,所使用的API非常接近 。
Zero-Copy-Capable Rich Byte Buffer :支持零拷贝特性的 Byte Buffer 实现。
Transport Services :传输( 通信 )服务,具体的网络传输的定义与实现。
Socket & Datagram :TCP 和 UDP 的传输实现。
HTTP Tunnel :HTTP 通道的传输实现。
In-VM Piple :JVM 内部的传输实现。
Protocol Support :协议支持。Netty 对常用协议的编解码实现。比如:HTTP、DNS、Redis、telnet、sctp 等,在Netty源码的 example模块有很多示例。
- 另外Netty在很多场景和框架也得到了应用:网络通信,Spark、Dubbo等
4.2 Netty 5
- Netty5 为什么被废弃?Netty5 使用 Fork-Join Pool,提高了复杂性,但是在性能上并没有明显提升,在github上已经被废弃,可以不在Netty5上花太多时间。
五、NIO到Netty
- 既然JDK的原生NIO就可以支持基于事件驱动的非阻塞IO,那为什么不直接使用JDK的NIO,而诞生并推荐使用Netty?通常选择一个替代方案或者一个新的方案有两点:做的更多或者做的更好,Netty 在做的更多更好这两方面体现在何处呢?
5.1 更多
-
支持应用层协议,比如用Netty实现Http协议就很方便,使用JDK的NIO,相关的编解码等工繁杂,相关协议细节复杂性可想而知
-
TCP半包粘包问题,应用层的信息在TCP层可能会拆成多个包或者多个请求合成一个包,JDK NIO它只是构建了一个通信的通道但是不会处理这些问题,netty可以帮我们解决
-
定制功能,比如流量整型,黑白名单等,在Netty中可以较为容易的实现
-
完善的断连、Idel、异常处理等,Netty已经支持的很好了
5.2 更好
-
Netty解决了Java Nio中的 epoll bug,(一个导致Cpu 100%的bug,在Linux 2.4下异常唤醒,却没有事件发生,导致Cpu100% ,称为:epoll bug 也叫空轮训bug,而且在JDK中并未根本解决,Netty中源码会判断Cpu空转的次数,超过一定的次数就会rebuild selector 解决该bug)
-
TCP协议中的IP_TOS参数,它控制了IP包的优先级和Qos选型,使用的时候会抛出异常,提示选项找不到,在JDK12才会解决该问题,是Netty的使用者发现由Netty维护者report该问题,Netty中直接不支持该选型,避免错误。
-
Api更加强大,友好;JDK中的Api友好性差,比如JDK中的buffer的单指针,内部不能扩容,而Netty读写切换更方便,可以扩容,另外ThreadLocal中,Netty中有对应的优化后的ThreadLocal,性能更好
-
Netty屏蔽了细节,比如NIO的切换,使用Java的NIO修改非常多,Netty可能只需要修改2行代码
-
屏蔽细节,Jdk的NIO我们需要关系事件,移除事件等,Netty更好用,很多时候我们只需要关注自己的Handler处理器就可以了。
5.3 组件区别
-
在NIO中有三剑客,在Netty中也有对应的体现,不过Netty 并没有直接使用 JDK NIO中原生的 Channel 和 Selector,
-
Channel: Netty 重新定义了 io.netty.channel.Channel 接口,其实现类也全部在 io.netty.channel 包下,对于 Channel 的理解我觉得对比 NIO 中的Channel 没什么问题,Netty 中重新定义只是说为了更好的使用和封装以及实现;
-
Buffer: Netty也没有直接使用NIO的Buffer,而是定义了 io.netty.buffer.ByteBuf,后面的文章我们对比这两个直接的区别,ByteBuf的实现类也都在io.netty.buffer包下;
-
Selector:对于Selector,Netty底层封装了它,但是代码上层体现的是事件循环组 EventLoopGroup ,内部有 EventLoop 代表事件循环,他会监听事件的发送,看起来和Selector功能一样,他的内部就封装了 Selector,其本质是一个线程池,能够不断的监听通道事件。
-
本文大体上介绍和对比了BIO,NIO,引入了Netty,后续先分析NIO的三大组件,再开始Netty的学习和分析。