<文章內(nèi)容如有錯(cuò)誤钙态,煩請(qǐng)不吝賜教>
代碼鏈接:https://github.com/levyc/NettyDemo/tree/master/src/chat
文章目錄
- 關(guān)于netty框架
- 項(xiàng)目結(jié)構(gòu)
- 代碼分析
- 服務(wù)端代碼分析
- 客戶端代碼分析
為什么使用netty框架
netty是一個(gè)基于NIO潮太,異步的耻卡,事件驅(qū)動(dòng)的網(wǎng)絡(luò)通信框架杆怕。由于使用Java提供 的NIO包中的API開發(fā)網(wǎng)絡(luò)服務(wù)器代碼量大,復(fù)雜,難保證穩(wěn)定性。netty這類的網(wǎng)絡(luò)框架應(yīng)運(yùn)而生箭昵。通過使用netty框架可以快速開發(fā)網(wǎng)絡(luò)通信服務(wù)端税朴,客戶端回季。
項(xiàng)目結(jié)構(gòu)
簡(jiǎn)單介紹一下項(xiàng)目。通過使用netty框架快速開發(fā)出簡(jiǎn)易多人聊天室正林,學(xué)會(huì)初步使用neety進(jìn)行簡(jiǎn)單服務(wù)端與客戶端的開發(fā)泡一。
項(xiàng)目結(jié)構(gòu)
Server端:
ServerMain:服務(wù)器啟動(dòng)類,負(fù)責(zé)服務(wù)器的綁定與啟動(dòng)
ServerChatHandler:核心類觅廓。負(fù)責(zé)連接消息的處理鼻忠,數(shù)據(jù)的讀寫。
ServerIniterHandler:負(fù)責(zé)將多個(gè)Handlers組織進(jìn)ChannelPipeline中杈绸。
Client端:
ClientMain:客戶端啟動(dòng)類帖蔓,負(fù)責(zé)客戶端與服務(wù)器的連接,啟動(dòng)瞳脓。
ClientChatHandler:核心類塑娇。負(fù)責(zé)連接消息的處理,數(shù)據(jù)讀寫劫侧。
ClientIniterHandler:負(fù)責(zé)將多個(gè)Handlers組織進(jìn)ChannelPipeline中埋酬。
代碼分析
服務(wù)端代碼分析
ServerMain類
package chat.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class ServerMain {
private int port;
public ServerMain(int port) {
this.port = port;
}
public static void main(String[] args) {
new ServerMain(Integer.parseInt(args[0])).run();
}
public void run() {
EventLoopGroup acceptor = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
bootstrap.group(acceptor, worker);//設(shè)置循環(huán)線程組,前者用于處理客戶端連接事件烧栋,后者用于處理網(wǎng)絡(luò)IO
bootstrap.channel(NioServerSocketChannel.class);//用于構(gòu)造socketchannel工廠
bootstrap.childHandler(new ServerIniterHandler());//為處理accept客戶端的channel中的pipeline添加自定義處理函數(shù)
try {
Channel channel = bootstrap.bind(port).sync().channel();//綁定端口(實(shí)際上是創(chuàng)建serversocketchannnel写妥,并注冊(cè)到eventloop上),同步等待完成审姓,返回相應(yīng)channel
System.out.println("server strart running in port:" + port);
channel.closeFuture().sync();//在這里阻塞珍特,等待關(guān)閉
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//優(yōu)雅退出
acceptor.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
EventLoopGroup:顧名思義,事件魔吐,循環(huán)扎筒,組,說白了即是用于管理多個(gè)channel的線程組画畅。就好比一組進(jìn)行循環(huán)使用的線程池中管理的線程砸琅。但是為什么分別有acceptor和worker2個(gè)呢?前者線程組負(fù)責(zé)連接的處理轴踱,即是用于接受客戶端的連接症脂,后者線程組負(fù)責(zé)handler消息數(shù)據(jù)的處理,即是用于處理SocketChannel網(wǎng)絡(luò)讀寫
ServerBootstrap:簡(jiǎn)言之就是為服務(wù)器啟動(dòng)進(jìn)行一些配置。調(diào)用的一些方法用處請(qǐng)看注釋诱篷。
ServerChatHandler類
package chat.server;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {
public static final ChannelGroup group = new DefaultChannelGroup(
GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext arg0, String arg1)
throws Exception {
Channel channel = arg0.channel();
for (Channel ch : group) {
if (ch == channel) {
ch.writeAndFlush("[you]:" + arg1 + "\n");
} else {
ch.writeAndFlush(
"[" + channel.remoteAddress() + "]: " + arg1 + "\n");
}
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
for (Channel ch : group) {
ch.writeAndFlush(
"[" + channel.remoteAddress() + "] " + "is comming");
}
group.add(channel);
}
@Override
public voiced handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
for (Channel ch : group) {
ch.writeAndFlush(
"[" + channel.remoteAddress() + "] " + "is comming");
}
group.remove(channel);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
System.out.println("[" + channel.remoteAddress() + "] " + "online");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Channel channel = ctx.channel();
System.out.println("[" + channel.remoteAddress() + "] " + "offline");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
System.out.println(
"[" + ctx.channel().remoteAddress() + "]" + "exit the room");
ctx.close().sync();
}
}
自定義的處理函類需要繼承ChannelHandlerAdapter壶唤,我這里就直接繼承SimpleChannelInboundHandler<T>,后面的泛型代表接受的消息的類型棕所。
因?yàn)槎嗳肆奶鞎?huì)有多個(gè)客戶端連接進(jìn)來闸盔,所以定義了一個(gè)ChannelGroup group去存儲(chǔ)連接進(jìn)來的channel當(dāng)作在線用戶。
下面介紹一下各方法的作用
channelRead0:當(dāng)channel的可讀就緒時(shí)琳省,該方法被調(diào)用迎吵,即是接收到客戶端發(fā)過來的消息時(shí)被調(diào)用。方法邏輯:遍歷group针贬,將當(dāng)前發(fā)送信息的channel發(fā)送‘you say:+消息體’击费,其余的channel則發(fā)送‘channle.remoteAdress+消息體’
handlerAdded:當(dāng)有客戶端channel被連接進(jìn)來時(shí),該方法被調(diào)用桦他。向在線的每個(gè)channel發(fā)送[IP]is comming
handlerRemoved:當(dāng)有客戶端channeld斷開網(wǎng)絡(luò)io蔫巩,該方法被調(diào)用。向在線的每個(gè)channel發(fā)送[IP]is leaving
channelActive:當(dāng)有客戶端channeld活躍時(shí)快压,該方法被調(diào)用圆仔。向在線的每個(gè)channel發(fā)送[IP]online
channelInactive:當(dāng)有客戶端channeld不活躍時(shí),該方法被調(diào)用蔫劣。向在線的每個(gè)channel發(fā)送[IP]offline
exceptionCaught:當(dāng)連接和網(wǎng)絡(luò)io發(fā)生異常時(shí)被調(diào)用坪郭。
那么,大家一定有疑惑拦宣,網(wǎng)絡(luò)通信中截粗,消息有很多種載體,例如文件鸵隧,圖片绸罗,字符串,那么在我的客戶端發(fā)送一個(gè)圖片給你服務(wù)器豆瘫,服務(wù)器又怎樣知道你發(fā)的是圖片呢珊蟀?里面就涉及到兩項(xiàng)技術(shù),協(xié)議和編解碼外驱。圖片和文件的傳輸后續(xù)再說育灸,先從當(dāng)前項(xiàng)目多人聊天說協(xié)議和編解碼。
假定我們簡(jiǎn)易多人聊天只允許發(fā)送字符串聊天昵宇,那么客戶端發(fā)送的字符串不可能直接就是字符串發(fā)送出去的磅崭,電路只能識(shí)別0和1,因此我們發(fā)送出去的字符串需要按某種編碼格式編碼成字節(jié)數(shù)組再發(fā)送出去瓦哎,服務(wù)器接受到我們的字節(jié)數(shù)組后砸喻,再按某個(gè)解碼格式解碼成字符串柔逼。如果自己去實(shí)現(xiàn)這樣的編碼解碼器,無疑加重開發(fā)工作量割岛。neety已經(jīng)提供很多種解碼器給我們使用愉适,而Stringencoder和StringDecoder就是neety提供的編解碼器。下面說說怎樣使用癣漆。
ServerIniterHandler類
package chat.server;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class ServerIniterHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
ChannelPipeline pipeline = arg0.pipeline();
pipeline.addLast("docode",new StringDecoder());
pipeline.addLast("encode",new StringEncoder());
pipeline.addLast("chat",new ChatServerHandler());
}
}
通過ChannelInitializer去給channel的pipelie添加上字符串編解碼器维咸。即可在網(wǎng)絡(luò)io中收發(fā)字符串。
客戶端代碼分析
ClientMain類
package chat.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class ClientMain {
private String host;
private int port;
private boolean stop = false;
public ClientMain(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws IOException {
new ClientMain(args[0], Integer.parseInt(args[1])).run();
}
public void run() throws IOException {
EventLoopGroup worker = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(worker);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ClientIniter());
try {
Channel channel = bootstrap.connect(host, port).sync().channel();
while (true) {
BufferedReader reader = new BufferedReader(
new InputStreamReader(System.in));
String input = reader.readLine();
if (input != null) {
if ("quit".equals(input)) {
System.exit(1);
}
channel.writeAndFlush(input);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
System.exit(1);
}
}
public boolean isStop() {
return stop;
}
public void setStop(boolean stop) {
this.stop = stop;
}
}
客戶端的啟動(dòng)比服務(wù)端簡(jiǎn)單一點(diǎn)惠爽,只使用到一個(gè)eventloopgroup癌蓖,用于處理網(wǎng)絡(luò)io。
注意:客戶端用的是bootstrap類
邏輯:當(dāng)connect方法調(diào)用后調(diào)用sync方法阻塞到連接成功疆股,然后進(jìn)行死循環(huán)——通過system.in去讀取輸入的文本字符串费坊,當(dāng)輸入‘quit’就結(jié)束進(jìn)程倒槐,否則就發(fā)送輸入的字符串給服務(wù)器旬痹。
ChatClientHandler類
package chat.client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext arg0, String arg1)
throws Exception {
System.out.println(arg1);
}
}
打印服務(wù)器發(fā)送回來的數(shù)據(jù)
ClientIniter類
package chat.client;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class ClientIniter extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
ChannelPipeline pipeline = arg0.pipeline();
pipeline.addLast("stringD", new StringDecoder());
pipeline.addLast("stringC", new StringEncoder());
pipeline.addLast("http", new HttpClientCodec());
pipeline.addLast("chat", new ChatClientHandler());
}
}
在channel的pipeline中添加編解碼器和自定義消息處理器。
運(yùn)行結(jié)果:
server的窗口:
server strart running in port:8888
[/127.0.0.1:49635] online
[/127.0.0.1:49648] online
client1的窗口
[/127.0.0.1:49648] is comming
[you]:49648讨越,hi
client2的窗口
[/127.0.0.1:49635] 49648两残,hi
總結(jié):
經(jīng)過上面的分析,可以得出其實(shí)用netty框架開發(fā)網(wǎng)絡(luò)應(yīng)用把跨,相比Java原生NIO的API簡(jiǎn)單快捷人弓,代碼量少。實(shí)際上用netty開發(fā)核心兩點(diǎn)就是啟動(dòng)類和編寫自定義處理類着逐。
**下一章預(yù)告:使用netty框架開發(fā)單對(duì)單聊天 **