netty(七) --基于redis搭建netty tcp通讯集群方案

基于redis搭建netty tcp通讯集群方案

简介

在实际应用中我们的tcp服务端经常会使用集群方式运行,这样增大了系统的性能和容灾,本文讲述简单的netty tcp服务端集群应用原理的部分代码,文章源码地址:https://github.com/itwwj/netty-learn.git中的netty-day08-cluster项目。

一、集群原理

在这里插入图片描述

  1. 服务端启动会向redis注册自己,以ip为key,以连接信息为value
  2. 客户端连接服务端时,服务端会向redis注册客户端信息,以channelId为key以客户端连接信息为value,同时在连接的服务端节点向map集合保存节点信息,以channelId为key以channel对象为value。
  3. 不同节点客户端互发消息时,client会把消息发送给连接的server节点,server节点会先在本节点查找是否有发送目标的channelId,如果没有就把消息转发给redis,由redis广播给对应的目标server节点,再由该节点发送给对象的client。

二、项目依赖

  <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <protostuff.version>1.0.10</protostuff.version>
        <objenesis.version>2.4</objenesis.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.0.4.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <!-- 添加redis依赖模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>
        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.0.4.RELEASE</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                    <mainClass>com.gitee.netty.cluster.ClusterApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>

三、部分代码

redis配置类:

/**
 * redisTemplate配置类
 * @author jie
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisMessageTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setDefaultSerializer(new FastJsonRedisSerializer<>(Object.class));
        return template;
    }
}

/**
 * 用户管道信息;记录某个用户分配到某个服务端
 * @author jie
 */
@Configuration
public class SubConfig {

    @Value("${netty.port}")
    private int port;

    /**
     * 接受广播消息配置 接受主题格式为:message_pub+本机ip+本程序端口
     * @param connectionFactory
     * @param msgAgreementListenerAdapter
     * @return
     * @throws UnknownHostException
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter msgAgreementListenerAdapter) throws UnknownHostException {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(msgAgreementListenerAdapter, new PatternTopic("message_pub"+ NetWorkUtils.getHost()+port));
        return container;
    }
}

redis工具类:

/**
 * @author jie
 */
@Service
public class RedisUtil {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 向redis存入设备连接信息
     *
     * @param deviceChannelInfo
     */
    public void pushObj(DeviceChannelInfo deviceChannelInfo) {
        redisTemplate.opsForHash().put("deviceIds", deviceChannelInfo.getChannelId(), JSON.toJSONString(deviceChannelInfo));
    }

    /**
     * 查询redis中的设备连接信息
     *
     * @return
     */
    public List<DeviceChannelInfo> popList() {
        List<Object> values = redisTemplate.opsForHash().values("deviceIds");
        if (null == values) {
            return new ArrayList<>();
        }
        List<DeviceChannelInfo> deviceChannelInfoList = new ArrayList<>();
        for (Object strJson : values) {
            deviceChannelInfoList.add(JSON.parseObject(strJson.toString(), DeviceChannelInfo.class));
        }
        return deviceChannelInfoList;
    }

    /**
     * 根据channelId查询连接信息
     *
     * @param channelId
     * @return
     */
    public DeviceChannelInfo selectByChannel(String channelId) {
        Object deviceIds = redisTemplate.opsForHash().get("deviceIds", channelId);
        if (deviceIds == null) {
            return null;
        }
        return JSON.parseObject(deviceIds.toString(), DeviceChannelInfo.class);
    }

    /**
     * 移除某个设备信息
     *
     * @param channelId
     */
    public void remove(String channelId) {
        redisTemplate.opsForHash().delete("deviceIds", channelId);
    }

    /**
     * 清空设备信息
     */
    public void clear() {
        redisTemplate.delete("deviceIds");
    }
}

发布消息到redis:


/**
 * 发布redis消息
 *
 * @author jie
 */
@Slf4j
@Component
public class MsgPub {
    @Autowired
    private StringRedisTemplate redisTemplate;

    public void pushMessage(String topic, MsgAgreement message) {
        log.info("向 "+topic+"发送消息:"+message);
        redisTemplate.convertAndSend(topic, MsgUtil.obj2Json(message));
    }
}

接受redis广播来的消息:

/**
 * redis订阅消息处理实现类
 *
 * @author jie
 */
@Slf4j
@Component
public class MsgReceiver extends MessageListenerAdapter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 接收redis推送的消息如果当前服务连接的有此设备就推送消息
     *
     * @param message 消息
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String msg = redisTemplate.getStringSerializer().deserialize(message.getBody());
        String topic = redisTemplate.getStringSerializer().deserialize(message.getChannel());
        log.info("来自" + topic + "的消息:" + msg);

        MsgAgreement msgAgreement = JSON.parseObject(msg, MsgAgreement.class);
        String toChannelId = msgAgreement.getToChannelId();
        Channel channel = CacheUtil.cacheChannel.get(toChannelId);
        if (null == channel) {
            return;
        }
        channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
    }
}

netty事件触发类:

/**
 * 操作类
 *
 * @author jie
 */
@Slf4j
public class MyServerHandler extends ChannelInboundHandlerAdapter {


    private CacheService cacheService;

