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 配置的实例,srcmainesourceshttp.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);

}

}

配置文件 srcmainesourceshttp.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 框架,以及源码阅读等。加油!!!

以上是 Netty笔记手写HTTP服务器 的全部内容, 来源链接: utcz.com/z/519019.html

回到顶部