使用springboot+netty框架基于websocket协议开发聊天室——开发+部署(阿里云centos)

     看这篇博文之前,你需要对netty的使用有基本的了解,比如服务端建立,handler中的事件等等有所了解。但并不需要太过深入。

     最近一段时间都在学习netty。强烈推荐《Z00317 NETTY权威指南(第2版)》这本书。

     废话不多说,结合自己的实践,在此给大家做一下使用springboot+netty开发聊天室的详细介绍。我力求话语简单直白,不给大家增加疑惑。

老套路,服务端+客户端。(详细的代码请参照我的GitHub

一、服务端

1、服务启动类代码

       服务启动类比较重要的东西是ChannelInitializer的内部类中的内容,其实整个服务端的东西核心是我下面将要说的处理事件的handler类,而这个服务启动类就看一下就行,不要深究。当netty掌握到一定程度时再详细的去学习启动过程。

@Component  //加入容器
public class WebSocketServer {

    @Value("${socket.server.port}")  //在.xml文件中配置属性注入
    private int port;

    @Value("${socket.server.address}")
    private String address;


    public void start() throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap sb = new ServerBootstrap();
                    sb.option(ChannelOption.SO_BACKLOG, 1024);
                    sb.group(group, bossGroup) // 绑定线程池
                    .channel(NioServerSocketChannel.class) // 指定使用的channel
                    .localAddress(this.port)// 绑定监听端口
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("收到新连接");
                            //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
                            ch.pipeline().addLast("http-codec",new HttpServerCodec());
                            //以块的方式来写的处理器
                            ch.pipeline().addLast("aggregator",new HttpObjectAggregator(8192));
                            ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());

                            ch.pipeline().addLast(new MyChannelHandler());
                        }
                    });
            ChannelFuture cf = sb.bind(address,port).sync(); // 服务器异步创建绑定
            System.out.println(WebSocketServer.class + " 启动正在监听: " + cf.channel().localAddress());
            cf.channel().closeFuture().sync(); // 关闭服务器通道
        } finally {
            group.shutdownGracefully().sync(); // 释放线程池资源
            bossGroup.shutdownGracefully().sync();
        }
    }
}

2、handler

重点来了,handler是一个服务端的核心,来看看handler中是怎么实现用户聊天的

比较重要的是这样几个事件方法:

1、channelActive--对应连接建立事件

2、channelRead0--对应服务端读事件

两个用户之间可以聊天归功于 private static volatile Vector<ChannelHandlerContext> contexts = new Vector<>(2),这个东西。请注意这里必须加上static和volatile关键字。因为每个用户进来都是对应不同的MyChannelHandler 实例,所以要是静态的,这样不同用户之间共享一个Vector。这里我只做了支持两个用户一对一聊天。

@ChannelHandler.Sharable  //此处要加Sharable注解,标注此handler可以被多个连接使用
public class MyChannelHandler extends SimpleChannelInboundHandler<Object>{

    private WebSocketServerHandshaker handshaker;
    private static final int MAX_CONN = 2;//指定最大连接数
    private static volatile int connectNum = 0;//当前连接数
    //channelHandlerContext表
    private static volatile Vector<ChannelHandlerContext> contexts = new Vector<>(2);


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
//        System.out.println("与客户端建立连接,通道开启!");
        // TODO Auto-generated method stub
        connectNum++;
        //控制客户端连接数量,超过则关闭
        if (connectNum > MAX_CONN) {
            ctx.writeAndFlush(new TextWebSocketFrame(Unpooled.copiedBuffer("达到人数上限".getBytes())));
            ctx.channel().close();
            //当前连接数的更新放在channelInactive()里
        }
        //更新contexts
        contexts.add(ctx);
        //控制台输出相关信息
        InetSocketAddress socket = (InetSocketAddress) ctx.channel().remoteAddress();
        System.out.println(socket.getAddress().getHostAddress() + ":" + socket.getPort() + "已连接");
        System.out.println("当前连接数:" + connectNum);
        ctx.writeAndFlush(new TextWebSocketFrame("hello client"));

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // TODO Auto-generated method stub
        //更新当前连接数
        connectNum--;
        //更新contexts数组
        contexts.remove(ctx);

        System.out.println("连接断开,当前连接数:" + connectNum);

        //控制台输出相关信息
        InetSocketAddress socket = (InetSocketAddress) ctx.channel().remoteAddress();
        System.out.println(new java.util.Date().toString() + ' ' + socket.getAddress().getHostAddress() + ":" + socket.getPort() + "已退出");
        //对另一个客户端发出通知
        if (contexts.size() == 1) {
            contexts.get(0).writeAndFlush(new TextWebSocketFrame("对方退出聊天"));
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        //传统的HTTP介入
        if(msg instanceof FullHttpRequest){
            handleHttpRequest(ctx,(FullHttpRequest) msg);
        }

        //WebSocket 接入
        else if(msg instanceof WebSocketFrame){
            handleWebSocketFrame(ctx,(WebSocketFrame) msg);
        }

    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {

        //判断是否是关闭链路的指令
        if(frame instanceof CloseWebSocketFrame){
            handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());
            return;
        }

        //判断是否是ping消息
        if(frame instanceof PongWebSocketFrame){
            PingWebSocketFrame ping = new PingWebSocketFrame(frame.content().retain());
            ctx.channel().writeAndFlush(ping);
            return ;
        }

        //判断是否是pong消息
        if(frame instanceof PingWebSocketFrame){
            PongWebSocketFrame pong = new PongWebSocketFrame(frame.content().retain());
            ctx.channel().writeAndFlush(pong);
            return ;
        }

        //仅支持文本消息,不支持二进制消息
        if(!(frame instanceof TextWebSocketFrame)){
            throw new UnsupportedOperationException("不支持二进制");
        }

        //返回应答消息
        //可以对消息进行处理
        //群发
        String request=((TextWebSocketFrame) frame).text();

        if (contexts.size() <= 1) {
            ctx.channel().write(new TextWebSocketFrame("对方不在线"));

            //return;
        }else{
            int currentIndex = contexts.indexOf(ctx);
            int anotherIndex = Math.abs(currentIndex - 1);
            //给另一个客户端转发信息
            contexts.get(anotherIndex).writeAndFlush(new TextWebSocketFrame(request));
        }
    }


    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception{
        //如果HTTP解码失败,则返回http异常
        if(!req.decoderResult().isSuccess()||(!"websocket".equals(req.headers().get("Upgrade")))){
            sendHttpResponse(ctx,req,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.BAD_REQUEST));
            return;
        }

