首頁技術文章正文

使用WebSocket實現(xiàn)網(wǎng)頁聊天室

更新時間:2022-11-17 來源:黑馬程序員 瀏覽量:

  一、文章導讀

  服務器推送你還在使用輪詢嗎?本文將帶你領略WebSocket的魅力,輕松實現(xiàn)服務器推送功能。本文將以下面兩方面讓你理解WebSocket并應用到具體的開發(fā)中。

  WebSocket概述

  使用WebSocket實現(xiàn)網(wǎng)頁聊天室

  二、WebSocket

      2.WebSocket介紹

  WebSocket 是一種網(wǎng)絡通信協(xié)議。RFC6455 定義了它的通信標準。

  WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協(xié)議。

  HTTP 協(xié)議是一種無狀態(tài)的、無連接的、單向的應用層協(xié)議。它采用了請求/響應模型。通信請求只能由客戶端發(fā)起,服務端對請求做出應答處理。

  這種通信模型有一個弊端:HTTP 協(xié)議無法實現(xiàn)服務器主動向客戶端發(fā)起消息。

  這種單向請求的特點,注定了如果服務器有連續(xù)的狀態(tài)變化,客戶端要獲知就非常麻煩。大多數(shù) Web 應用程序將通過頻繁的異步 AJAX 請求實現(xiàn)長輪詢。輪詢的效率低,非常浪費資源(因為必須不停連接,或者 HTTP 連接始終打開)。

  http協(xié)議:

1668665195820_1.jpg

  websocket協(xié)議:

1668665211246_2.jpg

  2. websocket協(xié)議

  本協(xié)議有兩部分:握手和數(shù)據(jù)傳輸。

  握手是基于http協(xié)議的。

  來自客戶端的握手看起來像如下形式:

GET ws://localhost/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Version: 13

  來自服務器的握手看起來像如下形式:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Extensions: permessage-deflate

  字段說明:

| 頭名稱                    | 說明                                                         |
| :------------------------ | ------------------------------------------------------------ |
| Connection:Upgrade       | 標識該HTTP請求是一個協(xié)議升級請求                             |
| Upgrade: WebSocket        | 協(xié)議升級為WebSocket協(xié)議                                      |
| Sec-WebSocket-Version: 13 | 客戶端支持WebSocket的版本                                    |
| Sec-WebSocket-Key:       | 客戶端采用base64編碼的24位隨機字符序列,服務器接受客戶端HTTP協(xié)議升級的證明。要求服務端響應一個對應加密的Sec-WebSocket-Accept頭信息作為應答 |
| Sec-WebSocket-Extensions  | 協(xié)議擴展類型                                                 |

  3. 客戶端(瀏覽器)實現(xiàn)

  3.1 websocket對象

  實現(xiàn) WebSockets 的 Web 瀏覽器將通過 WebSocket 對象公開所有必需的客戶端功能(主要指支持 Html5 的瀏覽器)。

  以下 API 用于創(chuàng)建 WebSocket 對象:

