JAVA October 09, 2018

基于Netty的http server实现

Words count 40k Reading time 36 mins. Read count 0

版本说明:

netty:4.1.17.Final

项目地址:https://github.com/shirukai/netty-http-server-demo.git

最近在看Spark源码的RPC框架实现的代码的时候,接触到Netty。在Spark1.6之后,Spark中RPC通信的框架由Netty逐渐代替了Akka。

为什么Netty可以取代Akka?首先毋庸置疑的是Akka可以做到的,Netty也可以做到,但是Netty可以做到,Akka却无法做到,原因是啥?在软件栈中,Akka相比Netty要Higher一点,它专门针对RPC做了很多事情,而Netty相比更加基础一点,可以为不同的应用层通信协议(RPC,FTP,HTTP等)提供支持,在早期的Akka版本,底层的NIO通信就是用的Netty;其次一个优雅的工程师是不会允许一个系统中容纳两套通信框架,恶心!最后,虽然Netty没有Akka协程级的性能优势,但是Netty内部高效的Reactor线程模型,无锁化的串行设计,高效的序列化,零拷贝,内存池等特性也保证了Netty不会存在性能问题。(摘自:http://www.aboutyun.com/thread-21115-1-1.html。关于为什么Spark要使用Netty替代Akka的详细内容,可以参考这篇文章)

1 什么是Netty?

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke’s Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

Essential Netty in Action 《Netty 实战(精髓)》:https://waylau.gitbooks.io/essential-netty-in-action/content/

关于Netty介绍:https://www.jianshu.com/p/b9f3f6a16911

2 Netty的简单使用

2.1 准备

在开始之前,我们需要创建一个maven项目。这里就不做演示了。项目的目录结构如下图所示:

关于Netty的简单使用的Demo在项目中netty.demo的包下。

相关jar包依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>netty.http.server</groupId>
    <artifactId>server</artifactId>
    <version>1.0</version>
    <dependencies>
        <!--netty-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.17.Final</version>
        </dependency>
        <!--log-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.5.8</version>
        </dependency>
        <!--json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.1.37</version>
        </dependency>
    </dependencies>

</project>

2.2 创建Netty 服务端

2.2.1 创建Server启动类

该类主要用于对Netty Server进行一些配置,如端口号、EventLoopGroup、ServerBootstrap以及chanal中的pipline等,以及netty服务的启动。代码如下所示:

package netty.demo;

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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;

/**
 * Created by shirukai on 2018/9/30
 * 基于Netty实现的Socket Server
 */
public class NettySocketServer {
    public final Logger log = LoggerFactory.getLogger(this.getClass());
    private int port;

    public NettySocketServer(int port) {
        this.port = port;
    }

    public void start() {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            bootstrap
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    // BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,
                    // 用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。
                    .group(bossGroup, workerGroup) //绑定线程池
                    .channel(NioServerSocketChannel.class)// 指定使用的channel
                    .localAddress(port)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        // 绑定客户端时触发的操作
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            log.info("Client connected service:{}", socketChannel);
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
                            pipeline.addLast(new NettySocketServerHandler());//服务器处理客户端请求
                            pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
                        }
                    });
            ChannelFuture channelFuture = bootstrap.bind().sync(); //服务器异步创建绑定
            log.info("Server is listening:{}", channelFuture.channel());
            channelFuture.channel().closeFuture().sync();//关闭服务器
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            try {
                // 释放线程池资源
                workerGroup.shutdownGracefully().sync();
                bossGroup.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                log.error(e.getMessage());
            }
        }

    }

    public static void main(String[] args) {
        NettySocketServer server = new NettySocketServer(9099);
        server.start();
    }
}

Bootstrap的option参数:https://blog.csdn.net/zhousenshan/article/details/72859923

2.2.2 Netty 服务端处理类

如上代码,我们在pipeline中指定了服务端处理客户端请求的实例

 pipeline.addLast(new NettySocketServerHandler());

下面我们就要实现NettySocketServerHandler这个类,代码如下所示:

package netty.demo;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created by shirukai on 2018/9/30
 * netty socket server 处理器
 */
