本文已經收錄進 : https://github.com/Snailclimb/netty-practical-tutorial (Netty 從入門到實戰:手寫 HTTP Server+RPC 框架),
相關專案:https://github.com/Snailclimb/jsoncat (仿 Spring Boot 但不同于 Spring Boot 的一個輕量級的 HTTP 框架)
目前正在寫的一個叫做 jsoncat 的輕量級 HTTP 框架內置的 HTTP 服務器是我自己基于 Netty 寫的,所有的核心代碼加起來不過就幾十行,這得益于 Netty 提供的各種開箱即用的組件,為我們節省了太多事情,
這篇文章我會手把手帶著小伙伴們實作一個簡易的 HTTP Server,
如果文章有任何需要改善和完善的地方,歡迎在評論區指出,共同進步!
開始之前為了避免有小伙伴不了解 Netty ,還是先來簡單介紹它!
什么是 Netty?
簡單用 3 點來概括一下 Netty 吧!
- Netty 是一個基于 NIO 的 client-server(客戶端服務器)框架,使用它可以快速簡單地開發網路應用程式,
- Netty 極大地簡化并優化了 TCP 和 UDP 套接字服務器等網路編程,并且性能以及安全性等很多方面都要更好,
- Netty 支持多種協議 如 FTP,SMTP,HTTP 以及各種二進制和基于文本的傳統協議,本文所要寫的 HTTP Server 就得益于 Netty 對 HTTP 協議(超文本傳輸協議)的支持,
Netty 應用場景有哪些?
憑借自己的了解,簡單說一下吧!理論上來說,NIO 可以做的事情 ,使用 Netty 都可以做并且更好,
不過,我們還是首先要明確的是 Netty 主要用來做網路通信 ,
- 實作框架的網路通信模塊 : Netty 幾乎滿足任何場景的網路通信需求,因此,框架的網路通信模塊可以基于 Netty 來做,拿 RPC 框架來說! 我們在分布式系統中,不同服務節點之間經常需要相互呼叫,這個時候就需要 RPC 框架了,不同服務指點的通信是如何做的呢?那就可以使用 Netty 來做了!比如我呼叫另外一個節點的方法的話,至少是要讓對方知道我呼叫的是哪個類中的哪個方法以及相關引數吧!
- 實作一個自己的 HTTP 服務器 :通過 Netty ,我們可以很方便地使用少量代碼實作一個簡單的 HTTP 服務器,Netty 自帶了編解碼器和訊息聚合器,為我們開發節省了很多事!
- 實作一個即時通訊系統 : 使用 Netty 我們可以實作一個可以聊天類似微信的即時通訊系統,這方面的開源專案還蠻多的,可以自行去 Github 找一找,
- 實作訊息推送系統 :市面上有很多訊息推送系統都是基于 Netty 來做的,
- ......
那些開源專案用到了 Netty?
我們平常經常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC 、Spring Cloud Gateway 等等都用到了 Netty,
可以說大量的開源專案都用到了 Netty,所以掌握 Netty 有助于你更好的使用這些開源專案并且讓你有能力對其進行二次開發,
實際上還有很多很多優秀的專案用到了 Netty,Netty 官方也做了統計,統計結果在這里:https://netty.io/wiki/related-projects.html ,
實作 HTTP Server 必知的前置知識
既然,我們要實作 HTTP Server 那必然先要回顧一下 HTTP 協議相關的基礎知識,
HTTP 協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)主要是為 Web 瀏覽器與 Web 服務器之間的通信而設計的,
當我們使用瀏覽器瀏覽網頁的時候,我們網頁就是通過 HTTP 請求進行加載的,整個程序如下圖所示,

