主頁 > 後端開發 > 微服務springcloud環境下基于Netty搭建websocket集群實作高并發,高性能,高可用的服務器訊息推送--經典案例(已在作業中實戰應用)netty是yyds!

微服務springcloud環境下基于Netty搭建websocket集群實作高并發,高性能,高可用的服務器訊息推送--經典案例(已在作業中實戰應用)netty是yyds!

2021-09-13 08:52:19 後端開發

netty搭建websocket

  • 1、背景
  • 2、websocket
  • 3、netty
    • 3.1 socket
    • 3.2 Java IO模型
    • 3.3 netty
      • 3.3.1 概念:
      • 3.3.2 三大特點:
      • 3.3.3 主從Reactor架構圖
      • 3.3.4 應用場景
  • 4、springboot環境下使用netty搭建websocket
    • 4.1 系統設計架構圖
    • 4.2 架構中存在的六大經典問題
    • 4.3 引入pom依賴和yml配置
    • 4.4 Websocket 初始化器
    • 4.5 websocket 通道初始化器
    • 4.6 websocket 入站處理器
    • 4.7 channel中任務佇列的執行緒任務
    • 4.8 websocket啟動程式
    • 4.9 問題六解決方案
    • 4.10 前端代碼
    • 4.11 wesocket在nginx中配置
    • 4.12 效果圖
  • 5、總結

1、背景

我們在實際的作業開發中,必然會遇到一些需要網頁與服務器端保持連接(起碼看上去是保持連接)的需求,比如類似微信網頁版的聊天類應用,比如需要頻繁更新頁面資料(實時資料例如天氣,電流電壓,pm2.5等等這樣類似的資料)的監控系統頁面或股票看盤頁面,比如服務器讀取MySQL或者redis或者第三方的資料主動推送給瀏覽器客戶端等等業務場景,我們通常采用如下幾種技術:

(1)短輪詢:前端老師利用ajax定期向服務器發起http請求,無論資料是否更新立馬回傳資料,這樣存在的缺點就是,一方面如果后端資料木有更新,那么這一次http請求就是無用的,另一方面高并發情況下,短鏈接的頻繁創建銷毀,以及客戶端數量過大造成過多無用的http請求,都會對服務器和帶寬造成壓力,短輪詢只適用于客戶端連接少,并發量不高的場景;
(2)長輪詢:利用comet不斷向服務器發起請求,服務器將請求暫時掛起,直到有新的資料的時候才回傳,相對短輪詢減少了請求次數得到了一定的優化,但是在高并發的場景下依然不適用;
(3)SSE:服務端推送(Server Send Event),在客戶端發起一次請求后會保持該連接,服務器端基于該連接持續向客戶端發送資料,從HTML5開始加入,
(4)Websocket:這是也是一種保持長連接的技術,并且是雙向的,從HTML5開始加入,并非完全基于HTTP,適合于頻繁和較大流量的雙向通訊場景,是服務器推送訊息功能的最佳實踐,而實作websocket的最佳方式,就是netty

網上的很多netty搭建websocket的博文都不夠全面,有很多問題都木有解決方式,我通過實際作業中的經驗,把常遇到的問題總結了方法,下文會說到!

2、websocket

什么是websocekt呢?
websocket是一種在單個TCP連接上進行全雙工通信的協議,也就是一種保持長連接的技術,并且是雙向的,
websocket協議本身是構建在http協議之上的升級協議,客戶端首先向服務器端去建立連接,這個連接本身就是http協議只是在頭資訊中包含了一些websocket協議的相關資訊,一旦http連接建立之后,服務器端讀到這些websocket協議的相關資訊就將此協議升級成websocket協議,WebSocket使得客戶端和服務器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料,在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,并進行雙向資料傳輸,
簡單理解,就是一種通訊協議,重點是websocket的實作方式–netty,

3、netty

什么是netty呢?
我們在大學里學習Java基礎的時候,想必都學過socket網路編程吧!我們在這里一起復習一下!

3.1 socket