public class NettySocketServerHandler extends ChannelInboundHandlerAdapter {
    public final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("The channel {} is ACTIVE.");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("The channel {} is INACTIVE.");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("message:{}", msg);
        ctx.channel().writeAndFlush("返回信息给客户端:" + msg);
    }

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

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

2.3 创建Netty 客户端

客户端的创建于服务端类似,也需要一个客户端配置类和处理类。

2.3.1 创建 Client 配置类

package netty.demo;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.nio.charset.Charset;

/**
 * Created by shirukai on 2018/9/30
 * netty socket client
 */
public class NettySocketClient {
    private String host;
    private int port;

    public NettySocketClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try {
            bootstrap
                    .group(group)//注册线程池
                    .channel(NioSocketChannel.class)// 使用NioSocketChannel来作为连接用的Channel类
                    .remoteAddress(host, port)//绑定远程服务的IP和端口
                    .handler(new ChannelInitializer<SocketChannel>() {//连接初始化
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            // 输入流编码为StringEncoder
                            pipeline.addLast(new StringDecoder(Charset.forName("UTF-8")));
                            pipeline.addLast(new NettySocketClientHandler());
                            // 输出流编码为StringDecoder
                            pipeline.addLast(new StringEncoder(Charset.forName("UTF-8")));
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect().sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                group.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        NettySocketClient client = new NettySocketClient("127.0.0.1", 9099);
        client.start();
    }
}

2.3.2 Netty 客户端处理类

package netty.demo;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Created by shirukai on 2018/9/30
 */
public class NettySocketClientHandler extends SimpleChannelInboundHandler<String> {
    public final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush("向客户端发送消息");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
        log.info(s);
    }
}

2.4 Demo 演示

首先启动服务端,然后启动客户端,效果如下图所示:

3 基于Netty的http server实现

上面已经记录了Netty的简单实用,接下来记录一下在学习实用Netty实现http server实现。从0到1的搭建一个http服务。通过实现Netty http服务,学到了以下几个知识点:

  • java 中注解的使用(模仿spring对请求路由和参数进行控制)

  • 通过反射机制执行路由指定的方法

  • 处理http请求

  • java链式调用

设计思路:

首先实现一个基于Netty的http server,使用HttpRequestDecoder和HttpResponseEncoder进行输入流输出流编解码。

然后使用自定义的HttpServerhandler类进行请求处理。模仿springmvc的controller层,使用注解的方式,设置方法路由。扫描指定类下的注解,将注解信息以及类信息缓存到Map里。

最后根据请求的url从Map中获取相关信息,路由到指定的方法,利用反射机制,实例化该方法,传入参数并执行。

对于请求参数,支持传统的url传参,使用?id=1或者/{id}两种url参数的形式获取,支持处理post请求中的json请求。其他请求,如formdata等传参形式没有支持,处理逻辑相似,对于请求的不同类型处理可以参考:https://www.cnblogs.com/cyfonly/p/5616493.html。

3.1 Http Server的实现

实现代码如下:

package netty.http.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import netty.http.server.entity.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * Created by shirukai on 2018/9/30
 * 基于netty 实现httpSever
 */
public class HttpServer {
    private HttpServerConfig config;
    private int port;
    private Map<String, Router> routers;

    public final Logger log = LoggerFactory.getLogger(this.getClass());

    public HttpServerConfig builder() {
        this.config = new HttpServerConfig(this);
        return config;
    }

    public void start() {
        ServerBootstrap bootstrap = new ServerBootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        try {
            bootstrap
                    .group(group)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline()
                                    .addLast("decoder", new HttpRequestDecoder())
                                    .addLast("encoder", new HttpResponseEncoder())
                                    .addLast("aggregator", new HttpObjectAggregator(512 * 1024))
                                    .addLast("handler", new HttpServerHandler(routers));
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE);
            bootstrap.bind(port).sync();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log.info("Start app server at port:{}", port);
    }

    public void setPort(int port) {
        this.port = port;
    }

