Netty高级应用实战篇(上)

507 阅读17分钟

WebSocket长连接

简介

​ WebSocket 是 HTML5 中的协议,是构建在 HTTP 协议之上的一个网络通信协议,其以长连接的方式实现了客户端与服务端的全双工通信。HTTP/1.1 版本协议中具有 keep-alive 属性,实现的是半双工通信。

握手原理

image-20201112111649171

  • Clinet端生产一个随机的Key,保存到浏览器和请求头中。
  • Clinet发送请求给Server端。
  • Server从请求头属性中判断出这事一个ws(WebSocket)请求,读到请求头中的key属性。
  • Server对key加密,放到响应头accept属性中。
  • Server将响应回应给Clinet端。
  • Clinet端从头中判断是一个ws(WebSocket)响应,读取头中的accpet属性。
  • Clinet端进行解密后,将结果保存在浏览中的key进行比较,如果匹配,则连接。否则不连接。

场景需求分析

​ 在页面上有两个左右并排的文本域,它们的中间有一个“发送”按钮。在左侧文本域中输入文本内容后,单击发送按钮,会显示到右侧文本域中。

客户端页面定义

​ 在 src/main 下定义一个目录 webapp。在其中定义 html 页面。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<script type="text/javascript">

    // 当前页面一打开就会执行的代码
    var socket;

    if(window.WebSocket) {
        // 创建一个WebSocket连接
        socket = new WebSocket("ws://localhost:8888/gc");

        // 当与服务端的ws连接创建成功后会触发onopen的执行
        socket.onopen = function (ev) {
            // 在右侧文本域中显示连接建立提示
            var ta = document.getElementById("responseText");
            ta.value = "连接已建立";
        }

        // 当接收到服务端发送的消息时会触发onmessage的执行
        socket.onmessage = function (ev) {
            // 将服务端发送来的消息在右侧文本域中显示,在原有内容基础上进行拼接
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n" + ev.data;
        }

        // 当与服务端的ws连接断开时会触发onclose的执行
        socket.onclose = function (ev) {
            // 将连接关闭消息在右侧文本域中显示,在原有内容基础上进行拼接
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n连接已关闭";
        }
    } else {
        alert("浏览器不支持WebSocket");
    }

    // 定义发送按钮的发送方法
    function send(msg) {
        // 若当前浏览器不支持WebSocket,则直接结束
        if(!window.WebSocket) return;

        // 若ws连接已打开,则向服务器发送消息
        if(socket.readyState == WebSocket.OPEN) {
            // 通过ws连接向服务器发送消息
            socket.send(msg);
        }
    }
</script>
<body>
    <form>
        <textarea id="message" style="width: 150px; height: 150px"></textarea>
        <input type="button" value="发送" onclick="send(this.form.message.value)">
        <textarea id="responseText" style="width: 150px; height: 150px"></textarea>
    </form>
</body>
</html>

服务端定义

package com.gc.socket.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-10 16:11
 **/
public class Server {
    public static void main(String[] args) {

        //定义用于处理客户端连接的EventLoopGroup
        EventLoopGroup parentEventLoop = new NioEventLoopGroup();
        //定义用于处理客户端请求的EventLoopGroup
        EventLoopGroup childEventLoop = new NioEventLoopGroup();
        try{

            //服务端专用Bootstrap.主要用于将属绑定起来。建立关系
            ServerBootstrap bootstrap = new ServerBootstrap();
            //绑定处理客户端的EventLoopGroup对象
            bootstrap.group(parentEventLoop, childEventLoop)
                    //使用NIO的方式
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.ERROR))
                    //绑定消息收发时的编码/解码器。 和自定义的Handler绑定
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //HttpRequestDecoder和HttpResponseEncoder的组合使
                            pipeline.addLast(new HttpServerCodec());
                            //可以毫无困难地发送大型数据流。
                            pipeline.addLast(new ChunkedWriteHandler());
                            //聚合处理器,聚合请求或者响应
                            pipeline.addLast(new HttpObjectAggregator(111));
                            //处理程序为你运行一个websocket服务器做了所有繁重的工作。
                            // 它负责websocket握手以及控制框架的处理(Close,Ping,Pong)。
                            // 文本和二进制数据帧被传递到管道中的下一个处理程序(由您执行)进行处理。
                            pipeline.addLast(new WebSocketServerProtocolHandler("/gc"));
                            //自定义业务处理器
                            pipeline.addLast(new ServerHandler());
                        }
                    });
            //声明服务端绑定的端口
            ChannelFuture future = bootstrap.bind(8888).sync();
            //当Channel调用了close()方法,并且成功关闭之后,才会调用此代码
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //优雅的关闭方式
            parentEventLoop.shutdownGracefully();
            childEventLoop.shutdownGracefully();
        }
    }
}

