Handwritten a spring boot like HTTP framework: dozens of lines of code to build an HTTP server based on netty

JavaGuide 2020-11-13 10:05:20
handwritten spring boot http framework


This article has been included in : https://github.com/Snailclimb/netty-practical-tutorial (Netty From introduction to practice : Handwriting HTTP Server+RPC frame ).
Related items :https://github.com/Snailclimb/jsoncat ( Imitation Spring Boot But it's different from Spring Boot A lightweight of HTTP frame )

One that is currently being written is called jsoncat Lightweight of HTTP frame-built HTTP The server is my own, based on Netty Written , All the core code adds up to just a few dozen lines . Thanks to Netty Various out of the box components available , It saves us too much .

In this article, I will take my friends hand in hand to achieve a simple HTTP Server.

If there is anything in the article that needs to be improved and perfected , Welcome to the comment area , Common progress !

Before the beginning, in order to avoid the small partner does not understand Netty , Let's start with a brief introduction to it !

What is? Netty?

Simple use 3 To sum up Netty Well !

  1. Netty It's based on NIO Of client-server( Client server ) frame , It can be used to develop web applications quickly and easily .
  2. Netty Greatly simplifies and optimizes TCP and UDP Socket server and other network programming , And performance and security and many other aspects need to be better .
  3. Netty Support multiple protocols Such as FTP,SMTP,HTTP And various binary and text-based traditional protocols . What this article is going to write about HTTP Server Benefit from Netty Yes HTTP agreement ( Hypertext transfer protocol ) Support for .

Netty What are the application scenarios ?

With your own understanding , Simply put ! In theory ,NIO What can be done , Use Netty Can be done and better .

however , The first thing we need to make clear is Netty It's mainly used to do Network communication .

  1. Implement the network communication module of the framework : Netty It can meet the needs of network communication in almost any scenario , therefore , The network communication module of the framework can be based on Netty To do it . take RPC Frame speaking ! We're in a distributed system , Different service nodes often need to call each other , This is the time RPC The framework . How does the communication of different services guide ? Then you can use it Netty Here we go ! For example, if I call the method of another node , At least let the other party know which method in the class I called and the related parameters !
  2. Realize one of your own HTTP The server : adopt Netty , We can easily use a small amount of code to implement a simple HTTP The server .Netty With codec and message aggregator , It saves us a lot of development !
  3. Implement an instant messaging system : Use Netty We can implement an instant messaging system that can chat like wechat , There are a lot of open source projects in this area , You can go on your own Github Look for it .
  4. Realize message push system : There are many message push systems on the market based on Netty To do the .

Those open source projects use Netty?

We usually come into contact with Dubbo、RocketMQ、Elasticsearch、gRPC 、Spring Cloud Gateway And so on Netty.

It can be said that a large number of open source projects have used Netty, So master Netty It helps you better use these open source projects and enables you to redevelop them .

In fact, there are a lot of great projects that use Netty,Netty The official statistics are also done , The statistics are here :https://netty.io/wiki/related-projects.html .

Realization HTTP Server The necessary pre knowledge

since , We want to achieve HTTP Server It must be reviewed first HTTP Basic knowledge of the agreement .

HTTP agreement

Hypertext transfer protocol (HTTP,HyperText Transfer Protocol) Mainly for Web Browser and Web Designed for communication between servers .

When we use browsers to browse the web , Our website is through HTTP Request to load , The whole process is shown in the following figure .

HTTP Request process

https://www.seobility.net/en/wiki/HTTP_headers

HTTP The protocol is based on TCP Agreed , therefore , send out HTTP Before a request is made, you have to create TCP To connect is to experience 3 The second handshake . Currently used HTTP Most of the agreements are 1.1. stay 1.1 In the agreement , The default is on Keep-Alive Of , In this way, the established connection can be reused in multiple requests .

I understand HTTP After the agreement , Let's take another look HTTP Message content , This part is very important !( The reference picture is from :https://iamgopikrishna.wordpress.com/2014/06/13/4/

HTTP Request message :

HTTP Request message

HTTP response message :

HTTP response message

our HTTP The server will parse HTTP Request message content , And then according to the content of the message processing and return HTTP The response message is sent to the client .

Netty codecs

If we want to pass Netty Handle HTTP request , We need to encode and decode first . The so-called encoding and decoding is in Netty The transmission of data ByteBuf and Netty Targeted at HTTP Objects provided by requests and responses, such as HttpRequest and HttpContent Switch between .