    public void setRouters(Map<String, Router> routers) {
        this.routers = routers;
    }
}

由上面代码可以看出,在HttpServer这个类中,我们设置了三个私有的属性:config、port、routers,分别为服务器配置、端口和路由信息。提供一个builder方法,返回HttpServerConfig实例,用来构建HttpServer。

3.2 HttpServerConfig的实现

该类主要是对服务的配置信息进行缓存。如端口号、controller等,实现代码如下:

package netty.http.server;

import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by shirukai on 2018/9/30
 * 用来配置HttpServer
 */
public class HttpServerConfig {
    // 初始化Map用来缓存配置信息
    private ConcurrentHashMap<String, Object> conf = new ConcurrentHashMap<>();
    private static final String SERVER_CONTROLLERS = "netty.http.server.controllers";
    private static final String SERVER_PORT = "netty.http.server.port";
    private HttpServer httpServer;

    public HttpServerConfig(HttpServer httpServer) {
        this.httpServer = httpServer;
    }

    public HttpServerConfig set(String name, Object value) {
        conf.put(name, value);
        return this;
    }

    /**
     * 设置端口号
     *
     * @param port 端口
     * @return conf
     */
    public HttpServerConfig setPort(int port) {
        conf.put(SERVER_PORT, port);
        return this;
    }

    /**
     * 设置多个Controller的class 以便进行注解扫描
     *
     * @param className class names
     * @return conf
     */
    public HttpServerConfig setControllers(Class<?>... className) {
        Class<?>[] oldClasses = getClasses();
        Class<?>[] newClasses = insertClasses(oldClasses, className);
        conf.put(SERVER_CONTROLLERS, newClasses);
        return this;
    }

    /**
     * 设置单个controller的class
     *
     * @param className class name
     * @return conf
     */
    public HttpServerConfig setController(Class<?> className) {
        //获取已有的class
        Class<?>[] oldClasses = getClasses();
        //新增class
        Class<?>[] newClasses = insertClass(oldClasses, className);
        conf.put(SERVER_CONTROLLERS, newClasses);
        return this;
    }

    /**
     * 获取String类型的值
     *
     * @param key key
     * @return value
     */
    public String getString(String key) {
        return conf.get(key).toString();
    }

    /**
     * 获取 int类型的值
     *
     * @param key key
     * @return value
     */
    public int getInt(String key) {
        return (int) conf.get(key);
    }

    /**
     * 获取所有的class
     *
     * @return classes
     */
    public Class<?>[] getClasses() {
        return (Class<?>[]) conf.get(SERVER_CONTROLLERS);
    }

    /**
     * 插入一个class
     *
     * @param oldClasses 旧的classes
     * @param addClass   要添加的class
     * @return 新的 classes
     */
    private Class<?>[] insertClass(Class<?>[] oldClasses, Class<?> addClass) {
        Class<?>[] newClasses;
        //判断原始的Class 数组是否为null
        if (oldClasses != null) {
            //获取数组长度
            int length = oldClasses.length;
            //创建一个新数组
            newClasses = new Class<?>[length + 1];
            //copy数组
            System.arraycopy(oldClasses, 0, newClasses, 0, length);
            newClasses[length + 1] = addClass;
        } else {
            newClasses = new Class<?>[]{addClass};
        }
        return newClasses;
    }

    /**
     * 插入多个class
     *
     * @param oldClasses 旧的classes
     * @param addClasses 要添加的classes
     * @return 新的classes
     */
    private Class<?>[] insertClasses(Class<?>[] oldClasses, Class<?>[] addClasses) {
        Class<?>[] newClasses;
        if (oldClasses != null) {
            int oldLength = oldClasses.length;
            int addLength = addClasses.length;
            newClasses = new Class<?>[oldLength + addLength];
            System.arraycopy(oldClasses, 0, newClasses, 0, oldLength);
            System.arraycopy(addClasses, 0, newClasses, oldLength, addLength);
        } else {
            newClasses = addClasses;
        }
        return newClasses;
    }

    public HttpServer create() {
        httpServer.setPort(this.getInt(SERVER_PORT));
        //获取controller类
        Class<?>[] classes = this.getClasses();
        //扫描注解
        httpServer.setRouters(AnnotationScan.getRouters(classes));
        return this.httpServer;
    }