业务处理器定义

package com.gc.socket.server;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.Date;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String text = ((TextWebSocketFrame)msg).text();
        ctx.channel().writeAndFlush(new TextWebSocketFrame("客户端:"+text));
        System.out.println("本地地址为:"+ctx.channel().localAddress());
        System.out.println("远程地址为:"+ctx.channel().remoteAddress()+" ---> 收到消息:"+msg);
    }

    //当发生Throwable异常时,会调用该方法
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
        ctx.close();
    }

}

Socket实现群聊

简介

​ 本例要实现一个网络群聊工具。参与聊天的客户端消息是通过服务端进行广播的。

服务端定义

package com.gc.socket.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-10 16:11
 **/
public class Server {
    public static void main(String[] args) {

        //定义用于处理客户端连接的EventLoopGroup
        EventLoopGroup parentEventLoop = new NioEventLoopGroup();
        //定义用于处理客户端请求的EventLoopGroup
        EventLoopGroup childEventLoop = new NioEventLoopGroup();
        try{

            //服务端专用Bootstrap.主要用于将属绑定起来。建立关系
            ServerBootstrap bootstrap = new ServerBootstrap();
            //绑定处理客户端的EventLoopGroup对象
            bootstrap.group(parentEventLoop, childEventLoop)
                    //使用NIO的方式
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.ERROR))
                    //绑定消息收发时的编码/解码器。 和自定义的Handler绑定
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();

                            //因为要传输Clinet端的消息,所以肯定是先收到消息,所以解码器放前面
                            pipeline.addLast(new LineBasedFrameDecoder(2024));
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            //自定义业务处理器
                            pipeline.addLast(new ServerHandler());
                        }
                    });
            //声明服务端绑定的端口
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("Server 启动成功");
            //当Channel调用了close()方法,并且成功关闭之后,才会调用此代码
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //优雅的关闭方式
            parentEventLoop.shutdownGracefully();
            childEventLoop.shutdownGracefully();
        }
    }
}

服务端业务处理器定义

package com.gc.socket.server;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.Date;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class ServerHandler extends ChannelInboundHandlerAdapter {


    //消息组,聊天的人都在这个组里面。类似社交软件的群聊
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 读取到发送的消息
     * @param ctx
     * @param msg
     * @throws Exception
     */
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.forEach(ch -> {
            if(ch != channel)//不是自己,显示别人的地址
                ch.writeAndFlush(channel.remoteAddress()+":"+msg+"\n");
            else//是自己,不需要显示
                ch.writeAndFlush(msg+"\n");

        });
    }

    /**
     * 用户上线,会触发此方法
     * @param ctx
     * @throws Exception
     */
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(channel.remoteAddress()+"上线"+"\n");
        channelGroup.add(channel);
    }

    /**
     * 用户下线,会触发此犯法
     * @param ctx
     * @throws Exception
     */
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(channel.remoteAddress()+"下线。当前在线人数:"+channelGroup.size()+"\n");
        //当触发此方法时,会自动从channelGroup中删除。以下代码是当channel在线时,将它踢出channelGroup
        //channelGroup.remove(channel);
    }

    //当发生Throwable异常时,会调用该方法
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
        ctx.close();
    }

}

客户端定义

package com.gc.socket.clinet;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

import java.io.*;
import java.net.SocketAddress;

public class Clinet {

