Netty与protobuf,线程池的使用

本文介绍了如何使用Netty构建服务器和客户端,以及结合Google的protobuf工具库进行数据传输。客户端发送包含uid和index字段的protobuf数据,服务器解析并打印。通过线程池,服务器能并行处理1000个数据包,确保同一UID的数据在同一时间仅占用一个线程。
摘要由CSDN通过智能技术生成

Netty

本文主要是介绍 Netty与protobuf,线程池的使用。主要是围绕一下几个问题进行介绍

  1. 使用Netty搭建一个服务器。
  2. 使用Netty搭建一个客户端。
  3. 客户端发送字符串,服务器端打印。
  4. 了解google的protobuf工具库。
  5. 客户端使用protobuf发送一个数据,包含两个字段:uid=10以内的随机,index=自增长整数。
  6. 服务器解析出这个数据包并打印内容。
  7. 客户端同时发送1000个包含上述两个字段的随机数据的数据包,服务器能成功接收并打印。
  8. 服务器使用线程池,可以并行的处理这1000条数据。
  9. 保证同一个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搭建服务器端,需要实现以下步骤:

  1. 创建 EventLoopGroup 对象,用于管理NIO线程
  2. 创建 ServerBootstrap 对象,用于启动服务端
  3. 配置事件处理器 ChannelInitializer
  4. 启动服务端,并绑定端口
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搭建客户端,需要实现以下步骤:

  1. 创建 EventLoopGroup 对象,用于管理NIO线程
  2. 创建 Bootstrap 对象,用于启动客户端
  3. 配置事件处理器 ChannelInitializer
  4. 启动客户端,并连接服务器
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.UserPOJOcom.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~,有更好的解决方法欢迎交流呦!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值