var ws = new WebSocket(url);

  > 參數(shù)url格式說明: ws://ip地址:端口號/資源名稱

  3.2 websocket事件

  WebSocket 對象的相關事件

  | 事件 | 事件處理程序 | 描述 |

  | ------- | ----------------------- | -------------------------- |

  | open | websocket對象.onopen | 連接建立時觸發(fā) |

  | message | websocket對象.onmessage | 客戶端接收服務端數(shù)據(jù)時觸發(fā) |

  | error | websocket對象.onerror | 通信發(fā)生錯誤時觸發(fā) |

  | close | websocket對象.onclose | 連接關閉時觸發(fā) |

  3.3 WebSocket方法

  WebSocket 對象的相關方法:

  | 方法 | 描述 |

  | ------ | ---------------- |

  | send() | 使用連接發(fā)送數(shù)據(jù) |

  4. 服務端實現(xiàn)

  Tomcat的7.0.5 版本開始支持WebSocket,并且實現(xiàn)了Java WebSocket規(guī)范(JSR356)。

  Java WebSocket應用由一系列的WebSocketEndpoint組成。Endpoint 是一個java對象,代表WebSocket鏈接的一端,對于服務端,我們可以視為處理具體WebSocket消息的接口, 就像Servlet之與http請求一樣。

  我們可以通過兩種方式定義Endpoint:

  第一種是編程式, 即繼承類 javax.websocket.Endpoint并實現(xiàn)其方法。

  第二種是注解式, 即定義一個POJO, 并添加 @ServerEndpoint相關注解。

  Endpoint實例在WebSocket握手時創(chuàng)建,并在客戶端與服務端鏈接過程中有效,最后在鏈接關閉時結束。在Endpoint接口中明確定義了與其生命周期相關的方法, 規(guī)范實現(xiàn)者確保生命周期的各個階段調用實例的相關方法。生命周期方法如下:

  | 方法 | 含義描述 | 注解 |

  | ------- | ------------------------------------------------------------ | -------- |

  | onClose | 當會話關閉時調用。 | @OnClose |

  | onOpen | 當開啟一個新的會話時調用, 該方法是客戶端與服務端握手成功后調用的方法。 | @OnOpen |

  | onError | 當連接過程中異常時調用。 | @OnError |

  服務端如何接收客戶端發(fā)送的數(shù)據(jù)呢?

  通過為 Session 添加 MessageHandler 消息處理器來接收消息,當采用注解方式定義Endpoint時,我們還可以通過 @OnMessage 注解指定接收消息的方法。

  服務端如何推送數(shù)據(jù)給客戶端呢?

  發(fā)送消息則由 RemoteEndpoint 完成, 其實例由 Session 維護, 根據(jù)使用情況, 我們可以通過Session.getBasicRemote 獲取同步消息發(fā)送的實例 , 然后調用其 sendXxx()方法就可以發(fā)送消息, 可以通過Session.getAsyncRemote 獲取異步消息發(fā)送實例。

  服務端代碼:

@ServerEndpoint("/robin")
public class ChatEndPoint {

    private static Set<ChatEndPoint> webSocketSet = new HashSet<>();

    private Session session;

    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        System.out.println("接收的消息是:" + message);
        System.out.println(session);
        //將消息發(fā)送給其他的用戶
        for (Chat chat : webSocketSet) {
            if(chat != this) {
                chat.session.getBasicRemote().sendText(message);
            }
        }
    }

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);
    }

    @OnClose
    public void onClose(Session seesion) {
        System.out.println("連接關閉了。。。");
    }

    @OnError
    public void onError(Session session,Throwable error) {
        System.out.println("出錯了。。。。" + error.getMessage());
    }
}

  三、基于WebSocket的網(wǎng)頁聊天室

      1.需求

  通過 websocket 實現(xiàn)一個簡易的聊天室功能 。

  1). 登陸聊天室

1668665475662_3.jpg

  2). 登陸之后,進入聊天界面進行聊天

  登陸成功后,呈現(xiàn)出以后的效果:

1668665493988_4.jpg

  當我們想和李四聊天時就可以點擊 `好友列表` 中的 `李四`,效果如下:

1668665506928_5.jpg

  接下來就可以進行聊天了,“張三”的界面如下:

1668665521021_6.jpg

  “李四” 的界面如下:

1668665543064_7.jpg

  2. 實現(xiàn)流程

1668665555307_8.jpg

  3. 消息格式

  客戶端 --> 服務端

  {"toName":"張三","message":"你好"}

  服務端 --> 客戶端

  系統(tǒng)消息格式:{"isSystem":true,"fromName":null,"message":["李四","王五"]}

  推送給某一個的消息格式:{"isSystem":false,"fromName":"張三","message":"你好"}

  4. 功能實現(xiàn)

  4.1 創(chuàng)建項目,導入相關jar包的坐標

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
   
    <!--devtools熱部署-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
        <scope>true</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- 打jar包時如果不配置該插件,打出來的jar包沒有清單文件 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

  4.2 引入靜態(tài)資源文件