    public static void main(String[] args) throws Exception {
        //客户端处理服务端的EventLoopGroup
        EventLoopGroup clint = new NioEventLoopGroup();
        //Bootstrap专门用于客户端的属性设置以及关系绑定
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(clint)
                //使用NIO客户端的Channel
                .channel(NioSocketChannel.class)
                //绑定编码器/解码器。以及自定义的Handler
                .handler(new ChannelInitializer<SocketChannel>(){
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //当上线后,Client端会收到Server端的提示信息,所以这里解码器放前面
                        pipeline.addLast(new LineBasedFrameDecoder(2048));
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new ClinetHandler());
                    }
                });
        //指定连接服务端的端口
        ChannelFuture future = bootstrap.connect("127.0.0.1",8888).sync();
        System.out.println("Clinet 启动成功");

        Channel channel = future.channel();
        InputStream in;
        InputStreamReader is = new InputStreamReader(System.in , "UTF-8");
        BufferedReader br = new BufferedReader(is);
        while (true) {
            channel.writeAndFlush(br.readLine()+"\n");
        }
        //这里不关闭资源,因为客户端要持续输入。
    }
}

客户端业务处理器定义

package com.gc.socket.clinet;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * ChannelInboundHandlerAdapter 类中的 channelRead方法不会自动释放资源。所以会不断的发送消息
 * SimpleChannelInboundHandler 类中的 channelRead0()方法不会自动释放资源。所以只要Clinet端触发了消息发送, 双方两段就会一直死循环发送消息。
 *
 */
public class ClinetHandler extends SimpleChannelInboundHandler<String> {


    /**
     * 读取服务端发送的消息
     * @param ctx
     * @param msg
     * @throws Exception
     */
    public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.err.println("Clinet:"+msg);
    }

    //当发生异常,捕获并关闭
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

读写空闲监测

服务端修改

image-20201112165646419

服务端业务处理器修改

image-20201112170158828

心跳机制

简介

​ 所谓心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还“活着”, 以确保 TCP 连接的有效性。

客户端定义

package com.gc.socket.clinet;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

import java.io.*;
import java.net.SocketAddress;

public class Clinet {

    public static void main(String[] args) throws Exception {
        //客户端处理服务端的EventLoopGroup
        EventLoopGroup clint = new NioEventLoopGroup();
        //Bootstrap专门用于客户端的属性设置以及关系绑定
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(clint)
                //使用NIO客户端的Channel
                .channel(NioSocketChannel.class)
                //绑定编码器/解码器。以及自定义的Handler
                .handler(new ChannelInitializer<SocketChannel>(){
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        //当上线后,Client端会收到Server端的提示信息,所以这里解码器放前面
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new ClinetHandler(bootstrap));
                    }
                });
        //指定连接服务端的端口
        ChannelFuture future = bootstrap.connect("127.0.0.1",8888).sync();
        System.out.println("Clinet 启动成功");
    }
}

客户端业务处理器

package com.gc.socket.clinet;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.concurrent.GenericFutureListener;
import io.netty.util.concurrent.ScheduledFuture;

import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * ChannelInboundHandlerAdapter 类中的 channelRead方法不会自动释放资源。所以会不断的发送消息
 * SimpleChannelInboundHandler 类中的 channelRead0()方法不会自动释放资源。所以只要Clinet端触发了消息发送, 双方两段就会一直死循环发送消息。
 *
 */
public class ClinetHandler extends ChannelInboundHandlerAdapter {


    //用于添加任务定时触发
    private ScheduledFuture<?> scheduled;
    //用于监听任务过期
    private GenericFutureListener listener;
    //用于客户端断链重连
    private Bootstrap bootstrap;


    public ClinetHandler(Bootstrap bootstrap){
        this.bootstrap = bootstrap;
    }

    /**
     * 用户上线后,触发此方法
     * @param ctx
     * @throws Exception
     */
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        setRandom(ctx.channel());
    }

    /**
     * 当用户下线后,触发此方法
     * @param ctx
     * @throws Exception
     */
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //下线后,删除监听任务。 否则数据量大了,可能会导致OOM
        scheduled.removeListener(listener);

        System.out.println("已断链, 准备重连。");
        bootstrap.connect("localhost", 8888).sync();
    }

    /**
     * 心跳监测时间
     * @throws Exception
     */
    public void setRandom(Channel ctx) throws Exception {
        //随机下次像Server端发送心跳的时间
        int random = new Random().nextInt(5)+1;
        System.out.println(random+"S后发送心跳");

        scheduled = ctx.eventLoop().schedule(()->{
            //当前channel处于活动状态, 说明没死,发送心跳监测
            if(ctx.isActive()){
                System.out.println("向服务端发送心跳");
                ctx.writeAndFlush("ping");
            }else{
                //已经死了
                System.out.println("心跳超时,连接断开");
            }
        },random, TimeUnit.SECONDS);

        //给当前方法定义监听器
        listener = future1 -> {
            setRandom(ctx);
        };

        //添加一个定时监听
        scheduled.addListener(listener);
    }


    //当发生异常,捕获并关闭
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

