WebSocket protocol understands and implements online chat

written in front

WebSocket referred to as ws
This article introduces ws, then uses the ws protocol step by step for front-end development and testing, and finally uses SpringBootand vueutilizes the ws protocol to implement a small demo of an online chat room (the source code is at the end of the article).

After reading this article, you will be able to complete such a small demo (need to use springboot, vue2 and element-ui)
insert image description here
insert image description here
insert image description here
insert image description here
insert image description here

Introduction to WebSocket

insert image description here
WebSocket is a kind of protocol that can communicate on the connection HTML5provided at the beginning. It can be simply understood as: WebSocket protocol and HTTP protocol comparison:单个TCP全双工网络通信协议

insert image description here

  • HTTP protocol: The HTTP protocol is a high-quality 无状态(请求完成后,连接彻底断开,节省资源), 无连接(客户端端向服务端请求数据,获取数据后即断开连接)application 单向(连接建立只能由客户端发起)-layer communication protocol that adopts a request/response model.
    • Features : The client asks the server for something, and the server tells it. After telling it, the client disappears without a trace without any problems. Will appear. The server is in the light and the client is in the dark.
    • Disadvantages , the client can ask the server for something, but the server can't find the client for something, because the connection is completely disconnected after the client completes the data acquisition, and in the hands, the matter can only be held between the client and the 建立连接的特权只在客户端service and tell it when the client recreates the request. When there is something urgent, it is impossible to contact the client, that is,服务端无法向客户端推送数据
    • Solution : Let the client frequently establish a connection with the server, so that the data on the server does not need to be held for too long
      • Existing problem: How long does it take to establish a connection with the client? First of all, frequent establishment consumes a lot of system resources, but the server rarely transmits data; secondly, how often do you establish a connection? If the connection is established too frequently, the client will not be able to bear it. If the frequency is low, the server's data will still have to be held for a while, or it will still not be reached 实时. Frequent visits consume huge resources and sprinkle a large net with little gain.

If only there was a protocol where the client could send data to the server and the server could also send data to the client! ie 全双工通讯. The client of this protocol cannot be disconnected from the server, so that the server can find the client to continue sending data. Although it 有状态连接will consume resources, it is much better than frequent connections of the HTTP protocol under specific requirements. This kind of agreement is WebSocket协议. To sum up, webSocket supports 持久connections so that both the server and the client can 实时communicate.协议

Application Scenarios of WebSocket

  • Web applications for timely communication: such as those that require real-time refresh of web page data实时数据展示网站
  • Game application: screen animation can be refreshed in real time
  • Chat application: sending messages can be received and pushed in time

insert image description here
recommended article

WebSocket implementation

HTTP method : When we write code that separates the front and back ends, the back end writes @RestControlleran interface to receive requests, and the front end writes a package axiosto send requests to obtain back-end data. In fact, this method follows the HTTP protocol for data interaction. The front-end uses axiosHTTP requests, and the back-end interface receives HTTP requests.
WS method :
Because the commonly used @RestControllerinterfaces and axiosrequest tools are all for HTTP协议服务的, WSalthough data interaction is this process (the backend specifies the interface, and the frontend obtains data), it must have its own set of implementation tools. Use @ServerEndpointto achieve similar @RestControllereffects, and use WebSocketobjects to achieve similar axioseffects. The specific use will be introduced later. The relationship between Endpoint and webScoket is like the relationship Servletwith Http


The life cycle
life cycle makes me understand, the original words are websocket事件
the front end

