1. BIO 同步阻塞IO
1.1 BIO服务器端示例代码
public class JavaBioServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(7777);
System.out.println("服务端启动...");
while (true) {
// 获取socket套接字
// accept()阻塞点
final Socket socket = serverSocket.accept();
System.out.println("有新客户端连接上来了...");
//一个客户端请求对应一个处理线程
new Thread(new Runnable() {
public void run() {
try {
// 获取客户端输入流
InputStream is = socket.getInputStream();
byte[] b = new byte[1024];
while (true) {
// 循环读取数据
// read() 阻塞点
int data = is.read(b);
if (data != -1) {
String info = new String(b, 0, data, "UTF-8");
System.out.println(info);
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
1.2 BIO客户端示例代码
public class JavaBioClient {
public static void main(String[] args) throws Exception{
java.net.Socket s = new java.net.Socket("localhost", 7777);
java.io.OutputStream out = s.getOutputStream();
String msg = "hello server bio";
// 阻塞,写完成
out.write(msg.getBytes("UTF-8"));
s.close();
}
}
1.3 BIO模式的缺点:
- 同步阻塞:accept、read、write操作都是阻塞方法
2. NIO 同步非阻塞
2.1 NIO基础知识
NIO由如下几部分组成:
- buffer
- channel
- selector
2.1.1 buffer
buffer的类图关系如上图所示,在buffer的实现类中,如ByteBufer的子类中,分为堆内存缓冲和直接内存缓冲两类实现。至于对buffer的操作api,这里暂不做说明。
2.1.2 channel
常见的channel如下图所示:
2.1.3 selector
在NIO中,一个selector可以管理多个channel的IO事件,并在IO事件触发时,得到通知。
2.2 示例代码
/**
* 服务端代码
*/
public class JavaNioServer {
public static void main(String[] args) throws Exception {
// 1. 创建Selector对象
Selector selector = Selector.open();
// 2. 向Selector对象绑定通道
// a. 创建可选择通道,并配置为非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// b. 绑定通道到指定端口
ServerSocket socket = server.socket();
socket.bind(new InetSocketAddress(7777));
// c. 向Selector中注册感兴趣的事件
server.register(selector, SelectionKey.OP_ACCEPT);
// 3. 处理事件
while (true) {
// 该调用会阻塞,直到至少有一个事件就绪、准备发生
selector.select();
// 一旦上述方法返回,线程就可以处理这些事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
//切记一定要remove
iter.remove();
//处理读写和具体业务逻辑,可以是在当前线程中处理,也可以在子线程中处理
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// 接收客户端的连接,并创建一个SocketChannel
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 将SocketChannel和感兴趣事件注册到selector
sc.register(selector, SelectionKey.OP_READ);
}else if (key.isReadable()) {
// 读数据的处理
}else if(key.isWritable()){
//写数据处理
}
}
}
}
}
}
2.3 selector和channel关系
在一个程序中(可以是客户端,也可以是服务器端),可以有多个selector(一般只有一个),有多个channel,每个channel需要向某个selector注册,并说明感兴趣的操作,注册操作返回一个selectionKey。即selectionKey是channel和selector之间的桥梁。
一个selector内部维护了3个selectkey集合,已注册的键集合,敢兴趣事件触发的计划,已经取消的集合。
一个selectkey,可以知道该key对应的selector和channel,该key感兴趣的操作,那些感兴趣的操作已经准备就绪了,以及一个附加对象。
2.4 优缺点
优点:同步非阻塞。可以用一个线程监控N个通道进行网络通信。
缺点:编程难道较大,如果以原生的NIO进行网络应用开发,需要进行复杂二次封装,并监控封装后框架的稳定性。
3. netty框架 异步非阻塞
netty是一个异步的事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络服务器和客户端程序。netty是在javaNIO基础上,进行了各种封装。
3.1 示例代码
/**
* 服务端
*/
public class MyServer2 {
public static void main(String[] args) {
//负责接收客户端的连接请求
EventLoopGroup boosGroup = new NioEventLoopGroup(1);
//负责接收客户端读写请求
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解码器
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//编码器
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MyServerHandler());
// pipeline.addLast(new MyServerHandler1());
// pipeline.addLast(new MyServerHandler2());
}
});
ChannelFuture channelFuture = bootstrap.bind(9872).sync();
System.out.println("系统启动成功!!!");
channelFuture.channel().closeFuture().sync();
System.out.println("系统执行完成!!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* 客户端
*/
public class MyClient2 {
public static void main(String[] args) {
EventLoopGroup group=new NioEventLoopGroup();
try {
Bootstrap boot = new Bootstrap();
boot.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline=ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MyClientHandler());
}
});
ChannelFuture channelFuture = boot.connect("localhost", 9872).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
上述代码都是模块代码,唯一需要开发的是Handler,如MyServerHandler。pipeline可以理解成一个拦截器链,handler可以理解成一个具体的拦截器(这种说法不太准确)。
3.2 netty结构说明
在ServerBootstrap类中,包含了2个事件循环组(EventLoopGroup),事件循环组可以理解成线程池,bossGroup这个事件循环组主要负责接收客户端的连接请求,workerGroup这个事件循环组主要负责读写IO请求。默认2个事件循环组里的EventLoop为CUP核数的2倍。
在事件循环组中,包含一个EventExecutor数组,用来保存N个EventLoop,EventLoop可以理解成是一个线程。同时还包含一个选择器,选择器作用是从EventLoop数组中选择一个EventLoop进行服务。
一个EventLoop包含一个selector,并且将负责的通道注册到这个selector上,当select方法返回时,创建一个pipeline,并在pipeline上添加相应的处理器,并根据pipeline上处理器,依次执行相关逻辑。
一个pipeline可以理解为一个拦截器链,链中包含一个headContext、一个TailContext以及多个业务相关的HandlerContext,一个HandlerContext对应一个Handler。
假设服务器为16核,那么默认一个事件循环组中应该包括32个事件循环,而一个事件循环中有一个selector,假设一个selector每秒可以同时管理1000个客户端连接事件,那么整个系统的并发为:1000*32=32000。
4. 网络模式 reactor模式
4.1 单线程模式
缺点:
- 服务端串行处理,且责任过重(需要建立连接、处理读写和具体业务逻辑)
4.2 多线程模式
相比单线程模式的优点:
- 服务端能并行处理,即服务端在主线程中和客户端进行连接的建立,子线程中进行读写和业务逻辑的处理。
缺点:
- 服务器子线程数量不可控,线程过多时会导致大量的线程上下文切换
4.3 线程池模式
相比多线程模式的优点:
- 服务端能并行处理,且能控制后台线程数量
缺点:
- 后台线程如果处理业务逻辑时间过长,则当线程池没有空闲线程时,将会阻塞服务端的主线程
4.4 IO与业务逻辑分离模式
将服务器端分为3个部分,用一个线程来完成连接的建立,用一个线程池来实现具体channel的读写,用一个线程池来实现具体业务逻辑处理。
注意:如果具体业务逻辑简单,执行时间较短,则可以直接用IO线程来执行。如果有耗时操作(如数据存库),最好将耗时操作用单独的线程执行。
优点:IO读写操作和具体业务逻辑处理分开,理论上能支持更大的并发读写操作。
缺点:需要根据并发情况调整IO线程池大小,需要根据业务逻辑的执行性能和并发情况调整业务线程池大小。
4.5 netty 网络模式
netty服务器端,有2个EventLoopGroup(boosGroup和workerGroup),职责分别为为客户端建立连接、处理IO读写,即实现了线程池模式,可根据具体业务逻辑的耗时,实现IO与业务逻辑分离模式。以前的代码的网络模式都属于线程池模式,以下代码为实现IO与业务逻辑分离模式。
public class MyServer2 {
public static void main(String[] args) {
//负责接收客户端的连接请求
EventLoopGroup boosGroup = new NioEventLoopGroup(1);
//负责接收客户端读写请求
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//解码器
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
//编码器
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new MyServerHandler3());
}
});
ChannelFuture channelFuture = bootstrap.bind(9872).sync();
System.out.println("系统启动成功!!!");
channelFuture.channel().closeFuture().sync();
System.out.println("系统执行完成!!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerHandler3 extends ChannelInboundHandlerAdapter {
private static ThreadPoolExecutor pool=new ThreadPoolExecutor(5,10,60,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(1024));
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务端收到的消息为:"+msg);
//将读取到的客户端数据 提交给线程池进行处理
pool.execute(new MyThread(ctx,msg));
}
private static class MyThread implements Runnable{
private ChannelHandlerContext ctx;
private Object msg;
public MyThread(ChannelHandlerContext ctx, Object msg){
this.ctx=ctx;
this.msg=msg;
}
public void run() {
//执行具体的耗时业务逻辑
this.ctx.writeAndFlush("执行结果:");
}
}
}