服务端定义

package com.gc.socket.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-10 16:11
 **/
public class Server {
    public static void main(String[] args) {

        //定义用于处理客户端连接的EventLoopGroup
        EventLoopGroup parentEventLoop = new NioEventLoopGroup();
        //定义用于处理客户端请求的EventLoopGroup
        EventLoopGroup childEventLoop = new NioEventLoopGroup();
        try{

            //服务端专用Bootstrap.主要用于将属绑定起来。建立关系
            ServerBootstrap bootstrap = new ServerBootstrap();
            //绑定处理客户端的EventLoopGroup对象
            bootstrap.group(parentEventLoop, childEventLoop)
                    //使用NIO的方式
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.ERROR))
                    //绑定消息收发时的编码/解码器。 和自定义的Handler绑定
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            /**
                             * @prarm1:在指定的时间内,服务端没发生读操作(时间单位:S)
                             * @prarm2:在指定的时间内,服务端没发生写操作(时间单位:S)
                             * @prarm3:在指定的时间内,服务端即要发生读操作,也要发生写操作。如果只出现了两个操作中的一个,时间到达时,还是会被连接中断(时间单位:S)
                             */
                            pipeline.addLast(new IdleStateHandler(3,0,0));
                            //自定义业务处理器
                            pipeline.addLast(new ServerHandler());
                        }
                    });
            //声明服务端绑定的端口
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("Server 启动成功");
            //当Channel调用了close()方法,并且成功关闭之后,才会调用此代码
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //优雅的关闭方式
            parentEventLoop.shutdownGracefully();
            childEventLoop.shutdownGracefully();
        }
    }
}

服务端业务处理器定义

package com.gc.socket.server;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;

public class ServerHandler extends ChannelInboundHandlerAdapter {


    /**
     * 读取客户端发送的消息
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("接收到Client发送的消息:" + msg);
    }

    /**
     * 异常捕捉
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    /**
     * 用于捕获当前Server中的各种事件
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleState state = ((IdleStateEvent) evt).state();
            if (state == IdleState.READER_IDLE) {
                System.out.println("将要断开连接");
                ctx.close();
            } else {
                super.userEventTriggered(ctx, evt);
            }
        }
    }
}

手写Tomcat

简介

​ 确切地说,这里要手写的是一个 Web 容器,一个类似于 Tomcat 的容器,用于处理 HTTP请求。该Web 容器没有实现 JavaEE 的 Servlet 规范,不是一个 Servlet 容器。但其是模拟着Tomcat 来写的,这里定义了自己的请求、响应及 Servlet,分别命名为了 NettyRequest,NettyResponse 与 Servnet。

​ 我们这里要定义一个 Tomcat,这个 Web 容器提供给用户后,用户只需要按照使用步骤就可以将其自定义的 Servnet 发布到该 Tomcat 中。我们现在给出用户对于该 Tomcat 的使用步骤:

  • 用户只需将自定义的 Servnet 放入到指定的包中。例如,com.abc.webapp 包中。
  • 用户在访问时,需要将自定义的 Servnet 的简单类名全小写后的字符串作为该 Servnet的 Name 进行访问。
  • 若没有指定的 Servnet,则访问默认的 Servnet。

思路定义

  • Servnet规范包

    1. NettyRequest 包含了传统HttpServletRequest中的方法。如:获取请求路径,获取URL中的参数,获取方法名称,获取参数等等。

    2. NettyResponse 包含了传统HttpServletResponse中的方法。如:读/写操作。

    3. Servnet

      和Servlet一样,有doPost(),doGet()

  • Tomcat内核包

    1. main()

      用于加载指定目录下的接口。

    2. DefaultNettyRequest,DefaultNettyResponse

      我们自定义NettyResponse,NettyResponse的具体实现。

    3. DefaultServnet

      我们自定义的Servnet 具体实现。

    4. TomcatHandler

      请求解析业务处理器,主要用于解析接收请求。

    5. TomcatServer

      当启动容器时,初始化资源到本地缓存。

  • webapp

    要访问的接口,都在该目录下。

项目结构

截屏2020-11-13 下午9.16.42

编码实现

定义NettyRequest规范

package com.gc.servnet;

import com.sun.deploy.net.HttpRequest;

import java.util.List;
import java.util.Map;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 15:56
 **/
public interface NettyRequest {