Netty Bring it with you 4 A common codec :

  1. HttpRequestEncoder (HTTP Request encoder ): take HttpRequest and HttpContent Encoded as ByteBuf .
  2. HttpRequestDecoder (HTTP Request decoder ): take ByteBuf Decoding for HttpRequest and HttpContent
  3. HttpResponsetEncoder (HTTP Response encoder ): take HttpResponse and HttpContent Encoded as ByteBuf .
  4. HttpResponseDecoder(HTTP Response decoder ): take ByteBuf Decoding for HttpResponst and HttpContent

Network communication is ultimately transmitted by byte stream . ByteBuf yes Netty A byte container provided by , Inside it is an array of bytes . When we go through Netty When transmitting data , It is through ByteBuf On going .

HTTP Server The end is used to receive HTTP Request, And then send HTTP Response. So we just need HttpRequestDecoder and HttpResponseEncoder that will do .

I drew a picture by hand , It should be easier to understand .

Netty Yes HTTP The abstraction of the message

In order to be able to express HTTP All kinds of news in ,Netty Designed and abstracted a complete set of HTTP Message structure chart , The core inheritance relationship is shown in the following figure .

  1. HttpObject : Whole HTTP Top level interface of message architecture .HttpObject Under the interface, there is HttpMessage and HttpContent Two core interfaces .
  2. HttpMessage: Definition HTTP news , by HttpRequest and HttpResponse Provides general properties
  3. HttpRequest : HttpRequest Corresponding HTTP request. adopt HttpRequest We can access the query parameters (Query Parameters) and Cookie. and Servlet API The difference is , Query parameters are queried by QueryStringEncoder and QueryStringDecoder To construct and parse query parameters .
  4. HttpResponseHttpResponse Corresponding HTTP response. and HttpMessage comparison ,HttpResponse Added status( Corresponding status code ) Properties and their corresponding methods .
  5. HttpContent : Block transfer coding Chunked transfer encoding) It's the hypertext transfer protocol (HTTP) A data transmission mechanism in (HTTP/1.1 Only then ), allow HTTP Sent by the application server to the client application ( It's usually a web browser ) The data can be divided into many “ block ”( When there is a large amount of data ). We can HttpContent Think of it as pieces of data .
  6. LastHttpContent : identification HTTP End of request , Also include HttpHeaders object .
  7. FullHttpRequest and FullHttpResponseHttpMessage and HttpContent The object obtained after aggregation .

HTTP Message aggregators

HttpObjectAggregator yes Netty Provided HTTP Message aggregators , Through it we can put HttpMessage and HttpContent Aggregate into one FullHttpRequest perhaps FullHttpResponse( It depends on whether to process the request or respond ), It's convenient for us to use .

in addition , If the message body is relatively large , It may also be divided into several message bodies to deal with ,HttpObjectAggregator You can aggregate these messages into a complete , It's convenient for us to deal with .

Usage method : take HttpObjectAggregator Add to ChannelPipeline in , If it's for handling HTTP Request Put it in the HttpResponseEncoder after , conversely , If used to deal with HTTP Response Put it in the HttpResponseDecoder after .

because ,HTTP Server The end is used to receive HTTP Request, The corresponding usage is as follows .

ChannelPipeline p = ...;
p.addLast("decoder", new HttpRequestDecoder())
.addLast("encoder", new HttpResponseEncoder())
.addLast("aggregator", new HttpObjectAggregator(512 * 1024))
.addLast("handler", new HttpServerHandler());

be based on Netty Achieve one HTTP Server

adopt Netty, We can easily build one with a small amount of code that can handle GET Request and POST Lightweight of requests HTTP Server.

Source code address :https://github.com/Snailclimb/netty-practical-tutorial/tree/master/example/http-server .

Add the required dependencies to pom.xml

First step , We need to implement HTTP Server The necessary third-party dependent coordinates are added to pom.xml in .

<!--netty-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
<!-- log -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!--commons-codec-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>

Create server