(1)網路編程本質就是說兩個設備之間資訊的發送與接收,通過操作相應API調度計算機硬體資源,并且利用管道(網線)進行資料互動的程序,相關技術點像ISO七層模型,TCP三次握手/四次揮手等網路編程的基礎不再贅述,
(2)而socket是對TCP/IP協議的封裝,Socket本身并不是協議,而是一個呼叫介面(API),通過Socket發起系統呼叫作業系統內核,我們才能使用TCP/IP協議,
(3)我們經常說的I/O,在計算機中指Input/Output,即輸入輸出,實質上IO分為兩種,一種是磁盤IO,磁盤上的資料讀取到用戶空間,那么這次資料轉移操作其實就是一次I/O操作,也就是一次檔案I/O,一種是網路IO,當一個客戶端和服務端之間相互通信,互動我們稱之為網路io(網路通訊)

3.2 Java IO模型

有BIO(同步阻塞IO)、NIO(同步非阻塞IO)、AIO(異步IO),netty就是一個NIO的高性能的框架,相關netty的技術點,關注小編,后續會出相關技術堆疊的細節,
(1)BIO:同步阻塞IO模型,適用于連接數目比較少且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中,服務器實作模式為一個連接一個執行緒,即客戶端有連 接請求時服務器端就需要啟動一個執行緒進行處理,如果這個連接不做任何事情會造 成不必要的執行緒開銷,可以通過執行緒池機制改善(實作多個客戶連接服務器),
在這里插入圖片描述

(2)NIO:同步非阻塞IO模型,適用于連接資料多且連接比較短的架構,如聊天服務器,彈幕系統,服務器間通訊等;服務器實作模式為一個執行緒處理多個連接(一個請求一個執行緒),包含Selector 、Channel 、Buffer三大組件,
在這里插入圖片描述

selector選擇器:用一個執行緒處理多個客戶端的連接,就會使用到selector選擇器,selector用于監聽多個通道上是否有事件發生(比如連接請求,資料到達等),如果有事件發生,便獲取事件然后針對于每個事件進行相應的處理,因此可以使用單個執行緒就可以監聽多個客戶端通道,
channel通道:Channel管道和Java IO中的Stream(流)是差不多一個等級的,只不過Stream是單向的,譬如:InputStream, OutputStream.而Channel是雙向的,同時進行讀寫資料,而流只能讀或者寫,可以實作異步讀寫資料,可以從緩沖區讀資料,也可以寫資料到緩沖區,
buffer緩沖區:Buffer本質上是一個可以讀寫的記憶體塊,可以理解成容器物件,底層是有一個陣列,通過buffer實作非阻塞機制,該物件提供了一組方法,可以輕松的使用記憶體塊,緩沖區物件內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況,Channel提供了從檔案,網路讀取資料的通道,但是讀取和寫入的資料必須經過buffer,
(3)AIO:異步IO模型,使用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分 呼叫OS參與并發操作,編程比較復雜,在Linux底層用epoll(一種輪詢模型),aio多包了一層封裝,aio的api更好用,Windows上的aio是自己實作的,不是輪詢模型是事件模型,完成埠實作的,要比linxu上的aio效率高,
在這里插入圖片描述

3.3 netty

3.3.1 概念:

netty是一個開源異步的事件驅動的網路應用程式框架,用于快速開發可維護的高性能協議服務器和客戶端,

3.3.2 三大特點:

①高并發:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)開發異步事件驅動的高性能網路通信框架,nio使用了select模型(多路復用器技術),從而使得系統在單執行緒的情況下可以同時處理多個客戶端請求,Netty使用了Reactor模型,Reactor模型有三種多執行緒模型,netty是在主從 Reactor 多執行緒模型上做了一定的改進,Netty有兩個執行緒組,一個作為bossGroup執行緒組,負責客戶端接收,一個workerGroup執行緒組負責作業執行緒的作業(與客戶端的IO操作和任務操作等等),Netty 的所有 IO 操作都是異步非阻塞的,通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得 IO 操作結果,他的并發性能得到了很大提高,
②傳輸快:Netty 的傳輸依賴于零拷貝特性,實作了更高效率的傳輸,零拷貝要求內核(kernel)直接將資料從磁盤檔案拷貝到Socket緩沖區(套接字),而無須通過應用程式,零拷貝減少不必要的記憶體拷貝,不僅提高了應用程式的性能,而且減少了內核態和用戶態背景關系切換,
③封裝好:Netty 封裝了 NIO 操作的很多細節,提供了易于使用呼叫介面,如果你用過或者見過nio代碼,你會感覺netty真的yyds!!!

