netty入门---实现一个im的相关配置流程

195 阅读3分钟

websocket基础

webssocket建立握手时发送http请求,当成功后.http转变成ws

大概思路 :ws建立连接 http接口(熟悉的mvc)发送消息(单聊或群发)
调用webservice,向连接发送消息

扩展:事件驱动_异步保存具体信息

netty服务端配置

这段代码属于netty入门的模板代码

我们自定义 HttpHeadersHandler 继承ChannelInboundHandlerAdapter 接口

定义 NettyWebSocketServerHandler extends SimpleChannelInboundHandler 其中TextWebSocketFrame就是传输数据的包装类

实现public void channelRead(ChannelHandlerContext ctx, Object msg) 方法

  1. pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));转变协议的代码实现
// 服务器启动引导对象
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    serverBootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .option(ChannelOption.SO_BACKLOG, 128)
            .option(ChannelOption.SO_KEEPALIVE, true)
            //处理日志
            .handler(new LoggingHandler(LogLevel.INFO)) // 为 bossGroup 添加 日志处理器
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ChannelPipeline pipeline = socketChannel.pipeline();
                    //30秒客户端没有向服务器发送心跳则关闭连接
                    pipeline.addLast(new IdleStateHandler(30, 0, 0));
                    // 因为使用http协议,所以需要使用http的编码器,解码器
                    pipeline.addLast(new HttpServerCodec());
                    // 以块方式写,添加 chunkedWriter 处理器
                    pipeline.addLast(new ChunkedWriteHandler());
                    /**
                     * 说明:
                     *  1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;
                     *  2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因
                     */
                    pipeline.addLast(new HttpObjectAggregator(8192));
                    //保存用户ip---最开始是一个http  然后升级为ws  起始
                    pipeline.addLast(new HttpHeadersHandler());
                    /**
                     * 说明:
                     *  1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;
                     *  2. 可以看到 WebSocketFrame 下面有6个子类
                     *  3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri
                     *  4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
                     *      是通过一个状态码 101 来切换的
                     */
                    pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
                    // 自定义handler ,处理业务逻辑
                    pipeline.addLast(new NettyWebSocketServerHandler());
                }
            });
    // 启动服务器,监听端口,阻塞直到启动成功
    serverBootstrap.bind(WEB_SOCKET_PORT).sync();
}

基础对象

简单肤浅的理解 channel:连接

  1. Channel.pipeline()

    • 返回一个ChannelPipeline对象,用于处理Channel的事件和数据。
  2. 属性值绑定chennel

<T> Attribute<T> attr(AttributeKey<T> var1);

<T> boolean hasAttr(AttributeKey<T> var1);

有上下文的绑定和获取,就可以实现一个工具类->对channel进行特殊绑定

public class NettyUtil {

    public static AttributeKey<String> TOKEN = AttributeKey.valueOf("token");
    public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
    public static AttributeKey<Long> UID = AttributeKey.valueOf("uid");


    public static <T> void setAttr(Channel channel, AttributeKey<T> attributeKey, T data) {
        Attribute<T> attr = channel.attr(attributeKey);
        attr.set(data);
    }

    public static <T> T getAttr(Channel channel, AttributeKey<T> ip) {
        return channel.attr(ip).get();
    }
}

处理器的实现

握手阶段

握手阶段为channel绑定相关的的数据(http)

token则是作为参数放在url上

ws协议不能携带请求头,但nginx可以设置请求头,若没有nginx,则直接通过channel获取

@Slf4j
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        log.info("loc5----http-IP");
        if (msg instanceof FullHttpRequest) {
            log.info("loc5  enter");
            FullHttpRequest request = (FullHttpRequest) msg;
            UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());


            // 获取token参数

            //上面获取token
            String token = Optional.ofNullable(urlBuilder.getQuery()).map(k->k.get("token")).map(CharSequence::toString).orElse("");
            NettyUtil.setAttr(ctx.channel(), NettyUtil.TOKEN, token);

            // 获取请求路径
            //请求头
            request.setUri(urlBuilder.getPath().toString());
            HttpHeaders headers = request.headers();
            String ip = headers.get("X-Real-IP");
            if (StringUtils.isEmpty(ip)) {//如果没经过nginx,就直接获取远端地址
                InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
                ip = address.getAddress().getHostAddress();
            }
            NettyUtil.setAttr(ctx.channel(), NettyUtil.IP, ip);
            ctx.pipeline().remove(this);
            ctx.fireChannelRead(request);
        }else
        {
            ctx.fireChannelRead(msg);
        }
    }
}

调试举例

ws://localhost:8090/websocket?token=hello

image.png

image.png

image.png

ws处理类

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
  log.info("传输数据");
   WSBaseReq wsBaseReq = JSONUtil.toBean(msg.text(), WSBaseReq.class);


   WSReqTypeEnum wsReqTypeEnum = WSReqTypeEnum.of(wsBaseReq.getType());
   switch (????) {
       case LOGIN:
          
//返回ws数据
      channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));

       case HEARTBEAT://心跳
           break;
       default:
           log.info("未知类型");
   }
}

框架父类上的其他生命周期

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.warn("异常发生,异常消息 ={}", cause);
//        super.exceptionCaught(ctx, cause);
        ctx.channel().close();
    }

发现超时后 主要是处理chanel相关的数据下线

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) 

离线

@Override
public void channelInactive(ChannelHandlerContext ctx) 

客户端离线

@Override
public void handlerRemoved(ChannelHandlerContext ctx)

心跳不响应的生命周期

image.png