    public ConcurrentHashMap<String, Object> getConf() {
        return this.conf;
    }

}

通过上面两个类,我们基本可以完成一个HttpServer的初始化操作:

import netty.http.server.HttpServer;
import netty.http.worker.controller.TestController;

/**
 * Created by shirukai on 2018/9/30
 * netty sever 启动类
 */
public class App {
    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.builder()
                .setPort(9090)
                .setController(TestController.class)
                .create().start();
    }
}

当然到目前为止,我们的服务是没有办法启动的,因为我们还没有HttpServerHandler类用来处理http请求和AnnotationScan类用来扫描指定Controller类下的注解信息。所以接下来,我们先实现AnnotationScan类,演示注解在该Demo中的使用。

3.3 路由控制

这里,我们模仿spring mvc 使用注解的方式,对Controller层进行路由控制。

3.3.1 注解类设置

在netty.http.server.annotation包下创建相关注解类。

3.3.1.1 @RouterMapping

@RouterMapping,功能类似于spring中的RequestMapping主要是对Controller层方法的路由。该注解作用于方法,根据用于发送的url,执行映射的方法。

package netty.http.server.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by shirukai on 2018/9/30
 * 路由
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterMapping {
    String api() default "";

    String method() default "GET,POST,PUT,DELETE";
}
3.3.1.2 @RequestParam

@RequestParam,功能类似于spring中的@RequestParam,作用于方法中的参数列表,服务会根据该注解自动去匹配请求参数。主要是从url中获取?id=1这样形式的参数。代码如下:

package netty.http.server.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by shirukai on 2018/9/30
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
    String value() default "";
}
3.3.1.3 @PathParam

@PathParam同样是作用于参数列表,用以获取/{id} 这样形式的参数,代码如下:

package netty.http.server.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by shirukai on 2018/9/30
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface PathParam {
    String value() default "";
}
3.3.1.4 @JsonParam

@JsonParam也是作用于参数列表,用于获取json参数格式的请求,代码如下:

package netty.http.server.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by shirukai on 2018/9/30
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonParam {
    String value() default "";
}

3.3.2 注解扫描

有了注解类之后,我们需要知道,我们的哪些方法使用了注解,哪些参数使用了注解。所以我们需要进行注解的扫描,并将扫描的注解信息缓存到Map里。AnnotationScan类的代码如下所示:

package netty.http.server;

import netty.http.server.annotation.JsonParam;
import netty.http.server.annotation.PathParam;
import netty.http.server.annotation.RequestParam;
import netty.http.server.annotation.RouterMapping;
import netty.http.server.entity.Param;
import netty.http.server.entity.Router;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by shirukai on 2018/9/30
 * 注解扫描
 */
public class AnnotationScan {
    public static Map<String, Router> getRouters(Class<?>... classes) {
        Map<String, Router> routers = new HashMap<>(16);
        // 遍历传入的class
        for (Class<?> tClass : classes) {
            // 遍历类中的方法
            for (Method method : tClass.getDeclaredMethods()) {
                // 扫描方法的注解
                RouterMapping routerMapping = method.getAnnotation(RouterMapping.class);
                // 如果注解不为空,扫描该方法内的参数注解
                if (routerMapping != null) {
                    Router router = new Router();
                    // 获取参数
                    List<Param> params = new ArrayList<>(16);
                    // 遍历方法中的参数
                    for (Parameter parameter : method.getParameters()) {
                        // 扫描参数注解
                        PathParam pathParam = parameter.getAnnotation(PathParam.class);
                        RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
                        JsonParam jsonParam = parameter.getAnnotation(JsonParam.class);
                        Param param = new Param();
                        // 将注解写入到信息写入到Param中
                        if (pathParam != null) {
                            param.setType("path");
                            param.setValue(pathParam.value());
                        }
                        if (requestParam != null) {
                            param.setType("request");
                            param.setValue(requestParam.value());
                        }
                        if (jsonParam != null) {
                            param.setType("json");
                            param.setValue(jsonParam.value());
                        }
                        // 将Param信息写入到List里
                        params.add(param);
                    }
                    router.setParams(params);
                    router.settClass(tClass);
                    router.setMethodType(routerMapping.method());
                    router.setMethod(method);
                    router.setUrl(routerMapping.api());
                    // 保存Router信息
                    routers.put(router.getUrl(), router);
                }
            }
        }
        return routers;
    }
}

