Netty+Websocket 聊天、推送(实战)

文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:尼恩Java面试宝典 最新版 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


推荐: 地表最强 开发环境 系列

工欲善其事 必先利其器
地表最强 开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制作详解)
地表最强 热部署:java SpringBoot SpringCloud 热部署 热加载 热调试
地表最强 发请求工具(再见吧, PostMan ):IDEA HTTP Client(史上最全)
地表最强 PPT 小工具: 屌炸天,像写代码一样写PPT
无编程不创客,无编程不创客,一大波编程高手正在疯狂创客圈交流、学习中! 找组织,GO

什么是Netty ?

Netty是由Jboss提供的一款著名的开源框架,常用于搭建 RPC中的TCP服务器、Websocket服务器,甚至是类似Tomcat的Web服务器,反正就是各种网络服务器,在处理高并发的项目中,有奇用!功能丰富且性能良好,基于Java中NIO的二次封装,具有比原生NIO更好更稳健的体验。

关于Netty 原理,请参见 《Netty Zookeeper Redis 高并发实战》 一书

说明:本文会以pdf格式持续更新,更多最新尼恩3高pdf笔记,请从下面的链接获取:语雀 或者 码云

为什么要使用 Netty 替代 Tomcat?

很多项目,都需要基于 Websocket 协议做在线客服、在线推送、在线聊天,虽然 Tomcat 内置支持 Websocket 协议,但是由于 Tomcat 的吞吐量、连接数都很低,作为测试是可以的。在生产环境,一定需要使用高吞吐量、高连接数的 Netty 服务器进行替代

之所以 Netty 性能高,因为其使用的是 Reactor 反应器模式。关于反应器模式原理,请参见 《Netty Zookeeper Redis 高并发实战》 一书。

Netty+WS 在线聊天(在线推送)功能演示

聊天过程gif 演示:
在这里插入图片描述

聊天示意图:
在这里插入图片描述

Springboot+Netty 项目结构

在这里插入图片描述

Netty 服务启动

Netty搭建的服务器基本上都是差不多的写法:

绑定主线程组和工作线程组,这部分对应架构图中的事件循环组。其原理,,请参见 《Netty Zookeeper Redis 高并发实战》 一书。

重点就是ChannelInitializer的配置,以异步的方式启动,最后是结束的时候关闭线程组。


    /**
     * 启动即时通讯服务器
     */
    public void start()
    {
        final WebSocketServer webSocketServer = new WebSocketServer();


        ChannelFuture channelFuture = null;
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChatServerInitializer());
        InetSocketAddress address = new InetSocketAddress(9999);
        channelFuture = bootstrap.bind(address);
        channelFuture.syncUninterruptibly();

        channel = channelFuture.channel();
        // 返回与当前Java应用程序关联的运行时对象
        Runtime.getRuntime().addShutdownHook(new Thread()
        {
            @Override
            public void run()
            {
                webSocketServer.stop();
            }
        });
        channelFuture.channel().closeFuture().syncUninterruptibly();
    }

    /**
     * 内部类
     */
    class ChatServerInitializer extends ChannelInitializer<Channel>
    {
        private static final int READ_IDLE_TIME_OUT = 60; // 读超时  s
        private static final int WRITE_IDLE_TIME_OUT = 0;// 写超时
        private static final int ALL_IDLE_TIME_OUT = 0; // 所有超时


        @Override
        protected void initChannel(Channel ch) throws Exception
        {
            ChannelPipeline pipeline = ch.pipeline();
            // HTTP请求的解码和编码
            pipeline.addLast(new HttpServerCodec());
            // 主要用于处理大数据流,比如一个1G大小的文件如果你直接传输肯定会撑暴jvm内存的; 增加之后就不用考虑这个问题了
            pipeline.addLast(new ChunkedWriteHandler());
            // 把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse,
            // 原因是HTTP解码器会在每个HTTP消息中生成多个消息对象HttpRequest/HttpResponse,HttpContent,LastHttpContent
            pipeline.addLast(new HttpObjectAggregator(64 * 1024));
            // WebSocket数据压缩
            pipeline.addLast(new WebSocketServerCompressionHandler());
            // 协议包长度限制
            pipeline.addLast(new WebSocketServerProtocolHandler("/ws", null, true, 10 * 1024));

            // //当连接在60秒内没有接收到消息时,进会触发一个 IdleStateEvent 事件,被 HeartbeatHandler 的 userEventTriggered 方法处理
            pipeline.addLast(new IdleStateHandler(READ_IDLE_TIME_OUT, WRITE_IDLE_TIME_OUT, ALL_IDLE_TIME_OUT, TimeUnit.SECONDS));
            pipeline.addLast(new TextWebSocketFrameHandler());

        }
    }

说明:本文会以pdf格式持续更新,更多最新尼恩3高pdf笔记,请从下面的链接获取:语雀 或者 码云

报文处理器

/**
 * Created by 尼恩 @ 疯狂创客圈
 * <p>
 * WebSocket 帧:WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧
 */