let ws=new webSocket(ws://ip:端口号/资源名称)
Function name describe
ws.onopen 连接triggers on build
ws.onmessage 接收Triggered when a message arrives at the backend
ws.onerror 错误Triggered when a communication process occurs
ws.onclose 关闭fires on connect

when sending dataws.send()

rear end

Annotation name describe
@onClose Fired when the connection is closed
@onOpen fires on initialization
@onError fires when an error occurs
@onMessage Automatically triggered after initialization to obtain the data passed by the front end

Summary of ws implementation process
The server is no longer only responsible for the data response of the client as in the past, but can also actively push data to the client There is one more connector container.
insert image description here
@RestController

  1. For request mapping use@ServerEndpoint("xx")
  2. When the request is initialized, put this request into the container of all connectors, so that when sending the request, you can find each connection that is currently connected to the system, using the @OnOpentag
  3. To get the data of the front end @OnMessage, you can use the same @ RequestMapping()annotation method to get the data, such as @PathParam("xxxx"); Note that this method will be called every time the front end sends data. but it is not called when initializing
  4. The data returned to the front end is not used directly return, but the connection session saved in the connection pool is used to obtain the connection session, and the returned information is passed through the session.
  5. The reception and return of the message have been completed, and the rest is supplementary. To close the connection is to call the method under @OnClosethe label , and call @OnErrorthe method under the label when an error occurs

webSocket has its own method of obtaining parameters, which is very similar to the method of HTTP Controller

  • 路径Getting @PathParam("xxx")data from request parameters works the @PathValue("xxx")same as

The methods under some annotations must contain certain parameters

  • @OnErrorThe method list under the logo must contain Throwableobjects, otherwise an error will be reported when starting
  • @OnMessageThe method identified by this annotation is automatically called when the front end sends a message to the back end. It is required that this method must contain parameters, otherwise an error will be reported when it starts

question?

  • How to call when the backend sends information to the frontend?
    There are two methods that can be automatically called during the entire life cycle when connecting, one is to call the @OnOpenmarked method during initialization, and the other is to call @OnMessagethe method when receiving the front-end request (this method will be called every time the front-end sends data). You can choose @OnMessagethe method under the label as an introduction for the backend to return data to the frontend, and call the method of returning data to the frontend in this method.

  • How to send to instant messaging effect?

  • This problem can be solved by solving the problem of how the backend sends data to the frontend. When A passes the message sent to B to the server through ws, the method of returning the front-end data is automatically called in the method of receiving the front-end data. In this method, the content of the message and the destination are obtained, and then go to the connector container to find the destination. Connect the object, and then use the data sent by A as the data returned to the B connection object. **This way of thinking requires B to be in the container of the connector, that is, B must remain connected**

  • How to make the foreground always receive the data generated by the backend itself
    The data generated by the backend itself means that the foreground can only be triggered once, and the rest of the data is continuously returned to the frontend by this trigger. You can call a method similar to listening to the blocking queue when it is triggered for the first time, and put the generated data into the queue continuously. When the data is monitored in the method, it will be taken out and returned to the front end. In this way, the listener is called by the first request, and the data is generated and monitored to trigger the data return

  • How does the front end receive the data returned by the back end?
    When the backend has data to return, the front-end object onmessage()will be triggered, where you can assign data and render the page

ws parameter passing

  • 路径传参: Pass parameters through the path during initialization. The backend uses it to be used in the mapping of the path. For example: login/{xxx}, and then used in the method parameter @PathParam("xxx")to receive

  • 消息传参: Use an object to send a message ws.send(“xxx”), you can convert some content into JSON form for sending, and then the @OnMessagebackend receives and converts it in the following method parameters

  • 请求头传参: Use the request header, because it has been introduced above, so I won’t repeat it here. The request header is the key here

    • The front-end request to add the request header
      ws is not as flexible as the HTTP request, there is no api to pass parameters, and the request header cannot be customized. So how to pass it on?
      • Use the websocket request header to contain Sec-WebSocket-Protocolthis attribute, you can assign a value to the attribute, and the backend takes it from this attribute. There is no separate Api for this property assignment, 只能在构造ws对象能够进行赋值.
      例如 :
      let ws = new WebSocket("ws://localhost:8888/xxxx", "请求参数1");
      
      后端获取请求头 Sec-WebSocket-Protocol  得到的就是 请求参数1
      
      let ws = new WebSocket("ws://localhost:8888/xxxx", ["请求参数1","请求参数2"]);
      
      后端获取请求头 Sec-WebSocket-Protocol  得到的就是 请求参数1   请求参数2
      
    • The backend obtains the request header.
      The backend here refers to springBoot. The backend obtains the request header @RequestHeaderdirectly Instead, it is obtained in the specified method to be inherited.

    insert image description here
    *

    @Configuration
    @Slf4j
    public class WebSocketConfig extends ServerEndpointConfig.Configurator {
          
          
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
          
          
            return new ServerEndpointExporter();
        }
    
        public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
          
          
    
            //获取请求头 请求头的名字是固定的
            List<String> list = request.getHeaders().get("Sec-WebSocket-Protocol");
    
    
            //当Sec-WebSocket-Protocol请求头不为空时,需要返回给前端相同的响应
            response.getHeaders().put("Sec-WebSocket-Protocol",list);
    
    
            super.modifyHandshake(sec, request, response);
        }
    }
    
    
    • Then also @ServerEndpointspecify the configuration class in the annotation
      insert image description here

    Note : This kind of method will only be executed when creating a connection, and will not be executed when sending a message, when an error occurs, or
    when disconnecting. For identity verification, is it enough to verify the same connection only once? HTTP uses an interceptor to verify once, because it is frequently verified because of its frequent connections? That is, the interceptor used when acting as http in this method?
    insert image description here

