本文作者:禹明明,叩丁狼高級講師。原創(chuàng)文章,轉載請注明出處。
作為一個已經(jīng)有了幾年工作經(jīng)驗的JAVA程序員,網(wǎng)絡編程是一個必須接觸的一個領域.如果你還只是停留在會用MVC框架,優(yōu)化只會用用緩存,平常寫寫CRUD的水平,那么你對編程的理解還停留在一個非常表層的階段!
如果想要深入理解各種服務器,Netty就是我們要過的第一道坎
Netty是什么
我在NIO的文章中簡單學習了NIO的使用,現(xiàn)在回想一下,可以發(fā)現(xiàn)NIO的API設計過于復雜,代碼量比較大,使用中需要考慮的細節(jié)也很多,如果對底層了解不深的同學寫出來的代碼可能會有各種各樣的BUG導致服務不夠穩(wěn)定.
Netty就是為了解決這個問題而開發(fā)的一套簡單易用封裝良好的NIO框架,使用Netty可以快速輕松地開發(fā)協(xié)議服務器和客戶端等網(wǎng)絡應用程序。它極大地降低了網(wǎng)絡編程的開發(fā)難度,極大簡化了開發(fā)過程。
Netty可以用來干什么?
- 開發(fā)自定義的HTTP服務器
- 開發(fā)自定義的FTP服務器
- 開發(fā)自定義的UDP服務器
- 開發(fā)自定義的RPC服務器,例如Dubbo就是基于Netty
- 開發(fā)自定義的WebSocket服務器
- 開發(fā)自定義的Proxy服務器,例如MySQL的Proxy服務器等
總之就是可以開發(fā)定制符合自己需求的各種自定義協(xié)議和服務器
為什么選擇Netty?
NIO框架有Netty , Mina , xSocket , Grizzly等,為什么選擇Netty呢?
雖然NIO框架有很多,但是使用比較廣泛的就是Netty和Mina,從學習成本和后期維護難度上來考慮選擇流行的框架可以降低開發(fā)維護難度和風險.
Netty 和Mina的作者其實都是同一個人Trustin Lee (韓國人),但是Netty出生的更晚, 作者在寫出了Mina之后又搞出了Netty, 所以從這方面來說Netty應該更加完善.
從使用上來講目前很多著名的開源項目比如阿里的Dubbo,Apache Spark , FaceBook Nifty , Google gRPC 等都是基于Netty.
其實最根本的還是超高的性能和簡單的API
Netty架構
這是Netty4.1官方架構圖,我們先大概了解一下
Netty中幾個重要概念
Channel
Channel是Netty最核心的接口,一個Channel就是一個聯(lián)絡Socket的通道,通過Channel,你可以對Socket進行各種操作。
ChannelHandler
ChannelHandler:每一個ChannelHandler都用來處理一些邏輯,所有的handler形成一個鏈表結構,作用類似于springMVC中的攔截棧或者一個個的過濾器
ChannelHandlerContext
ChannelHandlerContext是ChannelPipeline的上下文,負責傳遞上下文數(shù)據(jù). ChannelHandlerContext.channel()方法可以得到和Context綁定的Channel,調用ChannelHandlerContext.handler()方法可以得到和Context綁定的Handler。
ChannelPipeline
ChannelPipeline:可以把ChannelPipeline看成是一個ChandlerHandler的鏈表,當需要對Channel進行某種處理的時候,Pipeline負責依次調用每一個Handler進行處理。每個Channel都有一個屬于自己的Pipeline,調用XXChannel.pipeline()方法可以獲得Channel的Pipeline,調用XXPipeline.channel()方法可以獲得Pipeline的Channel。
他們的關系圖如下:
簡單了解完這些概念之后我們就可以寫個helloworld來體驗一把Netty了
要求JDK1.6或以上
Netty5.0官方已經(jīng)不再支持了,不建議大家使用
目前最新穩(wěn)定版是4.1
添加依賴
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
編寫Server端
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
//NioEventLoopGroup可以看做是一個線程池,parentGroup用來接收所有請求,childGroup用來處理具體IO任務
NioEventLoopGroup parentGroup = new NioEventLoopGroup();//用來處理服務器端接受客戶連接
NioEventLoopGroup childGroup = new NioEventLoopGroup(); //用來進行網(wǎng)絡通信(網(wǎng)絡讀寫)
ServerBootstrap bootstrap = new ServerBootstrap(); //創(chuàng)建服務器通道配置的輔助工具類
bootstrap.group(parentGroup,childGroup) //配置每個NioEventLoopGroup的用途
.channel(NioServerSocketChannel.class) //指定Nio模式為Server模式
.option(ChannelOption.SO_BACKLOG,1024) //指定tcp緩沖區(qū)
.option(ChannelOption.SO_SNDBUF,10*1024) //指定發(fā)送緩沖區(qū)大小
.option(ChannelOption.SO_RCVBUF,10*1024) //指定接收緩沖區(qū)大小
.option(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)//是否保持連接,默認true
.childHandler(new ChannelInitializer<SocketChannel>() {//具體的數(shù)據(jù)接收方法
@Override
protected void initChannel(SocketChannel sc) throws Exception { //添加ChannelHandler,handler用來自定義消息處理邏輯
sc.pipeline().addLast(new ServerHandler());//其實可以添加多個Handler實例對象
}
});
ChannelFuture cfuture = bootstrap.bind(9999).sync();//異步綁定端口
cfuture.channel().closeFuture().sync();//阻塞程序,等待關閉
parentGroup.shutdownGracefully();//關閉應用
childGroup.shutdownGracefully();
}
}
服務端涉及到了一個自定義的Handler用來處理接受到的數(shù)據(jù)
/*
ChannelInboundHandlerAdapter 中有很多方法可以覆蓋,這些方法覆蓋了一個請求處理的整個生命周期,
一般來說我們只需要關心channelRead和exceptionCaught 方法
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {
//數(shù)據(jù)讀取邏輯
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//ByteBuf如果只用來讀數(shù)據(jù)而沒有writeAndFlush寫數(shù)據(jù)則使用完必須使用調用release()方法,釋放內(nèi)存
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String rev = new String(bytes,"utf-8");
System.out.println("server收到數(shù)據(jù):"+ rev);
//給客戶端響應一條數(shù)據(jù)
ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是Server".getBytes()))
//添加監(jiān)聽器,寫出數(shù)據(jù)后關閉通道,原理上只要拿到Futrue對象server端和client端都可以主動關閉,一般在server端關閉較好
.addListener(ChannelFutureListener.CLOSE);
buf.release();//釋放ByteBuf
}
//拋出異常時處理邏輯
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
System.out.println("exceptionCaught");
}
}
編寫Client端
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//NioEventLoopGroup可以看做是一個線程池,客戶端只需要用來處理發(fā)送數(shù)據(jù)任務的NioEventLoopGroup即可
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)//指定Nio模式為Client模式
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new ClientHandler());//添加自定義的客戶端消息處理Handler
}
});
ChannelFuture cf = b.connect("127.0.0.1",9999); //連接指定host:ip
cf.channel().write(Unpooled.copiedBuffer("Hello I am Client ".getBytes()));//write是寫入緩沖區(qū),
cf.channel().flush(); //flush緩沖數(shù)據(jù),必須flush!! 或者使用writeAndFlush方法發(fā)送數(shù)據(jù)
cf.channel().closeFuture().sync();//異步監(jiān)聽,傳輸完畢才執(zhí)行此代碼,然后向下執(zhí)行關閉操作
group.shutdownGracefully(); //關閉應用,斷開和server連接
}
}
Client端負責數(shù)據(jù)處理的Handler
public class ClientHandler extends ChannelInboundHandlerAdapter {
//數(shù)據(jù)讀取邏輯
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try{
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
String rev = new String(bytes,"utf-8");
System.out.println("Client 收到數(shù)據(jù):"+ rev);
}catch (Exception e){
e.printStackTrace();
}finally {
ReferenceCountUtil.release(msg);//釋放ByteBuf
}
}
//拋出錯誤時處理邏輯
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
System.out.println("出錯了...");
}
}
啟動Server—>啟動Client—>查看日志
# server端日志:這里給大家展示覆蓋了ChannelInboundHandlerAdapter 中所有方法打印的日志,方便大家理解整個處理流程
handlerAdded
channelRegistered
channelActive
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 32768
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
18:13:27.683 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.bytebuf.checkAccessible: true
18:13:27.685 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@125622c7
server收到數(shù)據(jù):Hello I am Client
channelReadComplete
channelInactive
channelUnregistered
handlerRemoved
# Client端日志:
Client 收到數(shù)據(jù):你好,我是Server
18:13:29.903 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.PoolThreadCache - Freed 2 thread-local buffer(s) from thread: nioEventLoopGroup-2-1
單從代碼量來看用過NIO的同學應該就可以感受到使用Netty比直接使用NIO要簡單了太多了吧,但是Netty更大的好處其實是進行了完好的封裝,我們可以少關注很多繁瑣的細節(jié)的處理
參考資料:
Netty4.0文檔(官方推薦使用4.x版本):http://netty.io/4.0/api/index.html
w3cSchool的Netty手冊:https://www.w3cschool.cn/netty4userguide/
http://ifeve.com/netty-home/
http://www.cnblogs.com/shanyou/p/4085802.html
想獲取更多技術視頻,請前往叩丁狼官網(wǎng):http://www.wolfcode.cn/openClassWeb_listDetail.html