一起来写个SpringBoot[1] — — 使用Netty实现Http服务器

项目地址:https://github.com/xiaogou446/jsonboot
本节从第一个branch开始:feature/addNecessaryDependency
命令行: git checkout feature/addNecessaryDependency 即可

构建maven依赖

在正式开始搭建项目之前,先得把依赖捋一捋,我们通过maven来构建项目,首先先创建好一个maven项目,后将依赖导入到pom文件中。(com.df的df是学校的简称…不管它好不好我都爱它!)

<?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>com.df</groupId>
    <artifactId>jsonboot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <jackson.version>2.11.2</jackson.version>
        <netty.version>4.1.42.Final</netty.version>
        <slf4j.version>1.7.25</slf4j.version>
        <lombok.version>1.18.12</lombok.version>
        <junit.version>5.6.1</junit.version>
        <commons.codec.version>1.14</commons.codec.version>
        <reflections.version>0.9.12</reflections.version>
        <cglib.version>3.3.0</cglib.version>
        <yaml.version>1.23</yaml.version>
        <validation.api.version>2.0.1.Final</validation.api.version>
        <hibernate.validator.version>6.1.5.Final</hibernate.validator.version>
        <rest-assured.version>4.3.1</rest-assured.version>
    </properties>

    <dependencies>
        <!--jackson json 和springmvc官方使用方式结合 依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${
    
    jackson.version}</version>
        </dependency>
        <!--netty 依赖包-->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${
    
    netty.version}</version>
        </dependency>
        <!--slf4j 日志包-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${
    
    slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${
    
    slf4j.version}</version>
        </dependency>
        <!--反射注解扫描包-->
        <dependency>
            <groupId>org.reflections</groupId>
            <artifactId>reflections</artifactId>
            <version>${
    
    reflections.version}</version>
        </dependency>
        <!--编码解-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>${
    
    commons.codec.version}</version>
        </dependency>
        <!--cglib 动态代理-->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>${
    
    cglib.version}</version>
        </dependency>
        <!--yml 配置读取-->
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>${
    
    yaml.version}</version>
        </dependency>
        <!--jsr303 验证-->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>${
    
    validation.api.version}</version>
        </dependency>
        <!--hibrenate-validator 注解验证-->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${
    
    hibernate.validator.version}</version>
        </dependency>
        <!--语法糖 lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${
    
    lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <!--测试类-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${
    
    junit.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- 接口测试 -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>${
    
    rest-assured.version}</version>
        </dependency>
    </dependencies>
</project>

第一个branch就做了个依赖,之后切换到feature/buildNettyConstruct 正式开始
命令行: git checkout feature/feature/buildNettyConstruct

构建项目banner

在项目启动时可以出现这样一个Banner,有框架那味了!
在这里插入图片描述
定义接口打印banner的接口,完成它的实现类,会从我们的类路径下读取需要打印的banner文件。

package com.df.jsonboot.common;

import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * jsonboot启动时的banner展示
 *
 * @author qinghuo
 * @since 2021/03/17 3:54
 */
public class JsonBootBanner implements Banner{
    
    

    /**
     * 默认的启动文件名称
     */
    private static final String DEFAULT_BANNER_NAME = "jsonbootBanner.txt";

    @Override
    public void printBanner(String bannerName, PrintStream printStream) {
    
    
        //如果为空,使用默认的bannerName
        if(StringUtils.isBlank(bannerName)){
    
    
            bannerName = DEFAULT_BANNER_NAME;
        }
        //使用当前线程从项目根目录读取配置文件
        URL url = Thread.currentThread().getContextClassLoader().getResource(bannerName);
        if (url == null){
    
    
            return;
        }
        try {
    
    
            Path path = Paths.get(url.toURI());
            Files.lines(path).forEach(printStream::println);
            printStream.println();
        } catch (URISyntaxException | IOException e) {
    
    
            printStream.printf("banner文件加载错误 banner: %s, error: %s",bannerName, e);
        }

    }
}

最重要的是 jsonbootBanner.txt 文件,以上代码的作用就是做到打印这个txt文件内的内容,banner的制作可以参考这个网址:banner制作,输入文字和字体格式就会生成对应的banner,复制到txt文件就算成功。

Netty设置Http服务器

通过简单的netty实现一个http服务器,通过一个bossGroup监听连接,处理的方式设置为Non Blocking IO,非阻塞IO,可以更大效率的利用线程,后交给workerGroup进行对请求的处理。启动以后自动绑定设置的端口,成功后在channel没有关闭前都会进行阻塞,处理请求。

package com.df.jsonboot.server;

import com.df.jsonboot.common.SystemConstants;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
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 io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

/**
 * 通过netty编写http服务器接收请求
 *
 * @author qinghuo
 * @since 2021/03/19 15:20
 */
@Slf4j
public class HttpServer {
    
    

    /**
     * 需要使用的端口号
     */
    private int port = 8080;

    public HttpServer(){
    
    }

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


    public void run(){
    
    
        //设置用于连接的boss组, 可在构造器中定义使用的线程数  监听端口接收客户端连接,一个端口一个线程,然后转给worker组
        //boss组用于监听客户端连接请求,有连接传入时就生成连接channel传给worker,等worker 接收请求 io多路复用,
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //设置用于工作的工作组,用于处理io操作,执行任务 这俩实际上是reactor线程池
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
    
    
            //定义服务启动的引导程序
            ServerBootstrap b = new ServerBootstrap();
            //将两个组都放入引导程序中
            b.group(bossGroup, workerGroup)
                    //定义使用的通道 可以选择是NIO或者是OIO 代表了worker在处理socket channel时的不同情况。oio只能1对1, nio则没有1对1对关系
                    //当netty要处理长连接时最好使用NIO,不然如果要保证效率 需要创建大量的线程,和io多路复用一致
                    .channel(NioServerSocketChannel.class)
                    //表示系统定义存放三次握手的最大临时队列长度 如果建立连接频繁可以调大这个参数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    //xxx和childxxx的区别, xxx是对boss组起作用,而childxxx是对worker起作用。
                    //开启tcp底层心跳机制, 连接持续时间
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //boss组定义日志输出形式
                    .handler(new LoggingHandler(LogLevel.INFO))
                    //定义特殊的程序处理channel 相当于可以为pipeline添加新的功能 在worker组的线程会通过这里
                    .childHandler(new ChannelInitializer<SocketChannel>(){
    
    
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
    
    
                            //需要添加的处理事件在这里添加
                                socketChannel.pipeline()
                                        .addLast("decoder", new HttpRequestDecoder())
                                        .addLast("encoder", new HttpResponseEncoder())
                                        //处理post请求需要
                                        .addLast("aggregator", new HttpObjectAggregator(512 * 1024))
                                        .addLast("handler", new HttpRequestHandler());
                        }
                    });
            //sync()会阻塞直到bind完成
            ChannelFuture f = b.bind(port).sync();
            log.info(SystemConstants.LOG_PORT_BANNER, this.port);
            //同步 直到channel server结束
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            //关闭boss组和worker组
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

以上就是netty建立对外部的连接,接收并处理请求的一个过程,但是具体处理的请求的步骤,还是需要我们自己编写,由于我们是处理http请求,包括Get请求与Post请求,我们也需要针对定义对这两种请求不同的处理方式。

请求由workerGroup接手后会来到正式的处理类中,.addLast(“handler”, new HttpRequestHandler());,也就是由这里进来,到达channelRead0进行处理,通过请求的方法获取对应的Get处理器或者是Post处理器,对请求进行处理。

    /**
     * 定义允许使用的类型map
     */
    private static final Map<HttpMethod, RequestHandler> REQUEST_HANDLER_MAP;

    static {
    
    
        REQUEST_HANDLER_MAP = new HashMap<>();
        REQUEST_HANDLER_MAP.put(HttpMethod.GET, new GetRequestHandler());
        REQUEST_HANDLER_MAP.put(HttpMethod.POST, new PostRequestHandler());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
    
    
        String uri = request.uri();
        //如果是访问图标的请求或者为空,直接返回
        if (StringUtils.isBlank(uri) || StringUtils.equals(FAVICON_ICON, uri)){
    
    
            return;
        }
        //根据请求的类型在map中取出对应的处理器
        RequestHandler requestHandler = REQUEST_HANDLER_MAP.get(request.method());
        Object result = requestHandler.handler(request);
        //对获得的数据进行相应处理
        FullHttpResponse response = buildHttpResponse(result);
        boolean keepAlive = HttpUtil.isKeepAlive(request);
        if (!keepAlive){
    
    
            //如果不是长链接,则写入数据后关闭此次channel连接
            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
        }else{
    
    
            response.headers().set(CONNECTION, KEEP_ALIVE);
            ctx.write(response);
        }
    }

从handler处理器中获得结果后,对结果数据进行json序列化的解析,设置返回数据的类型,返回结果。

    /**
     * 对请求处理的结果进行一个封装
     *
     * @param result 请求处理后得到的数据结果
     * @return 封装好的响应
     */
    private FullHttpResponse buildHttpResponse(Object result){
    
    
        JacksonSerializer jacksonSerializer = new JacksonSerializer();
        byte[] bytes = jacksonSerializer.serialize(result);
        DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(bytes));
        response.headers().set(CONTENT_TYPE, APPLICATION_JSON);
        response.headers().set(CONTENT_LENGTH, response.content().readableBytes());
        return response;
    }

具体的处理方式还是可以看出在handler中,本章就搭了个大致的框架,针对不同的请求,获取他们的数据结构,方便后来处理。

Get请求通过QueryStringDecoder解析出url上附带的数据。

/**
 * 处理Get的http请求
 *
 * @author qinghuo
 * @since 2021/03/21 9:55
 */
@Slf4j
public class GetRequestHandler implements RequestHandler {
    
    

    @Override
    public Object handler(FullHttpRequest fullHttpRequest) {
    
    
        QueryStringDecoder queryDecoder = new QueryStringDecoder(fullHttpRequest.uri(), Charsets.toCharset(CharEncoding.UTF_8));
        Map<String, List<String>> parameters = queryDecoder.parameters();
        //暂时打印参数 先完成netty再处理后续代码
        for (Map.Entry<String, List<String>> parameter : parameters.entrySet()){
    
    
            log.info(parameter.getKey() + " = " + parameter.getValue());
        }

        return null;
    }
}

Post请求,通过校验请求是否是json格式,如果是json格式就通过ObjectMapper将json转换为对应类型的格式,可以将Object.class替换成任意符合格式的实体。

    /**
     * 转换格式
     */
    private final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public Object handler(FullHttpRequest fullHttpRequest) {
    
    
        Object result = null;
        String contentTypeStr = fullHttpRequest.headers().get(CONTENT_TYPE);
        if (StringUtils.isBlank(contentTypeStr)){
    
    
            return result;
        }
        String contentType = contentTypeStr.split(";")[0];
        if (StringUtils.equals(APPLICATION_JSON, contentType)){
    
    
            String jsonContent = fullHttpRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
            try {
    
    
                result = objectMapper.readValue(jsonContent, Object.class);
            } catch (JsonProcessingException e) {
    
    
                e.printStackTrace();
            }
        }
        return result;
    }

测试

我们使用postman来进行调用测试netty建立的服务器是否可行,结果是可以的!
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
嘿嘿,做到这里基本初具雏形了!

猜你喜欢

转载自blog.csdn.net/qq_41762594/article/details/115375135