用netty開發(fā)多人聊天

<文章內(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ì)單聊天 **

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末崔赌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子耸别,更是在濱河造成了極大的恐慌健芭,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秀姐,死亡現(xiàn)場(chǎng)離奇詭異慈迈,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)省有,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門痒留,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蠢沿,你說我怎么就攤上這事伸头。” “怎么了舷蟀?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵恤磷,是天一觀的道長(zhǎng)弧轧。 經(jīng)常有香客問我,道長(zhǎng)碗殷,這世上最難降的妖魔是什么精绎? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮锌妻,結(jié)果婚禮上代乃,老公的妹妹穿的比我還像新娘。我一直安慰自己仿粹,他們只是感情好搁吓,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著吭历,像睡著了一般堕仔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晌区,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天摩骨,我揣著相機(jī)與錄音,去河邊找鬼朗若。 笑死恼五,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哭懈。 我是一名探鬼主播灾馒,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼遣总!你這毒婦竟也來了睬罗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤旭斥,失蹤者是張志新(化名)和其女友劉穎容达,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琉预,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡董饰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了圆米。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卒暂。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖娄帖,靈堂內(nèi)的尸體忽然破棺而出也祠,到底是詐尸還是另有隱情,我是刑警寧澤近速,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布诈嘿,位于F島的核電站堪旧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏奖亚。R本人自食惡果不足惜淳梦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昔字。 院中可真熱鬧爆袍,春花似錦、人聲如沸作郭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夹攒。三九已至蜘醋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間咏尝,已是汗流浹背压语。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留状土,地道東北人无蜂。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蒙谓,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子训桶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理累驮,服務(wù)發(fā)現(xiàn),斷路器舵揭,智...
    卡卡羅2017閱讀 134,714評(píng)論 18 139
  • netty常用API學(xué)習(xí) netty簡(jiǎn)介 Netty是基于Java NIO的網(wǎng)絡(luò)應(yīng)用框架. Netty是一個(gè)NIO...
    花丶小偉閱讀 6,012評(píng)論 0 20
  • 前奏 https://tech.meituan.com/2016/11/04/nio.html 綜述 netty通...
    jiangmo閱讀 5,869評(píng)論 0 13
  • 1谤专、Netty基礎(chǔ)入門 Netty是由JBOSS提供的一個(gè)java開源框架。Netty提供異步的午绳、事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)...
    我是嘻哈大哥閱讀 4,693評(píng)論 0 31
  • 今日立春也是上班第一天置侍,忙到爆肝,上午做檢查一直沒停手拦焚,一上午做了二十人蜡坊,下午又是連軸轉(zhuǎn),十多個(gè)病人的檢查赎败,晚上回...
    流水蓮華閱讀 194評(píng)論 0 0