Netty
本文主要是介绍 Netty与protobuf,线程池的使用。主要是围绕一下几个问题进行介绍
- 使用Netty搭建一个服务器。
- 使用Netty搭建一个客户端。
- 客户端发送字符串,服务器端打印。
- 了解google的protobuf工具库。
- 客户端使用protobuf发送一个数据,包含两个字段:uid=10以内的随机,index=自增长整数。
- 服务器解析出这个数据包并打印内容。
- 客户端同时发送1000个包含上述两个字段的随机数据的数据包,服务器能成功接收并打印。
- 服务器使用线程池,可以并行的处理这1000条数据。
- 保证同一个UID的数据同时只占用一个线程。
Netty与protobuf,线程池的使用
首先,需要在项目的依赖中添加Netty和protobuf库。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>netty_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.25.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
</dependencies>
</project>
搭建服务器端
使用Netty搭建服务器端,需要实现以下步骤:
- 创建
EventLoopGroup
对象,用于管理NIO线程 - 创建
ServerBootstrap
对象,用于启动服务端 - 配置事件处理器
ChannelInitializer
- 启动服务端,并绑定端口
import com.bo.netty.handler.*;
import com.bo.netty.protobuf.UserPOJO;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
public class NettyServer {
public static void main(String[] args) {
// 用于接收客户端连接的线程工作组
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
// 用于对接收客户端连接读写操作的线程工作中
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 服务端启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup) // 绑定两个工作线程组
.channel(NioServerSocketChannel.class) // 设置NIO的模型
.option(ChannelOption.SO_BACKLOG, 1024) // 设置tcp缓冲区大小
.option(ChannelOption.SO_RCVBUF, 32 * 1024) // 设置发送数据的缓冲大小
.childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 为通道进行初始化 数据传输过来的时候会进行拦截和执行
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new NettyServerHandler());
}
});
System.out.println("服务器启动,端口:9090");
// 绑定端口启动
ChannelFuture sync = serverBootstrap.bind(9090).sync();
// 释放
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 接收客户端的线程组进行释放
bossGroup.shutdownGracefully();
// 用于处理客户端读取的线程组进行释放
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
NettyServerHandler
是自定义的事件处理器,用于处理接受到的消息。同时还添加了字符串的解码和编码器。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCountUtil;
/**
* 服务端处理通道.这里只是打印一下请求的内容,并不对请求进行任何的响应 DiscardServerHandler 继承自
* ChannelHandlerAdapter, 这个类实现了ChannelHandler接口, ChannelHandler提供了许多事件处理的接口方法,
* 然后你可以覆盖这些方法。 现在仅仅只需要继承ChannelHandlerAdapter类而不是你自己去实现接口方法。
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 这里我们覆盖了chanelRead()事件处理方法。 每当从客户端收到新的数据时, 这个方法会在收到消息时被调用,
* 这个例子中,收到的消息的类型是ByteBuf
*
* @param ctx 通道处理的上下文信息
* @param msg 接收的消息
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
ByteBuf in = (ByteBuf) msg;
// 打印客户端输入,传过来的字符
System.out.println(in.toString(CharsetUtil.UTF_8));
} catch (Exception e) {
} finally {
ReferenceCountUtil.release(msg);
}
}
/***
* 这个方法会在发生异常时触发
* exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO
* 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来 并且把关联的 channel
* 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不 同的实现,
* 比如你可能想在关闭连接之前发送一个错误码的响应消息。
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 出现异常就关闭
ctx.close();
}
}
搭建客户端
使用Netty搭建客户端,需要实现以下步骤:
- 创建
EventLoopGroup
对象,用于管理NIO线程 - 创建
Bootstrap
对象,用于启动客户端 - 配置事件处理器
ChannelInitializer
- 启动客户端,并连接服务器
EventLoopGroup workerGroup = new NioEventLoopGroup();
import com.bo.netty.handler.NettyClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class NettyClient {
public static void main(String[] args) throws Exception {
// 线程工作组
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
// 客户端启动类
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventExecutors)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new NettyClientHandler());
}
});
try {
System.out.println("客户端启动,端口:9090");
ChannelFuture sync = bootstrap.connect("127.0.0.1",9090).sync();
sync.channel().closeFuture().sync();
} finally {
eventExecutors.shutdownGracefully();
}
}
}
NettyClientHandler
NettyClientHandler
是自定义的事件处理器,用于处理接受到的消息。同时还添加了字符串的解码和编码器。
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String message = "Hello, World!";
ctx.writeAndFlush(message);
}
/**
* 当通道有数据的的是会触发
*
* @param ctx 上下文对象,管道pipeline 通道 channel 地址
* @param msg 消息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
for (int i = 0; i < 1000; i++) {
ctx.writeAndFlush("你好");
super.channelActive(ctx);
}
}
}
发送字符串
在客户端发送一个字符串后,服务端可以直接打印出来,因为服务器已经添加了相应的解码器和事件处理器。
String message = "Hello, World!";
ctx.writeAndFlush(message);
使用protobuf
使用protobuf需要先定义好数据协议,然后生成Java类,在代码中使用这些类就可以了。
首先,定义数据协议文件 User.proto
:
syntax = "proto3";
option java_outer_classname = "UserPOJO";
message user {
int32 uid = 1;
int32 index = 2;
}
使用protobuf插件生成Java类:
protoc --java_out=./ User.proto
生成的Java类:com.bo.netty.protobuf.UserPOJO
和 com.bo.netty.protobuf.UserPOJO.user
在客户端和服务器端都需要添加protobuf的编码器和解码器,并且使用 com.bo.netty.protobuf..UserPOJO
类型的对象作为消息的数据类型。
客户端发送一个protobuf消息
NettyClientHandler
是自定义的事件处理器,用于处理接受到的消息。同时还添加了字符串的解码和编码器。
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道就绪的时候会触发
*
* @param ctx 上下文对象,管道pipeline 通道 channel 地址
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
UserPOJO.user user = UserPOJO.user.newBuilder().setUid(new Random().nextInt(10)).setIndex(new Random().nextInt(10)).build();
ctx.writeAndFlush(user);
super.channelActive(ctx);
}
/**
* 当通道有数据的的是会触发
*
* @param ctx 上下文对象,管道pipeline 通道 channel 地址
* @param msg 消息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
UserPOJO.user user = UserPOJO.user.newBuilder().setUid(new Random().nextInt(10)).setIndex(new Random().nextInt(10)).build();
ctx.writeAndFlush(user);
super.channelRead(ctx, msg);
}
}
服务端接收并处理protobuf消息
NettyServer
protobuf数据 设置编码格式
public void start() {
// 用于接收客户端连接的线程工作组
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
// 用于对接收客户端连接读写操作的线程工作中
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 服务端启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup) // 绑定两个工作线程组
.channel(NioServerSocketChannel.class) // 设置NIO的模型
.option(ChannelOption.SO_BACKLOG, 1024) // 设置tcp缓冲区大小
.option(ChannelOption.SO_RCVBUF, 32 * 1024) // 设置发送数据的缓冲大小
.childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 为通道进行初始化 数据传输过来的时候会进行拦截和执行
ChannelPipeline pipeline = ch.pipeline();
// protobuf数据 设置编码格式
pipeline.addLast(new ProtobufDecoder(UserPOJO.user.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
});
System.out.println("服务器启动,端口:9090");
// 绑定端口启动
ChannelFuture sync = serverBootstrap.bind(9090).sync();
NettyServerHandler_6.runConsumer();
// 释放
sync.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 接收客户端的线程组进行释放
bossGroup.shutdownGracefully();
// 用于处理客户端读取的线程组进行释放
workerGroup.shutdownGracefully();
}
}
NettyClientHandler
是自定义的事件处理器
import com.bo.netty.protobuf.UserPOJO;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class NettyServerHandler extends SimpleChannelInboundHandler<UserPOJO.user> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, UserPOJO.user user) {
System.out.println("uid: " + user.getUid() + "index: " + user.getIndex());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端", CharsetUtil.UTF_8));
}
/***
* 这个方法会在发生异常时触发
* exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO
* 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来 并且把关联的 channel
* 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不 同的实现,
* 比如你可能想在关闭连接之前发送一个错误码的响应消息。
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 出现异常就关闭
ctx.close();
}
}
线程池处理数据
这里使用了线程池 executorService
来并行处理1000条数据
客户端发送数据
NettyClientHandler
发送1000条数据
import com.bo.netty.protobuf.UserPOJO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 当通道就绪的时候会触发
*
* @param ctx 上下文对象,管道pipeline 通道 channel 地址
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
atomicInteger.compareAndSet(atomicInteger.get(),
atomicInteger.get() + 1);
UserPOJO.user user = UserPOJO.user.newBuilder().setUid(new Random().nextInt(10)).setIndex(atomicInteger.get()).build();
ctx.writeAndFlush(user);
super.channelActive(ctx);
}
/**
* 当通道有数据的的是会触发
*
* @param ctx 上下文对象,管道pipeline 通道 channel 地址
* @param msg 消息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务器回复的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
if (atomicInteger.get() < 1000) {
atomicInteger.compareAndSet(atomicInteger.get(),
atomicInteger.get() + 1);
UserPOJO.user user = UserPOJO.user.newBuilder().setUid(new Random().nextInt(10)).setIndex(atomicInteger.get()).build();
ctx.writeAndFlush(user);
}
super.channelRead(ctx, msg);
}
}
服务端处理数据
NettyServerHandler
处理数据
public class NettyServerHandler extends SimpleChannelInboundHandler<UserPOJO.user> {
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
@Override
protected void channelRead0(ChannelHandlerContext ctx, UserPOJO.user user) {
THREAD_POOL.execute(() -> {
try {
// 模拟业务处理时间
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + "正在执行:[" + user.getUid() + "] 用户的第[" + user.getIndex() + "]任务");
ctx.writeAndFlush(Unpooled.copiedBuffer(Thread.currentThread().getName() + " 发送:hello,客户端", CharsetUtil.UTF_8));
});
}
/***
* 这个方法会在发生异常时触发
* exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,即当 Netty 由于 IO
* 错误或者处理器在处理事件时抛出的异常时。在大部分情况下,捕获的异常应该被记录下来 并且把关联的 channel
* 给关闭掉。然而这个方法的处理方式会在遇到不同异常的情况下有不 同的实现,
* 比如你可能想在关闭连接之前发送一个错误码的响应消息。
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 出现异常就关闭
ctx.close();
}
}
保证同一个UID的数据同时只占用一个线程
NettyServerHandler
处理数据时候 保证同一个UID的数据同时只占用一个线程,采用阻塞消费的思想来实现
import com.bo.netty.protobuf.UserDTO;
import com.bo.netty.protobuf.UserPOJO;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
import java.util.concurrent.*;
public class NettyServerHandler_6 extends SimpleChannelInboundHandler<UserPOJO.user> {
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
// QUEUE_MAP
private static ConcurrentHashMap<Integer, BlockingQueue<UserDTO>> QUEUE_MAP = new ConcurrentHashMap<>();
static {
QUEUE_MAP.put(0, new LinkedBlockingQueue<>());
QUEUE_MAP.put(1, new LinkedBlockingQueue<>());
QUEUE_MAP.put(2, new LinkedBlockingQueue<>());
QUEUE_MAP.put(3, new LinkedBlockingQueue<>());
QUEUE_MAP.put(4, new LinkedBlockingQueue<>());
QUEUE_MAP.put(5, new LinkedBlockingQueue<>());
QUEUE_MAP.put(6, new LinkedBlockingQueue<>());
QUEUE_MAP.put(7, new LinkedBlockingQueue<>());
QUEUE_MAP.put(8, new LinkedBlockingQueue<>());
QUEUE_MAP.put(9, new LinkedBlockingQueue<>());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, UserPOJO.user msg) throws Exception {
int uid = msg.getUid();
BlockingQueue<UserDTO> userBlockingQueue = QUEUE_MAP.computeIfAbsent(uid, k -> new LinkedBlockingQueue<>());
UserDTO userDTO = new UserDTO();
userDTO.setUser(msg);
userDTO.setCtx(ctx);
userBlockingQueue.add(userDTO);
}
/**
* 消费者
*/
public static void runConsumer() {
QUEUE_MAP.forEach((key, value) -> {
THREAD_POOL.execute(new Consumer(value));
});
}
static class Consumer implements Runnable {
private final BlockingQueue<UserDTO> queue;
public Consumer(BlockingQueue<UserDTO> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
// 阻塞消费
while (true) {
UserDTO num = queue.take();
consume(num);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void consume(UserDTO userDTO) throws Exception {
// 处理该UID的所有数据包的逻辑
ChannelHandlerContext ctx = userDTO.getCtx();
UserPOJO.user user = userDTO.getUser();
// 模拟业务处理时间
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
// 消费数据的代码
System.out.println(Thread.currentThread().getName() + "正在执行:[" + user.getUid() + "] 用户的第[" + user.getIndex() + "]任务");
ctx.writeAndFlush(Unpooled.copiedBuffer(Thread.currentThread().getName() + " 发送:hello,客户端", CharsetUtil.UTF_8));
}
}
}
需要注意下:在启动服务端的时候也要启动消费线程
end~,有更好的解决方法欢迎交流呦!!!