    public MyServerHandler(CacheService cacheService) {
        this.cacheService = cacheService;
    }


    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                log.info("客户端" + ctx.channel().id() + "长时间未通讯,即将剔除。");
                // 在规定时间内没有收到客户端的上行数据, 主动断开连接
                ctx.disconnect();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    /**
     * 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SocketChannel channel = (SocketChannel) ctx.channel();
        log.info("有一客户端链接到本服务端 channelId:" + channel.id());
        //保存设备信息
        DeviceChannelInfo deviceChannelInfo = DeviceChannelInfo.builder()
                .channelId(channel.id().toString())
                .ip(NetWorkUtils.getHost())
                .port(channel.localAddress().getPort())
                .linkDate(new Date())
                .build();
        cacheService.getRedisUtil().pushObj(deviceChannelInfo);
        CacheUtil.cacheChannel.put(channel.id().toString(), channel);
        ctx.writeAndFlush("ok \r\n");
    }

    /**
     * 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("客户端断开链接" + ctx.channel().id());
        cacheService.getRedisUtil().remove(ctx.channel().id().toString());
        CacheUtil.cacheChannel.remove(ctx.channel().id().toString(), ctx.channel());
    }

    /**
     * 处理通道内的数据
     *
     * @param ctx
     * @param objMsgJsonStr
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object objMsgJsonStr) throws Exception {
        String msg = objMsgJsonStr.toString();
        if (msg.length() < 5) {
            log.info("心跳消息:" + msg);
            return;
        }
        MsgAgreement msgAgreement = MsgUtil.json2Obj(msg);
        String toChannelId = msgAgreement.getToChannelId();
        //判断接收消息用户是否在本服务端
        Channel channel = CacheUtil.cacheChannel.get(toChannelId);
        if (null != channel) {
            channel.writeAndFlush(MsgUtil.obj2Json(msgAgreement));
            return;
        }
        //如果为NULL则接收消息的用户不在本服务端,需要push消息给全局
        cacheService.push(msgAgreement);
    }

    /**
     * 发现异常关闭连接打印日志
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
        cacheService.getRedisUtil().remove(ctx.channel().id().toString());
        CacheUtil.cacheChannel.remove(ctx.channel().id().toString(), ctx.channel());
        log.error("异常信息:\r\n" + cause.getMessage());
    }
}

netty出入栈:

/**
 *
 *  MyChannelInitializer的主要目的是为程序员提供了一个简单的工具,用于在某个Channel注册到EventLoop后,对这个Channel执行一些初始
 * 化操作。ChannelInitializer虽然会在一开始会被注册到Channel相关的pipeline里,但是在初始化完成之后,ChannelInitializer会将自己
 * 从pipeline中移除,不会影响后续的操作。
 * @author jie
 */
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

    private CacheService cacheService;

    public MyChannelInitializer(CacheService cacheService) {
        this.cacheService = cacheService;
    }

    @Override
    protected void initChannel(SocketChannel channel) {
        /**
         * 心跳监测
         * 1、readerIdleTimeSeconds 读超时时间
         * 2、writerIdleTimeSeconds 写超时时间
         * 3、allIdleTimeSeconds    读写超时时间
         * 4、TimeUnit.SECONDS 秒[默认为秒,可以指定]
         */
        channel.pipeline().addLast(new IdleStateHandler(60, 0, 0));
        // 基于换行符号
        channel.pipeline().addLast(new LineBasedFrameDecoder(1024));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
        // 解码转String,注意调整自己的编码格式GBK、UTF-8
        channel.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
        // 在管道中添加我们自己的接收数据实现方法
        channel.pipeline().addLast(new MyServerHandler(cacheService));
    }
}

缓存服务:

/**
 * @author jie
 */
@Service
public class CacheService {

    @Autowired
    private MsgPub msgPub;
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 将通道数据广播给指定的主题  主题格式为:message_pub+节点ip+节点端口
     * @param msgAgreement
     */
    public void push(MsgAgreement msgAgreement) {
        DeviceChannelInfo deviceChannelInfo = redisUtil.selectByChannel(msgAgreement.getToChannelId());
        if (deviceChannelInfo == null) {
            return;
        }
        msgPub.pushMessage("message_pub"+deviceChannelInfo.getIp()+deviceChannelInfo.getPort(), msgAgreement);
    }

    public RedisUtil getRedisUtil() {
        return redisUtil;
    }
}

缓存工具类:


/**
 * @author jie
 */
public class CacheUtil {
    /**
     * 线程池
     */
    public static ExecutorService executorService = Executors.newFixedThreadPool(3);
    /**
     * 缓存channel
     */
    public static Map<String, Channel> cacheChannel = new ConcurrentHashMap(new HashMap<>());
}

构建消息实体:

/**
 * 构建消息
 *
 * @author jie
 */
public class MsgUtil {

    public static MsgAgreement buildMsg(String channelId, String content) {
        return new MsgAgreement(channelId, content);
    }

    public static MsgAgreement json2Obj(String objJsonStr) {
        objJsonStr = objJsonStr.replace("\r\n", "");
        return JSON.parseObject(objJsonStr, MsgAgreement.class);
    }

    public static String obj2Json(MsgAgreement msgAgreement) {
        return JSON.toJSONString(msgAgreement) + "\r\n";
    }

}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值