3.3.3 请求处理

完成上面的工作之后,接下来是该demo的核心功能,就是对http请求进行处理。根据请求的url,获取参数,通过反射执行相应方法。

package netty.http.server;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.AsciiString;
import netty.http.server.entity.Param;
import netty.http.server.entity.Router;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Created by shirukai on 2018/9/30
 * Http服务处理器
 */
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private Map<String, Router> routers;
    private AsciiString contentType = HttpHeaderValues.APPLICATION_JSON;
    public final Logger log = LoggerFactory.getLogger(this.getClass());

    public HttpServerHandler(Map<String, Router> routers) {
        this.routers = routers;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpRequest request) {
        log.info("Handler request:{}", request);
        Object result = null;
        HttpResponseStatus status = null;
        try {
            // 初始化请求参数
            List<Object> params = new ArrayList<>(16);
            // 解析URL中的参数
            QueryStringDecoder decoder = new QueryStringDecoder(request.uri(), Charset.forName("UTF-8"));
            // 获取请求路径
            String url = decoder.path();
            // 获取请求方法
            String method = request.method().toString();
            // 获取URL中的参数
            Map<String, List<String>> urlParams = decoder.parameters();
            // 从扫描的注解中获取Router信息
            Router router = getRoute(url);
            //判断router里是否包含url
            if (router == null) {
                throw new RuntimeException("URL does not exist!");
            }
            //判断请求方法是否匹配
            if (!router.getMethodType().contains(method)) {
                throw new RuntimeException("Method does not match!");
            }
            //获取controller里请求参数信息
            List<Param> paramInfo = router.getParams();
            if (paramInfo != null) {
                //处理URL参数
                Map<String, String> pathParams = PathUrlHandler.getParams(url, router.getUrl());
                JSONObject initJson = null;
                //处理POST请求
                if (method.equals("POST")) {
                    String contentType = request.headers().get("Content-Type");
                    //处理application/json请求
                    if (contentType.contains("application/json")) {
                        initJson = JSON.parseObject(request.content().toString(Charset.forName("UTF-8")));
                    }
                }
                JSONObject finalJson = initJson;
                paramInfo.forEach(param -> {
                    String type = param.getType();
                    String value = param.getValue();
                    // 如果参数在url里形如/test/{id}时 添加参数
                    if (type.equals("path")) {
                        params.add(pathParams.get(value));
                    }
                    // 如果参数在url里形如/test?id=124时 添加参数
                    if (type.equals("request")) {
                        params.add(urlParams.get(value).get(0));
                    }
                    // 如果参数在body里以json形如传入时 添加参数
                    if (type.equals("json")) {
                        params.add(finalJson);
                    }
                });
                // 执行router映射的方法,并获取返回结果
                result = executeMethod(router, params.toArray());
                // 设置response状态为 OK
                status = HttpResponseStatus.OK;
            } else {
                // 如果没有参数,直接执行映射的方法
                result = executeMethod(router);
            }
        } catch (Exception e) {
            // 如果捕获到异常,将异常信息放到返回结果里
            result = e.getMessage();
        } finally {
            // 如果状态为null,将返回结果设置为500
            if (status == null) {
                status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
            }
        }
        // 包装response
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                status,
                Unpooled.wrappedBuffer(JSON.toJSONBytes(result)));

        // 设置response 的header
        HttpHeaders heads = response.headers();
        heads.add(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=UTF-8");
        heads.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
        heads.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        // 将返回结果返回
        channelHandlerContext.write(response);


    }

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

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

    /**
     * 获取Router信息
     *
     * @param url url
     * @return Router
     */
    private Router getRoute(String url) {
        AtomicReference<Router> router = new AtomicReference<>();
        routers.keySet().forEach(routerKey -> {
            if (PathUrlHandler.verify(url, routerKey)) {
                router.set(routers.get(routerKey));
            }
        });
        return router.get();
    }

    /**
     * 执行路由映射的方法
     *
     * @param router Router
     * @param params params
     * @return Object
     * @throws Exception 执行异常
     */
    private Object executeMethod(Router router, Object... params) throws Exception {
        Class<?> cls = router.gettClass();
        Object obj = cls.newInstance();
        Method method = router.getMethod();
        return method.invoke(obj, params);
    }

}

