Netty 笔记-手写 HTTP 服务器

源代码仓库 github.com/zhshuixian/netty-notes

这里将使用 Netty 编写一个简单的 HTTP 服务,可以自定义配置 Servlet,使用浏览器访问返回对应的响应。项目大体示意图如下:

  • 启动 Netty 的服务,负责监听 HTTP 请求,设置 HTTP 编码和解码器,并把请求交给 Handler 处理
  • Handler 解析 Http Request 请求的 URI 信息,根据 URI 查找对应的 Servlet 或者返回 404 错误
  • Servlet 是实际的业务代码,其继承一个共同的抽象类

在上一个项目的基础上,新建 02-netty-http 子模块,项目的依赖和 Maven 配置见 GitHub 的项目仓库。

1、Servlet 、Request、Response

这里将实现 Servlet 抽象类的定义,自定义的 HTTP Request 和 Response

HttpRequest.java 和 HttpResponse.java 不是必须的,主要为了方便使用,可以直接使用 Netty 自带 的 FullHttpRequest 和 FullHttpResponse。这里自定义实现是为了方便的使用 response.write() 等。

新建类 HttpRequest.java ,解析 Request 的 URI,参数 等信息。

package org.xian.http.common;

public class HttpRequest {
    // 日志使用 slf4j 具体看仓库的 pom.xml 文件 和 log4j.properties 配置
    private static final Logger log = LoggerFactory.getLogger(HttpRequest.class);
    private final ChannelHandlerContext context;
    private final FullHttpRequest fullHttpRequest;
    private final Map<String, List<String>> parameters;
    // 初始化
    public HttpRequest(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {
        this.context = context;
        this.fullHttpRequest = fullHttpRequest;
        this.parameters = new QueryStringDecoder(fullHttpRequest.uri()).parameters();
        log.info("处理来自 " + context.channel().remoteAddress() + ",访问 " + getUri() + " 的请求");
    }
    // 获取请求的 URI,去掉后面的参数
    public String getUri() {
        return this.fullHttpRequest.uri().split("\\?")[0];
    }
    // 获取 HTTP 的请求方法,如GET POST
    public String getMethod() {
        return fullHttpRequest.method().name();
    }
    // 获取所有的参数
    public Map<String, List<String>> getParameters() {
        return this.parameters;
    }
    // 根据参数名获取对应的参数
    public String getParameter(String name) {
        if (this.parameters.get(name) != null) {
            return this.parameters.get(name).get(0);
        } else {
            return null;
        }
    }
}

新建 HttpResponse.java ,主要是 write() 方法。

因为 DefaultFullHttpResponse 并没有提供类似 write() 的方法,如果直接使用,需要使用 repalce() 方法将 response 的内容替换。如果不使用 HttpResponse 可以直接使用替换功能,在 Handler 里面 flush() 即可。

// DefaultFullHttpResponse 的 replace 方法,在 Servlet 需要新建一个 ByteBuf 对象来替换,比较麻烦
public FullHttpResponse replace(ByteBuf content) {
    FullHttpResponse response = new DefaultFullHttpResponse(this.protocolVersion(), this.status(), content, this.headers().copy(), this.trailingHeaders().copy());
    response.setDecoderResult(this.decoderResult());
    return response;
}
package org.xian.http.common;
public class HttpResponse {
    // 其他代码省略
    public void write(String out) {
        if (out != null && out.length() != 0) {
            // response 返回输出的内容
            FullHttpResponse response = new DefaultFullHttpResponse(
                    // HTTP 1.1
                    HttpVersion.HTTP_1_1,
                    // HTTP 返回码 200
                    HttpResponseStatus.OK,
                    // 将输出的信息封装为 Netty 的 ByteBuffer
                    Unpooled.copiedBuffer(out, CharsetUtil.UTF_8)
            );
            // 设置 Http 的头部信息
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
            context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }
    }
}

新建 BaseServlet.java 抽象接口,作为所有 Servlet 的父类。

package org.xian.http.common;
public abstract class BaseServlet {
    public void service(HttpRequest request, HttpResponse response) {
        // 类似 Tomcat Servlet,根据请求的方法调用不同的方法
        if ("GET".equalsIgnoreCase(request.getMethod())) {
            doGet(request, response);
        } else {
            doPost(request, response);
        }
    }

    /** Get 方法访问的抽象函数*/
    public abstract void doGet(HttpRequest request, HttpResponse response);
    /** Post 方法访问的抽象函数*/
    public abstract void doPost(HttpRequest request, HttpResponse response);
}

新建 HttpServletMapping.java ,用于管理 URI 和 Servlet 对应的映射。

比如访问 /hello ,就使用  HelloServlet 这个类来处理。

public class HttpServletMapping {
    /** 存储 Servlet 的 URI 配置和对应的实例对象 */
    private volatile static Map<String, BaseServlet> servletMapping;

