SpringBoot整合Netty簡單Demo之網(wǎng)頁聊天室

利用WebSocket實(shí)現(xiàn)

說到網(wǎng)頁聊天室一般都是使用WebSocket長連接進(jìn)行數(shù)據(jù)交互和雙端數(shù)據(jù)發(fā)送,本人也已經(jīng)整合了一整套依賴于springboot-websocket包的網(wǎng)絡(luò)交互Demo唯竹,具體功能如下:

  1. 多用戶群聊
  2. 點(diǎn)對點(diǎn)私聊
  3. 實(shí)時消息通知
  4. 在線用戶顯示
  5. 上線唬渗、斷線等實(shí)時監(jiān)聽
  6. 其他在線通訊
WebSocket依賴包
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

SpringBoot簡單整合Netty

在Netty中可以集成WebSocket典阵,以下Demo只實(shí)現(xiàn)了用戶群聊,其他功能可加邏輯處理自行擴(kuò)展

  • NettyApplication(啟動類)
  @PropertySource(value= "classpath:/nettyserver.properties")
  @SpringBootApplication
  public class NettyApplication {

    @Value("${tcp.port}")
    private int tcpPort;

    @Value("${boss.thread.count}")
    private int bossCount;

    @Value("${worker.thread.count}")
    private int workerCount;

    @Value("${so.keepalive}")
    private boolean keepAlive;

    @Value("${so.backlog}")
    private int backlog;

    @Bean(name = "serverBootstrap")
    public ServerBootstrap bootstrap() {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup(), workerGroup())
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(nettyWebSocketChannelInitializer);
        Map<ChannelOption<?>, Object> tcpChannelOptions = tcpChannelOptions();
        Set<ChannelOption<?>> keySet = tcpChannelOptions.keySet();
        for (@SuppressWarnings("rawtypes") ChannelOption option : keySet) {
            b.option(option, tcpChannelOptions.get(option));
        }
        return b;
    }

    @Autowired
    @Qualifier("somethingChannelInitializer")
    private NettyWebSocketChannelInitializer nettyWebSocketChannelInitializer;

    @Bean(name = "tcpChannelOptions")
    public Map<ChannelOption<?>, Object> tcpChannelOptions() {
        Map<ChannelOption<?>, Object> options = new HashMap<ChannelOption<?>, Object>();
        options.put(ChannelOption.SO_KEEPALIVE, keepAlive);
        options.put(ChannelOption.SO_BACKLOG, backlog);
        return options;
    }

    @Bean(name = "bossGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup bossGroup() {
        return new NioEventLoopGroup(bossCount);
    }

    @Bean(name = "workerGroup", destroyMethod = "shutdownGracefully")
    public NioEventLoopGroup workerGroup() {
        return new NioEventLoopGroup(workerCount);
    }

    @Bean(name = "tcpSocketAddress")
    public InetSocketAddress tcpPort() {
        return new InetSocketAddress(tcpPort);
    }

    public static void main(String[] args) throws Exception{
        ConfigurableApplicationContext context = SpringApplication.run(NettyApplication.class, args);
        TCPServer tcpServer = context.getBean(TCPServer.class);
        tcpServer.start();
    }
}
  • TCPServer(啟動Netty服務(wù))
@Component
public class TCPServer {

   @Autowired
   @Qualifier("serverBootstrap")
   private ServerBootstrap serverBootstrap;

   @Autowired
   @Qualifier("tcpSocketAddress")
   private InetSocketAddress tcpPort;

   private Channel serverChannel;

   public void start() throws Exception {
       serverChannel =  serverBootstrap.bind(tcpPort).sync().channel().closeFuture().sync().channel();
   }

   @PreDestroy
   public void stop() throws Exception {
       serverChannel.close();
       serverChannel.parent().close();
   }

   public ServerBootstrap getServerBootstrap() {
       return serverBootstrap;
   }

   public void setServerBootstrap(ServerBootstrap serverBootstrap) {
       this.serverBootstrap = serverBootstrap;
   }

   public InetSocketAddress getTcpPort() {
       return tcpPort;
   }

   public void setTcpPort(InetSocketAddress tcpPort) {
       this.tcpPort = tcpPort;
   }
}
  • NettyWebSocketChannelInitializer(添加自定義handler)
@Component
@Qualifier("somethingChannelInitializer")
public class NettyWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Autowired
    private TextWebSocketFrameHandler textWebSocketFrameHandler;

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new HttpObjectAggregator(65536));
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
        pipeline.addLast(textWebSocketFrameHandler);   //這里不能使用new镊逝,不然在handler中不能注入依賴

    }
}
  • TextWebSocketFrameHandler(自定義操作類)
