第一次深入了解nio,都会感觉非常的难理解阻塞与非阻塞,同步和异步阻塞,非阻塞,下面来了解一下吧
一、理论篇
1、nio 和 netty 是什么?
1、Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始)
2、 Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。
3、主要面向于网络通信
4、分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯等也是使用就是netty。
5、游戏开发中,底层使用netty通讯。
2、 BIO / NIO / AIO 了解
BIO (IO)
同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善,也就是伪异步。
NIO
同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO2.0)
异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
伪异步(线程池处理 IO )
由于BIO一个客户端需要一个线程去处理,因此我们进行优化,使用线程池来处理多个客户端的请求接入
通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
原理:
当有新的客户端接入时,将客户端的Socket封装成一个Task(该Task任务实现了java的Runnable接口)投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小以及线程池的最大值,因此,它的资源占用是可控的,无论多少个客户端的并发访问,都不会导致资源的耗尽或宕机
3、阻塞与分阻塞
1 、IO(BIO)和NIO区别:其本质就是阻塞和非阻塞的区别
2、 阻塞概念:应用程序在获取网络数据的时候,如果网络传输数据很慢,就会一直等待,直到传输完毕为止。
非阻塞概念:应用程序直接可以获取已经准备就绪好的数据,无需等待。
IO为同步阻塞形式,
NIO为同步非阻塞形式,NIO并没有实现异步,在JDK1.7后升级NIO库包,支持异步非阻塞
NIO2.0(AIO)异步非阻塞
关系图
3.1、阻塞
应用程序在获取网络数据的时候,如果程序传输数据很慢,那么程序就一直等待着,直到数据传输完成。
3.2、非阻塞
应用程序直接可以获取已经准备就绪的数据, 无须等待。
BIO和NIO的本质区别就是阻塞和非阻塞。
3.3、同步
应用程序会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某个方法上,直到数据准备完毕。
3.4、异步
所有的IO操作交个操作系统处理,与我们应用程序没有直接关系,我们程序不需要关系IO读写,当操作系统完成了 IO读写的时候,会给我们应用程序发出通知,数据传输完毕。
3.5、同步阻塞
白话文
老张把水壶放到火上,立等水开。老张觉得自己有点傻
示意:
1、客服端向服务器端发了一个大文件,在发文件的时候只能等文件发送完成才能继续执行后面的业务代码
2、服务器端接收文件的时候需要很长的时间,当文件数据传输没有全部传输完的时候,程序一直等待文件传输完毕,没完毕前无法执行执行其他的代码块,程序被阻塞掉了
3.6、同步非阻塞
白话文
老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。
示意:
1、客服端向服务器端发了一个大文件,在发文件的时候只能等文件发送完成才能继续执行后面的业务代码,
2、服务器端接收文件的时候需要很长的时间,这次不直接传输了,而是传输到一个中间程序去,程序监听到文件已经传输完了,才去读取文件数据,程序就不会阻塞掉了
3.7、异步阻塞
白话文
老张把响水壶放到火上,立等水开。老张觉得这样傻等意义不大
示意:
1、客服端向服务器端发了一个大文件,发文件的时候开一个线程去发送文件,并监听文件数据是否全部传输完成,然后继续执行后面的业务代码
2、服务器端接收文件的时候需要很长的时间,当文件数据传输没有全部传输完的时候,程序一直等待文件传输完毕,没完毕前无法执行执行其他的代码块,程序被阻塞掉了
3.8、异步非阻塞
白话文
老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。
示意:
1、客服端向服务器端发了一个大文件,发文件的时候开一个线程去发送文件,并监听文件数据是否全部传输完成,然后继续执行后面的业务代码
2、服务器端接收文件的时候需要很长的时间,这次不直接传输了,而是传输到一个中间程序去,程序监听到文件已经传输完了,才去读取文件数据,程序就不会阻塞掉了
二、NIO 异步非阻塞代码示例
可以看出NIO 的代码非常复杂,所有看看就好,可以拿去运行一下
netty 完美的封装了NIO,一般不会直接使用下列代码的
package com.itmayiedu;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;
/**
* nio 异步非阻塞 客户端
*/
class Client {
public static void main(String[] args) throws IOException {
System.out.println("客户端已经启动....");
// 1.创建通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
// 2.切换异步非阻塞
sChannel.configureBlocking(false);
// 3.指定缓冲区大小
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scanner= new Scanner(System.in);
while (scanner.hasNext()) {
String str=scanner.next();
byteBuffer.put((new Date().toString()+"\n"+str).getBytes());
// 4.切换读取模式
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();
}
sChannel.close();
}
}
/**
* nio 异步非阻塞 服务器端
*/
class Server {
public static void main(String[] args) throws IOException {
System.out.println("服务器端已经启动....");
// 1.创建通道
ServerSocketChannel sChannel = ServerSocketChannel.open();
// 2.切换读取模式
sChannel.configureBlocking(false);
// 3.绑定连接
sChannel.bind(new InetSocketAddress(8080));
// 4.获取选择器
Selector selector = Selector.open();
// 5.将通道注册到选择器 "并且指定监听接受事件"
sChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 轮训式 获取选择 "已经准备就绪"的事件
while (selector.select() > 0) {
// 7.获取当前选择器所有注册的"选择键(已经就绪的监听事件)"
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
// 8.获取准备就绪的事件
SelectionKey sk = it.next();
// 9.判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
// 10.若"接受就绪",获取客户端连接
SocketChannel socketChannel = sChannel.accept();
// 11.设置阻塞模式
socketChannel.configureBlocking(false);
// 12.将该通道注册到服务器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
// 13.获取当前选择器"就绪" 状态的通道
SocketChannel socketChannel = (SocketChannel) sk.channel();
// 14.读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = socketChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
it.remove();
}
}
}
}
三、netty
1、netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。
2、 Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。
3、主要面向于网络通信
4、分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯等也是使用就是netty。
5、游戏开发中,底层使用netty通讯。
创建服务端与客服端
建议用4.X,这里先用5.x演示
pom.xml
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>5.0.0.Alpha2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling -->
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling</artifactId>
<version>1.3.19.GA</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.jboss.marshalling/jboss-marshalling-serial -->
<dependency>
<groupId>org.jboss.marshalling</groupId>
<artifactId>jboss-marshalling-serial</artifactId>
<version>1.3.18.GA</version>
<scope>test</scope>
</dependency>
1、服务器端
package com.itmayiedu;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
/**
* netty 监听类
*/
class ServerHandler extends ChannelHandlerAdapter {
/**
* 当通道被调用,执行方法(拿到数据)
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String value = (String) msg;
System.out.println("服务器端收到客户端msg:" + value);
// 回復客戶端
ctx.writeAndFlush("hello 6666");
}
}
/**
* netty 服务器端
*/
public class NettyServer {
public static void main(String[] args) {
try {
System.out.println("服务器端启动...");
// 1.创建两个线程池,一个负责接收客户端,一个进行传输
NioEventLoopGroup pGroup = new NioEventLoopGroup();
NioEventLoopGroup cGroup = new NioEventLoopGroup();
// 2.创建辅助类.ServerBootstrap 和 Bootstrap都继承自AbstractBootstrap功能都是作为入口,构建服务端和客户端
ServerBootstrap b = new ServerBootstrap();
// 3.创建通道(NioServerSocketChannel ),并设置socket的标准参数
// https://www.cnblogs.com/xiaoyongsz/p/6133266.html?utm_source=itdadao&utm_medium=referral
b.group(pGroup, cGroup).channel(NioServerSocketChannel.class)
//3.1设置缓冲区与发送区大小标识用于临时存放已完成三次握手的请求的队列的最大长度。未设置或所设置的值小于1,默认值50。
.option(ChannelOption.SO_BACKLOG, 1024)
//3.2定义接收或者传输的系统缓冲区buf的大小
.option(ChannelOption.SO_SNDBUF, 32 * 1024)
.option(ChannelOption.SO_RCVBUF, 32 * 1024)
//监听事件
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 设置未string类型
sc.pipeline().addLast(new StringDecoder());
// 使用自定义类ServerHandler 继承ChannelHandlerAdapter 来监听
sc.pipeline().addLast(new ServerHandler());
}
});
// 启动
ChannelFuture cf = b.bind(8080).sync();
// 关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
} catch (Exception e) {
// TODO: handle exception
}
}
}
2、客户端
package com.itmayiedu;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
class ClientHandler extends ChannelHandlerAdapter {
/**
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx )throws Exception {
System.out.println("output connected!");
}
/**
* 通道被调用,执行该方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接收数据
String value = (String) msg;
System.out.println("client msg:" + value);
}
}
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
System.out.println("客户端已经启动....");
// 创建负责接收客户端连接
NioEventLoopGroup pGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
//创建通道
b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
// 连接
ChannelFuture cf = b.connect("127.0.0.1", 8080).sync();
// 发送消息
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("hello".getBytes()));
// 等待客户端端口号关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
}
}
粘包/拆包
1、什么是粘包/拆包
一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
简单来说:
拆包
就是客户端向服务器端发送了一次100个字符的字符串,服务端却接收到了两次数据,把100个字符分成了两个50的字符,或者两个不等长的数据40/60,35/65 什么的
粘包
就是客户端向服务器端发送了两次或多次10个字符的字符串,服务器只接收到了一次数据,并把两次或多次发送的字符串合并到了一起
Netty如果处理TCP协议自动拆包和封包问题
处理办法一(消息定长)
消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
处理办法二(特殊分隔符)
包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
ByteBuf buf = Unpooled.copiedBuffer("_mayi".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
处理办法三(暂不说明)
将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段
代码示例
package com.itmayiedu;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
class ServerHandler extends ChannelHandlerAdapter {
/**
* 当通道被调用,执行方法(拿到数据)
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String value = (String) msg;
System.out.println("服务器端收到客户端msg:" + value);
// 回復客戶端
ctx.writeAndFlush("hello 6666");
}
}
/**
* netty 服务器端
*/
public class NettyServer {
public static void main(String[] args) {
try {
System.out.println("服务器端启动...");
// 1.创建两个线程池,一个负责接收客户端,一个进行传输
NioEventLoopGroup pGroup = new NioEventLoopGroup();
NioEventLoopGroup cGroup = new NioEventLoopGroup();
// 2.创建辅助类.ServerBootstrap 和 Bootstrap都继承自AbstractBootstrap功能都是作为入口,构建服务端和客户端
ServerBootstrap b = new ServerBootstrap();
// 3.创建通道(NioServerSocketChannel ),并设置socket的标准参数
// https://www.cnblogs.com/xiaoyongsz/p/6133266.html?utm_source=itdadao&utm_medium=referral
b.group(pGroup, cGroup).channel(NioServerSocketChannel.class)
//3.1设置缓冲区与发送区大小标识用于临时存放已完成三次握手的请求的队列的最大长度。未设置或所设置的值小于1,默认值50。
.option(ChannelOption.SO_BACKLOG, 1024)
//3.2定义接收或者传输的系统缓冲区buf的大小
.option(ChannelOption.SO_SNDBUF, 32 * 1024)
.option(ChannelOption.SO_RCVBUF, 32 * 1024)
//监听事件
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 消息定长(拆包和封包)
//sc.pi peline().addLast(new FixedLengthFrameDecoder(10));
// 包尾添加特殊分隔符,客户端与服务器端必须一致(拆包和封包)
ByteBuf buf = Unpooled.copiedBuffer("_hello".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
// 设置未string类型
sc.pipeline().addLast(new StringDecoder());
// 使用自定义类ServerHandler 继承ChannelHandlerAdapter 来监听
sc.pipeline().addLast(new ServerHandler());
}
});
// 启动
ChannelFuture cf = b.bind(8080).sync();
// 关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
cGroup.shutdownGracefully();
} catch (Exception e) {
// TODO: handle exception
}
}
}
客户端
package com.itmayiedu;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
class ClientHandler extends ChannelHandlerAdapter {
/**
* 获得服务器发送的消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx )throws Exception {
System.out.println("output connected!");
}
/**
* 通道被调用,执行该方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接收数据
String value = (String) msg;
System.out.println("client msg:" + value);
}
}
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
System.out.println("客户端已经启动....");
// 创建负责接收客户端连接
NioEventLoopGroup pGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
//创建通道
b.group(pGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
// 消息定长(拆包和封包)
// sc.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 包尾添加特殊分隔符,客户端与服务器端必须一致(拆包和封包)
ByteBuf buf = Unpooled.copiedBuffer("_hello".getBytes());
sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
sc.pipeline().addLast(new StringDecoder());
sc.pipeline().addLast(new ClientHandler());
}
});
// 连接
ChannelFuture cf = b.connect("127.0.0.1", 8080).sync();
// 发送消息()
System.out.println("发送消息--包尾添加特殊分隔符(拆包和封包测试)");
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("66666_hello".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("77777_hello".getBytes()));
cf.channel().writeAndFlush(Unpooled.wrappedBuffer("88888_hello".getBytes()));
// 等待客户端端口号关闭
cf.channel().closeFuture().sync();
pGroup.shutdownGracefully();
}
}
四、序列化
另外需要传递对象需要序列化,不然是无法传递对象的, 取数据的时在反序列化成对象
简单来说就是转换成 json /xml 格式的字符串来传递,一边把对象转字符串,一边把json/xml 字符串转对象
1、对象添加序列化方法
继承 implements Serializable,生成序列化唯一id/uuid:private static final long serialVersionUID = 7628556840434732521L;
如下
public class User implements Serializable{
private static final long serialVersionUID = 7628556840434732521L;
}
idea 自动生成序列化教程
https://blog.csdn.net/qq_41463655/article/details/100022451
五、另外建议用4.X (5.x不稳定)
1、netty4.x - w3c教程文档地址:https://www.w3cschool.cn/netty_4_user_guide/6worbozt.html
2、Git 游戏服务器源码:https://github.com/root-wyj/springboot_im
--------说明:使用springboot + nettysocketio开发的一个简易卡牌游戏。有兴趣的可以去学习一下
4.X 依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
4.x 继承ChannelHandlerAdapter 会报错
5.x 使用
class ClientHandler extends ChannelHandlerAdapter{
4.x 使用
class ClientHandler extends ChannelInboundHandlerAdapter
到此结束…