    //获取URL中的参数
    String getUri();

    //获取方法名称
    String getMethod();

    //获取请求路径
    String getPath();

    //获取本次请求中所有的参数
    Map<String, List<String>> getParameters();

    //获取指定名称的参数值
    List<String> getParameters(String name);

    //指定参数名称获取参数值(第一个值)
    String getParameter(String name);

}

定义NettyResponse规范

package com.gc.servnet;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 15:56
 **/
public interface NettyResponse {

    void write(String content) throws Exception;
}

定义Servnet规范

package com.gc.servnet;

public abstract class Servnet {

    //定义Servlet中的doGet
    public abstract void doGet(NettyRequest request, NettyResponse response) throws Exception;

    //定义Servlet中的doPost
    public abstract void doPost(NettyRequest request, NettyResponse response) throws Exception;


}

DefaultNettyRequest具体实现

package com.gc.tomcat;

import com.gc.servnet.NettyRequest;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.QueryStringDecoder;

import java.util.List;
import java.util.Map;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 16:25
 **/
public class DefaultNettyRequest implements NettyRequest {

    HttpRequest request;

    public DefaultNettyRequest(HttpRequest request){
        this.request = request;
    }

    @Override
    public String getUri() {
        return request.uri();
    }

    @Override
    public String getMethod() {
        return request.method().name();
    }

    @Override
    public String getPath() {
        QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
        return decoder.path();
    }

    @Override
    public Map<String, List<String>> getParameters() {
        QueryStringDecoder decoder = new QueryStringDecoder(getUri());
        return decoder.parameters();
    }

    private int count = 0;
    @Override
    public List<String> getParameters(String name) {
        Map<String, List<String>> map = getParameters();
        return map.get(name);
    }

    @Override
    public String getParameter(String name) {
        List<String> list = getParameters(name);
        if(null == list){
            System.out.println("要获取的参数不存在");
            return null;
        }
        return list.get(0);
    }
}

DefaultNettyResponse具体实现

package com.gc.tomcat;

import com.gc.servnet.NettyResponse;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import io.netty.util.internal.StringUtil;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 16:34
 **/
public class DefaultNettyResponse implements NettyResponse {
    private HttpRequest request;
    private ChannelHandlerContext context;

    public DefaultNettyResponse(HttpRequest request, ChannelHandlerContext context) {
        this.request = request;
        this.context = context;
    }

    @Override
    public void write(String content) throws Exception {
        // 处理content为空的情况
        if (StringUtil.isNullOrEmpty(content)) {
            return;
        }
        // 创建响应对象
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                // 根据响应体内容大小为response对象分配存储空间
                Unpooled.wrappedBuffer(content.getBytes("UTF-8")));

        // 获取响应头
        HttpHeaders headers = response.headers();
        // 设置响应体类型
        headers.set(HttpHeaderNames.CONTENT_TYPE, "text/json");
        // 设置响应体长度
        headers.set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        // 设置缓存过期时间
        headers.set(HttpHeaderNames.EXPIRES, 0);
        // 若HTTP请求是长连接,则响应也使用长连接
        if (HttpUtil.isKeepAlive(request)) {
            headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        context.writeAndFlush(response);
    }
}

DefaultServnet具体实现

package com.gc.tomcat;

import com.gc.servnet.NettyRequest;
import com.gc.servnet.NettyResponse;
import com.gc.servnet.Servnet;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 16:40
 **/
public class DefaultServnet extends Servnet {

    //定义Servlet中的doGet
    @Override
    public void doGet(NettyRequest request, NettyResponse response) throws Exception {
        String sernetName = request.getUri().split("/")[1];
        response.write("404 - no this servnet : " + sernetName);
    }


    //定义Servlet中的doPost
    @Override
    public void doPost(NettyRequest request, NettyResponse response) throws Exception {
        doGet(request, response);
    }

}

编写自定义TomcatHandler处理器

package com.gc.tomcat;

import com.gc.servnet.NettyRequest;
import com.gc.servnet.NettyResponse;
import com.gc.servnet.Servnet;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpRequest;

import java.util.Map;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 16:47
 **/
public class TomcatHandler extends ChannelInboundHandlerAdapter {