3.3.3 主從Reactor架構圖

在這里插入圖片描述
說明:①Reactor回應式編程(事件驅動模型):一般有一個主回圈和一個任務佇列,所有事件只管往佇列里塞,主回圈則從佇列里取出并處理,如果不依賴于多路復用處理多個任務就會需要多執行緒(與連接數對等) ,但是依賴于多路復用,這個回圈就可以在單執行緒的情況下處理多個連接,無論是哪個連接發生了什么事件,都會被主回圈從佇列取出并處理(可能用回呼函式處理等) ,也就是說程式的走向由事件驅動.
②mainReactor:主Reactor負責 單執行緒就可以接受所有客戶端連接
③subReactor:子Reactor負責 多執行緒處理客戶端的讀寫IO事件
④ThreadPool:執行緒池負責 處理業務耗時的操作

3.3.4 應用場景

①現在物聯網的應用無處不在,大量的專案都牽涉到應用傳感器和服務器端的資料通信,Netty作為基礎通信組件進行網路編程,
②現在互聯網系統講究的都是高并發、分布式、微服務,各類訊息滿天飛,Netty在這類架構里面的應用可謂是如魚得水,如果你對當前的各種應用服務器不爽,那么完全可以基于Netty來實作自己的HTTP服務器,FTP服務器,UDP服務器,RPC服務器,WebSocket服務器,Redis的Proxy服務器,MySQL的Proxy服務器等等,
現在非常多的開源軟體都是基于netty開發的,例如阿里分布式服務框架 Dubbo 的 RPC 框架,淘寶的訊息中間件 RocketMQ;
③游戲行業:無論是手游服務端還是大型的網路游戲,Java 語言得到了越來越廣泛的應用,Netty 作為高性能的基礎通信組件,它本身提供了 TCP/UDP 和 HTTP 協議堆疊,地圖服務器之間可以方便的通過 Netty 進行高性能的通信,
④大資料:開源集群運算框架 Spark;分布式計算框架 Storm;

4、springboot環境下使用netty搭建websocket

4.1 系統設計架構圖

在這里插入圖片描述

4.2 架構中存在的六大經典問題

第一個問題:客戶端和服務端單獨通信,怎么實作?
第二個問題:單機中websocekt主動向所有客戶端推送訊息如何實作?在集群中如何實作?
第三個問題:單機如何統計同時在線的客戶數量?websocket集群如何統計在線的客戶數量呢?
第四個問題:由于客戶端和websocket服務器集群中的某個節點建立長連接是隨機的,如何解決服務端向某個或某些部分客戶端推送訊息?
第五個問題:websocket服務端周期性向客戶端推送訊息,單機或集群中如何實作?
第六個問題:websocket集群中一個客戶端向其他客戶端主動發送訊息,如何實作?
福利來啦!!以上所有問題,已在代碼中全部解決并實踐!!!

4.3 引入pom依賴和yml配置

(1)pom依賴

	<dependencies>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.36.Final</version>
		</dependency>
	</dependencies>

(2)yml配置

websocket:
  port: 7000 #埠
  url: /msg	#訪問url

(3) 客戶端和服務端互動的訊息體

package com.wander.netty.websocket.yeelight;

import lombok.Data;

import java.io.Serializable;

/**
 * @Author WDYin
 * @Date 2021/8/13
 * @Description
 **/
@Data
public class MessageRequest implements Serializable {

    private Long unionId;

    private Integer current = 1;

    private Integer size = 10;
}

4.4 Websocket 初始化器

package com.wander.netty.websocket.yeelight;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * @Author WDYin
 * @Date 2021/6/10
 * @Description websocket初始化器
 **/
@Slf4j
@Component
public class WebsocketInitialization {