@Slf4j
public class HttpServer {

private static final int PORT = 8080;
public void start() {

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// TCP The default is on Nagle Algorithm , The function of this algorithm is to send big data as fast as possible , Reduce network transmission .TCP_NODELAY The function of the parameter is to control whether it is enabled or not Nagle Algorithm .
.childOption(ChannelOption.TCP_NODELAY, true)
// Open or not TCP The underlying heartbeat mechanism 
.childOption(ChannelOption.SO_KEEPALIVE, true)
// Represents the maximum length of the queue used by the system to temporarily hold requests that have completed three handshakes , If the connection is established frequently , The server is slow to create new connections , You can increase this parameter properly 
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel ch) {

ch.pipeline().addLast("decoder", new HttpRequestDecoder())
.addLast("encoder", new HttpResponseEncoder())
.addLast("aggregator", new HttpObjectAggregator(512 * 1024))
.addLast("handler", new HttpServerHandler());
}
});
Channel ch = b.bind(PORT).sync().channel();
log.info("Netty Http Server started on port {}.", PORT);
ch.closeFuture().sync();
} catch (InterruptedException e) {

log.error("occur exception when start server:", e);
} finally {

log.error("shutdown bossGroup and workerGroup");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

Simply analyze the creation process of the server side !

1. I created two NioEventLoopGroup Object instances :bossGroup and workerGroup.

  • bossGroup : For processing clients TCP Connection request .
  • workerGroup : Responsible for the processing logic of the specific read and write data of each connection , Truly responsible I/O Read and write operations , Leave it to the corresponding Handler Handle .

for instance : We treat the boss of the company as bossGroup, Employees act as workerGroup,bossGroup After picking up work outside , Throw to workerGroup To deal with . We usually specify bossGroup Of The number of threads is 1( When the number of concurrent connections is small ) ,workGroup The number of threads for is CPU The core number *2 . in addition , According to the source code , Use NioEventLoopGroup Class's nonparametric constructor sets the default value for the number of threads CPU The core number *2 .

2. Create a server boot / Auxiliary class : ServerBootstrap, This class will guide us to start the server .

3. adopt .group() Method to guide class ServerBootstrap Configure two major thread groups , The thread model is determined .

4. adopt channel() Method to guide class ServerBootstrap It specifies IO The model is NIO

  • NioServerSocketChannel : Specify the server side of IO The model is NIO, And BIO In the programming model ServerSocket Corresponding
  • NioSocketChannel : Specify the IO The model is NIO, And BIO In the programming model Socket Corresponding

5. adopt .childHandler() Create a ChannelInitializer , Then it specifies the business processing logic of the server-side message, that is, the custom ChannelHandler object

6. call ServerBootstrap Class bind() Method binding port .

//bind() It's asynchronous , however , You can go through sync() Method to make it synchronous .
ChannelFuture f = b.bind(port).sync();

Custom server ChannelHandler Handle HTTP request

We inherit SimpleChannelInboundHandler , And rewrite the following 3 A way :

  1. channelRead() : The server receives and processes the data sent by the client HTTP The method called by the request .
  2. exceptionCaught() : Processing client sent HTTP The request is called when an exception occurs .
  3. channelReadComplete() : The server consumes the client sent HTTP The method that is called after the request .

in addition , client HTTP The request parameter type is FullHttpRequest. We can FullHttpRequest The object is seen as HTTP Requesting a message Java The representation of objects .

@Slf4j
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

private static final String FAVICON_ICO = "/favicon.ico";
private static final AsciiString CONNECTION = AsciiString.cached("Connection");
private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive");
private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type");
private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length");
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) {

log.info("Handle http request:{}", fullHttpRequest);
String uri = fullHttpRequest.uri();
if (uri.equals(FAVICON_ICO)) {

return;
}
RequestHandler requestHandler = RequestHandlerFactory.create(fullHttpRequest.method());
Object result;
FullHttpResponse response;
try {

result = requestHandler.handle(fullHttpRequest);
String responseHtml = "<html><body>" + result + "</body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes));
response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
} catch (IllegalArgumentException e) {

e.printStackTrace();
String responseHtml = "<html><body>" + e.toString() + "</body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, Unpooled.wrappedBuffer(responseBytes));
response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
}
boolean keepAlive = HttpUtil.isKeepAlive(fullHttpRequest);
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) {

cause.printStackTrace();
ctx.close();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {

ctx.flush();
}
}

The message body we return to the client is FullHttpResponse object . adopt FullHttpResponse object , We can set HTTP In response to a message HTTP Protocol version 、 The specific content of the response The content such as .

We can FullHttpResponse The object is seen as HTTP In response to a message Java The representation of objects .

FullHttpResponse response;
String responseHtml = "<html><body>" + result + "</body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
// initialization FullHttpResponse , And set up HTTP agreement 、 Response status code 、 The specific content of the response 
response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes));

We go through FullHttpResponse Of headers() Method to get HttpHeaders, there HttpHeaders Corresponding to HTTP The header of the response message . adopt HttpHeaders object , We can be right HTTP The contents of the header of the response message, such as Content-Typ Set it up .

response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());

In this case , In order to cover up the Content-Type by text/html , That is to return to html Format data to client .

common Content-Type

