爲什麼學習 Netty
在前面已經學習了 SOCKET 和 NIO ,從上幾章也知道,傳統的 NIO 編程,就是一個線程,對應一個selector,客戶端的接入、數據讀寫都在一個線程,這樣導致的後果就是沒利用好CPU,且當接收客戶端阻塞時,數據讀寫是進行不了的。
另外,NIO 的空轉100%cpu佔用率的問題,我們也沒有解決;
筆者曾經對 NIO 進行了擴展 ,比如單獨一個 線程池對應 selector 的 accept 客戶端,另外的兩個線程池,對應 selector 的READ 和 WRITE 操作;雖然,線程數進行了控制,且對 byteBuffer 也進行了擴展和填充,避免了數據黏包的問題,但是在 文件傳輸和要進行其他擴展時,總覺得難以進行,故而學習一下 Netty 是很有必要的。
至於 Netty 是什麼,相信你已經對它進行過了解了,總之就是叼得一逼,例子和輪胎都不錯,可以先看4.x的文檔:
Netty 文檔 基本跟着敲一遍都有一個很好的瞭解。
代碼工程:https://github.com/LillteZheng/SocketDemo
該教程,後面回去研究一下 Netty 的源碼,再根據裏面的思想,對以前的項目進行一個擴展。
一個聊天室
先看效果:
跟以前的做法一樣,就是服務端充當中轉站,把客戶端的信息接收並傳給其他客戶端;
接着,來看看Netty 的服務端的配置和 傳統的 NIO 有什麼不同
public class ChatServer {
public static void main(String[] args) throws InterruptedException {
/**
* NioEventLoopGroup 是用來處理I/O操作的多線程事件循環器
* boss 可以理解是 selector 的 accept 單獨一個線程
* worker 可以理解是 selector 的 read 和 write
*/
final EventLoopGroup bossGroup = new NioEventLoopGroup();
final EventLoopGroup workerGroup = new NioEventLoopGroup();
// ServerBootstrap 是一個啓動 NIO 服務的輔助啓動類
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
//channel 實例化 NioServerSocketChannel
.channel(NioServerSocketChannel.class)
// 用來處理 handler ,設置連入服務端的 Client 的 SocketChannel 的處理器
.childHandler(new ChatServerInitializer())
//option 針對NioServerSocketChannel,比如這裏 128 個客戶端之後,纔開始排隊
.option(ChannelOption.SO_BACKLOG,128)
// childOption 針對childHandler 的handler
.childOption(ChannelOption.SO_KEEPALIVE,true);
//這裏的啓動時異步的,阻塞等待
ChannelFuture future = b.bind(Constants.PORT).sync();
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("服務端啓動成功");
}
}
});
// 等待服務器 socket 關閉 。
// 在這個例子中,這不會發生,但你可以優雅地關閉你的服務器。
future.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
上面的註釋已經很清楚了,需要注意幾個類,比如 EventLoopGroup 對象,可以理解它爲一個線程池,一個用於接收新的客戶單,一個專注於數據讀寫,這樣的好處是充分結合多線程和 selector 的模式,如果你想要深入瞭解,可以搜索 Netty 的 Readctro 模型。
ServerBootstrap 是NIO服務啓動的一個輔助類,一般 NIO 的配置都是比較麻煩的, Netty 這裏通過 Builder 的模式,可以省略很多步驟。
而 channel 和 childHandler 則是配置服務端和接入的 socketchannel 的屬性的。這裏用 ChatServerInitializer 來實現,後面看具體實現。
最後通過 bind 綁定端口並阻塞接收客戶端的接入。
注意 closeFuture 方法,他是 監聽 服務器關閉,不是關閉服務器,而是監聽 關閉。
接着,繼續看 ChatServerInitializer 的代碼:
public class ChatServerInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//採用分隔符處理器,處理黏包問題,防止數據過大導致的黏包問題
pipeline.addLast(new DelimiterBasedFrameDecoder(2048, Delimiters.lineDelimiter()));
//編碼
pipeline.addLast(new StringDecoder());
//解碼
pipeline.addLast(new StringEncoder());
//添加處理器,這裏爲邏輯的處理
pipeline.addLast(new ChatServerHandler());
}
}
重點看 ChatServerHandler 它爲服務端主要的業務代碼。這裏爲 聊天室:
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
//單例
static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//提示其他客戶端,有新客戶端加入
group.writeAndFlush("SERVER - "+channel.remoteAddress()+"加入羣聊\n");
group.add(channel);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
//提示其他客戶端,有新客戶端加入
System.out.println("handlerRemoved");
group.writeAndFlush("SERVER - "+channel.remoteAddress()+"離開\n");
// group.remove(channel);
// A closed Channel is automatically removed from ChannelGroup,
// so there is no need to do "channels.remove(ctx.channel());"
}
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
Channel clientChannel = channelHandlerContext.channel();
//打印信息
for (Channel channel : group) {
if (channel != clientChannel){
channel.writeAndFlush("[" + clientChannel.remoteAddress() + "]" + s + "\n");
}else{
channel.writeAndFlush("[you]" + s + "\n");
}
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelInactive");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel incoming = ctx.channel();
System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"異常");
cause.printStackTrace();
ctx.close();
}
}
可以看到這裏繼承的是 SimpleChannelInboundHandler 。當然,它也可以繼承 ChannelInboundHandlerAdapter ,區別是 SimpleChannelInboundHandler 可以通過泛型指定數據類型,且在接收到數據之後,會自動 release ,避免 byteBuffer 被佔用,而 ChannelInboundHandlerAdapter 則不會自動釋放,需要自己 ReferenceCountUtil.release() ;教程都會說,記得回去看官方說明。
這裏因爲都是字符串類型,所以統一用 SimpleChannelInboundHandler ,當然服務端建議採用 ChannelInboundHandlerAdapter ,因爲有多個不同類型的客戶端接入,在客戶端做區分,並做好釋放即可。客戶端的話,可以用SimpleChannelInboundHandler ,畢竟這個也比較單一。
這樣,服務端的代碼就寫好了。
接着看 客戶端的代碼:
很多都是相似的,先看 ChatClient 的代碼:
public class ChatClient {
public static void main(String[] args) throws InterruptedException, IOException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(bossGroup)
.channel(NioSocketChannel.class)
.handler(new ChatClientInitializer());
//連接服務器
final ChannelFuture future = bootstrap.connect("localhost", Constants.PORT).sync();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String msg = br.readLine();
if (msg.equals("bye")){
return;
}
future.channel().writeAndFlush(msg+"\n");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
}
}
}
基本與 服務端一直,因爲是客戶端,所以只要配置 channel 和 handler 即可。其中 ChatClientInitializer與服務端代碼基本一直,只是業務邏輯那塊,需要換成**ChatClientHandler **:
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
//收到服務端消息
System.out.println(s);
}
}
這樣,就完成了。