1668665676650_流程圖.jpg

  4.3 引入公共資源

  pojo類

/**
   * @version v1.0
   * @ClassName: Message
   * @Description: 瀏覽器發(fā)送給服務器的websocket數(shù)據(jù)
   * @Author: 黑馬程序員
   */
  public class Message {
      private String toName;
      private String message;
 
      public String getToName() {
          return toName;
      }
 
      public void setToName(String toName) {
          this.toName = toName;
      }
 
      public String getMessage() {
          return message;
      }
 
      public void setMessage(String message) {
          this.message = message;
      }
  }
/**
   * @version v1.0
   * @ClassName: ResultMessage
   * @Description: 服務器發(fā)送給瀏覽器的websocket數(shù)據(jù)
   * @Author: 黑馬程序員
   */
  public class ResultMessage {
 
      private boolean isSystem;
      private String fromName;
      private Object message;//如果是系統(tǒng)消息是數(shù)組
 
      public boolean getIsSystem() {
          return isSystem;
      }
 
      public void setIsSystem(boolean isSystem) {
          this.isSystem = isSystem;
      }
 
      public String getFromName() {
          return fromName;
      }
 
      public void setFromName(String fromName) {
          this.fromName = fromName;
      }
 
      public Object getMessage() {
          return message;
      }
 
      public void setMessage(Object message) {
          this.message = message;
      }
  }
/**
   * @version v1.0
   * @ClassName: Result
   * @Description: 用于登陸響應回給瀏覽器的數(shù)據(jù)
   * @Author: 黑馬程序員
   */
  public class Result {
      private boolean flag;
      private String message;
 
      public boolean isFlag() {
          return flag;
      }
 
      public void setFlag(boolean flag) {
          this.flag = flag;
      }
 
      public String getMessage() {
          return message;
      }
 
      public void setMessage(String message) {
          this.message = message;
      }
  }

  MessageUtils工具類

/**
   * @version v1.0
   * @ClassName: MessageUtils
   * @Description: 用來封裝消息的工具類
   * @Author: 黑馬程序員
   */
  public class MessageUtils {
 
      public static String getMessage(boolean isSystemMessage,String fromName, Object message) {
          try {
              ResultMessage result = new ResultMessage();
              result.setIsSystem(isSystemMessage);
              result.setMessage(message);
              if(fromName != null) {
                  result.setFromName(fromName);
              }
              ObjectMapper mapper = new ObjectMapper();
 
              return mapper.writeValueAsString(result);
          } catch (JsonProcessingException e) {
              e.printStackTrace();
          }
          return null;
      }
  }

  4.4 登陸功能實現(xiàn)

  login.html:使用異步進行請求發(fā)送

  $(function() {
      $("#btn").click(function() {
          $.get("login",$("#loginForm").serialize(),function(res) {
              if(res.flag) {
                  //跳轉到 main.html頁面
                  location.href = "main.html";
              } else {
                  $("#err_msg").html(res.message);
              }
          },"json");
      });
  })

  UserController:進行登陸邏輯處理