Content-Type explain
text/html html Format
text/plain Plain text format
text/css css Format
text/javascript js Format
application/json json Format ( Front and back separation projects are commonly used )
image/gif gif Image format
image/jpeg jpg Image format
image/png png Image format

The specific processing logic of the request is realized

Because there's... Here POST Request and GET request . So we need to define a process first HTTP Request The interface of .

public interface RequestHandler {

Object handle(FullHttpRequest fullHttpRequest);
}

HTTP Method It's not just that GET and POST, Other common ones are PUT、DELETE、PATCH. It's just the implementation in this case HTTP Server Only considered GET and POST.

  • GET : Request specific resources from the server . for instance :GET /classes( Get all classes )
  • POST : Create a new resource on the server . for instance :POST /classes( Create a class )
  • PUT : Update resources on the server ( The client provides the whole updated resource ). for instance :PUT /classes/12( The update number is 12 Class )
  • DELETE : Remove specific resources from the server . for instance :DELETE /classes/12( Delete No 12 Class )
  • PATCH : Update resources on the server ( The client provides the changed properties , It can be seen that the affectation is part of the update ), Less used , There is no example here .

GET Processing of requests

@Slf4j
public class GetRequestHandler implements RequestHandler {

@Override
public Object handle(FullHttpRequest fullHttpRequest) {

String requestUri = fullHttpRequest.uri();
Map<String, String> queryParameterMappings = this.getQueryParams(requestUri);
return queryParameterMappings.toString();
}
private Map<String, String> getQueryParams(String uri) {

QueryStringDecoder queryDecoder = new QueryStringDecoder(uri, Charsets.toCharset(CharEncoding.UTF_8));
Map<String, List<String>> parameters = queryDecoder.parameters();
Map<String, String> queryParams = new HashMap<>();
for (Map.Entry<String, List<String>> attr : parameters.entrySet()) {

for (String attrVal : attr.getValue()) {

queryParams.put(attr.getKey(), attrVal);
}
}
return queryParams;
}
}

I'm just trying to make this simple URI The corresponding relationship of query parameters of is returned to the client directly .

actually , To obtain the URI The corresponding relation of query parameters of , Combined with reflection and annotation related knowledge , It's easy to implement something like Spring Boot Of @RequestParam Note the .

Suggest that you want to learn , You can do it on your own . If you don't know how , You can refer to my lightweight open source HTTP frame jsoncat ( Imitation Spring Boot But it's different from Spring Boot A lightweight of HTTP frame ).

POST Processing of requests

@Slf4j
public class PostRequestHandler implements RequestHandler {

@Override
public Object handle(FullHttpRequest fullHttpRequest) {

String requestUri = fullHttpRequest.uri();
log.info("request uri :[{}]", requestUri);
String contentType = this.getContentType(fullHttpRequest.headers());
if (contentType.equals("application/json")) {

return fullHttpRequest.content().toString(Charsets.toCharset(CharEncoding.UTF_8));
} else {

throw new IllegalArgumentException("only receive application/json type data");
}
}
private String getContentType(HttpHeaders headers) {

String typeStr = headers.get("Content-Type");
String[] list = typeStr.split(";");
return list[0];
}
}

about POST Processing of requests , We only accept processing here Content-Type by application/json The data of , If POST The request didn't come from application/json Data of type , We just throw exceptions .

actually , We've got the client's json After the format of the data , Combined with reflection and annotation related knowledge , It's easy to implement something like Spring Boot Of @RequestBody Note the .

Suggest that you want to learn , You can do it on your own . If you don't know how , You can refer to my lightweight open source HTTP frame jsoncat ( Imitation Spring Boot But it's different from Spring Boot A lightweight of HTTP frame ).

Request processing factory class

public class RequestHandlerFactory {

public static final Map<HttpMethod, RequestHandler> REQUEST_HANDLERS = new HashMap<>();
static {

REQUEST_HANDLERS.put(HttpMethod.GET, new GetRequestHandler());
REQUEST_HANDLERS.put(HttpMethod.POST, new PostRequestHandler());
}
public static RequestHandler create(HttpMethod httpMethod) {

return REQUEST_HANDLERS.get(httpMethod);
}
}

I used the factory model here , When we deal with new HTTP Method Method time , Direct realization RequestHandler Interface , Then add the implementation class to RequestHandlerFactory that will do .

Start class

public class HttpServerApplication {

public static void main(String[] args) {

HttpServer httpServer = new HttpServer();
httpServer.start();
}
}

effect

function HttpServerApplication Of main() Method , The console prints out :