mock test

backend simulation

Back-end use springBoot, front-end use postmanto send ws request

Back-end preparation: note that tomcat7the webSocket protocol will be supported only in the future

  • In addition to environmental dependencies, the backend needs to import key WebSocket dependencies

      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
      </dependency>
    
  • environment dependent

     <parent>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> 
     </parent>
    
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
    <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-websocket</artifactId>
     </dependency>
     <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <optional>true</optional>
     </dependency>
    
  • Inject the ServerEndpointExporter object into the container
    Create a configuration class and inject the object into the container in the class

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    @Configuration
    public class WebSocketConfig {
          
          
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
          
          
            return new ServerEndpointExporter();
        }
    }
    
    
  • Then you can write an application similar to the interface

Front end preparation

  • Create a ws-type connection in postMan ( the original HTTP method cannot be used )
    insert image description here

The first test : After the connection is established, the front end sends data to the back end, the back end receives data, and sends data to the front end; there is only one connection (do not use connected session containers)


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

@ServerEndpoint(value = "/wsserver/{username}")
@Component
@Slf4j
public class WebSocketServer {
    
    
    private Session session;
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session){
    
    
        this.session=session;
        log.info(username);
        log.info("连接创建初始化");
    }

    @OnMessage
    public void onMessage(Session session,String message) throws IOException, InterruptedException {
    
    
        log.info("接收到数据"+message);
        Thread.sleep(10000);

        for (int i=0;i<10;i++){
    
    
            this.session.getBasicRemote().sendText("来自后端的数据");
        }
    }
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        log.error("发生错误");
        error.printStackTrace();
    }
    @OnClose
    public void onClose(){
    
    
        log.info("关闭连接");
    }
}


note

insert image description here

Test results
insert image description here
The second test : Create two independent connections A and B respectively. A sends data to B, B sends data to A, and the sender must be specified in the data

Implementation idea:

  • indicated in the data 发送方, 接收方and 数据. Indicate the sender in the path parameter, set the rules in the data format, and extract the receiver and data.
  • 标识Each connection will add its own sum to the public cache when it is initialized session对象, so that it is convenient to find the connected session according to the receiver of the data, and then return the data through the receiver's session