    @Resource
    private WebsocketChannelInitializer websocketChannelInitializer;

    @Value("${websocket.port}")
    private Integer port;

    @Async
    public void init() throws InterruptedException {

        //bossGroup連接執行緒組,主要負責接受客戶端連接,一般一個執行緒足矣
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //workerGroup作業執行緒組,主要負責網路IO讀寫
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            //啟動輔助類
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            //bootstrap系結兩個執行緒組
            serverBootstrap.group(bossGroup, workerGroup);
            //設定通道為NioChannel
            serverBootstrap.channel(NioServerSocketChannel.class);
            //可以對入站\出站事件進行日志記錄,從而方便我們進行問題排查,
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            //設定自定義的通道初始化器,用于入站操作
            serverBootstrap.childHandler(websocketChannelInitializer);
            //啟動服務器,本質是Java程式發起系統呼叫,然后內核底層起了一個處于監聽狀態的服務,生成一個檔案描述符FD
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            //異步
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

4.5 websocket 通道初始化器

package com.wander.netty.websocket.yeelight;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Author WDYin
 * @Date 2021/6/10
 * @Description websocket通道初始化器
 **/
@Component
public class WebsocketChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Autowired
    private WebSocketHandler webSocketHandler;

    @Value("${websocket.url}")
    private String websocketUrl;

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {

        //獲取pipeline通道
        ChannelPipeline pipeline = socketChannel.pipeline();
        //因為基于http協議,使用http的編碼和解碼器
        pipeline.addLast(new HttpServerCodec());
        //是以塊方式寫,添加ChunkedWriteHandler處理器
        pipeline.addLast(new ChunkedWriteHandler());
        /*
          說明
          1. http資料在傳輸程序中是分段, HttpObjectAggregator ,就是可以將多個段聚合
          2. 這就就是為什么,當瀏覽器發送大量資料時,就會發出多次http請求
        */
        pipeline.addLast(new HttpObjectAggregator(8192));
        /* 說明
          1. 對應websocket ,它的資料是以 幀(frame) 形式傳遞
          2. 可以看到WebSocketFrame 下面有六個子類
          3. 瀏覽器請求時 ws://localhost:7000/msg 表示請求的uri
          4. WebSocketServerProtocolHandler 核心功能是將 http協議升級為 ws協議 , 保持長連接
          5. 是通過一個 狀態碼 101
        */
        pipeline.addLast(new WebSocketServerProtocolHandler(websocketUrl));
        //自定義的handler ,處理業務邏輯
        pipeline.addLast(webSocketHandler);
    }
}

4.6 websocket 入站處理器

package com.wander.netty.websocket.yeelight;

import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.Future;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @Author WDYin
 * @Date 2021/6/10
 * @Description websocket處理器
 **/
@Slf4j
@Component
@ChannelHandler.Sharable//保證處理器,在整個生命周期中就是以單例的形式存在,方便統計客戶端的在線數量
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    //通道map,存盤channel,用于群發訊息,以及統計客戶端的在線數量,解決問題問題三,如果是集群環境使用redis的hash資料型別存盤即可
    private static Map<String, Channel> channelMap = new ConcurrentHashMap<>();
    //任務map,存盤future,用于停止佇列任務
    private static Map<String, Future> futureMap = new ConcurrentHashMap<>();
    //存盤channel的id和用戶主鍵的映射,客戶端保證用戶主鍵傳入的是唯一值,解決問題四,如果是集群中需要換成redis的hash資料型別存盤即可
    private static Map<String, Long> clientMap = new ConcurrentHashMap<>();