[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] REGISTERED
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a] BIND: 0.0.0.0/0.0.0.0:8080
[nioEventLoopGroup-2-1] INFO io.netty.handler.logging.LoggingHandler - [id: 0x9bb1012a, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
[main] INFO server.HttpServer - Netty Http Server started on port 8080.

GET request

POST request

Reference resources

  1. Netty Learning notes -http objects

My open source project recommends

  1. JavaGuide :「Java Study + Interview guide 」 One covers most Java The core knowledge that programmers need to master . Get ready Java interview , The preferred JavaGuide!
  2. guide-rpc-framework :A custom RPC framework implemented by Netty+Kyro+Zookeeper.( Based on a Netty+Kyro+Zookeeper Implemented customization RPC frame - The detailed implementation process and related tutorials are attached )
  3. jsoncat : Imitation Spring Boot But it's different from Spring Boot A lightweight of HTTP frame
  4. programmer-advancement : Programmers should have some good habits + Interview must know !
  5. springboot-guide :Not only Spring Boot but also important knowledge of Spring( It's not just SpringBoot also Spring Important knowledge points )
  6. awesome-java :Collection of awesome Java project on Github(Github It's great Java Open source project collection ).
版权声明
本文为[JavaGuide]所创,转载请带上原文链接,感谢

  1. [front end -- JavaScript] knowledge point (IV) -- memory leakage in the project (I)
  2. This mechanism in JS
  3. Vue 3.0 source code learning 1 --- rendering process of components
  4. Learning the realization of canvas and simple drawing
  5. gin里获取http请求过来的参数
  6. vue3的新特性
  7. Get the parameters from HTTP request in gin
  8. New features of vue3
  9. vue-cli 引入腾讯地图(最新 api,rocketmq原理面试
  10. Vue 学习笔记(3,免费Java高级工程师学习资源
  11. Vue 学习笔记(2,Java编程视频教程
  12. Vue cli introduces Tencent maps (the latest API, rocketmq)
  13. Vue learning notes (3, free Java senior engineer learning resources)
  14. Vue learning notes (2, Java programming video tutorial)
  15. 【Vue】—props属性
  16. 【Vue】—创建组件
  17. [Vue] - props attribute
  18. [Vue] - create component
  19. 浅谈vue响应式原理及发布订阅模式和观察者模式
  20. On Vue responsive principle, publish subscribe mode and observer mode
  21. 浅谈vue响应式原理及发布订阅模式和观察者模式
  22. On Vue responsive principle, publish subscribe mode and observer mode
  23. Xiaobai can understand it. It only takes 4 steps to solve the problem of Vue keep alive cache component
  24. Publish, subscribe and observer of design patterns
  25. Summary of common content added in ES6 + (II)
  26. No.8 Vue element admin learning (III) vuex learning and login method analysis
  27. Write a mini webpack project construction tool
  28. Shopping cart (front-end static page preparation)
  29. Introduction to the fluent platform
  30. Webpack5 cache
  31. The difference between drop-down box select option and datalist
  32. CSS review (III)
  33. Node.js学习笔记【七】
  34. Node.js learning notes [VII]
  35. Vue Router根据后台数据加载不同的组件(思考-&gt;实现-&gt;不止于实现)
  36. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  37. 【JQuery框架,Java编程教程视频下载
  38. [jQuery framework, Java programming tutorial video download
  39. Vue Router根据后台数据加载不同的组件(思考-&gt;实现-&gt;不止于实现)
  40. Vue router loads different components according to background data (thinking - & gt; Implementation - & gt; (more than implementation)
  41. 【Vue,阿里P8大佬亲自教你
  42. 【Vue基础知识总结 5,字节跳动算法工程师面试经验
  43. [Vue, Ali P8 teaches you personally
  44. [Vue basic knowledge summary 5. Interview experience of byte beating Algorithm Engineer
  45. 【问题记录】- 谷歌浏览器 Html生成PDF
  46. [problem record] - PDF generated by Google browser HTML
  47. 【问题记录】- 谷歌浏览器 Html生成PDF
  48. [problem record] - PDF generated by Google browser HTML
  49. 【JavaScript】查漏补缺 —数组中reduce()方法
  50. [JavaScript] leak checking and defect filling - reduce() method in array
  51. 【重识 HTML (3),350道Java面试真题分享
  52. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  53. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  54. [re recognize HTML (3) and share 350 real Java interview questions
  55. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  56. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  57. 【重识 HTML ,nginx面试题阿里
  58. 【重识 HTML (4),ELK原来这么简单
  59. [re recognize HTML, nginx interview questions]
  60. [re recognize HTML (4). Elk is so simple