@ServerEndpoint(value = "/wsserver/{username}")
@Component
@Slf4j
public class WebSocketServer {
    
    
    public static final Map<String,Session> sessionMap=new ConcurrentHashMap<>();
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session){
    
    
        sessionMap.put(username,session);
        log.info("数据来自"+username);
        log.info("连接创建初始化");

        for (Map.Entry<String,Session> entry:sessionMap.entrySet()) {
    
    
            System.out.println(entry);
        }
    }

    @OnMessage
    public void onMessage(Session session,String message) throws IOException, InterruptedException {
    
    
        log.info("接收到数据"+message);
        String[] split = message.split(":");
        sessionMap.get(split[0]).getBasicRemote().sendText(split[1]);
    }
    @OnError
    public void onError(Session session, Throwable error) {
    
    
        log.error("发生错误");
        error.printStackTrace();
    }
    @OnClose
    public void onClose(){
    
    
        log.info("关闭连接");
    }
}


note
insert image description here

Test Results
insert image description here


Note : Each request corresponds to an WebSocketServerobject, and each change will call the method specified in the object, for example: the method under @onOpen will be triggered once when each request is connected, and the method under the @onMessage logo will be called once when each request sends data …

front page

Create a vue project, because it will be used in later projects. There is no need to create it here, as long as there is an environment that can run js, the easiest way is to create a .htmlpage and open it in the browser

The first test establishes a connection with the server

Create an wsobject, make use of this object 向后端发送数据, 获取后端数据etc. The creation method is very simple, and the connection will be made when it is created (it refers to when the execution reaches here, not after the new comes out)

insert image description here
After the page is run, the server will immediately create a connection
insert image description here

In the second test, A in postman sends data to C in browsing, and C receives the data and prints it to the page;

Based on the completion of the back-end test 2 above

insert image description here
Note : onmessage is always listening to the data sent by the server

The third test sends data to A of postMan on the basis of test 2

Use the send("xxx") method of the wc object

<template>
    <div>
        {
    
    {
    
    receptionData}}
        <el-input size="mini" v-model="data"></el-input>
        <el-button @click="toSendMsg()">发送</el-button>
    </div>
</template>
<script>
export default {
    
    
    data(){
    
    
        return{
    
    
            data:'',
            ws:undefined,
            receptionData:undefined
        }
    },
    created(){
    
    
      this.ws= new WebSocket('ws://localhost:8080/wsserver/C');
        this.ws.onmessage=(msg)=>{
    
    
            this.receptionData=msg.data
        }
    },
    methods:{
    
    
        toSendMsg(){
    
    
           this.ws.send("A:cccccc")
        }
    }
}
</script>

Test results:
insert image description here
Note : When the front-end webpage is completed, the closing process of the back-end will be automatically triggered

In addition, there are sc.close() some front-end processing that is manually closed, and sc.onerror()some processing that occurs when exceptions occur. The usage onmessageis the same as that of assigning a function to it.

During the test, it was found that the data can only be consumed once, that is, when multiple clients connect, only one client can receive the data from the server. Because different client connections have different connection sessions. Since the code on the server side uses map storage, the key is the unique identifier, and the value is the session. When a user logs in on multiple clients, the old value of the same key in the map will be overwritten by the latest value, and only the last opened web page can receive the data from the server.



Live chat system development

This system is original, based on the mixed development of HTTP protocol (eg login, registration) and ws protocol (eg: real-time chat), the purpose is to use the ws protocol proficiently, so it is relatively omitted in other aspects, and there may be bugs or errors Please correct me

front end

The front-end uses vue and Element package library ( 非必要), uses axios to send HTTP requests, and uses webSocket objects to send ws requests.

  1. Create a vue project using npm
  2. Download Element-ui and import it in the main.js file
  3. Download axios, create a tool class to encapsulate the axios request, and then import it in the js part of the required page.
  4. configure routing
  5. delete useless pages
  6. Then test whether the project can run normally, whether the element is imported successfully, etc.

The Vue project initialization part of the project will not be introduced here one by one.
Login style layout

insert image description here

code part:

<template>
  <div class="wrapper">
    <div class="login_div">
   
        <span class="title"><h2> 在线聊天室</h2></span>
        <div class="form_div">
          <el-form ref="form" :model="form" label-width="80px">
            <el-form-item>
              <el-input v-model="form.name" size="mini" placeholder="输入姓名"></el-input>
          </el-form-item>
            <el-form-item>
              <el-input v-model="form.password" size="mini" placeholder="输入密码" type="password"></el-input>
            </el-form-item>
             <el-form-item>
              <a href="#" style="color:blue">还没有账号?点我去注册</a>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm()" size="small" style="width:100%">提交</el-button>
          </el-form-item>
        </el-form>
        </div>
    </div>
  </div>
</template>

<script>
import {
    
    login} from '../api/userApi'
export default {
    
    
 
  data() {
    
    
    return {
    
    
      form:{
    
    
        name:'',
        password:''

      },
      mock:{
    
    
      
      }
    }
  },
  methods: {
    
    
      submitForm(){
    
    
        login(this.form).then(res=>{
    
    
           
        })
      }
  }
   
}
</script>
<style scoped>
.wrapper {
    
    
  height: 100vh;
  background-image: linear-gradient(to bottom right, #FC466B, #3F5EFB);
  width: 100%;
  overflow: hidden;
  position: relative;
 
}
.login_div{
    
    
  width:500px;
  height: 300px;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -70%);
  background-color:aliceblue;
  opacity:0.9;  
}
.title{
    
    
  text-align: center  ;
}
.form_div{
    
    
  margin-right: 100px;
  margin-top: 50px;
}
</style>

Chat page layout
Use css to draw a gradient background, and then draw the overall layout of the page
insert image description here

<template>
  <div style="min-height: 100vh" class="window_div">
      <div class="chat_div">
          <div class="chat_header">
            <span class="hrader_title">当前用户:xxx</span>
            <span class="hrader_friend">正在和xxx 聊天</span>
          </div>

          <div class="chat_left">
              <div class="message_div"></div>
               <div class="input_div"></div>
          </div>
          <div class="chat_right">
            <div class="friend_div">
              <div class="friend_list_title"></div>
              <div class="friend_list"></div>
            </div>
            <div class="sys_info_div">
              <div class="sys_info_title"></div>
              <div class="sys_info"></div>
            </div>
          </div>
      </div>
  <div>
    </div>
  </div>
</template>

<script>


export default {
    
    
}
</script>