对于url中类似于/{id}这样参数的处理,单独写到了PathUrlHandler类里,代码如下:

package netty.http.server;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by shirukai on 2018/10/8
 * 处理URL中的参数
 */
public class PathUrlHandler {
    private static final String pattern = "(\\{[^}]*})";


    /**
     * 校验请求url和路由中的url是否匹配
     * 如:/test/111 匹配 /test/{id}
     *
     * @param requestUrl 请求的url
     * @param routerUrl  路由中设置的url
     * @return boolean
     */
    public static boolean verify(String requestUrl, String routerUrl) {
        Matcher keyMatcher = Pattern.compile(pattern).matcher(routerUrl);
        String replacePattern = keyMatcher.replaceAll("(.*)");
        Matcher valueMatcher = Pattern.compile(replacePattern).matcher(requestUrl);
        return valueMatcher.matches();
    }

    /**
     * 获取参数
     * 请求URL:/test/111
     * 路由URL:/test/{id}
     * 参数为{"id":"111"}
     *
     * @param requestUrl 请求的url
     * @param routerUrl  路由中设置的url
     * @return map
     */
    public static Map<String, String> getParams(String requestUrl, String routerUrl) {
        Map<String, String> params = new HashMap<>(16);
        Matcher keyMatcher = Pattern.compile(pattern).matcher(routerUrl);
        List<String> keys = new ArrayList<>(16);
        List<String> values = new ArrayList<>(16);
        while (keyMatcher.find()) {
            keys.add(keyMatcher.group(1).replace("{", "").replace("}", ""));
        }
        String replacePattern = keyMatcher.replaceAll("(.*)");
        Matcher valueMatcher = Pattern.compile(replacePattern).matcher(requestUrl);
        if (valueMatcher.find()) {
            int count = valueMatcher.groupCount();
            for (int i = 1; i <= count; i++) {
                values.add(valueMatcher.group(i));
            }
        }
        int valueSize = values.size();
        for (int i = 0; i < keys.size(); i++) {
            String value = i < valueSize ? values.get(i) : "";
            params.put(keys.get(i), value);
        }
        return params;
    }

}

3.4 Demo演示

完成以上工作之后,我们的Http服务相关的操作都已经完成了,下面我们将写一个简单的例子,来测试我们的服务。

在netty.http.worker.controller包下创建一个TestController类,该类里面使用了我们之前设置好的@RouterMapping、 @RequestParam、@PathParam、@JsonParam注解。并且分别展示了GET请求和POST请求。

代码如下:

package netty.http.worker.controller;

import com.alibaba.fastjson.JSONObject;
import netty.http.server.annotation.JsonParam;
import netty.http.server.annotation.PathParam;
import netty.http.server.annotation.RequestParam;
import netty.http.server.annotation.RouterMapping;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by shirukai on 2018/9/30
 * controller
 */
public class TestController {
    /**
     * 测试GET请求
     *
     * @param name name
     * @param id   id
     * @return map
     */
    @RouterMapping(api = "/api/v1/test/get/{id}", method = "GET")
    public Map<String, Object> testGet(
            @RequestParam("name") String name,
            @PathParam("id") String id
    ) {
        Map<String, Object> map = new HashMap<>(16);
        map.put("name", name);
        map.put("id", id);
        return map;
    }

    /**
     * 测试POST请求
     *
     * @param json json
     * @return json
     */
    @RouterMapping(api = "/api/v1/test/post", method = "POST")
    public JSONObject testPost(
            @JsonParam("json") JSONObject json
    ) {
        return json;
    }
}

在App类里我们启动Http Server,设置我们刚才创建的Controller类到Http Server

import netty.http.server.HttpServer;
import netty.http.worker.controller.TestController;

/**
 * Created by shirukai on 2018/9/30
 * netty sever 启动类
 */
public class App {
    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.builder()
                .setPort(9090)
                .setController(TestController.class)
                .create().start();
    }
}

使用PostMan向服务器发送GET请求:

服务器日志:

使用PostMan向服务器发送POST请求

服务器日志:

0%