源代码仓库 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 框架,以及源码阅读等。加油!!!