        //构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory wsfactory=new WebSocketServerHandshakerFactory("ws://localhost:7788/websocket",null,false);
        //注意,这第一个参数别被误导了,其实这里填写什么都无所谓,WS协议消息的接收不受这里控制

        handshaker=wsfactory.newHandshaker(req);
        if (handshaker==null){
            WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
        } else{
            handshaker.handshake(ctx.channel(),req);
        }

    }

    private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res){
        if(res.status().code()!=200){
            ByteBuf buf= Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
        }

        //如果不是长连接,则关闭连接
        ChannelFuture future = ctx.channel().writeAndFlush(res);
        if (HttpHeaders.isKeepAlive(req)||res.status().code()!=200){
            future.addListener(ChannelFutureListener.CLOSE);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //发生异常 关闭连接
        ctx.close();
    }
}

       整个handler代码看下来其实大体的东西很简单,核心就是channelRead0这个方法。我们知道根据websocket协议,建立连接时需要使用http协议进行一次握手,握手成功之后根据tcp协议传输数据。知道了这些再看这个方法就显而易见了。

首先第一个if语句中的“handleHttpRequest(ctx,(FullHttpRequest) msg)”方法,是自定义的方法,作用就是进行http握手。

else if语句中的“handleWebSocketFrame(ctx,(WebSocketFrame) msg)”方法,也是自定义的方法,作用就是传输聊天内容。

@Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {

        //传统的HTTP介入
        if(msg instanceof FullHttpRequest){
            handleHttpRequest(ctx,(FullHttpRequest) msg);
        }

        //WebSocket 接入
        else if(msg instanceof WebSocketFrame){
            handleWebSocketFrame(ctx,(WebSocketFrame) msg);
        }

    }
 

着重讲一下handleWebSocketFrame(ctx,(WebSocketFrame) msg)方法的实现。

private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {

        //判断是否是关闭链路的指令
        if(frame instanceof CloseWebSocketFrame){
            handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());
            return;
        }

        //判断是否是ping消息
        if(frame instanceof PongWebSocketFrame){
            PingWebSocketFrame ping = new PingWebSocketFrame(frame.content().retain());
            ctx.channel().writeAndFlush(ping);
            return ;
        }

        //判断是否是pong消息
        if(frame instanceof PingWebSocketFrame){
            PongWebSocketFrame pong = new PongWebSocketFrame(frame.content().retain());
            ctx.channel().writeAndFlush(pong);
            return ;
        }

        //仅支持文本消息,不支持二进制消息
        if(!(frame instanceof TextWebSocketFrame)){
            throw new UnsupportedOperationException("不支持二进制");
        }

        //返回应答消息
        //可以对消息进行处理

        //获取接收的消息
        String request=((TextWebSocketFrame) frame).text();


        if (contexts.size() <= 1) {
            ctx.channel().write(new TextWebSocketFrame("对方不在线"));

            //return;
        }else{
            int currentIndex = contexts.indexOf(ctx);
            int anotherIndex = Math.abs(currentIndex - 1);
            //给另一个客户端转发信息
            contexts.get(anotherIndex).writeAndFlush(new TextWebSocketFrame(request));
        }
    }

     看完代码我们知道了,原来两个用户之间实现消息的发送与接受其实就是获取到对方的ChannelHandlerContext对象,即Vector中所存储的,是不是豁然开朗。

3、通过springboot启动

既然是通过springboot整合的,那需要在springboot启动的时候就开启服务端。很简单,代码如下:

@SpringBootApplication
public class SocketServerApplication implements CommandLineRunner {

	@Resource
	WebSocketServer webSocketServer;

	public static void main(String[] args) throws Exception {
		SpringApplication.run(SocketServerApplication.class, args);
	}

    //启动websocket服务端
	@Override
	public void run(String... strings) throws Exception {
		webSocketServer.start();
	}
}

二、客户端

客户端的代码就没有那么复杂了,就是一个简单的web项目。

1、访问到聊天页面的controller

@Controller
public class Client_Entrance {

    @GetMapping(value = "chat")
    public String test(){
        return "index";
    }
}

2、静态资源

静态资源没什么可说的,通过JavaScript向服务端发送websocket请求的路劲要写对:

socket = new WebSocket("ws://ip:端口/websocket");

三、部署到阿里云

1、首先需要将客户端和服务端打成jar包,idea中打jar包有个小坑,正确的步骤请参照 使用idea打jar包正确教程

2、坑!

     重要的事情说三遍:服务端部署到阿里云的时候,绑定的地址不是公网ip,是内网ip!是内网ip!是内网ip!

但是客户端JavaScript中访问的地址是:公网ip!

四、效果展示

经过上述步骤,一个简单的聊天室就就好了

猜你喜欢

转载自blog.csdn.net/qq_40259907/article/details/84326104