netty实现http服务器

发布时间 2023-07-21 10:01:30作者: 公众号/架构师与哈苏

pom.xml

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.94.Final</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.35</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

HttpServer.java netty服务类

package com.example.springfileupload3.netty.http;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

/**
 * httpServer
 *
 * @Description
 * @Author wzq
 **/
@Slf4j
public class HttpServer {

    static final int PORT = 8888;

    public void run() throws Exception {
        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.option(ChannelOption.SO_BACKLOG, 1024);
            b.childOption(ChannelOption.TCP_NODELAY, true);
            b.childOption(ChannelOption.SO_KEEPALIVE, true);
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new HttpServerInitializer());

            ChannelFuture cf = b.bind(PORT).sync();
            cf.addListener(future -> {
                if (future.isSuccess()) {
                    log.info("netty服务器绑定端口成功:" + PORT);
                } else {
                    log.error("netty服务器绑定端口失败:" + PORT);
                }
            });
            log.info("netty启动完成!");
            //等待netty完成,会堵塞主线程
            //cf.channel().closeFuture().sync();
        } catch (Exception e){
            log.info("netty启动异常了!");
            e.printStackTrace();
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

HttpServerInitializer.java netty初始化配置类

package com.example.springfileupload3.netty.http;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;

/**
 * HttpServerInitializer
 *
 * @Description
 * @Author wzq
 **/
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {


    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline p = ch.pipeline();
        /**
         * 或者使用HttpRequestDecoder & HttpResponseEncoder
         */
        p.addLast(new HttpServerCodec());
        /**
         * 在处理Post消息时需要加上
         */
        p.addLast(new HttpObjectAggregator(1024*1024));
        p.addLast(new HttpServerExpectContinueHandler());
        p.addLast(new HttpServerHandler());
    }
}

HttpServerInitializer.java 处理类

package com.example.springfileupload3.netty.http;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.internal.StringUtil;
import lombok.extern.slf4j.Slf4j;
import io.netty.util.AsciiString;

import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * HttpServerHandler
 *
 * @Description
 * @Author wzq
 **/
@Slf4j
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    private static final String FAVICON_ICO = "/favicon.ico";
    private HttpRequest request;
    private HttpHeaders headers;
    private FullHttpRequest fullRequest;


    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject httpObject) throws Exception {
        User user = new User();
        user.setUserName("GoslingWu");
        user.setDate(new Date());

        if (httpObject instanceof HttpRequest) {
            request = (HttpRequest) httpObject;
            headers = request.headers();
            String uri = request.uri();
            log.info("http uri:" + uri);
            if (FAVICON_ICO.equals(uri)) {
                return;
            }
            HttpMethod method = request.method();
            if (method.equals(HttpMethod.GET)) {
                log.info("http method GET");
                QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, StandardCharsets.UTF_8);
                Map<String, List<String>> uriParameters = queryDecoder.parameters();
                // 打印
                uriParameters.forEach((key, value) -> {
                    log.info("key:{} , value:{}", key, value);
                });
                user.setMethod("get");
            } else if (method.equals(HttpMethod.POST)) {
                log.info("http method POST");
                fullRequest = (FullHttpRequest) httpObject;
                //根据不同的Content_Type处理body数据
                dealWithContentType();
                user.setMethod("post");
            }
        }

        JSONSerializer jsonSerializer = new JSONSerializer();
        byte[] content = jsonSerializer.serialize(user);

        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content));
        response.headers().set("Content-Type", "text/plain");
        response.headers().setInt("Content-Length", response.content().readableBytes());


        boolean keepAlive = HttpUtil.isKeepAlive(request);
        if (!keepAlive) {
            ctx.write(response).addListener(ChannelFutureListener.CLOSE);
        } else {
            response.headers().set("Connection","keep-alive");
            ctx.write(response);
        }
    }


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

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

    /**
     * 解析ContentType
     *
     * @throws Exception
     */
    private void dealWithContentType() throws Exception {
        String contentType = getContentType();
        // 可以使用HttpJsonDecoder
        if ("application/json".equals(contentType)) {
            String jsonStr = fullRequest.content().toString(StandardCharsets.UTF_8);
            JSONObject jsonObject = JSON.parseObject(jsonStr);
            jsonObject.forEach((key, value) -> {
                log.info("key:{} , value:{}", key, value);
            });
        } else if ("application/x-www-form-urlencoded".equals(contentType)) {
            //方式一:使用 QueryStringDecoder
            String jsonStr = fullRequest.content().toString(StandardCharsets.UTF_8);
            QueryStringDecoder queryDecoder = new QueryStringDecoder(jsonStr, false);
            Map<String, List<String>> uriAttributes = queryDecoder.parameters();
            for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()) {
                for (String attrVal : attr.getValue()) {
                    log.info(attr.getKey() + "=" + attrVal);
                }
            }
        } else if ("multipart/form-data".equals(contentType)) {
            //TODO 用于文件上传
        } else {
            //do nothing...
        }

    }

    /**
     * 从请求头中获取 Content-Type
     *
     * @return
     */
    private String getContentType() {
        String typeStr = headers.get("Content-Type").toString();
        String[] list = typeStr.split(";");
        return list[0];
    }


}

序列化接口

package com.example.springfileupload3.netty.http;

/**
 * 序列化接口类
 *
 * @Description
 * @Author wzq
 **/
public interface Serializer {

    /**
     * java 对象转换成二进制
     * @param object
     * @return
     */
    byte[] serialize(Object object);

    /**
     * 二进制转换成 java 对象
     * @param clazz
     * @param bytes
     * @return
     * @param <T>
     */
    <T> T deserialize(Class<T> clazz, byte[] bytes);

}

JSONSerializer.java 序列化类

package com.example.springfileupload3.netty.http;

import com.alibaba.fastjson2.JSON;

import java.util.Date;

/**
 * JSON序列化
 *
 * @Description
 * @Author wzq
 **/
public class JSONSerializer implements Serializer {

    @Override
    public byte[] serialize(Object object) {
        return JSON.toJSONBytes(object);
    }

    @Override
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return JSON.parseObject(bytes, clazz);
    }

}

User.java response返回类

package com.example.springfileupload3.netty.http;

import lombok.Data;

import java.util.Date;

/**
 * User
 *
 * @Description
 * @Author wzq
 **/
@Data
public class User {

    private String userName;

    private String method;

    private Date date;

}

ProjectCommandLineRunner.java 项目启动时启动类

package com.example.springfileupload3.config;

import com.example.springfileupload3.netty.http.HttpServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Configuration;

/**
 * ProjectCommandLineRunner
 *
 * @Description
 * @Author wzq
 * @Date 2023/7/20 11:47
 **/
@Configuration
@Slf4j
public class ProjectCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        log.info("ProjectCommandLineRunner run...");
        HttpServer httpServer = new HttpServer();
        httpServer.run();
    }

}