Java網絡編程-netty
Java之Netty網絡編程
為什么要學Netty?
- Netty基于NIO(NIO是一種同步非阻塞的I/O模型,在Java1.4中引入了NIO)。使用Netty可以極大地簡化TCP和UP套接字服務器等網絡編程,并且性能以及安全等很多方面非常優秀;
- 平常經常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC、Spark、Elasticsearch 等等熱門開源項目都用到了 Netty。
- 大部分微服務框架底層涉及到網絡通信的部分都是基于 Netty 來做的,比如說 Spring Cloud 生態系統中的網關 Spring Cloud Gateway 。
一、Netty的起源
1.傳統的Socket實現
早期的 Java 網絡相關的 API(java.net
包) 使用 Socket(套接字)進行網絡通信,不過只支持阻塞函數使用。
要通過互聯網進行通信,至少需要一對套接字:
- 運行于服務器端的 Server Socket。
- 運行于客戶機端的 Client Socket
Socket 網絡通信過程如下圖所示:
Socket 網絡通信過程簡單來說分為下面 4 步:
- 建立服務端并且監聽客戶端請求
- 客戶端請求,服務端和客戶端建立連接
- 兩端之間可以傳遞數據
- 關閉資源
對應到服務端和客戶端的話,是下面這樣的。
服務器端:
- 創建
ServerSocket
對象并且綁定地址(ip)和端口號(port):server.bind(new InetSocketAddress(host, port))
- 通過
accept()
方法監聽客戶端請求 - 連接建立后,通過輸入流讀取客戶端發送的請求信息
- 通過輸出流向客戶端發送響應信息
- 關閉相關資源
客戶端:
- 創建
Socket
對象并且連接指定的服務器的地址(ip)和端口號(port):socket.connect(inetSocketAddress)
- 連接建立后,通過輸出流向服務器端發送請求信息
- 通過輸入流獲取服務器響應的信息
- 關閉相關資源
1.1一對一的demo
服務端:
package com.lcz.io;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服務器端
*/
public class HelloServer {
// 日志文件
private static final Log log = LogFactory.getLog(HelloServer.class);
// 方法
public void start(int port){
// 1.創建serversocket對象并且綁定端口
try (ServerSocket server = new ServerSocket(port);){
Socket socket;
//2.通過accept()方法監聽客戶端請求,
while((socket=server.accept())!=null){
log.info("client connected!");
try(ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())){
// 3.通過輸入流讀取客戶端發送的請求信息
Message message = (Message)objectInputStream.readObject();
log.info("server receive message:" + message.getContent());
message.setContent("new content");
// 4.通過輸入流向客戶端發送相應消息
objectOutputStream.writeObject(message);
objectOutputStream.flush();
}catch (IOException | ClassNotFoundException e){
log.error("occur exception:", e);
}
}
}catch (IOException e){
log.error("occur IOException:",e);
}
}
// 主函數
public static void main(String[] args){
HelloServer helloServer = new HelloServer();
helloServer.start(6666);
}
}
ServerSocket
的 accept()
方法是阻塞方法,也就是說 ServerSocket
在調用 accept()
等待客戶端的連接請求時會阻塞,直到收到客戶端發送的連接請求才會繼續往下執行代碼,因此我們需要要為每個 Socket 連接開啟一個線程(可以通過線程池來做)。
上述服務端的代碼只是為了演示,并沒有考慮多個客戶端連接并發的情況。
客戶端:
package com.lcz.io;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
public class HelloClient {
private static final Log log = LogFactory.getLog(HelloClient.class);
// 發送
public Object send(Message message,String host,int port){
//1.創建socket對象并且制定服務器的地址和初始化
try(Socket socket = new Socket(host,port)){
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//2.通過輸出流向服務器發送請求消息
objectOutputStream.writeObject(message);
// 3.通過輸入流獲取服務器相應的信息
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
return objectInputStream.readObject();
}catch (IOException | ClassNotFoundException e) {
log.error("occur exception:", e);
}
return null;
}
// 主函數
public static void main(String[] args){
HelloClient helloClient = new HelloClient();
Message message = (Message) helloClient.send(new Message("content from client"),"127.0.0.1",6666);
System.out.println("client receive message:" + message.getContent());
}
}
發送的消息實體類:
package com.lcz.io;
import java.io.Serializable;
/**
* 發送消息的實體類
*/
public class Message implements Serializable {
private String content;
public Message(){
}
public Message(String content){
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
其中用到對象序列化,Serializable接口是棄用序列化功能的接口。
序列化的過程就是一個freeze的過程,將一個對象freeze凍住,然后進行存儲;等到需要的時候,再將這個對象de-freeze解凍就可使用;打開一個java的Serializable這個接口的源碼,會發現這個一個空接口,其告訴JVM此類可被序列化,可被默認的序列化機制序列化。
序列化可以很快捷的創建一個副本,便于數據傳輸。
首先運行服務端,然后再運行客戶端,控制臺輸出如下:
服務端:
十一月 30, 2020 1:06:02 下午 com.lcz.io.HelloServer start
信息: client connected!
十一月 30, 2020 1:06:02 下午 com.lcz.io.HelloServer start
信息: server receive message:content from client
客戶端:
client receive message:new content
資源消耗嚴重的問題
很明顯,我上面演示的代碼片段有一個很嚴重的問題:只能同時處理一個客戶端的連接,如果需要管理多個客戶端的話,就需要為我們請求的客戶端單獨創建一個線程。 如下圖所示:
1.2 多對一(多線程)的demo
服務器端的Java代碼
package com.lcz.io;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服務器端
*/
public class HelloServer_2 {
// 日志文件
private static final Log log = LogFactory.getLog(HelloServer_2.class);
// 方法
public void start(int port){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 1.創建serversocket對象并且綁定端口
try (ServerSocket server = new ServerSocket(port);){
Socket socket;
//2.通過accept()方法監聽客戶端請求,
while((socket=server.accept())!=null){
log.info("client connected!");
try(ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())){
// 3.通過輸入流讀取客戶端發送的請求信息
Message message = (Message)objectInputStream.readObject();
log.info("server receive message:" + message.getContent());
message.setContent("new content");
// 4.通過輸入流向客戶端發送相應消息
objectOutputStream.writeObject(message);
objectOutputStream.flush();
}catch (IOException | ClassNotFoundException e){
log.error("occur exception:", e);
}
}
}catch (IOException e){
log.error("occur IOException:",e);
}
}
});
thread.start();
}
// 主函數
public static void main(String[] args){
HelloServer_2 helloServer = new HelloServer_2();
helloServer.start(6666);
}
}
但是,這樣會導致一個很嚴重的問題:資源浪費。
我們知道線程是很寶貴的資源,如果我們為每一次連接都用一個線程處理的話,就會導致線程越來越好,最好達到了極限之后,就無法再創建線程處理請求了。處理的不好的話,甚至可能直接就宕機掉了。
很多人就會問了:那有沒有改進的方法呢?
線程池雖可以改善,但終究未從根本解決問題
當然有! 比較簡單并且實際的改進方法就是使用線程池。線程池還可以讓線程的創建和回收成本相對較低,并且我們可以指定線程池的可創建線程的最大數量,這樣就不會導致線程創建過多,機器資源被不合理消耗。
package com.lcz.io;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
/**
* 服務器端
*/
public class HelloServer_3 {
// 日志文件
private static final Log log = LogFactory.getLog(HelloServer_3.class);
// 方法
public void start(int port){
ThreadFactory threadFactory = Executors.defaultThreadFactory();
ExecutorService threadPool = new ThreadPoolExecutor(10,100,1,TimeUnit.MINUTES,new ArrayBlockingQueue<>(100),threadFactory);
threadPool.execute(()->{
// 1.創建serversocket對象并且綁定端口
try (ServerSocket server = new ServerSocket(port);){
Socket socket;
//2.通過accept()方法監聽客戶端請求,
while((socket=server.accept())!=null){
log.info("client connected!");
try(ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())){
// 3.通過輸入流讀取客戶端發送的請求信息
Message message = (Message)objectInputStream.readObject();
log.info("server receive message:" + message.getContent());
message.setContent("new content");
// 4.通過輸入流向客戶端發送相應消息
objectOutputStream.writeObject(message);
objectOutputStream.flush();
}catch (IOException | ClassNotFoundException e){
log.error("occur exception:", e);
}
}
}catch (IOException e){
log.error("occur IOException:",e);
}
});
}
// 主函數
public static void main(String[] args){
HelloServer_3 helloServer = new HelloServer_3();
helloServer.start(6666);
}
}
但是,即使你再怎么優化和改變。也改變不了它的底層仍然是同步阻塞的 BIO 模型的事實,因此無法從根本上解決問題。
為了解決上述的問題,Java 1.4 中引入了 NIO ,一種同步非阻塞的 I/O 模型。
2.NIO
Netty 實際上就基于 Java NIO 技術封裝完善之后得到一個高性能框架,熟悉 NIO 的基本概念對于學習和更好地理解 Netty 還是很有必要的!
例子:假如有10000個連接,4核CPU ,那么bio 就需要一萬個線程,而nio大概就需要5個線程(一個接收請求,四個處理請求)。如果這10000個連接同時請求,那么bio就有10000個線程搶四個CPU ,幾乎每個CPU 平均執行2500次上下文切換,而nio 四個處理線程,幾乎每個線程都對應一個CPU ,也就是幾乎沒有上下文切換。效率就體現出來了。
2.1初識 NIO
NIO 是一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,對應 java.nio
包,提供了 Channel , Selector,Buffer 等抽象。
NIO 中的 N 可以理解為 Non-blocking,已經不在是 New 了(已經出來很長時間了)。
NIO 支持面向緩沖(Buffer)的,基于通道(Channel)的 I/O 操作方法。
NIO 提供了與傳統 BIO 模型中的 Socket
和 ServerSocket
相對應的 SocketChannel
和 ServerSocketChannel
兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式:
- 阻塞模式 : 基本不會被使用到。使用起來就像傳統的網絡編程一樣,比較簡單,但是性能和可靠性都不好。對于低負載、低并發的應用程序,勉強可以用一下以提升開發速率和更好的維護性
- 非阻塞模式 : 與阻塞模式正好相反,非阻塞模式對于高負載、高并發的(網絡)應用來說非常友好,但是編程麻煩,這個是大部分人詬病的地方。所以, 也就導致了 Netty 的誕生。
2.2 NIO 核心組件解讀
NIO 包含下面幾個核心的組件:
- Channel
- Buffer
- Selector
- Selection Key
這些組件之間的關系是怎么的呢?
- NIO 使用 Channel(通道)和 Buffer(緩沖區)傳輸數據,數據總是從緩沖區寫入通道,并從通道讀取到緩沖區。在面向流的 I/O 中,可以將數據直接寫入或者將數據直接讀到 Stream 對象中。在 NIO 庫中,所有數據都是通過 Buffer(緩沖區)處理的。 Channel 可以看作是 Netty 的網絡操作抽象類,對應于 JDK 底層的 Socket
- NIO 利用 Selector (選擇器)來監視多個通道的對象,如數據到達,連接打開等。因此,單線程可以監視多個通道中的數據。
- 當我們將 Channel 注冊到 Selector 中的時候, 會返回一個 Selection Key 對象, Selection Key 則表示了一個特定的通道對象和一個特定的選擇器對象之間的注冊關系。通過 Selection Key 我們可以獲取哪些 IO 事件已經就緒了,并且可以通過其獲取 Channel 并對其進行操作。
Selector(選擇器,也可以理解為多路復用器)是 NIO(非阻塞 IO)實現的關鍵。它使用了事件通知相關的 API 來實現選擇已經就緒也就是能夠進行 I/O 相關的操作的任務的能力。
簡單來說,整個過程是這樣的:
- 將 Channel 注冊到 Selector 中。
- 調用 Selector 的
select()
方法,這個方法會阻塞; - 到注冊在 Selector 中的某個 Channel 有新的 TCP 連接或者可讀寫事件的話,這個 Channel 就會處于就緒狀態,會被 Selector 輪詢出來。
- 然后通過 SelectionKey 可以獲取就緒 Channel 的集合,進行后續的 I/O 操作。
2.3 NIO 為啥更好?
相比于傳統的 BIO 模型來說, NIO 模型的最大改進是:
- 使用比較少的線程便可以管理多個客戶端的連接,提高了并發量并且減少的資源消耗(減少了線程的上下文切換的開銷)
- 在沒有 I/O 操作相關的事情的時候,線程可以被安排在其他任務上面,以讓線程資源得到充分利用。
2.4 使用 NIO 編寫代碼太難了
一個使用 NIO 編寫的 Server 端如下,可以看出還是整體還是比較復雜的,并且代碼讀起來不是很直觀,并且還可能由于 NIO 本身會存在 Bug。
很少使用 NIO,很大情況下也是因為使用 NIO 來創建正確并且安全的應用程序的開發成本和維護成本都比較大。所以,一般情況下我們都會使用 Netty 這個比較成熟的高性能框架來做(Apace Mina 與之類似,但是 Netty 使用的更多一點)。
3.Netty出現
-
Netty 是一個基于 NIO 的 client-server(客戶端服務器)框架,使用它可以快速簡單地開發網絡應用程序。
-
它極大地簡化并簡化了 TCP 和 UDP 套接字服務器等網絡編程,并且性能以及安全性等很多方面甚至都要更好。
-
支持多種協議如 FTP,SMTP,HTTP 以及各種二進制和基于文本的傳統協議。
用官方的總結就是:Netty 成功地找到了一種在不妥協可維護性和性能的情況下實現易于開發,性能,穩定性和靈活性的方法。
3.1 Netty架構總覽
下面是Netty的模塊設計部分:
Netty提供了通用的傳輸API(TCP/UDP…);多種網絡協議(HTTP/WebSocket…);基于事件驅動的IO模型; 超高性能的零拷貝…
3.2 使用 Netty 能做什么?
這個應該是老鐵們最關心的一個問題了,憑借自己的了解,簡單說一下,理論上 NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用來做網絡通信 :
- 作為 RPC 框架的網絡通信工具 : 我們在分布式系統中,不同服務節點之間經常需要相互調用,這個時候就需要 RPC 框架了。不同服務指點的通信是如何做的呢?可以使用 Netty 來做。比如我調用另外一個節點的方法的話,至少是要讓對方知道我調用的是哪個類中的哪個方法以及相關參數吧!
- 實現一個自己的 HTTP 服務器 :通過 Netty 我們可以自己實現一個簡單的 HTTP 服務器,這個大家應該不陌生。說到 HTTP 服務器的話,作為 Java 后端開發,我們一般使用 Tomcat 比較多。一個最基本的 HTTP 服務器可要以處理常見的 HTTP Method 的請求,比如 POST 請求、GET 請求等等。
- 實現一個即時通訊系統 : 使用 Netty 我們可以實現一個可以聊天類似微信的即時通訊系統,這方面的開源項目還蠻多的,可以自行去 Github 找一找。
- 消息推送系統 :市面上有很多消息推送系統都是基于 Netty 來做的。
3.3 哪些開源項目用到了 Netty?
我們平常經常接觸的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。
可以說大量的開源項目都用到了 Netty,所以掌握 Netty 有助于你更好的使用這些開源項目并且讓你有能力對其進行二次開發。
實際上還有很多很多優秀的項目用到了 Netty,Netty 官方也做了統計,統計結果在這里:https://netty.io/wiki/related-projects.html 。
二、Netty的Hello World程序
IDEA中建立一個maven項目,其依賴為:
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
</dependencies>
1.服務端
我們可以通過 ServerBootstrap
來引導我們啟動一個簡單的 Netty 服務端,為此,你必須要為其指定下面三類屬性:
- 線程組(一般需要兩個線程組,一個負責處理客戶端的連接,一個負責具體的 IO 處理)
- IO 模型(BIO/NIO)
- 自定義
ChannelHandler
(處理客戶端發過來的數據并返回數據給客戶端)
1.1創建服務端
/**
* @author shuang.kou
* @createTime 2020年05月14日 20:28:00
*/
public final class HelloServer {
private final int port;
public HelloServer(int port) {
this.port = port;
}
private void start() throws InterruptedException {
// 1.bossGroup 用于接收連接,workerGroup 用于具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啟動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup)
// (非必備)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定義客戶端消息的業務處理邏輯
p.addLast(new HelloServerHandler());
}
});
// 6.綁定端口,調用 sync 方法阻塞知道綁定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服務器Channel關閉(closeFuture()方法獲取Channel 的CloseFuture對象,然后調用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.優雅關閉相關線程組資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
new HelloServer(8080).start();
}
}
簡單解析一下服務端的創建過程具體是怎樣的:
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
配置兩大線程組,確定了線程模型。
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
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();
1.2自定義服務端 ChannelHandler 處理消息
HelloServerHandler.java
/**
* @author shuang.kou
* @createTime 2020年05月14日 20:39:00
*/
@Sharable
public class HelloServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
ByteBuf in = (ByteBuf) msg;
System.out.println("message from client:" + in.toString(CharsetUtil.UTF_8));
// 發送消息給客戶端
ctx.writeAndFlush(Unpooled.copiedBuffer("你也好!", CharsetUtil.UTF_8));
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}
這個邏輯處理器繼承自ChannelInboundHandlerAdapter
并重寫了下面 2 個方法:
channelRead()
:服務端接收客戶端發送數據調用的方法exceptionCaught()
:處理客戶端消息發生異常的時候被調用
2.客戶端
2.1創建客戶端
public final class HelloClient {
private final String host;
private final int port;
private final String message;
public HelloClient(String host, int port, String message) {
this.host = host;
this.port = port;
this.message = message;
}
private void start() throws InterruptedException {
//1.創建一個 NioEventLoopGroup 對象實例
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.創建客戶端啟動引導/輔助類:Bootstrap
Bootstrap b = new Bootstrap();
//3.指定線程組
b.group(group)
//4.指定 IO 模型
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 5.這里可以自定義消息的業務處理邏輯
p.addLast(new HelloClientHandler(message));
}
});
// 6.嘗試建立連接
ChannelFuture f = b.connect(host, port).sync();
// 7.等待連接關閉(阻塞,直到Channel關閉)
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new HelloClient("127.0.0.1",8080, "你好,你真帥啊!哥哥!").start();
}
}
繼續分析一下客戶端的創建流程:
1.創建一個 NioEventLoopGroup
對象實例 (服務端創建了兩個 NioEventLoopGroup
對象)
2.創建客戶端啟動的引導類是 Bootstrap
3.通過 .group()
方法給引導類 Bootstrap
配置一個線程組
4.通過channel()
方法給引導類 Bootstrap
指定了 IO 模型為NIO
5.通過 .childHandler()
給引導類創建一個ChannelInitializer
,然后指定了客戶端消息的業務處理邏輯也就是自定義的ChannelHandler
對象
6.調用 Bootstrap
類的 connect()
方法連接服務端,這個方法需要指定兩個參數:
inetHost
: ip 地址inetPort
: 端口號
public ChannelFuture connect(String inetHost, int inetPort) {
return this.connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}
public ChannelFuture connect(SocketAddress remoteAddress) {
ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
this.validate();
return this.doResolveAndConnect(remoteAddress, this.config.localAddress());
}
connect
方法返回的是一個 Future
類型的對象
public interface ChannelFuture extends Future<Void> {
......
}
也就是說這個方是異步的,我們通過 addListener
方法可以監聽到連接是否成功,進而打印出連接信息。具體做法很簡單,只需要對代碼進行以下改動:
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("連接成功!");
} else {
System.err.println("連接失敗!");
}
}).sync();
2.2自定義客戶端 ChannelHandler 處理消息
HelloClientHandler.java
/**
* @author shuang.kou
* @createTime 2020年05月14日 20:46:00
*/
@Sharable
public class HelloClientHandler extends ChannelInboundHandlerAdapter {
private final String message;
public HelloClientHandler(String message) {
this.message = message;
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("client sen msg to server " + message);
ctx.writeAndFlush(Unpooled.copiedBuffer(message, CharsetUtil.UTF_8));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf in = (ByteBuf) msg;
try {
System.out.println("client receive msg from server: " + in.toString(CharsetUtil.UTF_8));
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
這個邏輯處理器繼承自 ChannelInboundHandlerAdapter
,并且覆蓋了下面三個方法:
channelActive()
:客戶端和服務端的連接建立之后就會被調用channelRead
:客戶端接收服務端發送數據調用的方法exceptionCaught
:處理消息發生異常的時候被調用
3.運行程序
首先運行服務端 ,然后再運行客戶端。
如果你看到,服務端控制臺打印出:
message from client:你好,你真帥啊!哥哥!
客戶端控制臺打印出:
client sen msg to server 你好,你真帥啊!哥哥!
client receive msg from server: 你也好!
說明你的 Netty 版的 Hello World 已經完成了!
三、Netty的核心組件
-
Bytebuf(字節容器)
-
Boostrap和ServerBootstrap(啟動引導類)
-
Channel(網絡操作抽象類)
-
EventLoop(事件循環)
- Eventloop介紹
- Channel和EventLoop的關系
- EventloopGroup和EventLoop的關系
-
ChannelHandler(消息處理器)和ChannelPipeline(對象鏈表)
-
ChannelFuture(操作執行結果)
1.Bytebuf(字節容器)
網絡通信最終都是通過字節流進行傳輸的。 ByteBuf
就是 Netty 提供的一個字節容器,其內部是一個字節數組。 當我們通過 Netty 傳輸數據的時候,就是通過 ByteBuf
進行的。
我們可以將 ByteBuf
看作是 Netty 對 Java NIO 提供了 ByteBuffer
字節容器的封裝和抽象。
有很多小伙伴可能就要問了 : 為什么不直接使用 Java NIO 提供的 ByteBuffer
呢?
因為 ByteBuffer
這個類使用起來過于復雜和繁瑣。
2.Bootstrap 和 ServerBootstrap(啟動引導類)
Bootstrap
是客戶端的啟動引導類/輔助類,具體使用方法如下:
EventLoopGroup group = new NioEventLoopGroup();
try {
//創建客戶端啟動引導/輔助類:Bootstrap
Bootstrap b = new Bootstrap();
//指定線程模型
b.group(group).
......
// 嘗試建立連接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 優雅關閉相關線程組資源
group.shutdownGracefully();
}
ServerBootstrap
客戶端的啟動引導類/輔助類,具體使用方法如下:
// 1.bossGroup 用于接收連接,workerGroup 用于具體的處理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.創建服務端啟動引導/輔助類:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.給引導類配置兩大線程組,確定了線程模型
b.group(bossGroup, workerGroup).
......
// 6.綁定端口
ChannelFuture f = b.bind(port).sync();
// 等待連接關閉
f.channel().closeFuture().sync();
} finally {
//7.優雅關閉相關線程組資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
從上面的示例中,我們可以看出:
Bootstrap
通常使用connet()
方法連接到遠程的主機和端口,作為一個 Netty TCP 協議通信中的客戶端。另外,Bootstrap
也可以通過bind()
方法綁定本地的一個端口,作為 UDP 協議通信中的一端。ServerBootstrap
通常使用bind()
方法綁定本地的端口上,然后等待客戶端的連接。Bootstrap
只需要配置一個線程組—EventLoopGroup
,而ServerBootstrap
需要配置兩個線程組—EventLoopGroup
,一個用于接收連接,一個用于具體的 IO 處理。
3.Channel(網絡操作抽象類)
Channel
接口是 Netty 對網絡操作抽象類。通過 Channel
我們可以進行 I/O 操作。
一旦客戶端成功連接服務端,就會新建一個 Channel
同該用戶端進行綁定,示例代碼如下:
// 通過 Bootstrap 的 connect 方法連接到服務端
public Channel doConnect(InetSocketAddress inetSocketAddress) {
CompletableFuture<Channel> completableFuture = new CompletableFuture<>();
bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
completableFuture.complete(future.channel());
} else {
throw new IllegalStateException();
}
});
return completableFuture.get();
}
比較常用的Channel
接口實現類是 :
NioServerSocketChannel
(服務端)NioSocketChannel
(客戶端)
這兩個 Channel
可以和 BIO 編程模型中的ServerSocket
以及Socket
兩個概念對應上。
4.EventLoop(事件循環)
4.1 EventLoop 介紹
這么說吧!EventLoop
(事件循環)接口可以說是 Netty 中最核心的概念了!
《Netty 實戰》這本書是這樣介紹它的:
EventLoop
定義了 Netty 的核心抽象,用于處理連接的生命周期中所發生的事件。
是不是很難理解?說實話,我學習 Netty 的時候看到這句話是沒太能理解的。
說白了,EventLoop
的主要作用實際就是責監聽網絡事件并調用事件處理器進行相關 I/O 操作(讀寫)的處理。
4.2Channel 和 EventLoop 的關系
那 Channel
和 EventLoop
直接有啥聯系呢?
Channel
為 Netty 網絡操作(讀寫等操作)抽象類,EventLoop
負責處理注冊到其上的Channel
的 I/O 操作,兩者配合進行 I/O 操作。
4.3EventloopGroup 和 EventLoop 的關系
EventLoopGroup
包含多個 EventLoop
(每一個 EventLoop
通常內部包含一個線程),它管理著所有的 EventLoop
的生命周期。
并且,EventLoop
處理的 I/O 事件都將在它專有的 Thread
上被處理,即 Thread
和 EventLoop
屬于 1 : 1 的關系,從而保證線程安全。
下圖是 Netty NIO 模型對應的 EventLoop
模型。通過這個圖應該可以將EventloopGroup
、EventLoop
、 Channel
三者聯系起來。
5.ChannelHandler(消息處理器) 和 ChannelPipeline(ChannelHandler 對象鏈表)
下面這段代碼使用過 Netty 的小伙伴應該不會陌生,我們指定了序列化編解碼器以及自定義的 ChannelHandler
處理消息。
b.group(eventLoopGroup)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
ch.pipeline().addLast(new KryoClientHandler());
}
});
ChannelHandler
是消息的具體處理器,主要負責處理客戶端/服務端接收和發送的數據。
當 Channel
被創建時,它會被自動地分配到它專屬的 ChannelPipeline
。 一個Channel
包含一個 ChannelPipeline
。 ChannelPipeline
為 ChannelHandler
的鏈,一個 pipeline 上可以有多個 ChannelHandler
。
我們可以在 ChannelPipeline
上通過 addLast()
方法添加一個或者多個ChannelHandler
(一個數據或者事件可能會被多個 Handler 處理) 。當一個 ChannelHandler
處理完之后就將數據交給下一個 ChannelHandler
。
當 ChannelHandler
被添加到的 ChannelPipeline
它得到一個 ChannelHandlerContext
,它代表一個 ChannelHandler
和 ChannelPipeline
之間的“綁定”。 ChannelPipeline
通過 ChannelHandlerContext
來間接管理 ChannelHandler
。
6.ChannelFuture(操作執行結果)
public interface ChannelFuture extends Future<Void> {
Channel channel();
ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
......
ChannelFuture sync() throws InterruptedException;
}
Netty 是異步非阻塞的,所有的 I/O 操作都為異步的。
因此,我們不能立刻得到操作是否執行成功,但是,你可以通過 ChannelFuture
接口的 addListener()
方法注冊一個 ChannelFutureListener
,當操作執行成功或者失敗時,監聽就會自動觸發返回結果。
ChannelFuture f = b.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("連接成功!");
} else {
System.err.println("連接失敗!");
}
}).sync();
并且,你還可以通過ChannelFuture
的 channel()
方法獲取連接相關聯的Channel
。
Channel channel = f.channel();
另外,我們還可以通過 ChannelFuture
接口的 sync()
方法讓異步的操作編程同步的。
//bind()是異步的,但是,你可以通過 `sync()`方法將其變為同步。
ChannelFuture f = b.bind(port).sync();
四、Netty實現一個HTTP Server
1.實現 HTTP Server 必知的前置知識
既然,我們要實現 HTTP Server 那必然先要回顧一下 HTTP 協議相關的基礎知識。
1.1 HTTP 協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)主要是為 Web 瀏覽器與 Web 服務器之間的通信而設計的。
當我們使用瀏覽器瀏覽網頁的時候,我們網頁就是通過 HTTP 請求進行加載的,整個過程如下圖所示。
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 響應報文給客戶端。
1.2 Netty 編解碼器
如果我們要通過 Netty 處理 HTTP 請求,需要先進行編解碼。所謂編解碼說白了就是在 Netty 傳輸數據所用的 ByteBuf
和 Netty 中針對 HTTP 請求和響應所提供的對象比如 HttpRequest
和 HttpContent
之間互相轉換。
Netty 自帶了 4 個常用的編解碼器:
HttpRequestEncoder
(HTTP 請求編碼器)HttpRequestDecoder
(HTTP 請求解碼器)HttpResponsetEncoder
(HTTP 響應編碼器)HttpResponseDecoder
(HTTP 響應解碼器)
網絡通信最終都是通過字節流進行傳輸的。 ByteBuf
是 Netty 提供的一個字節容器,其內部是一個字節數組。 當我們通過 Netty 傳輸數據的時候,就是通過 ByteBuf
進行的。
HTTP Server 端用于接收 HTTP Request,然后發送 HTTP Response。因此我們只需要 HttpRequestDecoder
和 HttpResponseEncoder
即可。
我手繪了一張圖,這樣看著應該更容易理解了。
1.3 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
聚合后得到的對象。
1.4 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());
2.基于 Netty 實現一個 HTTP Server
通過 Netty,我們可以很方便地使用少量代碼構建一個可以正確處理 GET 請求和 POST 請求的輕量級 HTTP Server。
2.1 添加所需依賴到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>
2.2 創建服務端
@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();
2.3 自定義服務端 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 channelRead(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 圖片格式 |
2.4 請求的具體處理邏輯實現
因為這里要分別處理 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 :更新服務器上的資源(客戶端提供更改的屬性,可以看做作是部分更新),使用的比較少,這里就不舉例子了。
1.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 框架)。
2.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 框架)。
3.請求處理工廠類
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
即可。
2.5 啟動類
public class HttpServerApplication {
public static void main(String[] args) {
HttpServer httpServer = new HttpServer();
httpServer.start();
}
}
2.6 效果
運行 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.
1.GET 請求
2.POST 請求
智能推薦
Java網絡編程之Netty入門案例-yellowcong
Netty入門案例,講解Netty的客戶端 和服務器端的實現,Netty是NIO框架,同類型的還有Mina,不過Netty的使用,比Mina更容易簡單。 Netty國內的入門案例:http://ifeve.com/netty5-user-guide/ CSDN的案例:http://blog.csdn.net/column/details/enjoynetty.html Netty的系統架構中,實現...
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
猜你喜歡
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...
統計學習方法 - 樸素貝葉斯
引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...
styled-components —— React 中的 CSS 最佳實踐
https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...