    /** 用于加载配置文件 */
    private static final Properties PROPERTIES = new Properties();
    /** 初始化 servletMapping 其他代码省略 */
    private static synchronized void init() {
        if (servletMapping == null) {
            servletMapping = new HashMap<>(32);
            // 读取配置文件 http.properties ,同时初始化 ServletMapping
            try {
                // Properties 会自动读取 .xml 或者 .properties,然后映射为 K-V 类型的数据结构
                String path = HttpServletMapping.class.getResource("/").getPath();
                FileInputStream fs = new FileInputStream(path + "http.properties");
                PROPERTIES.load(fs);

                for (Object obj : PROPERTIES.keySet()) {
                    String key = obj.toString();
                    // servlet. 开头和 .uri 结尾的 Key 定义为 Servlet 配置
                    if (key.startsWith("servlet.") && key.endsWith(".uri")) {
                        // 获取 uri 的值
                        String uri = PROPERTIES.getProperty(key);
                        // 获取 class name,替换 .uri 为 .class
                        String className = PROPERTIES.getProperty(key.replace(".uri", ".class"));
                        // 使用反射实例化这个对象
                        BaseServlet servlet = (BaseServlet) Class.forName(className)
                                .getDeclaredConstructor().newInstance();
                        servletMapping.put(uri, servlet);
                    }
                }
            } catch (Exception e) {
                log.debug("请检查 http.properties 的 Servlet 配置字段");
                e.printStackTrace();
            }
        }
    }
}

http.properties 配置文件示例

# Servlet 配置的实例,src\main\resources\http.properties 文件
# Hello Servlet 的 uri 和 class 配置
servlet.hello.uri=/hello
servlet.hello.class=org.xian.servlet.HelloServlet

2、Handler 处理器

在前面提到到,Netty 会将事件(一个请求)派发给 Handler 进行处理,这里实现的 Handler 的目的是将请求根据 URI ,调用其对应的 Servlet 的方法。

代码 HttpRequestHandler.java** **

package org.xian.http;
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext context, FullHttpRequest fullHttpRequest) {\
        // 封装成我们自定义的 HttpRequest
        HttpRequest request = new HttpRequest(context, fullHttpRequest);
        // 请求的 uri
        String uri = request.getUri();
        // 封装成自定义的 Response,因为自带的 DefaultFullHttpResponse 的 content(ByteBuffer)是使用替换的方式处理的
        HttpResponse response = new HttpResponse(context, request);
        // Http 路径和其实例的对象
        Map<String, BaseServlet> servletMapping = HttpServletMapping.getServletMapping();
        // 根据 servletMapping 调用 Servlet 的 service() 方法
        if (servletMapping != null && servletMapping.containsKey(uri)) {
            servletMapping.get(uri).service(request, response);
        } else {
            response.write("404 -- Not Found");
        }
    }
}

3、HTTP 服务器

代码 HttpServer.java ,使用 Bootstrap (引导)将 ChannelHandler 和 ChannelPipeline 组合起来,指定 HTTP 的编码和解码器,以及监听服务器端端口号。

可以看到跟上一节的 PongServer.java 的区别就在于这里指定了 HTTP 编码和解码器

package org.xian.http;
public class HttpServer {
    private static final Logger log = LoggerFactory.getLogger(HttpServer.class);
    private final int port;
    public void start() {
        log.info("开始启动 Http 服务,端口号是:" + port);
        // Netty 会为每个 Channel 分配一个 EventLoop
        // 一个 EventLoop 可以管理多个 Channel
        EventLoopGroup executors = new NioEventLoopGroup();
        try {
            // 引导,功能是将 ChannelHandler,ChannelPipeline、EventLoop 组织起来,
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(executors)
                    // 使用 NIO 的 Channel
                    .channel(NioServerSocketChannel.class)
                    // 绑定 Handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // Http 编码器和解码器
                            ch.pipeline().addLast(new HttpServerCodec());// http 编解码
                            ch.pipeline().addLast("httpAggregator",
                                    new HttpObjectAggregator(512 * 1024)); // http 消息聚合器
                            // 将 HttpRequestHandler 绑定到 ChannelPipeline
                            ch.pipeline().addLast(new HttpRequestHandler());
                        }
                    });
            // bind 绑定监听端口号
            ChannelFuture future = bootstrap.bind(new InetSocketAddress(this.port));
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                executors.shutdownGracefully().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码 HttpApplication.java 启动类,运行其 main 启动 HTTP 服务。

public class HttpApplication {
    public static void main(String[] args) {
        new HttpServer(8080).start();
    }
}

到此,基于 Netty 手写一个简单的 HTTP 服务器基本工作已经完成,下面将新建几个 Servlet 用于测试。

4、新建 Servlet

代码 HelloServlet.java

package org.xian.servlet;
public class HelloServlet extends BaseServlet {
    @Override
    public void doGet(HttpRequest request, HttpResponse response) {
        doPost(request, response);
    }
    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        response.write("Hello,HTTP Server by Netty");
    }
}

代码 SecondServlet.java

package org.xian.servlet;
public class SecondServlet extends BaseServlet {
    @Override
    public void doGet(HttpRequest request, HttpResponse response) {
        doPost(request, response);
    }
    @Override
    public void doPost(HttpRequest request, HttpResponse response) {
        String username = request.getParameter("username");
        response.write("用户名是 " + username);
    }
}

配置文件 src\main\resources\http.properties

# Hello Servlet 的 uri 和 class 配置
servlet.hello.uri=/hello
servlet.hello.class=org.xian.servlet.HelloServlet
# Second Servlet
servlet.second.uri=/second
servlet.second.class=org.xian.servlet.SecondServlet

启动项目。分别访问 /hello 、/second 以及不在 http.properties 配置的 URI,查看其返回结果。

/hello 返回的是
Hello,HTTP Server by Netty
/second?username=Netty%20笔记 返回的是
用户名是 Netty 笔记
/404 返回的是
404 -- Not Found

感谢阅读,「Netty 笔记」小专栏接下去的安排是使用 Netty 手写一个简单的 RPC 框架,以及源码阅读等。加油!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值