整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          github項目:LiveRoomDemo 動手打造

          github項目:LiveRoomDemo 動手打造屬于自己的直播間

          直播行業是當前火熱的行業,誰都想從中分得一杯羹,直播養活了一大批人,一個平臺主播粗略估計就有幾千號人,但是實時在線觀看量有的居然到了驚人的百萬級別,特別是游戲主播,可想而知,直播間是一個磁鐵式的廣告傳播媒介,也難怪這么多巨頭公司都搶著做直播。我不太清楚直播行業技術有多深,畢竟自己沒做過,但是咱們可以自己實現一個滿足幾百號人同時觀看的直播間。


          最終成果

          • 演示地址(電腦端與移動端效果不同哦)
          • 服務端項目地址https://github.com/jack-hoo/LiveRoomDemo_Server.git
          • 客戶端項目地址

          手機端效果

          這個場景很熟悉吧~~ 通過obs推流軟件來推流。

          戶外直播,通過yasea手機端推流軟件,使用手機攝像頭推流。

          電腦端效果
          播放香港衛視

          直播畫面

          項目總覽

          項目分為三個部分:

          1. 客戶端

          直播間視頻拉流、播放和聊天室,炫酷的彈幕以及直播間信息

          1. 服務端

          處理直播間、用戶的數據業務,聊天室消息的處理

          1. 服務器部署
            視頻服務器和web服務器

          技術棧

          移動客戶端

          • VUE全家桶
          • UI層vonic
          • axios
          • 視頻播放器: vue-video-player + videojs-contrib-hls
          • websocket客戶端: vue-stomp
          • 彈幕插件: vue-barrage
          • 打包工具:webpack

          電腦端客戶端

          • 項目架構: Jquery + BootStrap
          • 視頻播放器: video.js
          • websocket客戶端: stomp.js + sockjs.js
          • 彈幕插件: Jquery.danmu.js
          • 模版引擎: thymeleaf

          服務端

          • IDE: IntelliJ IDEA
          • 項目架構: SpringBoot1.5.4 +Maven3.0
          • 主數據庫: Mysql5.7
          • 輔數據庫: redis3.2
          • 數據庫訪問層: spring-boot-starter-data-jpa + spring-boot-starter-data-redis
          • websocket: spring-boot-starter-websocket
          • 消息中間件: RabbitMQ/3.6.10

          服務器部署

          • 視頻直播模塊: nginx-rtmp-module
          • web應用服務器: tomcat8.0
          • 服務器: 騰訊云centos6.5

          技術點講解

          直播間主要涉及到兩個主要功能:第一是視頻直播、第二是聊天室。這兩個都是非常講究實時性。

          • 視頻直播

          說到直播我們先了解下幾個常用的直播流協議,看了挺多的流媒體協議文章博客,但都是非常粗略,這里有個比較詳細的 流媒體協議介紹,如果想詳細了解協議內容估計去要看看專業書籍了。這里我們用到的只是rtmp和hls,實踐后發現:rtmp只能夠在電腦端播放,hls只能夠在手機端播放。而且rtmp是相當快的盡管沒有rtsp那么快,延遲只有幾秒,我測試的就差不多2-5秒,但是hls大概有10幾秒。所以如果你體驗過demo,就會發現手機延遲比較多。

          直播的流程:
          直播分為推流和拉流兩個過程,那么流推向哪里,拉流又從哪里拉取呢?那當然需要視頻服務器啦,千萬不要以為視頻直播服務器很復雜,其實在nginx服務器中一切都變得簡單。后面我會講解如何部署Nginx服務器并配置視頻模塊(nginx-rtmp-module).

          首先主播通過推流軟件,比如OBS Studio推流軟件,這個是比較專業級別的,很多直播平臺的推薦主播使用這個軟件來推送視頻流,這里我也推薦一個開源的安卓端推流工具Yasea,下載地址,文件很小,但是很強大。
          直播內容推送到服務器后,就可以在服務器端使用視頻編碼工具進行轉碼了,可以轉換成各種高清,標清,超清的分辨率視頻,也就是為什么我們在各個視頻網站都可以選擇視頻清晰度。這里我們沒有轉碼,只是通過前端視頻播放器(video.js)來拉取視頻.這樣整個視頻推流拉流過程就完成了。

          • 聊天室

          直播間里面的聊天室跟我們的群聊天差不多,只不過它變成了web端,web端的即時通信方案有很多,這里我們選擇websocket協議來與服務端通信,websocket是基于http之上的傳輸協議,客戶端向服務端發送http請求,并攜帶Upgrade:websocket升級頭信息表示轉換websocket協議,通過與服務端握手成功后就可以建立tcp通道,由此來傳遞消息,它與http最大的差別就是,服務端可以主動向客戶端發送消息。

          既然建立了消息通道,那我們就需要往通道里發消息,但是總得需要一個東西來管控消息該發給誰吧,要不然全亂套了,所以我們選擇了消息中間件RabbitMQ.使用它來負責消息的路由去向。


          理論知識都講完啦,實操時間到!

          移動客戶端實操

          源碼地址

          工程結構

          |—— build                        構建服務和webpack配置        
          |—— congfig                      項目不同環境的配置
          |—— dist                         build生成生產目錄
          |—— static                       靜態資源
          |—— package.json                 項目配置文件
          |—— src                          開發源代碼目錄
              |—— api                      通過axios導出的api目錄
              |—— components               頁面和組件
              |—— public                   公有組件
              |—— vuex                     全局狀態
              |—— main.js                  應用啟動配置點

          功能模塊

          • 拉取服務器的直播視頻流(hls)并播放直播畫面
          • 與服務端創建websocket連接,收發聊天室消息
          • 通過websocket獲取消息并發送到彈幕
          • 通過websocket實時更新在線用戶
          • 結合服務端獲取訪問歷史記錄
          • 問題反饋模塊

          效果圖

          項目說明

          請參考源碼 https://github.com/jack-hoo/LiveRoomDemo_Server.git

          服務端實操

          源碼地址 https://github.com/jack-hoo/LiveRoomDemo_Server.git

          由于個人比較喜歡接觸新的東西,所以后端選擇了springboot,前端選擇了Vue.js年輕人嘛總得跟上潮流。SpringBoot實踐過后發現真的太省心了,不用再理會各種配置文件,全自動化裝配。
          這里貼一下pom.xml

          <?xml version="1.0" encoding="UTF-8"?>
          <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
              <modelVersion>4.0.0</modelVersion>
          
              <groupId>com.hushangjie</groupId>
              <artifactId>rtmp-demo</artifactId>
              <version>0.0.1-SNAPSHOT</version>
              <packaging>jar</packaging>
          
              <name>rtmp-demo</name>
              <description>Demo project for Spring Boot</description>
          
              <parent>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-parent</artifactId>
                  <version>1.5.4.RELEASE</version>
                  <relativePath/> <!-- lookup parent from repository -->
              </parent>
          
              <properties>
                  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                  <java.version>1.8</java.version>
              </properties>
          
              <dependencies>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-devtools</artifactId>
                      <optional>true</optional>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-actuator</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-actuator-docs</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-data-jpa</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-data-redis</artifactId>
                  </dependency>
                  <!--<dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-security</artifactId>
                  </dependency>-->
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-thymeleaf</artifactId>
                  </dependency>
                  <!--非嚴格模式解析HTML5-->
                  <dependency>
                      <groupId>net.sourceforge.nekohtml</groupId>
                      <artifactId>nekohtml</artifactId>
                      <version>1.9.22</version>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-web</artifactId>
                      <!-- 打包成war時可以移除嵌入式tomcat插件 -->
                      <!--<exclusions>
                          <exclusion>
                              <groupId>org.springframework.boot</groupId>
                              <artifactId>spring-boot-starter-tomcat</artifactId>
                          </exclusion>
                      </exclusions>-->
                  </dependency>
                  <!--<dependency>
                      <groupId>javax.servlet</groupId>
                      <artifactId>javax.servlet-api</artifactId>
                      <version>3.1.0</version>
                      <scope>provided</scope>
                  </dependency>-->
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-websocket</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-test</artifactId>
                      <scope>test</scope>
                  </dependency>
                  <dependency>
                      <groupId>org.webjars</groupId>
                      <artifactId>vue</artifactId>
                      <version>2.1.3</version>
                  </dependency>
                  <dependency>
                      <groupId>mysql</groupId>
                      <artifactId>mysql-connector-java</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>joda-time</groupId>
                      <artifactId>joda-time</artifactId>
                      <version>2.9.2</version>
                  </dependency>
                  <!-- RabbitMQ相關配置-->
                  <dependency>
                      <groupId>io.projectreactor</groupId>
                      <artifactId>reactor-core</artifactId>
                      <version>2.0.8.RELEASE</version>
                  </dependency>
          
                  <dependency>
                      <groupId>io.projectreactor</groupId>
                      <artifactId>reactor-net</artifactId>
                      <version>2.0.8.RELEASE</version>
                  </dependency>
          
                  <dependency>
                      <groupId>io.netty</groupId>
                      <artifactId>netty-all</artifactId>
                      <version>4.1.6.Final</version>
                  </dependency>
          
              </dependencies>
          
              <build>
                  <plugins>
                      <plugin>
                          <groupId>org.springframework.boot</groupId>
                          <artifactId>spring-boot-maven-plugin</artifactId>
                          <configuration>
                              <fork>true</fork>
                          </configuration>
                      </plugin>
                  </plugins>
              </build>
          
          
          </project>
          

          application.properties文件

          spring.datasource.url=jdbc:mysql://host:3306/database?characterEncoding=utf8&useSSL=false
          spring.datasource.username=username
          spring.datasource.password=password
          spring.datasource.driver-class-name=com.mysql.jdbc.Driver
          spring.thymeleaf.mode=LEGACYHTML5
          server.port=8085
          # REDIS (RedisProperties)
          # Redis數據庫索引(默認為0)
          spring.redis.database=0  
          # Redis服務器地址
          spring.redis.host=127.0.0.1
          # Redis服務器連接端口
          spring.redis.port=6379  
          # Redis服務器連接密碼(默認為空)
          spring.redis.password=# 連接池最大連接數(使用負值表示沒有限制)
          spring.redis.pool.max-active=8  
          # 連接池最大阻塞等待時間(使用負值表示沒有限制)
          spring.redis.pool.max-wait=-1  
          # 連接池中的最大空閑連接
          spring.redis.pool.max-idle=8  
          # 連接池中的最小空閑連接
          spring.redis.pool.min-idle=0  
          # 連接超時時間(毫秒)
          spring.redis.timeout=0 

          websocket配置

          @Configuration
          @EnableWebSocketMessageBroker
          public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
              //攔截器注入service失敗解決辦法
              @Bean
              public MyChannelInterceptor myChannelInterceptor(){
                  return new MyChannelInterceptor();
              }
              @Override
              public void registerStompEndpoints(StompEndpointRegistry registry) {
                  //添加訪問域名限制可以防止跨域socket連接
                  //setAllowedOrigins("http://localhost:8085")
                  registry.addEndpoint("/live").setAllowedOrigins("*").addInterceptors(new HandShkeInceptor()).withSockJS();
          
              }
          
              @Override
              public void configureMessageBroker(MessageBrokerRegistry registry) {
                  /*.enableSimpleBroker("/topic","/queue");*/
                  //假如需要第三方消息代理,比如rabitMQ,activeMq,在這里配置
                  registry.setApplicationDestinationPrefixes("/demo")
                          .enableStompBrokerRelay("/topic","/queue")
                          .setRelayHost("127.0.0.1")
                          .setRelayPort(61613)
                          .setClientLogin("guest")
                          .setClientPasscode("guest")
                          .setSystemLogin("guest")
                          .setSystemPasscode("guest")
                          .setSystemHeartbeatSendInterval(5000)
                          .setSystemHeartbeatReceiveInterval(4000);
              }
          
              @Override
              public void configureClientInboundChannel(ChannelRegistration registration) {
                  ChannelRegistration channelRegistration=registration.setInterceptors(myChannelInterceptor());
                  super.configureClientInboundChannel(registration);
              }
          
              @Override
              public void configureClientOutboundChannel(ChannelRegistration registration) {
                  super.configureClientOutboundChannel(registration);
              }
          
          }
          

          配置類繼承了消息代理配置類,意味著我們將使用消息代理rabbitmq.使用registerStompEndpoints方法注冊一個websocket終端連接。這里我們需要了解兩個東西,第一個是stomp和sockjs,sockjs是啥呢,其實它是對于websocket的封裝,因為如果單純使用websocket的話效率會非常低,我們需要的編碼量也會增多,而且如果瀏覽器不支持websocket,sockjs會自動降級為輪詢策略,并模擬websocket,保證客戶端和服務端可以通信。
          stomp有是什么看這里

          stomp是一種簡單(流)文本定向消息協議,它提供了一個可互操作的連接格式,允許STOMP客戶端與任意STOMP消息代理(Broker)進行交互,也就是我們上面的RabbbitMQ,它就是一個消息代理。
          我們可以通過configureMessageBroker來配置消息代理,需要注意的是我們將要部署的服務器也應該要有RabbitMQ,因為它是一個中間件,安裝非常容易,這里就不說明了。這里我們配置了“/topic,/queue”兩個代理轉播策略,就是說客戶端訂閱了前綴為“/topic,/queue”頻道都會通過消息代理(RabbitMQ)來轉發。跟spring沒啥關系啦,完全解耦。

          websocke如何保證安全

          一開始接觸 stomp的時候一直有個問題困擾我,客戶端只要與服務端通過websocket建立了連接,那么他就可以訂閱任何內容,意味著可以接受任何消息,這樣豈不是亂了套啦,于是我翻閱了大量博客文章,很多都是官方的例子并沒有解決實際問題。經過琢磨,其實websocket是要考慮安全性的。具體在以下幾個方面

          1. 跨域websocket連接
          2. 協議升級前握手攔截器
          3. 消息信道攔截器

          對于跨域問題,我們可以通過setAllowedOrigins方法來設置可連接的域名,防止跨站連接。

          對于站內用戶是否允許連接我們可以如下配置

          public class HandShkeInceptor extends HttpSessionHandshakeInterceptor {
              private static final Set<UserEntity> ONLINE_USERS=new HashSet<>();
              @Override
              public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
          
                  System.out.println("握手前"+request.getURI());
                  //http協議轉換websoket協議進行前,通常這個攔截器可以用來判斷用戶合法性等
                  //鑒別用戶
                 if (request instanceof ServletServerHttpRequest) {
                      ServletServerHttpRequest servletRequest=(ServletServerHttpRequest) request;
                     //這句話很重要如果getSession(true)會導致移動端無法握手成功
                     //request.getSession(true):若存在會話則返回該會話,否則新建一個會話。
                     //request.getSession(false):若存在會話則返回該會話,否則返回NULL
                     //HttpSession session=servletRequest.getServletRequest().getSession(false);
                      HttpSession session=servletRequest.getServletRequest().getSession();
                      UserEntity user=(UserEntity) session.getAttribute("user");
                      if (user !=null) {
                          //這里只使用簡單的session來存儲用戶,如果使用了springsecurity可以直接使用principal
                          return super.beforeHandshake(request, response, wsHandler, attributes);
                      }else {
                          System.out.println("用戶未登錄,握手失敗!");
                          return false;
                      }
                  }
                  return false;
              }
          
              @Override
              public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
                  //握手成功后,通常用來注冊用戶信息
                  System.out.println("握手后");
                  super.afterHandshake(request, response, wsHandler, ex);
              }
          }

          HttpSessionHandshakeInterceptor 這個攔截器用來管理握手和握手后的事情,我們可以通過請求信息,比如token、或者session判用戶是否可以連接,這樣就能夠防范非法用戶。

          那如何限制用戶只能訂閱指定內容呢?我們接著往下看

          public class MyChannelInterceptor extends ChannelInterceptorAdapter {
              @Autowired
              private StatDao statDao;
              @Autowired
              private SimpMessagingTemplate simpMessagingTemplate;
          
              @Override
              public boolean preReceive(MessageChannel channel) {
                  System.out.println("preReceive");
                  return super.preReceive(channel);
              }
          
              @Override
              public Message<?> preSend(Message<?> message, MessageChannel channel) {
                  StompHeaderAccessor accessor=StompHeaderAccessor.wrap(message);
                  StompCommand command=accessor.getCommand();
                  //檢測用戶訂閱內容(防止用戶訂閱不合法頻道)
                  if (StompCommand.SUBSCRIBE.equals(command)) {
                      //從數據庫獲取用戶訂閱頻道進行對比(這里為了演示直接使用set集合代替)
                      Set<String> subedChannelInDB=new HashSet<>();
                      subedChannelInDB.add("/topic/group");
                      subedChannelInDB.add("/topic/online_user");
                      if (subedChannelInDB.contains(accessor.getDestination())) {
                          //該用戶訂閱的頻道合法
                          return super.preSend(message, channel);
                      } else {
                          //該用戶訂閱的頻道不合法直接返回null前端用戶就接受不到該頻道信息。
                          return null;
                      }
                  } else {
                      return super.preSend(message, channel);
                  }
          
              }
              @Override
              public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
                  //System.out.println("afterSendCompletion");
                  //檢測用戶是否連接成功,搜集在線的用戶信息如果數據量過大我們可以選擇使用緩存數據庫比如redis,
                  //這里由于需要頻繁的刪除和增加集合內容,我們選擇set集合來存儲在線用戶
                  StompHeaderAccessor accessor=StompHeaderAccessor.wrap(message);
                  StompCommand command=accessor.getCommand();
                  if (StompCommand.SUBSCRIBE.equals(command)){
                      Map<String,UserEntity> map=(Map<String, UserEntity>) accessor.getHeader("simpSessionAttributes");
                      //ONLINE_USERS.add(map.get("user"));
                      UserEntity user=map.get("user");
                      if(user !=null){
                          statDao.pushOnlineUser(user);
                          Guest guest=new Guest();
                          guest.setUserEntity(user);
                          guest.setAccessTime(Calendar.getInstance().getTimeInMillis());
                          statDao.pushGuestHistory(guest);
                          //通過websocket實時返回在線人數
                          this.simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());
                      }
          
                  }
                  //如果用戶斷開連接,刪除用戶信息
                  if (StompCommand.DISCONNECT.equals(command)){
                      Map<String,UserEntity> map=(Map<String, UserEntity>) accessor.getHeader("simpSessionAttributes");
                      //ONLINE_USERS.remove(map.get("user"));
                      UserEntity user=map.get("user");
                      if (user !=null){
                          statDao.popOnlineUser(user);
                          simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());
                      }
          
                  }
                  super.afterSendCompletion(message, channel, sent, ex);
              }
          
          }

          在stomp里面,Channel信道就是消息傳送的通道,客戶端與服務端建立了連接就相當于建立了通道,以后的信息就是通過這個通道來傳輸。所有的消息都有消息頭,被封裝在了spring 的messag接口中,比如建立連接時候消息頭就含有CONNECT,當然還有一些其他的信息。客戶端訂閱的時候也有訂閱頭信息SUBSCRIBE,那么我是不是可以在這個攔截器ChannelInterceptorAdapter 中攔截每個人的訂閱信息,然后與數據庫的信息作比對,最后決定這個用戶是否可以訂閱這個頻道的信息呢,對的,這是我的想法,按照這樣的思路,做單聊不是迎刃而解了嗎。
          那客戶端通過websocket發送的消息如何到達訂閱者手中呢,按照rabbitmq的規則,訂閱者屬于消費者,發送消息的一方屬于生產者,生產者通過websocket把消息發送到服務端,服務端通過轉發給消息代理(rabbitmq),消息代理負責存儲消息,管理發送規則,推送消息給訂閱者,看下面的代碼

              @MessageMapping(value="/chat")
              @SendTo("/topic/group")
              public MsgEntity testWst(String message , @Header(value="simpSessionAttributes") Map<String,Object> session){
                  UserEntity user=(UserEntity) session.get("user");
                  String username=user.getRandomName();
                  MsgEntity msg=new MsgEntity();
                  msg.setCreator(username);
                  msg.setsTime(Calendar.getInstance());
                  msg.setMsgBody(message);
                  return msg;
              }

          @MessageMapping看起來跟springmvc方法特別像,它即可以用在類級別上也可以用在方法級別上
          當發送者往‘/chat’發送消息后,服務端接受到消息,再發送給“/topic/group”的訂閱者,@SendTo就是發送給誰,這里需要注意的有,如果我們沒有配置消息代理,只使用了enableSimpleBroker("/topic","/queue")簡單消息代理,那么就是直接發送到消息訂閱者,如果配置了消息代理,那還要通過消息代理,由它來轉發。

          如果我們想在服務端隨時發送消息,而不是在客戶端發送(這樣的場景很常見,比如發送全局通知),可以使用SimpMessagingTemplate類,通過注入該bean,在合適的業務場景中發送消息。

          Redis統計數據

          直播間經常需要統計數據,比如實時在線人數,訪問量,貢獻排行榜,訂閱量。我選擇的方案是使用redis來計數,盡管這個demo可能不會太多人訪問,但是我的目的是學習如何使用redis
          先看springboot中redis的配置

          @Configuration
          public class RedisConfig extends CachingConfigurerSupport{
              /**
               * 生成key的策略
               *
               * @return
               */
              @Bean
              public KeyGenerator keyGenerator() {
                  return new KeyGenerator() {
                      @Override
                      public Object generate(Object target, Method method, Object... params) {
                          StringBuilder sb=new StringBuilder();
                          sb.append(target.getClass().getName());
                          sb.append(method.getName());
                          for (Object obj : params) {
                              sb.append(obj.toString());
                          }
                          return sb.toString();
                      }
                  };
              }
          
              /**
               * 管理緩存
               *
               * @param redisTemplate
               * @return
               */
              @SuppressWarnings("rawtypes")
              @Bean
              public CacheManager cacheManager(RedisTemplate redisTemplate) {
                  RedisCacheManager rcm=new RedisCacheManager(redisTemplate);
                  //設置緩存過期時間
                  // rcm.setDefaultExpiration(60);//秒
                  //設置value的過期時間
                  Map<String,Long> map=new HashMap();
                  map.put("test",60L);
                  rcm.setExpires(map);
                  return rcm;
              }
          
              /**
               * RedisTemplate配置
               * @param factory
               * @return
               */
              @Bean
              public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
                  StringRedisTemplate template=new StringRedisTemplate(factory);
                  Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
                  ObjectMapper om=new ObjectMapper();
                  om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
                  om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
                  jackson2JsonRedisSerializer.setObjectMapper(om);
                  template.setValueSerializer(jackson2JsonRedisSerializer);//如果key是String 需要配置一下StringSerializer,不然key會亂碼 /XX/XX
                  template.afterPropertiesSet();
                  //template.setStringSerializer();
                  return template;
              }
          }

          redis數據統計Dao的實現

          @Repository
          public class StatDao {
              @Autowired
              RedisTemplate redisTemplate;
              public void pushOnlineUser(UserEntity userEntity){
                  redisTemplate.opsForSet().add("OnlineUser",userEntity);
              }
              public void popOnlineUser(UserEntity userEntity){
                  redisTemplate.opsForSet().remove("OnlineUser" ,userEntity);
              }
              public Set getAllUserOnline(){
                  return redisTemplate.opsForSet().members("OnlineUser");
              }
              public void pushGuestHistory(Guest guest){
                  //最多存儲指定個數的訪客
                  if (redisTemplate.opsForList().size("Guest")==200l){
                      redisTemplate.opsForList().rightPop("Guest");
                  }
                  redisTemplate.opsForList().leftPush("Guest",guest);
              }
              public List getGuestHistory(){
                  return redisTemplate.opsForList().range("Guest",0,-1);
              }
          }
          

          Dao層非常簡單,因為我們只需要統計在線人數和訪客。但是在線人數是實時更新的,既然我們使用了websocket實時數據更新就非常容易了,前面我們講過,通過信道攔截器可以攔截連接,訂閱,斷開連接等等事件信息,所以我們就可以當用戶連接時存儲在線用戶,通過websocket返回在線用戶信息。

          public class MyChannelInterceptor extends ChannelInterceptorAdapter {
              @Autowired
              private StatDao statDao;
              @Autowired
              private SimpMessagingTemplate simpMessagingTemplate;
          
              @Override
              public boolean preReceive(MessageChannel channel) {
                  System.out.println("preReceive");
                  return super.preReceive(channel);
              }
          
              @Override
              public Message<?> preSend(Message<?> message, MessageChannel channel) {
                  StompHeaderAccessor accessor=StompHeaderAccessor.wrap(message);
                  StompCommand command=accessor.getCommand();
                  //檢測用戶訂閱內容(防止用戶訂閱不合法頻道)
                  if (StompCommand.SUBSCRIBE.equals(command)) {
                      //從數據庫獲取用戶訂閱頻道進行對比(這里為了演示直接使用set集合代替)
                      Set<String> subedChannelInDB=new HashSet<>();
                      subedChannelInDB.add("/topic/group");
                      subedChannelInDB.add("/topic/online_user");
                      if (subedChannelInDB.contains(accessor.getDestination())) {
                          //該用戶訂閱的頻道合法
                          return super.preSend(message, channel);
                      } else {
                          //該用戶訂閱的頻道不合法直接返回null前端用戶就接受不到該頻道信息。
                          return null;
                      }
                  } else {
                      return super.preSend(message, channel);
                  }
          
              }
              @Override
              public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
                  //System.out.println("afterSendCompletion");
                  //檢測用戶是否連接成功,搜集在線的用戶信息如果數據量過大我們可以選擇使用緩存數據庫比如redis,
                  //這里由于需要頻繁的刪除和增加集合內容,我們選擇set集合來存儲在線用戶
                  StompHeaderAccessor accessor=StompHeaderAccessor.wrap(message);
                  StompCommand command=accessor.getCommand();
                  if (StompCommand.SUBSCRIBE.equals(command)){
                      Map<String,UserEntity> map=(Map<String, UserEntity>) accessor.getHeader("simpSessionAttributes");
                      //ONLINE_USERS.add(map.get("user"));
                      UserEntity user=map.get("user");
                      if(user !=null){
                          statDao.pushOnlineUser(user);
                          Guest guest=new Guest();
                          guest.setUserEntity(user);
                          guest.setAccessTime(Calendar.getInstance().getTimeInMillis());
                          statDao.pushGuestHistory(guest);
                          //通過websocket實時返回在線人數
                          this.simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());
                      }
          
                  }
                  //如果用戶斷開連接,刪除用戶信息
                  if (StompCommand.DISCONNECT.equals(command)){
                      Map<String,UserEntity> map=(Map<String, UserEntity>) accessor.getHeader("simpSessionAttributes");
                      //ONLINE_USERS.remove(map.get("user"));
                      UserEntity user=map.get("user");
                      if (user !=null){
                          statDao.popOnlineUser(user);
                          simpMessagingTemplate.convertAndSend("/topic/online_user",statDao.getAllUserOnline());
                      }
          
                  }
                  super.afterSendCompletion(message, channel, sent, ex);
              }
          
          }

          由于這個項目有移動端和電腦端,所以需要根據請求代理UserAgent來判斷客戶端屬于哪一種類型。這個工具類在源碼上有。我就不貼了。

          服務器部署

          說了這么多即時通信,卻沒發現視頻直播。不要著急我們馬上進入視頻環節。文章開頭就說明了幾種媒體流協議,這里不講解詳細的協議流程,只需要知道,我們是通過推流軟件采集視頻信息,如何采集也不是我們關注的。采集到信息后通過軟件來推送到指定的服務器,如下圖

          obs推流設置

          yasea手機端推流設置

          紅色部分是服務器開放的獲取流接口。

          Nginx-rtmp-module配置

          視頻服務器有很多,也支持很多媒體流協議。這里我們選擇nginx-rtmp-module來做視頻服務,接下來我們需要在linux下安裝nginx,并安裝rtmp模塊。本人也是linux初學者,一步步摸索著把服務器搭建好,聽說tomcat和nginx很配哦,所以作為免費開源的當然首選這兩個。
          接下來需要在linux安裝一下軟件和服務。

          1. Nginx以及Nginx-rtmp-module
          2. Tomcat
          3. Mysql
          4. Redis
          5. RabbitMQ

          安裝步驟我就不說了,大家搜索一下啦,這里貼一下nginx.conf文件配置

          rtmp {
              server {
                  listen 1935;
                  chunk_size 4096;
          
                  application video {
                          play /yjdata/www/www/video;
                  }
                  application live {
                          live on;
                          hls on;
                          hls_path /yjdata/www/www/live/hls/;
                          hls_fragment 5s;
                  }
              }
          }
          

          上面代碼是配置rtmp模塊, play /yjdata/www/www/video 指的是配置點播模塊,可以直接播放/yjdata/www/www/video路徑下的視頻。hls_path制定hls分塊存放路徑,因為hls是通過獲取到推送的視頻流信息,分塊存儲在服務器。所以它的延時比rtmp要更高。

           server {
                  listen       80;
                  server_name  localhost;
          
                  #charset koi8-r;
                  index index.jsp index.html;
                  root /yjdata/www/www;
                  #access_log  logs/host.access.log  main;
          
                  location / {
                      proxy_pass  http://127.0.0.1:8080;
                  }
                  location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css|docx|pdf|doc|ppt|html|properties)$ {
                          expires 30d;
                          root /yjdata/www/www/static/;
                  }
                  location /hls {
                      types {
                          application/vnd.apple.mpegurl m3u8;
                          #application/x-mpegURL;
                          video/mp2t ts;
                      }
                      alias /yjdata/www/www/live/hls/;
                      expires -1;
                      add_header Cache-Control no-cache;
                  }
          
                  location /stat {
                           rtmp_stat all;
                           rtmp_stat_stylesheet stat.xsl;
                  }
          
                  location /stat.xsl {
                          root /soft/nginx/nginx-rtmp-module/;
                   }
          

          上面配置了location 指向/hls,別名是/yjdata/www/www/live/hls/,所以可以在前端直接通過域名+/hls/+文件名.m3u8獲取直播視頻。
          關于nginx的配置還有很多,我也在學習當中。總而言之nginx非常強大。

          總結

          通過從前端=>后臺=>服務器,整個流程走下來還是需要花很多心思。但是收獲也是很

          名和網址是互聯網中經常使用的術語,它們之間有一些關聯性,但也存在明顯的區別。讓我們分別了解一下它們的定義和功能。

          域名(Domain Name): 域名是用于在互聯網上表示某個網站或網絡服務的一種標識符。它是一個由字母、數字和連字符組成的字符串,通常以點(.)分隔的多個部分構成。例如,“example.com”就是一個域名。域名的作用是幫助用戶更方便地記住和訪問網站,而不需要記住復雜數字組成的IP地址。互聯網上的每個域名都是唯一的,并由域名系統(DNS)進行管理和解析。

          網址(URL,Uniform Resource Locator): 網址是互聯網上資源的統一標識符。它表示了一個資源(例如網頁、圖片或文件)在互聯網上的具體位置,并提供了訪問該資源所需的協議。一個完整的網址包括協議、域名、端口(可選)、路徑和查詢字符串等部分。例如,“https://www.example.com/path/to/page.html”就是一個網址。

          總結一下它們之間的區別:

          1. 定義:域名是用于表示網站或網絡服務的標識符,而網址是用于表示互聯網上資源位置的統一標識符。
          2. 組成部分:域名通常由字母、數字和連字符組成,以點(.)分隔;而網址包括協議、域名、端口(可選)、路徑和查詢字符串等部分。
          3. 功能:域名用于幫助用戶記住和訪問網站,網址則用于表示互聯網上某個資源的具體位置。

          簡單來說,域名是網址的一部分。域名幫助用戶更方便地訪問和記住網站,而網址則提供了訪問特定網絡資源的詳細信息。

          名是什么,很多人都好奇,其實域名就是網站的網址,比如說這個網址all55.com,它就是一個域名。all55這是域名的前綴,這個是用戶自定義的。.com這個呢是域名的后綴是指代表一個域名類型的符號。 不同后綴的域名有不同的含義。目前共分為兩類域名格式:國別域名(ccTLD),例如中國的.cn、美國的.us、俄羅斯的.ru、以及國際通用域名(gTLD),例如.com、.xyz、.top、.wang、pub、.xin、.net等1000多種,所有域名后綴作用無差異,僅外觀和本身含義不同,但只有少數例如舉例中的域名后綴可以在國內支持網站的備案。只有在工信部備案,網站才能在中國范圍正常運營。

          每個域名都指向一個IP地址,相當與我們遠程鏈接別人的電腦一樣,域名指向的是文件夾,我們訪問域名的是時候就相當于訪問了一個個文件夾,通過瀏覽器展將html頁面顯現出來。點擊網頁的其他鏈接就跳轉到其他的html文件展現給我們。

          而這些頁面就是網站工程師通過代碼編寫出來的了。


          主站蜘蛛池模板: 天美传媒一区二区三区| 日韩一区二区a片免费观看| 精品少妇一区二区三区在线| 国产高清在线精品一区二区| 精品国产区一区二区三区在线观看 | 亚洲无线码在线一区观看| 无码精品人妻一区二区三区漫画| 亚洲sm另类一区二区三区| 北岛玲在线一区二区| 国产婷婷一区二区三区| 麻豆文化传媒精品一区二区| 精品一区二区久久久久久久网精| 一区二区三区免费电影| 日韩免费无码一区二区三区| 在线视频亚洲一区| 国产婷婷色一区二区三区| 亚洲欧美日韩一区二区三区| 亚洲国产精品一区二区第一页免 | 丰满人妻一区二区三区视频53| 国99精品无码一区二区三区| 韩国一区二区三区| 国产成人一区二区三区视频免费| 午夜AV内射一区二区三区红桃视| 亚洲免费一区二区| 亚洲国产AV一区二区三区四区| 中日韩一区二区三区| 欧美日韩综合一区二区三区| 亚洲毛片αv无线播放一区| 在线观看国产一区亚洲bd| 亚洲欧洲精品一区二区三区| V一区无码内射国产| 亚洲日韩AV一区二区三区四区 | 久久久久人妻精品一区蜜桃| 国产精品亚洲专一区二区三区| 一区二区在线免费观看| 久99精品视频在线观看婷亚洲片国产一区一级在线 | 国产在线精品一区二区| 日韩毛片基地一区二区三区| 亚洲一区二区三区香蕉| 深夜福利一区二区| 免费一本色道久久一区|