@RestController
  public class UserController {
 
      @RequestMapping("/login")
      public Result login(User user, HttpSession session) {
          Result result = new Result();
          if(user != null && "123".equals(user.getPassword())) {
              result.setFlag(true);
              //將用戶名存儲到session對象中
              session.setAttribute("user",user.getUsername());
          } else {
              result.setFlag(false);
              result.setMessage("登陸失敗");
          }
 
          return result;
      }
  }

  4.5 獲取當前登錄的用戶名

  main.html:頁面加載完畢后,發(fā)送請求獲取當前登錄的用戶名

  var username;
  $(function() {
      $.ajax({
          url:"getUsername",
          success:function(res) {
              username = res;
              $("#userName").html("用戶:" + res + "<span style='float: right;color: green'>在線</span>");
          },
          async:false
      });
  }

  UserController

  在UserController中添加一個getUsername方法,用來從session中獲取當前登錄的用戶名并響應回給瀏覽器

  @RequestMapping("/getUsername")
  public String getUsername(HttpSession session) {
      String username = (String) session.getAttribute("user");
      return username;
  }

  4.6 聊天室功能

  客戶端實現(xiàn)

  在main.html頁面實現(xiàn)前端代碼:

var toName;
          var username;
          function showChat(name) {
              toName = name;
              //清除聊天區(qū)的數(shù)據(jù)
              $("#msgs").html("");
              //現(xiàn)在聊天對話框
              $("#chatArea").css("display","inline");
            //顯示“正在和誰聊天”
              $("#chatMes").html("正在和 <font face=\"楷體\">"+toName+"</font> 聊天");
 
              //切換用戶,需要將聊天記錄渲染到聊天區(qū)
              var storeData = sessionStorage.getItem(toName);
              if(storeData != null) {
                  $("#msgs").html(storeData);
              }
          }
 
          $(function() {
              $.ajax({
                  url:"getUsername",
                  success:function(res) {
                      username = res;
                      //顯示在線信息
                      $("#userName").html(" 用戶:"+res+"<span style='float: right;color: green'>在線</span>");
                  },
                  async: false
              })
 
              //創(chuàng)建websocket
              var ws;
              if(window.WebSocket) {
                  ws = new WebSocket("ws://localhost/chat");
              }
 
              //綁定事件
              ws.onopen = function(evt) {
                  //顯示在線信息
                  $("#userName").html(" 用戶:"+username+"<span style='float: right;color: green'>在線</span>");
              }
 
              ws.onmessage = function(evt) {
                  //接收服務器推送的消息
                  var data = evt.data;
                  //將該字符串數(shù)據(jù)轉換為json
                  var res = JSON.parse(data);
                  //判斷是系統(tǒng)消息還是推送給個人的消息
                  if(res.isSystem) {
                      //系統(tǒng)消息
                      var names = res.message;
                      var userListStr = "";
                      var broadcastStr = "";
                      for(var name of names) {
                          if(name != username) {
                              userListStr += "<li class=\"rel-item\"><a onclick='showChat(\""+name+"\")'>"+name+"</a></li>";
                              broadcastStr += "<li class=\"rel-item\" style=\"color: #9d9d9d;font-family: 宋體\">您的好友 "+name+" 已上線</li>";
                          }
                      }
                      //將數(shù)據(jù)渲染到頁面
                      $("#userlist").html(userListStr);
                      $("#broadcastList").html(broadcastStr);
                  } else {
                      //非系統(tǒng)消息
                      var content = res.message;
 
                      //拼接聊天區(qū)展示的數(shù)據(jù)
                      var str = "<div class=\"msg robot\"><div class=\"msg-left\" worker=\"\"><div class=\"msg-host photo\" style=\"background-image: url(img/avatar/Member002.jpg)\"></div><div class=\"msg-ball\">"+content+"</div></div></div>";
 
 
                      //有可能現(xiàn)在不是和指定用戶的聊天框,所以需要進行判斷
                      var storeData = sessionStorage.getItem(res.fromName);
                      if(storeData != null) {
                          storeData += str;
                      } else {
                          storeData = str;
                      }
                      sessionStorage.setItem(res.fromName,storeData);
                      if(toName == res.fromName) {
                          //將數(shù)據(jù)追加到聊天區(qū)
                          $("#msgs").append(str);
                      }
                  }
              }
 
              ws.onclose = function() {
                  //顯示在線信息
                  $("#userName").html(" 用戶:"+username+"<span style='float: right;color: red'>離線</span>");
              }
 
              //給發(fā)送按鈕綁定點擊事件
              $("#submit").click(function() {
                  //獲取輸入的內(nèi)容
                  var data = $("#context_text").val();
                  //將該文本框清空
                  $("#context_text").val("");
                  //拼接消息
                  var str = "<div class=\"msg guest\"><div class=\"msg-right\"><div class=\"msg-host headDefault\"></div><div class=\"msg-ball\">"+data+"</div></div></div>";
                  $("#msgs").append(str);
                  //將聊天記錄進行存儲sessionStorage
                  var storeData = sessionStorage.getItem(toName);
                  if(storeData != null) {
                      //將此次的內(nèi)容拼接到storeData中
                      str = storeData + str;
                  }
                  //將消息存儲到sessionStorage中
                  sessionStorage.setItem(toName,str);
 
                  //定義服務端需要的數(shù)據(jù)格式
                  var message = {toName:toName,message:data};
                  //將輸入的數(shù)據(jù)發(fā)送給服務器
                  ws.send(JSON.stringify(message));
              });
          })

  服務端代碼實現(xiàn)

  `WebSocketConfig` 類實現(xiàn)

  開啟 springboot 對websocket的支持

@Configuration
  public class WebSocketConfig {
 
      @Bean
      //注入ServerEndpointExporter,自動注冊使用@ServerEndpoint注解的
      public ServerEndpointExporter serverEndpointExporter() {
          return new ServerEndpointExporter();
      }
  }

  `ChatEndPoint` 類實現(xiàn)

@ServerEndpoint(value = "/chat",configurator = GetHttpSessionConfigurator.class)
  @Component
  public class ChatEndpoint {
 
      //用來存儲每一個客戶端對象對應的ChatEndpoint對象
      private static Map<String,ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();
 
      //和某個客戶端連接對象,需要通過他來給客戶端發(fā)送數(shù)據(jù)
      private Session session;
 
      //httpSession中存儲著當前登錄的用戶名
      private HttpSession httpSession;
 
      @OnOpen
      //連接建立成功調用
      public void onOpen(Session session, EndpointConfig config) {
          //需要通知其他的客戶端,將所有的用戶的用戶名發(fā)送給客戶端
          this.session = session;
          //獲取HttpSession對象
          HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
          //將該httpSession賦值給成員httpSession
          this.httpSession = httpSession;
          //獲取用戶名
          String username = (String) httpSession.getAttribute("user");
          //存儲該鏈接對象
          onlineUsers.put(username,this);
          //獲取需要推送的消息
          String message = MessageUtils.getMessage(true, null, getNames());
          //廣播給所有的用戶
          broadcastAllUsers(message);
      }
 
      private void broadcastAllUsers(String message) {
          try {
              //遍歷 onlineUsers 集合
              Set<String> names = onlineUsers.keySet();
              for (String name : names) {
                  //獲取該用戶對應的ChatEndpoint對象
                  ChatEndpoint chatEndpoint = onlineUsers.get(name);
                  //發(fā)送消息
                  chatEndpoint.session.getBasicRemote().sendText(message);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
 
      private Set<String> getNames() {
          return onlineUsers.keySet();
      }
 
      @OnMessage
      //接收到消息時調用
      public void onMessage(String message,Session session) {
          try {
              //獲取客戶端發(fā)送來的數(shù)據(jù)  {"toName":"張三","message":"你好"}
              ObjectMapper mapper = new ObjectMapper();
              Message mess = mapper.readValue(message, Message.class);
              //獲取當前登錄的用戶名
              String username = (String) httpSession.getAttribute("user");
              //拼接推送的消息
              String data = MessageUtils.getMessage(false, username, mess.getMessage());
              //將數(shù)據(jù)推送給指定的客戶端
              ChatEndpoint chatEndpoint = onlineUsers.get(mess.getToName());
              chatEndpoint.session.getBasicRemote().sendText(data);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
 
      @OnClose
      //連接關閉時調用
      public void onClose(Session session) {
          //獲取用戶名
          String username = (String) httpSession.getAttribute("user");
          //移除連接對象
          onlineUsers.remove(username);
          //獲取需要推送的消息
          String message = MessageUtils.getMessage(true, null, getNames());
          //廣播給所有的用戶
          broadcastAllUsers(message);
      }
  }

  `GetHttpSessionConfigurator` 配置類實現(xiàn)

  public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
      @Override
      public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
          HttpSession httpSession = (HttpSession) request.getHttpSession();
          config.getUserProperties().put(HttpSession.class.getName(),httpSession);
      }
  }


分享到:
在線咨詢 我要報名
和我們在線交談!