文章目录
一、前言
我会重写这一篇的。。。。
1. BIO、NIO、AIO
1.1 介绍
-
BIO - 同步阻塞IO : 传统的IO操作,在读/写文件时线程会一直阻塞,直到文件读/写结束。
-
NIO - 同步非阻塞IO:NIO在读/写文件时并不阻塞当前线程,也就是说在读/写文件时是线程是可以继续执行其他任务的。NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程。IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高
NIO多路复用主要步骤和元素:
1. 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。
2. 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。
3. Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。
4. 在 具体的 方法中,通过 SocketChannel 和 Buffer 进行数据操作 -
AIO - 异步非阻塞IO:AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
关于这一部分的详细内容可以参考 : https://www.cnblogs.com/sxkgeek/p/9488703.html
1.2 NIO 核心思路
详参: https://mp.weixin.qq.com/s/wVoHfhh28Vh5sgKQbPXk8w
结合示例代码,总结NIO的核心思路:
-
NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在上面例子中serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读
-
服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用BIO模型中1w 个while循环在阻塞,参见(1)
-
clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过clientSelector.select(1)方法可以轮询出来,进而批量处理,参见(2)
-
数据的读写面向 Buffer,参见(3)
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 对应IO编程中服务端启动
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 面向 Buffer
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
2. Netty简介
2.1 什么是Netty
Netty 是一个基于 JAVA NIO 类库的异步通信框架,它的架构特点是:异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性。
2.2 Netty应用场景
1.分布式开源框架中dubbo、Zookeeper,RocketMQ底层rpc通讯使用就是netty。
2.游戏开发中,底层使用netty通讯。
2.3 为什么选择netty
我们总结下为什么不建议开发者直接使用JDK的NIO类库进行开发的原因:
- NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等;
- 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序;
- 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大;
- JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。
二、 Netty 基本使用
1. 基本使用
Netty 客户端消息处理器
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @Data: 2019/10/18
* @Des: 客户端处理器
*/
class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 接收数据
System.out.println("客户端收到消息 : " + msg);
}
}
Netty 服务端消息处理器
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @Data: 2019/10/18
* @Des: 服务端处理器
*
* ` channelActive() ` > 在到服务器的连接已经建立之后将被调用(成为活跃状态)
* ` channelRead0()` > 当从服务器接受到一条消息时被调用
* ` exceptionCaught()` > 在处理过程中引发异常时调用
* ` channelReigster() ` > 注册到EventLoop上
* ` handlerAdd() ` > Channel被添加方法
* ` handlerRemoved()` > Channel被删除方法
* ` channelInActive() ` > Channel离开活跃状态,不再连接到某一远端时被调用
* ` channelUnRegistered()` > Channel从EventLoop上解除注册
* ` channelReadComplete()` > 当Channel上的某个读操作完成时被调用
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
System.out.println("handlerAdded....");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
System.out.println("exceptionCaught...");
}
/**
* 读取客户端请求,向客户端响应的方法,所以这里要构造响应返回给客户端。
* 注意:这里面跟Servlet没有任何关系,也符合Servlet规范,所以不会涉及到HttpServerltRequest和HttpServeletResponse对象。
*
* @param ctx
* @param msg
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String value = (String) msg;
System.out.println("服务器端收到客户端msg:\n" + value);
// 回复客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("我收到了你的信息\r\n".getBytes()));
}
}
Netty 客户端
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
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;
import io.netty.handler.codec.string.StringEncoder;
/**
* @Data: 2019/10/18
* @Des: Netty 客户端
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// 固定消息定长,长度不够空格补全,不实用
// ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
Channel channel = channelFuture.channel();
channel.writeAndFlush(Unpooled.wrappedBuffer("天下第一666\r\n".getBytes()));
channel.writeAndFlush(Unpooled.wrappedBuffer("天下第二666\r\n".getBytes()));
channel.writeAndFlush(Unpooled.wrappedBuffer("天下第三666\r\n".getBytes()));
System.out.println("消息已发送");
// 等待客户端端口号关闭
channelFuture.channel().closeFuture().sync();
nioEventLoopGroup.shutdownGracefully();
}
}
Netty 服务端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
/**
* @Data: 2019/10/17
* @Des: Netty 服务端
* <p>
* **基于netty构建服务基本流程总结:**
* 1. 创建EventLoopGroup实例
* 2. 通过ServerBootstrap启动服务,bind到一个端口. 如果是客户端,则使用Bootstrap,连接主机和端口.
* 3. 创建ChannelInitializer实例,通过ChannelPipieline初始化处理器链.
* 4. 创建ChannelServerHandler实例,继承SimpleChannelInboundHandler,重写channelRead0方法(netty4.x).
* 5. 将ChannelServerHandler实例addLast到ChannelPipeline上.
* 6. 将ChannelInitializer实例childHandler到bootstrap上.
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建两个线程池,一个负责接收消息,一个负责发送消息
// bossGroup目的是获取客户端连接,连接接收到之后再将连接转发给workerGroup去处理。
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup wordGroup = new NioEventLoopGroup();
// 创建一个辅助操作类
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, wordGroup).channel(NioServerSocketChannel.class)
// 缓存队列缓存的请求的大小,默认50
.option(ChannelOption.SO_BACKLOG, 1024)
// 设置发送与接收的缓冲区的大小
.option(ChannelOption.SO_SNDBUF, 32 * 1024).option(ChannelOption.SO_RCVBUF, 32 * 1024)
// 通过ChannelPipeline初始化处理器,类似于拦截器Chain,当每个客户端首次连接后即调用initChannel方法完成初始化动作。
// 初始化"拦截器"链
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
System.out.println(" 有客户端连接...");
// 固定消息定长,长度不够空格补全,不实用
// ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ServerHandler());
}
});
// 启动
ChannelFuture channelFuture = serverBootstrap.bind("localhost", 8080).sync();
// 关闭
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
wordGroup.shutdownGracefully();
}
}
2. Option 参数详解
参数名 | 释义 |
---|---|
ChannelOption.SO_BACKLOG | ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小 |
ChannelOption.SO_REUSEADDR | ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口, 比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用, 比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。 |
ChannelOption.SO_KEEPALIVE | Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。 |
ChannelOption.SO_SNDBUF | ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。 |
ChannelOption.SO_RCVBUF | ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。 |
ChannelOption.SO_LINGER | ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送 |
ChannelOption.TCP_NODELAY | ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。 |
IP_TOS | IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。 |
ALLOW_HALF_CLOSURE | Netty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。 |
3. Netty粘包、拆包问题
什么是粘包/拆包
一个完整的业务可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题。
下面可以看一张图,是客户端向服务端发送包:
- 第一种情况,Data1和Data2都分开发送到了Server端,没有产生粘包和拆包的情况。
- 第二种情况,Data1和Data2数据粘在了一起,打成了一个大的包发送到Server端,这个情况就是粘包。
- 第三种情况,Data2被分离成Data2_1和Data2_2,并且Data2_1在Data1之前到达了服务端,这种情况就产生了拆包。
由于网络的复杂性,可能数据会被分离成N多个复杂的拆包/粘包的情况,所以在做TCP服务器的时候就需要首先解决拆包/
解决方案:
- 消息定长 : 报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。不过这样对于超过定长的报文一定会拆开,所以一般不使用这种方式。
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
- 特殊分隔符分隔: 给每条报文结束后都添加特殊字符作为结束符进行分隔。
// 包尾添加特殊分隔符, 以特殊分隔符作为结尾(接收消息时会把特殊分隔符去掉)
ByteBuf buf = Unpooled.copiedBuffer("\r\n".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
- 自定义协议: 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段。下面详述。
三、Netty 自定义协议
一般所谓的TCP粘包是在一次接收数据不能完全地体现一个完整的消息数据。TCP通讯为何存在粘包呢?
主要原因是TCP是以流的方式来处理数据,再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。自定义协议通过定义了协议格式,来规范了报文头和报文长度来使得Netty可以知道每次的报文数据有多大,从而可以解析出来每次的报文。
- 自定义协议封装类
/**
* @Data: 2019/10/19
* @Des: 自定义协议封装类,发送报文时按照这个类型进行发送
*/
public class NettyDataProtocol {
private static final int hearder = 0X76; // 自定义协议,报文开始标志,十六进制
private long contentLength; // 数据长度
private byte[] content; // 报文内容
public NettyDataProtocol(long contentLength, byte[] content) {
this.contentLength = contentLength;
this.content = content;
}
public int getHearder() {
return hearder;
}
public long getContentLength() {
return contentLength;
}
public void setContentLength(long contentLength) {
this.contentLength = contentLength;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
- 自定义报文编码器
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* @Data: 2019/10/19
* @Des: 自定义协议的编码器
*/
public class NettyDataProtocolEncoder extends MessageToByteEncoder<NettyDataProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, NettyDataProtocol msg, ByteBuf out) throws Exception {
out.writeInt(msg.getHearder());
out.writeLong(msg.getContentLength());
out.writeBytes(msg.getContent());
}
}
- 自定义协议解码器
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
* @Data: 2019/10/19
* @Des: 自定义协议的解码器 : 当接收报文数据时,通过此解码器进行解码
*/
public class NettyDataProtocolDecoder extends ByteToMessageDecoder {
// 报文开始,header int型,占4个字节,contentLength long型,占8个字节,所以基础长是12字节
public final int BASE_LENGTH = 12;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 可读长度必须大于基本长度
if (in.readableBytes() >= BASE_LENGTH) {
// 防止socket字节流攻击
// 防止客户端传来的数据过大(太大的数据是不合理的)
if (in.readableBytes() > 2048) {
in.skipBytes(in.readableBytes());
}
// 记录包头开始的index,用于后面不匹配时还原
int beginReader;
// 循环目的是为找到报文的header下标。
while (true) {
// 获取包头开始的index
beginReader = in.readerIndex();
// 标记包头开始的index
in.markReaderIndex();
// 读到协议的开始标志,结束while循环,则下面一段数据应为一段报文,所以跳出循环
if (in.readInt() == 0X76) {
break;
}
// 未读到包头,缓缓报头标记位
in.resetReaderIndex();
// 跳过一个字节
in.readByte();
// 判断数据报长度是否满足最低限制,若不满足,则结束此次解码,等待后边数据流的到达
if (in.readableBytes() < BASE_LENGTH) {
return;
}
}
// 获取到报文数据报长度
long length = in.readLong();
// 判断数据是否完整
if (in.readableBytes() < length) { // 不完整,回退读指针
// 还原读指针
in.readerIndex(beginReader);
return;
}
// 至此,读到一条完整报文
byte[] data = new byte[(int)length];
in.readBytes(data);
NettyDataProtocol protocol = new NettyDataProtocol(data.length, data);
out.add(protocol);
}
}
}
- 服务端:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建两个线程池,一个负责接收消息,一个负责发送消息
// bossGroup目的是获取客户端连接,连接接收到之后再将连接转发给workerGroup去处理。
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup wordGroup = new NioEventLoopGroup();
// 创建一个辅助操作类
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, wordGroup).channel(NioServerSocketChannel.class)
// 缓存队列缓存的请求的大小,默认50
.option(ChannelOption.SO_BACKLOG, 1024)
// 设置发送与接收的缓冲区的大小
.option(ChannelOption.SO_SNDBUF, 32 * 1024).option(ChannelOption.SO_RCVBUF, 32 * 1024)
// 通过ChannelPipeline初始化处理器,类似于拦截器Chain,当每个客户端首次连接后即调用initChannel方法完成初始化动作。
// 初始化"拦截器"链
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
System.out.println(" 有客户端连接...");
// 添加自定义的协议编码、解码器
ch.pipeline().addLast(new NettyDataProtocolDecoder());
ch.pipeline().addLast(new NettyDataProtocolEncoder());
ch.pipeline().addLast(new ServerHandler());
}
});
// 启动
ChannelFuture channelFuture = serverBootstrap.bind("localhost", 8080).sync();
// 关闭
channelFuture.channel().closeFuture().sync();
bossGroup.shutdownGracefully();
wordGroup.shutdownGracefully();
}
}
- 服务端处理器
public class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
System.out.println("exceptionCaught...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 接收自定义报文
NettyDataProtocol value = (NettyDataProtocol) msg;
try {
System.out.println("服务器端收到客户端msg: " + new String(value.getContent(), "utf-8"));
// 回复客户端
String reply = "收到了你的信息";
ctx.writeAndFlush(new NettyDataProtocol(reply.getBytes().length, reply.getBytes("utf-8")));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
- 客户端:
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
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 java.io.UnsupportedEncodingException;
/**
* @Data: 2019/10/18
* @Des: Netty 客户端
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
bootstrap.group(nioEventLoopGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyDataProtocolDecoder());
ch.pipeline().addLast(new NettyDataProtocolEncoder());
ch.pipeline().addLast(new ClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
Channel channel = channelFuture.channel();
// 向服务端发送消息
for (int i = 0; i < 10; i++) {
try {
String msg = "....................................天下第" + i + "..." + i;
channel.writeAndFlush(new NettyDataProtocol(msg.getBytes().length, msg.getBytes("utf-8")));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
System.out.println("消息已发送");
// 等待客户端端口号关闭
channelFuture.channel().closeFuture().sync();
nioEventLoopGroup.shutdownGracefully();
}
}
- 客户端处理器
class ClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道被调用,执行该方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyDataProtocol value = (NettyDataProtocol) msg;
try {
System.out.println("客户端收到服务端回复: " + new String(value.getContent(), "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
结果如下:
客户端发送消息,并受到服务端的回复
服务端接收到消息并回复
四、 Spring 整合 Netty
https://www.cnblogs.com/tdg-yyx/p/8376842.html
以上:内容部分参考
《Sping实战》
https://www.cnblogs.com/sidesky/p/6913109.html
https://www.jianshu.com/p/975b30171352
https://www.cnblogs.com/imstudy/p/9908791.html
https://www.cnblogs.com/sxkgeek/p/9488703.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正