    /**
     * 客戶端發送給服務端的訊息
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        try {
            //接受客戶端發送的訊息
            MessageRequest messageRequest = JSON.parseObject(msg.text(), MessageRequest.class);

            //每個channel都有id,asLongText是全域channel唯一id
            String key = ctx.channel().id().asLongText();
            //存盤channel的id和用戶的主鍵
            clientMap.put(key, messageRequest.getUnionId());
            log.info("接受客戶端的訊息......" + ctx.channel().remoteAddress() + "-引數[" + messageRequest.getUnionId() + "]");

            if (!channelMap.containsKey(key)) {
                //使用channel中的任務佇列,做周期回圈推送客戶端訊息,解決問題二和問題五
                Future future = ctx.channel().eventLoop().scheduleAtFixedRate(new WebsocketRunnable(ctx, messageRequest), 0, 10, TimeUnit.SECONDS);
                //存盤客戶端和服務的通信的Chanel
                channelMap.put(key, ctx.channel());
                //存盤每個channel中的future,保證每個channel中有一個定時任務在執行
                futureMap.put(key, future);
            } else {
                //每次客戶端和服務的主動通信,和服務端周期向客戶端推送訊息互不影響 解決問題一
                ctx.channel().writeAndFlush(new TextWebSocketFrame(Thread.currentThread().getName() + "服務器時間" + LocalDateTime.now() + "wdy"));
            }

        } catch (Exception e) {

            log.error("websocket服務器推送訊息發生錯誤:", e);

        }
    }

    /**
     * 客戶端連接時候的操作
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("一個客戶端連接......" + ctx.channel().remoteAddress() + Thread.currentThread().getName());
    }

    /**
     * 客戶端掉線時的操作
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        String key = ctx.channel().id().asLongText();
        //移除通信過的channel
        channelMap.remove(key);
        //移除和用戶系結的channel
        clientMap.remove(key);
        //關閉掉線客戶端的future
        Optional.ofNullable(futureMap.get(key)).ifPresent(future -> {
            future.cancel(true);
            futureMap.remove(key);
        });
        log.info("一個客戶端移除......" + ctx.channel().remoteAddress());
        ctx.close(); //關閉連接
    }

    /**
     * 發生例外時執行的操作
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        String key = ctx.channel().id().asLongText();
        //移除通信過的channel
        channelMap.remove(key);
        //移除和用戶系結的channel
        clientMap.remove(key);
        //移除定時任務
        Optional.ofNullable(futureMap.get(key)).ifPresent(future -> {
            future.cancel(true);
            futureMap.remove(key);
        });
        //關閉長連接
        ctx.close();
        log.info("例外發生 " + cause.getMessage());
    }

    public static Map<String, Channel> getChannelMap() {
        return channelMap;
    }

    public static Map<String, Future> getFutureMap() {
        return futureMap;
    }

    public static Map<String, Long> getClientMap() {
        return clientMap;
    }
}

4.7 channel中任務佇列的執行緒任務

package com.wander.netty.websocket.yeelight;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author WDYin
 * @Date 2021/8/10
 * @Description websocket程式
 **/
@Slf4j
@Component
public class WebsocketApplication {

    @Resource
    private WebsocketInitialization websocketInitialization;

    @PostConstruct
    public void start() {
        try {
            log.info(Thread.currentThread().getName() + ":websocket啟動中......");
            websocketInitialization.init();
            log.info(Thread.currentThread().getName() + ":websocket啟動成功!!!");
        } catch (Exception e) {
            log.error("websocket發生錯誤:",e);
        }
    }
}

4.8 websocket啟動程式

package com.wander.netty.websocket.yeelight;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author WDYin
 * @Date 2021/8/10
 * @Description websocket程式
 **/
@Slf4j
@Component
public class WebsocketApplication {

    @Resource
    private WebsocketInitialization websocketInitialization;

    @PostConstruct
    public void start() {
        try {
            log.info(Thread.currentThread().getName() + ":websocket啟動中......");
            websocketInitialization.init();
            log.info(Thread.currentThread().getName() + ":websocket啟動成功!!!");
        } catch (Exception e) {
            log.error("websocket發生錯誤:",e);
        }
    }
}

4.9 問題六解決方案

package com.wander.netty.websocket.wdy;

import com.wander.netty.websocket.yeelight.WebSocketHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelId;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * @Author WDYin
 * @Date 2021/9/12
 * @Description
 **/
@RequestMapping("index")
@Controller
public class WebsocketController {