https://www.seobility.net/en/wiki/HTTP_headers
HTTP 協議是基于 TCP 協議的,因此,發送 HTTP 請求之前首先要建立 TCP 連接也就是要經歷 3 次握手,目前使用的 HTTP 協議大部分都是 1.1,在 1.1 的協議里面,默認是開啟了 Keep-Alive 的,這樣的話建立的連接就可以在多次請求中被復用了,
了解了 HTTP 協議之后,我們再來看一下 HTTP 報文的內容,這部分內容很重要!(參考圖片來自:https://iamgopikrishna.wordpress.com/2014/06/13/4/)
HTTP 請求報文:

HTTP 回應報文:

我們的 HTTP 服務器會在后臺決議 HTTP 請求報文內容,然后根據報文內容進行處理之后回傳 HTTP 回應報文給客戶端,
Netty 編解碼器
如果我們要通過 Netty 處理 HTTP 請求,需要先進行編解碼,所謂編解碼說白了就是在 Netty 傳輸資料所用的 ByteBuf 和 Netty 中針對 HTTP 請求和回應所提供的物件比如 HttpRequest 和 HttpContent之間互相轉換,
Netty 自帶了 4 個常用的編解碼器:
HttpRequestEncoder(HTTP 請求編碼器):將HttpRequest和HttpContent編碼為ByteBuf,HttpRequestDecoder(HTTP 請求解碼器):將ByteBuf解碼為HttpRequest和HttpContentHttpResponsetEncoder(HTTP 回應編碼器):將HttpResponse和HttpContent編碼為ByteBuf,HttpResponseDecoder(HTTP 回應解碼器):將ByteBuf解碼為HttpResponst和HttpContent
網路通信最終都是通過位元組流進行傳輸的, ByteBuf 是 Netty 提供的一個位元組容器,其內部是一個位元組陣列, 當我們通過 Netty 傳輸資料的時候,就是通過 ByteBuf 進行的,
HTTP Server 端用于接收 HTTP Request,然后發送 HTTP Response,因此我們只需要 HttpRequestDecoder 和 HttpResponseEncoder 即可,
我手繪了一張圖,這樣看著應該更容易理解了,

Netty 對 HTTP 訊息的抽象
為了能夠表示 HTTP 中的各種訊息,Netty 設計了抽象了一套完整的 HTTP 訊息結構圖,核心繼承關系如下圖所示,

HttpObject: 整個 HTTP 訊息體系結構的最上層介面,HttpObject介面下又有HttpMessage和HttpContent兩大核心介面,HttpMessage: 定義 HTTP 訊息,為HttpRequest和HttpResponse提供通用屬性HttpRequest:HttpRequest對應 HTTP request,通過HttpRequest我們可以訪問查詢引數(Query Parameters)和 Cookie,和 Servlet API 不同的是,查詢引數是通過QueryStringEncoder和QueryStringDecoder來構造和決議查詢查詢引數,HttpResponse:HttpResponse對應 HTTP response,和HttpMessage相比,HttpResponse增加了 status(相應狀態碼) 屬性及其對應的方法,HttpContent: 分塊傳輸編碼(Chunked transfer encoding)是超文本傳輸協議(HTTP)中的一種資料傳輸機制(HTTP/1.1 才有),允許 HTTP 由應用服務器發送給客戶端應用( 通常是網頁瀏覽器)的資料可以分成多“塊”(資料量比較大的情況),我們可以把HttpContent看作是這一塊一塊的資料,LastHttpContent: 標識 HTTP 請求結束,同時包含HttpHeaders物件,FullHttpRequest和FullHttpResponse:HttpMessage和HttpContent聚合后得到的物件,

HTTP 訊息聚合器
HttpObjectAggregator 是 Netty 提供的 HTTP 訊息聚合器,通過它可以把 HttpMessage 和 HttpContent 聚合成一個 FullHttpRequest 或者 FullHttpResponse(取決于是處理請求還是回應),方便我們使用,
另外,訊息體比較大的話,可能還會分成好幾個訊息體來處理,HttpObjectAggregator 可以將這些訊息聚合成一個完整的,方便我們處理,
使用方法:將 HttpObjectAggregator 添加到 ChannelPipeline 中,如果是用于處理 HTTP Request 就將其放在 HttpResponseEncoder 之后,反之,如果用于處理 HTTP Response 就將其放在 HttpResponseDecoder 之后,
因為,HTTP Server 端用于接收 HTTP Request,對應的使用方式如下,
ChannelPipeline p = ...;
p.addLast("decoder", new HttpRequestDecoder())
.addLast("encoder", new HttpResponseEncoder())
.addLast("aggregator", new HttpObjectAggregator(512 * 1024))
.addLast("handler", new HttpServerHandler());
基于 Netty 實作一個 HTTP Server
通過 Netty,我們可以很方便地使用少量代碼構建一個可以正確處理 GET 請求和 POST 請求的輕量級 HTTP Server,
源代碼地址:https://github.com/Snailclimb/netty-practical-tutorial/tree/master/example/http-server ,
添加所需依賴到 pom.xml
第一步,我們需要將實作 HTTP Server 所必需的第三方依賴的坐標添加到 pom.xml中,
<!--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>
創建服務端
@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默認開啟了 Nagle 演算法,該演算法的作用是盡可能的發送大資料快,減少網路傳輸,TCP_NODELAY 引數的作用就是控制是否啟用 Nagle 演算法,
.childOption(ChannelOption.TCP_NODELAY, true)
// 是否開啟 TCP 底層心跳機制
.childOption(ChannelOption.SO_KEEPALIVE, true)
//表示系統用于臨時存放已完成三次握手的請求的佇列的最大長度,如果連接建立頻繁,服務器處理創建新連接較慢,可以適當調大這個引數
.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();
}
}
}
簡單決議一下服務端的創建程序具體是怎樣的!
1.創建了兩個 NioEventLoopGroup 物件實體:bossGroup 和 workerGroup,
bossGroup: 用于處理客戶端的 TCP 連接請求,workerGroup: 負責每一條連接的具體讀寫資料的處理邏輯,真正負責 I/O 讀寫操作,交由對應的 Handler 處理,
舉個例子:我們把公司的老板當做 bossGroup,員工當做 workerGroup,bossGroup 在外面接完活之后,扔給 workerGroup 去處理,一般情況下我們會指定 bossGroup 的 執行緒數為 1(并發連接量不大的時候) ,workGroup 的執行緒數量為 CPU 核心數 *2 ,另外,根據原始碼來看,使用 NioEventLoopGroup 類的無參建構式設定執行緒數量的默認值就是 CPU 核心數 *2 ,
2.創建一個服務端啟動引導/輔助類: ServerBootstrap,這個類將引導我們進行服務端的啟動作業,
3.通過 .group() 方法給引導類 ServerBootstrap 配置兩大執行緒組,確定了執行緒模型,
4.通過channel()方法給引導類 ServerBootstrap指定了 IO 模型為NIO
NioServerSocketChannel:指定服務端的 IO 模型為 NIO,與 BIO 編程模型中的ServerSocket對應NioSocketChannel: 指定客戶端的 IO 模型為 NIO, 與 BIO 編程模型中的Socket對應
5.通過 .childHandler()給引導類創建一個ChannelInitializer ,然后指定了服務端訊息的業務處理邏輯也就是自定義的ChannelHandler 物件
6.呼叫 ServerBootstrap 類的 bind()方法系結埠 ,
//bind()是異步的,但是,你可以通過 sync()方法將其變為同步,
ChannelFuture f = b.bind(port).sync();
自定義服務端 ChannelHandler 處理 HTTP 請求
我們繼承SimpleChannelInboundHandler ,并重寫下面 3 個方法:
channelRead():服務端接收并處理客戶端發送的 HTTP 請求呼叫的方法,exceptionCaught():處理客戶端發送的 HTTP 請求發生例外的時候被呼叫,channelReadComplete(): 服務端消費完客戶端發送的 HTTP 請求之后呼叫的方法,
另外,客戶端 HTTP 請求引數型別為 FullHttpRequest,我們可以把 FullHttpRequest物件看作是 HTTP 請求報文的 Java 物件的表現形式,
@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();
}
}
我們回傳給客戶端的訊息體是 FullHttpResponse 物件,通過 FullHttpResponse 物件,我們可以設定 HTTP 回應報文的 HTTP 協議版本、回應的具體內容 等內容,
我們可以把 FullHttpResponse 物件看作是 HTTP 回應報文的 Java 物件的表現形式,
FullHttpResponse response;
String responseHtml = "<html><body>" + result + "</body></html>";
byte[] responseBytes = responseHtml.getBytes(StandardCharsets.UTF_8);
// 初始化 FullHttpResponse ,并設定 HTTP 協議 、回應狀態碼、回應的具體內容
response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(responseBytes));
我們通過 FullHttpResponse的headers()方法獲取到 HttpHeaders,這里的 HttpHeaders 對應于 HTTP 回應報文的頭部,通過 HttpHeaders物件,我們就可以對 HTTP 回應報文的頭部的內容比如 Content-Typ 進行設定,
response.headers().set(CONTENT_TYPE, "text/html; charset=utf-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
本案例中,為了掩飾我們設定的 Content-Type 為 text/html ,也就是回傳 html 格式的資料給客戶端,
常見的 Content-Type
| Content-Type | 解釋 |
|---|---|
| text/html | html 格式 |
| text/plain | 純文本格式 |
| text/css | css 格式 |
| text/javascript | js 格式 |
| application/json | json 格式(前后端分離專案常用) |
| image/gif | gif 圖片格式 |
| image/jpeg | jpg 圖片格式 |
| image/png | png 圖片格式 |
請求的具體處理邏輯實作
因為有這里有 POST 請求和 GET 請求,因此我們需要首先定義一個處理 HTTP Request 的介面,
public interface RequestHandler {
Object handle(FullHttpRequest fullHttpRequest);
}
HTTP Method 不只是有 GET 和 POST,其他常見的還有 PUT、DELETE、PATCH,只是本案例中實作的 HTTP Server 只考慮了 GET 和 POST,
- GET :請求從服務器獲取特定資源,舉個例子:
GET /classes(獲取所有班級) - POST :在服務器上創建一個新的資源,舉個例子:
POST /classes(創建班級) - PUT :更新服務器上的資源(客戶端提供更新后的整個資源),舉個例子:
PUT /classes/12(更新編號為 12 的班級) - DELETE :從服務器洗掉特定的資源,舉個例子:
DELETE /classes/12(洗掉編號為 12 的班級) - PATCH :更新服務器上的資源(客戶端提供更改的屬性,可以看做作是部分更新),使用的比較少,這里就不舉例子了,
GET 請求的處理
@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;
}
}
我這里只是簡單得把 URI 的查詢引數的對應關系直接回傳給客戶端了,
實際上,獲得了 URI 的查詢引數的對應關系,再結合反射和注解相關的知識,我們很容易實作類似于 Spring Boot 的 @RequestParam 注解了,
建議想要學習的小伙伴,可以自己獨立實作一下,不知道如何實作的話,你可以參考我開源的輕量級 HTTP 框架jsoncat (仿 Spring Boot 但不同于 Spring Boot 的一個輕量級的 HTTP 框架),
POST 請求的處理
@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];
}
}
對于 POST 請求的處理,我們這里只接受處理 Content-Type 為 application/json 的資料,如果 POST 請求傳過來的不是 application/json 型別的資料,我們就直接拋出例外,
實際上,我們獲得了客戶端傳來的 json 格式的資料之后,再結合反射和注解相關的知識,我們很容易實作類似于 Spring Boot 的 @RequestBody 注解了,
建議想要學習的小伙伴,可以自己獨立實作一下,不知道如何實作的話,你可以參考我開源的輕量級 HTTP 框架jsoncat (仿 Spring Boot 但不同于 Spring Boot 的一個輕量級的 HTTP 框架),
請求處理工廠類
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);
}
}
我這里用到了工廠模式,當我們額外處理新的 HTTP Method 方法的時候,直接實作 RequestHandler 介面,然后將實作類添加到 RequestHandlerFactory 即可,
啟動類
public class HttpServerApplication {
public static void main(String[] args) {
HttpServer httpServer = new HttpServer();
httpServer.start();
}
}
效果
運行 HttpServerApplication 的main()方法,控制臺列印出:
[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 請求

POST 請求

參考
- Netty 學習筆記-http objects
我的開源專案推薦
- JavaGuide :「Java學習+面試指南」一份涵蓋大部分Java程式員所需要掌握的核心知識,準備 Java 面試,首選 JavaGuide!
- guide-rpc-framework :A custom RPC framework implemented by Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 實作的自定義 RPC 框架-附詳細實作程序和相關教程)
- jsoncat :仿 Spring Boot 但不同于 Spring Boot 的一個輕量級的 HTTP 框架
- programmer-advancement :程式員應該有的一些好習慣+面試必知事項!
- springboot-guide :Not only Spring Boot but also important knowledge of Spring(不只是SpringBoot還有Spring重要知識點)
- awesome-java :Collection of awesome Java project on Github(Github 上非常棒的 Java 開源專案集合).
我是 Guide 哥,一 Java 后端開發,會一點前端,自由的少年,我們下期再見!微信搜“JavaGuide”回復“面試突擊”領取我整理的 4 本原創PDF
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163393.html
標籤:Java