    //启动时候加载到这个Map
    private Map<String, Servnet> nameToServnetMap;

    //如果代码被放射的方式或者其它的方式修改过,则重写加载
    private Map<String, String> nameToClassNameMap;


    //定义一个构造器,当Netty启动时候加载到Netty的Handler中被Netty管理
    public TomcatHandler(Map<String, Servnet> nameToServnetMap, Map<String, String> nameToClassNameMap){
        this.nameToServnetMap = nameToServnetMap;
        this.nameToClassNameMap = nameToClassNameMap;
    }



    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //如果是Http请求,则进入解析
        if(msg instanceof HttpRequest){
            HttpRequest request = (HttpRequest) msg;
            //拿到Servnet名称
            String serverName = request.uri().split("/")[1];
            serverName = serverName.substring(0,serverName.indexOf("?"));
            //获得客户端要访问的Servnet
            Servnet servnet = new DefaultServnet();

            //一级缓存是否有这个key
            if(nameToServnetMap.containsKey(serverName)){
                //直接拿出去
                servnet = nameToServnetMap.get(serverName);
            }else if(nameToClassNameMap.containsKey(serverName)){//一级缓存没有,二级缓存是否有这个key
                //双重检测,防止并发请求时,重复创建
                if(!nameToServnetMap.containsKey(serverName)) {
                    synchronized (this) {
                        if(!nameToServnetMap.containsKey(serverName)) {
                            //有,获取到类的全路径限定路径
                            String classPath = nameToClassNameMap.get(serverName);
                            servnet = (Servnet) Class.forName(classPath).newInstance();
                            nameToServnetMap.put(serverName, servnet);
                        }
                    }
                }
            }

            //开始根据路径处理请求
            //用于获取当前请求中的参数,路径等信息
            NettyRequest defultRequest = new DefaultNettyRequest(request);
            //用于获取当前请求中的上下文数据
            NettyResponse defultResponse = new DefaultNettyResponse(request, ctx);

            // 根据不同的请求类型,调用servnet实例的不同方法
            String methodName = request.method().name();
            if (methodName.equalsIgnoreCase("GET")) {
                servnet.doGet(defultRequest, defultResponse);
            } else if(methodName.equalsIgnoreCase("POST")) {
                servnet.doPost(defultRequest, defultResponse);
            }

            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

}

编写容器启动时初始化TomcatServer

TomcatServerpackage com.gc.tomcat;

import com.gc.servnet.Servnet;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 17:24
 **/
public class TomcatServer {
    // key为servnet的简单类名,value为对应servnet实例
    private Map<String, Servnet> nameToServnetMap = new ConcurrentHashMap<>();
    // key为servnet的简单类名,value为对应servnet类的全限定性类名
    private Map<String, String> nameToClassNameMap = new HashMap<>();
    //加载该路径下的类为Servnet
    private String basePackage;

    public TomcatServer(String basePackage) {
        this.basePackage = basePackage;
    }

    // 启动tomcat
    public void start() throws Exception {
        // 加载指定包中的所有Servnet的类名
        cacheClassName(basePackage);
        //资源准备完毕,启动Netty
        runServer();
    }

    private void cacheClassName(String basePackage) throws UnsupportedEncodingException {
        //获取指定路径下的类
        URL resource = this.getClass().getClassLoader().getResource(basePackage.replaceAll("\\.", "/"));
        if(null == resource){
            System.out.println("没有找到访问资源");
            return;
        }

        //将取到的资源转成一个File
        File dir = new File(resource.getPath());
        //找出该路径下所有的类
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                // 若当前遍历的file为目录,则递归调用当前方法
                cacheClassName(basePackage + "." + file.getName());
            } else if (file.getName().endsWith(".class")) {
                //读到了将文件后缀去掉
                String simpleClassName = file.getName().replace(".class", "").trim();
                nameToClassNameMap.put(simpleClassName.toLowerCase(), basePackage+"."+simpleClassName);
            }
        }
        System.out.println(nameToClassNameMap);

    }