    /**
     *
     * @param id 用戶主鍵
     * @param idList 要把訊息發送給其他用戶的主鍵
     */
    @RequestMapping("hello1")
    private void hello(Long id, List<Long> idList){
        //獲取所有連接的客戶端,如果是集群環境使用redis的hash資料型別存盤即可
        Map<String, Channel> channelMap = WebSocketHandler.getChannelMap();
        //獲取與用戶主鍵系結的channel,如果是集群環境使用redis的hash資料型別存盤即可
        Map<String, Long> clientMap = WebSocketHandler.getClientMap();
        //解決問題六,websocket集群中一個客戶端向其他客戶端主動發送訊息,如何實作?
        clientMap.forEach((k,v)->{
            if (idList.contains(v)){
                Channel channel = channelMap.get(k);
                channel.eventLoop().execute(() -> channel.writeAndFlush(new TextWebSocketFrame(Thread.currentThread().getName()+"服務器時間" + LocalDateTime.now() + "wdy")));
            }
        });
    }
}

4.10 前端代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    //判斷當前瀏覽器是否支持websocket
    if(window.WebSocket) {
        socket = new WebSocket("ws://localhost:7000/msg");
        //相當于channelReado, ev 收到服務器端回送的訊息
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + ev.data;
        }

        //相當于連接開啟(感知到連接開啟)
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "連接開啟了.."
        }

        //相當于連接關閉(感知到連接關閉)
        socket.onclose = function (ev) {

            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "連接關閉了.."
        }
    } else {
        alert("當前瀏覽器不支持websocket")
    }

    //發送訊息到服務器
    function send(websocketMessage) {
        if(!window.socket) { //先判斷socket是否創建好
            return;
        }
        if(socket.readyState == WebSocket.OPEN) {
            //通過socket 發送訊息
            socket.send(websocketMessage)
        } else {
            alert("連接沒有開啟");
        }
    }
</script>
    <form onsubmit="return false">
        <textarea name="websocketMessage" style="height: 300px; width: 300px"></textarea>
        <input type="button" value="發生訊息" onclick="send(this.form.websocketMessage.value)">
        <textarea id="responseText" style="height: 300px; width: 300px"></textarea>
        <input type="button" value="清空內容" onclick="document.getElementById('responseText').value=''">
    </form>
</body>
</html>

4.11 wesocket在nginx中配置

nginx.conf中的配置

#第一步:
upstream websocket-router {
        server 192.168.1.31:7000 max_fails=10 weight=1 fail_timeout=5s;
        keepalive 1000;
}
#第二步:
server {
        listen      80;	#監聽80埠
        server_name websocket.wdy.com;	#域名配置

        ssl_session_cache    shared:SSL:1m;		
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            client_max_body_size 100M;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header Upgrade $http_upgrade;    #支持wss
            proxy_set_header Connection "upgrade";    #支持wssi
            proxy_pass http://websocket-router;	#代理路由
            root   html;
            index  index.html index.htm;
        }
    }

4.12 效果圖

springboot啟動后,用瀏覽器打開前端頁面:
在這里插入圖片描述

5、總結

本文主要講述了網路編程相關的IO模型以及NIO框架netty 如何搭建websocket,小伙伴有遇到說明問題,可評論留言,我會及時回復,關注小編,后續會在博客中講解netty詳細的技術堆疊!
至此,一個高性能,高并發的websocket集群搭建完畢!可以找領導漲薪啦!!

對設計模式不是很理解的童靴可以看小編另一篇文章
2.5萬字詳解Java 23種設計模式的簡介和創建型模式(簡單工廠、工廠方法、抽象工廠、單例-多執行緒安全詳解、建造者、原型)的詳細解讀、UML類圖、及代碼演示

代碼中如何干掉太多的if else即if else的多種替代方案以提高代碼質量通過公司代碼審查

感興趣的小伙伴可以看看小編其他文章
微服務 分布式 集群 負載均衡詳述

MySQL查詢陳述句執行順序以及各關鍵字的詳解

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/299659.html

標籤:java

上一篇:Sa-Token之注解鑒權:優雅的將鑒權與業務代碼分離!

下一篇:HTML+CSS+JS實作 ??svg圖片透明層文本顯示??

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more