@Slf4j
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>
{

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception
    {
        //增加消息的引用计数(保留消息),并将他写到 ChannelGroup 中所有已经连接的客户端

        ServerSession session = ServerSession.getSession(ctx);
        Map<String, String> result = ChatProcesser.inst().onMessage(msg.text(), session);

        if (result != null && null!=result.get("type"))
        {
            switch (result.get("type"))
            {
                case "msg":
                    SessionMap.inst().sendToOthers(result, session);
                    break;
                case "init":
                    SessionMap.inst().addSession(result, session);
                    break;
            }
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception
    {
        //是否握手成功,升级为 Websocket 协议
        if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE)
        {
            // 握手成功,移除 HttpRequestHandler,因此将不会接收到任何消息
            // 并把握手成功的 Channel 加入到 ChannelGroup 中
            ServerSession session = new ServerSession(ctx.channel());
            String echo = ChatProcesser.inst().onOpen(session);
            SessionMap.inst().sendMsg(ctx, echo);

        } else if (evt instanceof IdleStateEvent)
        {
            IdleStateEvent stateEvent = (IdleStateEvent) evt;
            if (stateEvent.state() == IdleState.READER_IDLE)
            {
                ServerSession session = ServerSession.getSession(ctx);
                SessionMap.inst().remove(session);
                session.processError(null);

            }
        } else
        {
            super.userEventTriggered(ctx, evt);
        }
    }



}

业务处理器

下面是用websocket做聊天室的逻辑:

  • 使用 Json 传递实体消息;

  • ServerSession 存储了每个会话,保存对 Channel和 User,使用User 表示连接上来用户

  • 前端要求填入用户和房间(群组)后,模拟登录,并返回用户列表。进入后可以发送群组消息。

package com.crazymaker.websocket.processer;

import com.crazymaker.websocket.Model.User;
import com.crazymaker.websocket.session.ServerSession;
import com.crazymaker.websocket.session.SessionMap;
import com.crazymaker.websocket.util.JsonUtil;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * 业务处理器
 * Created by 尼恩 @ 疯狂创客圈
 */
@Slf4j
public class ChatProcesser
{
    private static final Logger logger = LoggerFactory.getLogger(ChatProcesser.class);


    /**
     * 单例
     */
    private static ChatProcesser singleInstance = new ChatProcesser();

    public static ChatProcesser inst()
    {
        return singleInstance;
    }

    /**
     * 连接建立成功调用的方法
     *
     * @param s 会话
     */
    public String onOpen(ServerSession s) throws IOException
    {
        Map<String, String> result = new HashMap<>();
        result.put("type", "bing");
        result.put("sendUser", "系统消息");
        result.put("id", s.getId());

        String json = JsonUtil.pojoToJson(result);
        return json;
    }

    /**
     * 连接关闭调用的方法
     */
    public String onClose(ServerSession s)
    {
        User user = s.getUser();
        if (user != null)
        {
            String nick = user.getNickname();
            Map<String, String> result = new HashMap<>();
            result.put("type", "init");
            result.put("msg", nick + "离开房间");
            result.put("sendUser", "系统消息");
            String json = JsonUtil.pojoToJson(result);
            return json;
        }
        return null;
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 消息内容
     * @param session 会哈
     */
    public Map<String, String> onMessage(String message, ServerSession session)
    {
        TypeToken typeToken = new TypeToken<HashMap<String, String>>()
        {
        };
        Map<String, String> map = JsonUtil.jsonToPojo(message, typeToken);
        Map<String, String> result = new HashMap<>();
        User user = null;
        switch (map.get("type"))
        {
            case "msg":
                user = session.getUser();
                result.put("type", "msg");
                result.put("msg", map.get("msg"));
                result.put("sendUser", user.getNickname());
                break;
            case "init":
                String room = map.get("room");
                session.setGroup(room);
                String nick = map.get("nick");
                user = new User(session.getId(), nick);
                session.setUser(user);
                result.put("type", "init");
                result.put("msg", nick + "成功加入房间");
                result.put("sendUser", "系统消息");

                break;
            case "ping":
                break;
        }

        return result;
    }

    /**
     * 连接发生错误时的调用方法
     *
     * @param session 会话
     * @param error   异常
     */
    public String onError(ServerSession session, Throwable error)
    {

        //捕捉异常信息
        if (null != error)
        {
            log.error(error.getMessage());
        }

        User user = session.getUser();
        if (user == null)
        {
            return null;
        }
        String nick = user.getNickname();

        Map<String, String> result = new HashMap<>();
        result.put("type", "init");
        result.put("msg", nick + "离开房间");
        result.put("sendUser", "系统消息");

        String json = JsonUtil.pojoToJson(result);
        return json;
    }


}

说明:本文会以pdf格式持续更新,更多最新尼恩3高pdf笔记,请从下面的链接获取:语雀 或者 码云

posted @ 2020-04-21 18:50  疯狂创客圈  阅读(3675)  评论(1编辑  收藏  举报