    private void runServer() {

        EventLoopGroup parentLoopGroup = new NioEventLoopGroup();
        EventLoopGroup childLoopGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(parentLoopGroup, childLoopGroup)
                    //当出现并发时,设置这个队列的上限长度
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    // 指定是否启用心跳机制来检测长连接的存活性,即客户端的存活性
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel channel) throws Exception {
                            ChannelPipeline channelPipeline = channel.pipeline();
                            channelPipeline.addLast(new HttpServerCodec());
                            channelPipeline.addLast(new TomcatHandler(nameToServnetMap, nameToClassNameMap));
                        }
                    });

            ChannelFuture future = serverBootstrap.bind(8888).sync();
            System.out.println("启动成功");
            future.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            parentLoopGroup.shutdownGracefully();
            childLoopGroup.shutdownGracefully();
        }
    }


}

编写需要访问接口(Servnet)

package com.gc.webapp;

import com.gc.servnet.NettyRequest;
import com.gc.servnet.NettyResponse;
import com.gc.servnet.Servnet;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 17:44
 **/
public class GC1 extends Servnet {
    @Override
    public void doGet(NettyRequest request, NettyResponse response) throws Exception {
        String uri = request.getUri();
        String path = request.getPath();
        String method = request.getMethod();
        String name = request.getParameter("name");

        String content = "uri = " + uri + "\n" +
                "path = " + path + "\n" +
                "method = " + method + "\n" +
                "param = " + name;
        response.write(content);
    }

    @Override
    public void doPost(NettyRequest request, NettyResponse response) throws Exception {
        doGet(request, response);
    }

}

启动类

package com.gc.tomcat;

/**
 * @description:
 * @author: GC
 * @create: 2020-11-13 16:24
 **/
public class RunTomcat {
    public static void main(String[] args) throws Exception {
        TomcatServer tomcatServer = new TomcatServer("com.gc.webapp");
        tomcatServer.start();
    }
}

手写Tomcat总结

​ 在写之前,我们把思路整理了一下,然后把思路落地。

Servnet规范包

​ 在Servnet包下,我们把传统的Servlet的基本规则和常用的方法功能以接口的形式定义出来了。包括Request中的获取参数,方法名称,访问路径等基本的操作。

​ 以及Response向客户端的写操作定义。

​ 还有Servnet中的doGet和doPos,我们这里只定义具体的声明,把具体的业务逻辑实现交给用户自己灵活编写。

Tomcat核心包

​ 这里面主要是重写各个接口和抽象类的方法,把它们的定义的方法变成我们自己的具体业务实现。

  1. 我们使用DefaultNettyRequest和DefaultNettyResponse实现了NettyRequest和NettyResponse,在这里我们把定义实现了具体的操作。
  2. 我们使用DefaultServnet继承了Servent,分别处理get货post请求。让用户可以在自己的Servent中灵活的做业务逻辑。
  3. 我们编写了TomcatHandler来用于接受客户端的请求解析并处理。在TomcatHandler类中我们具体做的事情:
    • 接受用户的http请求。
    • 从一级缓存中拿到用户要访问的Servnet实例,如果没有则去二级缓存拿到类的全路径限定名,使用反射机制来动态创建出一个Servnet实例提供给本次请求需要用到的实例,创建好后把实例放到一级缓存。值得一提的是,我们怕当客户端出现并发访问出现线程安全问题可能会导致Map中的Servnet实例被重复创建,所以使用了双重检查锁来预防这个问题。
    • 当Servnet实例创建好后,我们将组装Request和Response,其中Respone中我们传递了本次客户端请求的上下文,从该上下文中拿到相关信息组装成一个请求头。
    • 然后获取到请求方式,分别执行响应的方法。
  4. TomcatServer的主要作用有两点:
    • 当容器启动时,模仿真正的Tomcat加载指定目录下的类(我们也可以称为具体的接口地址),到Tomcat的二级缓存中。其中Map的Key为具体的Servnet实例名称,Value为Servent源文件的具体路径。
    • 第二件事是我们在这个类中定义了Netty的启动代码。主要定义了:
      1. 当出现并发时,队列可缓存的最大成功。
      2. 启用心跳机制来检测客户端的存活性,及时的释放链接,减轻服务器的负载压力。
      3. 定义了Http的编码/解码为一体的编解码器。以及实现我们自己的Handler。
  5. 最后,我们定义了启动方法main() 。在这里面我们指定了需要扫描加载成Servlet实例的具体包名路径。