<style scoped>
.window_div{
    
    
  background-image: linear-gradient(to bottom right, #b1b1c5, #3F5EFB);
  overflow: hidden;
  position: relative;
}
.chat_div{
    
    
  width: 60%;
  height: 65vh;
  border: 1px solid red;
  margin: 10vh auto;
}
.chat_header{
    
    
  display: flex;
  width: 100%;
  height: 10vh;
  border: 1px solid red;
}

.chat_left{
    
    
  width: 70%;
  height: 55vh;
  border: 1px solid red;
  float: left; 
}

.chat_right{
    
    
  width: 30%;
  height: 55vh;
  border: 1px solid red;
  float: right; 
}
.hrader_title{
    
    
  display: flexbox;
  line-height: 10vh;
  margin-left: 6%;
  color: white;
  width: 30%;
  border: 1px solid red;
}
.hrader_friend{
    
    
  display: inline-block;
  margin-top: 6vh;
  height: 2vh;
  color: white;
  width: 25%;

  font-size: 12px;
  border: 1px solid red;
}
.friend_list_title{
    
    
  width: 100%;
  height: 5vh;
  border: 1px solid red;
}
.friend_list{
    
    
  width: 100%;
  height: 20vh;
  border: 1px solid red;
}
.friend_div{
    
    
  width: 100%;
  height: 25vh;
  border: 1px solid black;
}
.sys_info_div{
    
    
  width: 100%;
  height: 30vh;
  border: 1px solid black;
}
.sys_info_title{
    
    
  width: 100%;
  height: 5vh;
  border: 1px solid red;
}
.sys_info{
    
    
  width: 100%;
  height: 25vh;
  border: 1px solid red;
}
.message_div{
    
    
  width: 100%;
  height: 40vh;
  border: 1px solid red;
}
.input_div{
    
    
  width: 100%;
  height: 15vh;
  border: 1px solid red;
}

</style>

After removing the edge for filling
insert image description here

At this point, the front-end style has been completed except for the message part, and the rest is to debug the back-end


backend part

Design features:

  1. Log in
  2. Display a list of users who are online
  3. live chat
  4. Online reminder, offline reminder

Design user name to be unique

flow chart

insert image description here

Message type
Messages sent between users, user online notification messages, user offline notification messages, friend list messages, system notifications (some errors, etc.)

insert image description here

Message body
sender, receiver, message type, message content
insert image description here

Summary of ideas :

  1. How to ensure security? It was originally planned that each request carries the request header to be verified in the interceptor like an http request. After testing, it was found that each time an http request is a different connection, it needs to be verified every time. ws can only verify once when creating a connection. A token is returned when logging in, and the server uses a map to save a token record. Carry this token when ws creates a connection, the server searches it from the map, and judges whether the connection is legal based on whether it is found.
  2. How to determine the sender of the message? When sending a message, set the sending method in the request body (may not be safe)
  3. How to put the user connection into the session map? After verifying the token of ws, put the found user name into LocalThrad, and when putting it into sessionMap, take the user name from LocalThread as the key, and the value is the session connected
  4. What does initialization do? Put the connection into the session map during initialization, and then return the friend list through the session, because the name is used as the key in this code, so you only need to traverse all the keys in the session map. Then send a system message to all users that the user is already online
  5. How to distinguish between different types of messages? Setting the message format can distinguish different types of messages, and send system messages, friend lists, and friend messages in a unified format.
  6. How is the online friend list updated? When the initial connection is made, the backend will send all current online users to this connection, and the frontend will update when the system prompts you to go online or offline later.
  7. How to deal with exceptions and disconnections? Encapsulate a method to notify other sessions that the user has gone offline. In this method, according to the session in the parameter list, go to the sessionMap to find the key according to the value, use the key as a message, and set the message type to the user offline to other users sent in session. The front end receives a message of the user's offline type, prompts, and removes this name from the online friend list.

code comment

Back-end core code : (Because the space is too long, here are only comments, the detailed code has been uploaded to gitee, you can visit and view it yourself)
insert image description here
insert image description here
At this point, all functions of the back-end have been basically completed, only the front-end receives different types of messages, and assign it to render

insert image description here
insert image description here
insert image description here
Here the console test has successfully sent and received messages, and the next thing is the most troublesome message display of this project

Using the method of circular array, this method will be a little different from the usual chat habits, so this version will not be changed.
insert image description here
In addition, the online person list also uses the same method
insert image description here

Live chat version 1.0 is now complete

source code

The complete source code has been uploaded to gitee, please click to visit https://gitee.com/wang-yongyan188/websocketchat.git
Note : Because the token of the ws creation request is stored in localStorage, even if the same browser logs in to different accounts It is also the same user, so use different browsers to test. If you only test the backend, it is recommended to use postman to test
insert image description here

All the codes are original. The blog and the codes took two days. Any mistakes are welcome. If you think it’s okay, please like it.
The purpose of this project is to understand the ws protocol, and to be able to use springboot to achieve mixed development of the two protocols.
The code page is relatively simple and the functions are not perfect (registration needs to be manually added to the database, and only chat with online users, etc.), if you like it, follow-up will launch such as: add friends, message unread reminder, group chat, message saving, etc., further Perfect as a small project.

Guess you like

Origin blog.csdn.net/m0_52889702/article/details/128246831