@Component
@Qualifier("textWebSocketFrameHandler")
@ChannelHandler.Sharable
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    @Autowired
    private RedisDao redisDao;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx,
                                TextWebSocketFrame msg) throws Exception {
        Channel incoming = ctx.channel();
        String uName = redisDao.getString(incoming.id()+"");
        for (Channel channel : channels) {
            if (channel != incoming){
                channel.writeAndFlush(new TextWebSocketFrame("[" + uName + "]" + msg.text()));
            } else {
                channel.writeAndFlush(new TextWebSocketFrame("[you]" + msg.text() ));
            }
        }
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {  
        System.out.println(ctx.channel().remoteAddress());
        String uName = new RandomName().getRandomName();  //用來獲取一個隨機(jī)的用戶名壮啊,可以用其他方式代替

        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[新用戶] - " + uName + " 加入"));
        }
        redisDao.saveString(incoming.id()+"",uName);   //存儲用戶
        channels.add(ctx.channel());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 
        Channel incoming = ctx.channel();
        String uName = redisDao.getString(String.valueOf(incoming.id()));
        for (Channel channel : channels) {
            channel.writeAndFlush(new TextWebSocketFrame("[用戶] - " + uName + " 離開"));
        }
        redisDao.deleteString(String.valueOf(incoming.id()));   //刪除用戶
        redisDao.saveString("cacheName",redisDao.getString("cacheName").replaceAll(uName,""));   //標(biāo)準(zhǔn)已經(jīng)使用的用戶名
        channels.remove(ctx.channel());  
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { 
        Channel incoming = ctx.channel();
        System.out.println("用戶:"+redisDao.getString(incoming.id()+"")+"在線");
    }


    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { 
        Channel incoming = ctx.channel();
        System.out.println("用戶:"+redisDao.getString(incoming.id()+"")+"掉線");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        Channel incoming = ctx.channel();
        System.out.println("用戶:"+redisDao.getString(incoming.id()+"")+"異常");
        cause.printStackTrace();
        ctx.close();
    }

}

這邊使用Redis保存用戶名和ChannelId來不同瀏覽器登錄的用戶

  • channelRead0:定義接收到消息的操作
  • handlerAdded:定義新用戶連接的操作
  • handlerRemoved:定義用戶離開的操作
  • channelActive:定義用戶在線的操作
  • channelInactive:定義用戶離線的操作
  • exceptionCaught:定義用戶異常的操作

如果要在Controller中使用Channel向客戶端發(fā)送數(shù)據(jù),只要注入TextWebSocketFrameHandler撑蒜,取得其中的ChannelGroup歹啼,再通過自己邏輯處理后存儲的ChannelId來取得對應(yīng)的Channel玄渗,即可向客戶端發(fā)送消息

Netty依賴包
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.16.Final</version>
</dependency>
  • 前端代碼
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Chat</title>
</head>
<body>
<script type="text/javascript">
    var socket;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8090/ws");
        socket.onmessage = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = ta.value + '\n' + event.data
        };
        socket.onopen = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = "連接開啟!";
        };
        socket.onclose = function(event) {
            var ta = document.getElementById('responseText');
            ta.value = ta.value + "連接被關(guān)閉";
        };
    } else {
        alert("你的瀏覽器不支持 WebSocket!");
    }

    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            socket.send(message);
        } else {
            alert("連接沒有開啟.");
        }
    }
    window.onbeforeunload = function(event) {
        event.returnValue = "刷新提醒";
    };
</script>
<form onsubmit="return false;">
    <h3>netty 聊天室:</h3>
    <textarea id="responseText" style="width: 400px; height: 300px;"></textarea>
    <br>
    <input type="text" name="message"  style="width: 300px" value="測試數(shù)據(jù)">
    <input type="button" value="發(fā)送消息" onclick="send(this.form.message.value)">
</form>
<br>
<br>
</body>
</html>
  • nettyserver.properties
tcp.port=8090
boss.thread.count=2
worker.thread.count=2
so.keepalive=true
so.backlog=100

效果截圖

群聊效果截圖

Git地址
https://github.com/zyf970617/springboot-netty-demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狸眼,一起剝皮案震驚了整個濱河市藤树,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拓萌,老刑警劉巖岁钓,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異微王,居然都是意外死亡屡限,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門炕倘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钧大,“玉大人,你說我怎么就攤上這事罩旋“⊙耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵涨醋,是天一觀的道長瓜饥。 經(jīng)常有香客問我,道長东帅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任球拦,我火速辦了婚禮靠闭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坎炼。我一直安慰自己愧膀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布谣光。 她就那樣靜靜地躺著檩淋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萄金。 梳的紋絲不亂的頭發(fā)上蟀悦,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音氧敢,去河邊找鬼日戈。 笑死,一個胖子當(dāng)著我的面吹牛孙乖,可吹牛的內(nèi)容都是我干的浙炼。 我是一名探鬼主播份氧,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弯屈!你這毒婦竟也來了蜗帜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤资厉,失蹤者是張志新(化名)和其女友劉穎厅缺,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酌住,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡店归,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了酪我。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片消痛。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖都哭,靈堂內(nèi)的尸體忽然破棺而出秩伞,到底是詐尸還是另有隱情,我是刑警寧澤欺矫,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布纱新,位于F島的核電站,受9級特大地震影響穆趴,放射性物質(zhì)發(fā)生泄漏脸爱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一未妹、第九天 我趴在偏房一處隱蔽的房頂上張望簿废。 院中可真熱鬧,春花似錦络它、人聲如沸族檬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单料。三九已至,卻和暖如春点楼,著一層夾襖步出監(jiān)牢的瞬間扫尖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工掠廓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留藏斩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓却盘,卻偏偏與公主長得像狰域,于是被迫代替她去往敵國和親媳拴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355