项目地址: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建立的服务器是否可行,结果是可以的!
嘿嘿,做到这里基本初具雏形了!