Netty核心技术
- 一、Netty介绍和应用场景
- 1.介绍
- 2. Netty的应用场景
- 二、JAVA的BIO
- 1.介绍
- 2.应用示例
- 3.代码追踪
- 4.BIO优劣分析
- 三、Java的NIO
- 1.介绍
- 2. 应用示例
- 3.代码追踪
- 4.NIO优劣分析
一、Netty介绍和应用场景
1.介绍
官网:“Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器“”
- Netty主要针对在TCP协议下,面对Client的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输应用
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
综上:要彻底理解Netty,需要先搞懂NIO
2. Netty的应用场景
- 互联网行业:互联网行业: 在分布式系统中, 各个节点之间需要远程服务调用, 高性能的 RPC 框架必不可少, Netty 作为异步高性能的通信框架, 往往作为基础通信组件被这些 RPC 框架使用。
- 游戏行业:无论是手游服务端、还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信。
- 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架, 默认采用 Netty 进行跨界点通信。
- 企业软件:企业和 IT 集成需要 ESB,Netty 对多协议支持、私有协议定制的简洁性和高性能是 ESB RPC 框架的首选通信组件。事实上,很多企业总线厂商会选择 Netty 作为基础通信组件,用于企业的 IT 集成。
- 通信行业:Netty 的异步高性能、高可靠性和高成熟度的优点,使它在通信行业得到了大量的应用
二、JAVA的BIO
1.介绍
- java BIO:java block IO,即java阻塞式IO,到底什么是阻塞式IO,哪里阻塞了,请看下图JAVA的BIO模型图及对比代码即可了解
2.应用示例
- 使用BIO模型编写一个服务器端,监听9091端口,当有客户端连接时,就启动一个线程与之通讯。
- 要求使用线程池机制改善,可以连接多个客户端.
- 服务器端可以接收客户端发送的数据(通过命令:“nc IP地址 端口”)。
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {
public static void main(String[] args) throws IOException {
//1、创建一个线程池,若服务端收到一个客户端连接就启动一个线程与客户端通讯
ExecutorService pool = Executors.newCachedThreadPool();
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(9091);//服务端Socket,绑定到9091端口,用于接收客户端连接请求
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("server started");
while (true) {
/**
* 服务端接收客户端连接,这个地方会阻塞,
*/
final Socket client = serverSocket.accept();//
System.out.println("connect a client");
pool.execute(new Runnable() {
public void run() {
handler(client);
}
});
}
}
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
try {
/**
* 读取客户端发送过来的数据,这个地方会阻塞
*/
InputStream inputStream = socket.getInputStream();
while (true) {
int read = inputStream.read(bytes);
if (read != -1) {
System.out.print(Thread.currentThread().getId() + " : " + Thread.currentThread().getName()+": ");
System.out.println(new String(bytes, 0, read));
} else {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.代码追踪
- 服务端启动步骤
服务端三部曲 创建socket、绑定端口,进行端口监听
启动BIO代码追踪(启动后会在目录下生成许多out.*文件,一般文件大小最大的为主线程的执行步骤) - 查看out.1929文件,在启动过程中,linux内核主要会执行以下操作
①创建socket(会返回一个数字5的文件描述符,相当于java代码中xxxRef的一个引用的对象,这里的5相当于服务端socket的一个引用) - 对应的java代码
②绑定端口(服务端socket绑定端口9091,对应到代码中new ServerSocket(9091);)
对应的java代码
③启动监听
对应的java代码
- 客户端与服务端通信
①客户端连接9091端口,并发送消息( nc 192.168.56.101 9091), - ②服务端收到客户端连接请求,并打印发送过来的数据(线程名为: pool-1-thread-1)
- ③再来一个客户端,并发送数据
- ④服务端收到客户端请求,并打印发送过来的数据(线程名为: pool-1-thread-2)
- 从系统调用角度理解BIO
服务端启动后一直处于等待客户端连接状态,当一个客户端连接过来,accept指令就会收到该客户端的文件描述符(6,可以理解为这个客户端的引用),当服务端没收到客户端请求时,一直会处于accpept阻塞状态,不会执行后续步骤,收到一个客户端请求后,返回一个该客户端的文件描述符,并执行后续步骤
主线程所对应的out.1729文件 - 主线程收到一个客户端连接,起一个线程(clone指令,相当于起一个线程),返回1767,对应一个out.1767文件
- 新起的线程的执行步骤会在out.1767文件中可以看到
- 线程池开辟一个线程后,用于接收文件描述符为6的客户端发送过来的数据,若没发送数据过来,就会一直处于recv状态。这就解释了为啥读写是阻塞的问题(读写数据为新起的线程,所以读写操作在out.1767文件中看到)
- 客户端发送数据过来(aaaaaa)
- 线程收到数据后,执行write操作,当数据写完之后,又处于recv状态,等待客户端发送数据过来
4.BIO优劣分析
优势:
- 容易理解并应用
- 一个线程一个连接、可以建立很多连接
- 问题
客户端连接多的话,会有很多线程开启,浪费资源 - 解决方案:使用NIO模型
三、Java的NIO
1.介绍
- Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java
提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器) - NIO是 面向缓冲区 ,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
- Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
2. 应用示例
NIO代码展示
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;
public class NIOServer {
public static void main(String[] args) throws IOException, InterruptedException {
LinkedList<SocketChannel> clients = new LinkedList<SocketChannel>();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9091));
ssc.configureBlocking(false);//设置非阻塞
while (true) {
Thread.sleep(1000);
SocketChannel client = ssc.accept();//这里不会阻塞,要么返回null,要么有值,BIO这块是阻塞的
if (client == null) {
String time = LocalDateTime.now().format(DateTimeFormatter.ISO_TIME).toString();
System.out.println("current time" +time);
} else {
client.configureBlocking(false);
int port = client.socket().getPort();
String time = LocalDateTime.now().format(DateTimeFormatter.ISO_TIME).toString();
System.out.println("current time is " + time + " client port :" + client.socket().hashCode() + " : " + port);
clients.add(client);//若有新的客户单会将此客户端添加到客户端列表中
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
for (SocketChannel c : clients) {//遍历所有客户端,输出客户端数据
int num = c.read(buffer);//这里也不会阻塞,要么读取到数据返回非0,要么读不到数据,BIO这一块是阻塞的
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}else {
System.out.println("client "+c.socket().getLocalAddress()+"connected ,but no send data to server");
}
}
}
}
}
代码对应流程图
3.代码追踪
- 执行命令:strace -ff -o out java NIOServer
- 每隔1s会获取一次客户端连接,若没有客户端连接则打印null,但不会阻塞
- 客户端连接了但没发送数据,又进行下一次获取客户端操作,也没有发生阻塞
- 客户端发送数据过来了(注意,若有某个或某几个客户端发送大量数据过来,服务端多单线程处理打印客户端发送过来的数据,这个地方是同步执行的)
系统调用追踪
server启动三部曲:创建socket、绑定端口、执行监听-
- 启动server后每隔1s,accept一次,没有客户端连接返回-1,不会阻塞(若阻塞就直指是accpet状态等,处于等待返回值状态)
- 客户端一旦建立连接,accpet返回值变为5(这个5就是客户端的一个文件描述符,相当于java中的xxxRef)
- 客户端连接后没发送数据,服务端又接收下一个客户端的连接,没收到客户端连接,返回值为-1
- 客户端发送数据到服务端之前,read的返回值一直是-1(若为BIO则这里会阻塞(参考上文RecvFrom),等待客户端发送数据过来,),发送数据过来口read返回值为72了,读取数据完了之后,又进行下一轮循环,read返回值为-1.
说到这里,应该对NIO有了深刻的认识,那相对于BIO,NIO有什么优势和不足呢??
4.NIO优劣分析
优势:一个线程可以同时接受客户端请求和处理客户端发送过来的数据,避免了多线程资源浪费
问题:如下所示
观察如下代码:每秒循环一次,会遍历所有的client
那么!!问题来了,假如我有100000个客户端连接,只有100个客户端处于活跃状态(即会向服务端发送数据),每秒进行一次while循环,会遍历100000个客户端,拿到有用的客户端才100个,代价是不是太大了。
怎么解决呢,想象一下,要是有什么办法能够直接获取到这活跃的100个客户端,那么每秒进行一次while循环,只需要获取到这100个活跃客户端的连接不就行了。