Simple Online Chat Websocket

Preface

  • What is Webscoket ?

  • websocket application scenarios

  • Brief Group Chat Implementation

  • Code examples

  • Summary

Webscoket

Websokcet is a single TCP On Connection full duplex Protocol for communication, through HTTP /1.1 Protocol 101 status code for handshake.

http://websocket.org

Websocket application scenarios

Web ocket and Http are both web communication protocols. What is the difference between them?Let's start with Http, which is a request response protocol. This model determines that only client requests can be answered passively by the server.What if we have requirements that the server actively pushes to the client?For example, for a stock website, we will choose the active polling, that is, pull mode.

You can think about what the problem of proactive polling is?

Active polling actually produces a large number of invalid requests, increasing server pressure.

Therefore, the supplement of websocket protocol brings us new solutions.

Brief Group Chat Implementation

Use the webocket to implement a simple group chat function, to deepen the understanding of webocket.

  1. Assume that both Li Lei and Han Meimei are online;
  2. Li Lei sends messages through the browser to the nginx proxy to the Ws server;
  3. The Ws server loads all online session broadcast messages;
  4. Han Meimei received the message.

Code examples

Backend (shop-server)

  1. IntroducePom.xmlrely on

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-websocket</artifactId>
      </dependency>
    
  2. Configuration Class

    package com.onlythinking.shop.websocket;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * <p> The describe </p>
     *
     * @author Li Xingping
     */
    @Slf4j
    @Configuration
    public class WebSocketConfiguration {
    
        @Bean
        public ServerEndpointExporter endpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
  3. Accept Request Endpoint

    package com.onlythinking.shop.websocket;
    
    import com.alibaba.fastjson.JSON;
    import com.google.common.collect.Maps;
    import com.onlythinking.shop.websocket.handler.ChatWsHandler;
    import com.onlythinking.shop.websocket.handler.KfWsHandler;
    import com.onlythinking.shop.websocket.handler.WsHandler;
    import com.onlythinking.shop.websocket.store.WsReqPayLoad;
    import com.onlythinking.shop.websocket.store.WsRespPayLoad;
    import com.onlythinking.shop.websocket.store.WsStore;
    import com.onlythinking.shop.websocket.store.WsUser;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Component;
    
    import javax.websocket.*;
    import javax.websocket.server.ServerEndpoint;
    import java.io.IOException;
    import java.util.Map;
    
    /**
     * <p> The describe </p>
     *
     * @author Li Xingping
     */
    @Slf4j
    @Component
    @ServerEndpoint("/ws")
    public class WebsocketServerEndpoint {
    
        private static Map<String, WsHandler> wsHandler = Maps.newConcurrentMap();
    
        static {
            wsHandler.put("robot", new KfWsHandler());
            wsHandler.put("chat", new ChatWsHandler());
        }
    
        @OnOpen
        public void onOpen(Session session) {
            log.info("New ws connection {} ", session.getId());
            WsStore.put(session.getId(), WsUser.builder().id(session.getId()).session(session).build());
            respMsg(session, WsRespPayLoad.ok().toJson());
        }
    
        @OnClose
        public void onClose(Session session, CloseReason closeReason) {
            WsStore.remove(session.getId());
            log.warn("ws closed´╝îreason:{}", closeReason);
        }
    
        @OnMessage
        public void onMessage(String message, Session session) {
            log.info("accept client messages: {}" + message);
            WsReqPayLoad payLoad = JSON.parseObject(message, WsReqPayLoad.class);
            if (StringUtils.isBlank(payLoad.getType())) {
                respMsg(session, WsRespPayLoad.ofError("Type is null.").toJson());
                return;
            }
            WsUser wsUser = WsStore.get(session.getId());
            if (null == wsUser || StringUtils.isBlank(wsUser.getUsername())) {
                WsStore.put(session.getId(), WsUser.builder()
                  .id(session.getId())
                  .username(payLoad.getUsername())
                  .avatar(payLoad.getAvatar())
                  .session(session)
                  .build()
                );
            }
            WsHandler handler = wsHandler.get(payLoad.getType());
            if (null != handler) {
                WsRespPayLoad resp = handler.onMessage(session, payLoad);
                if (null != resp) {
                    respMsg(session, resp.toJson());
                }
            } else {
                respMsg(session, WsRespPayLoad.ok().toJson());
            }
        }
    
        @OnError
        public void onError(Session session, Throwable e) {
            WsStore.remove(session.getId());
            log.error("WS Error: ", e);
        }
    
        private void respMsg(Session session, String content) {
            try {
                session.getBasicRemote().sendText(content);
            } catch (IOException e) {
                log.error("Ws resp msg error {} {}", content, e);
            }
        }
    }
    
    
  4. Chat Business Processor

    package com.onlythinking.shop.websocket.handler;
    
    import com.onlythinking.shop.websocket.store.*;
    import lombok.extern.slf4j.Slf4j;
    
    import javax.websocket.Session;
    import java.util.Date;
    import java.util.List;
    
    /**
     * <p> The describe </p>
     *
     * @author Li Xingping
     */
    @Slf4j
    public class ChatWsHandler implements WsHandler {
    
        @Override
        public WsRespPayLoad onMessage(Session session, WsReqPayLoad payLoad) {
            // Broadcast message
            List<WsUser> allSessions = WsStore.getAll();
            for (WsUser s : allSessions) {
                WsRespPayLoad resp = WsRespPayLoad.builder()
                  .data(
                    WsChatResp.builder()
                      .username(payLoad.getUsername())
                      .avatar(payLoad.getAvatar())
                      .msg(payLoad.getData())
                      .createdTime(new Date())
                      .self(s.getId().equals(session.getId()))
                      .build()
                  )
                  .build();
                log.info("Broadcast message {} {} ", s.getId(), s.getUsername());
                s.getSession().getAsyncRemote().sendText(resp.toJson());
            }
            return null;
        }
    }
    
    

Front End (shop-web-mgt)

  1. Introducing dependencies

    npm install vue-native-websocket --save
    
  2. Add Store

    import Vue from 'vue'
    
    const ws = {
      state: {
        wsData: {
          hasNewMsg: false,
        },
        socket: {
          isConnected: false,
          message: '',
          reconnectError: false,
        }
      },
      mutations: {
        SET_WSDATA(state, data) {
          state.wsData.hasNewMsg = data.hasNewMsg
        },
        RESET_WSDATA(state, data) {
          state.wsData.hasNewMsg = false
        },
        SOCKET_ONOPEN(state, event) {
          Vue.prototype.$socket = event.currentTarget;
          state.socket.isConnected = true
        },
        SOCKET_ONCLOSE(state, event) {
          state.socket.isConnected = false
        },
        SOCKET_ONERROR(state, event) {
          console.error(state, event)
        },
        // default handler called for all methods
        SOCKET_ONMESSAGE(state, message) {
          state.socket.message = message
        },
        // mutations for reconnect methods
        SOCKET_RECONNECT(state, count) {
          console.info(state, count)
        },
        SOCKET_RECONNECT_ERROR(state) {
          state.socket.reconnectError = true;
        },
      },
      actions: {
        AskRobot({rootGetters}, data) {
          return new Promise((resolve, reject) => {
            console.log('Ask robot msg', data);
            const payLoad = {
              type: 'robot',
              username: rootGetters.loginName,
              data: data
            };
            Vue.prototype.$socket.sendObj(payLoad)
            resolve(1)
          })
        },
        SendChatMsg({rootGetters}, data) {
          return new Promise((resolve, reject) => {
            console.log('Send chat msg', data);
            const payLoad = {
              type: 'chat',
              username: rootGetters.loginName,
              data: data
            };
            Vue.prototype.$socket.sendObj(payLoad)
            resolve(1)
          })
        },
        MessageRead({commit, state}, data) {
          commit('RESET_WSDATA', {})
        },
      }
    };
    
    export default ws
    
    
  3. Write Components

    <template>
      <div>
        <ot-drawer
          title="chat"
          :visible.sync="chatVisible"
          direction="rtl"
          :before-close="handleClose">
          <div class="chat-body">
            <div id="msgList" style="margin-bottom: 200px" class="chat-msg">
              <div class="chat-msg-item" v-for="item in msgList">
                <div v-if="!item.self">
                  <div class="msg-header">
                    <img
                      :src="baseUrl+'/api/insecure/avatar?code='+item.avatar+'&size=64'"
                      class="user-avatar"
                    >
                    <span class="avatar-name">{{item.username}}</span>&nbsp;&nbsp;
                    <div style="display: inline-block; float: right">
                      {{item.createdTime | parseTime('{h}:{i}')}}
                    </div>
                  </div>
                  <div class="msg-body" style="float: left;">
                    {{item.msg}}
                  </div>
                </div>
                <div v-else>
                  <div class="msg-header clearfix">
                    <img
                      :src="baseUrl+'/api/insecure/avatar?code='+item.avatar+'&size=64'"
                      class="user-avatar"
                      style="float: right"
                    >
                  </div>
                  <div class="msg-body" style="float: right;background-color: #67C23A">
                    {{item.msg}}
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="chat-send">
            <el-input
              v-model="text"
              autocomplete="off"
              placeholder="Please enter what you want to say..."
              @keyup.enter.native="handleSendMsg"
            ></el-input>
            <div class="chat-btns">
    
              <el-button
                class="action-item"
                @click="handleClearMsg"
              >empty
              </el-button>
              <el-button
                type="success"
                class="action-item"
                @click="handleSendMsg"
                v-scroll-to="{ el: '#msgList', offset: 140 }"
              >Send out
              </el-button>
            </div>
          </div>
        </ot-drawer>
      </div>
    </template>
    
    <script>
    
      import {mapGetters} from 'vuex'
      import store from '@/store'
      import {config} from '@/utils/config'
      import OtDrawer from '@/components/OtDrawer'
      import Cookies from 'js-cookie'
    
      export default {
        name: 'UserChat',
        components: {OtDrawer},
        props: {
          visible: {
            type: Boolean,
            default: false
          }
        },
        data() {
          return {
            baseUrl: config.baseUrl,
            text: '',
            msgList: [],
          }
        },
        computed: {
          ...mapGetters([
            'roles', 'isConnected', 'message', 'reconnectError'
          ]),
          chatVisible: {
            get() {
              return this.visible
            },
            set(val) {
              this.$emit('update:visible', val)
            }
          }
        },
        beforeDestroy() {
          if (this.isConnected) {
            this.$disconnect()
          }
        },
        mounted() {
          console.log('Chat mounted.')
          if (!this.isConnected) {
            this.$connect(config.wsUrl, {
              format: 'json',
              store: store
            })
          }
          // Listen for message reception
          this.$options.sockets.onmessage = (res) => {
            const data = JSON.parse(res.data);
            console.log('Received Message', data);
            if (data.code === 0) {
              // Connection established successfully
              if (!data.data.msg) {
                return;
              }
              this.msgList.push(data.data)
            } else if (data.code === 400) {
              this.$message({
                type: 'warning',
                message: data.data
              })
            }
          };
        },
        methods: {
          handleSendMsg() {
            if (!this.text) {
              this.$message({
                type: 'warning',
                message: 'Please enter content'
              });
              return;
            }
            this.$store.dispatch('SendChatMsg', this.text).then(data => {
              this.text = ''
            })
          },
          handleClearMsg() {
            this.msgList = [];
            Cookies.remove('chatMsg');
            // delete
          },
          // Before chat closes
          handleClose() {
            // Cache messages locally
            Cookies.set('chatMsg', JSON.stringify(this.msgList));
            this.$emit('update:visible', false)
          }
        },
        created() {
          // Loading cached data
          const chatMsg = Cookies.get('chatMsg');
          if (chatMsg) {
            this.msgList = JSON.parse(chatMsg);
          }
        }
      }
    </script>
    
    <style>
      .el-drawer__body {
        height: 100%;
        box-sizing: border-box;
        overflow-y: auto;
        background-color: rgba(244, 244, 244, 1);
        scroll-snap-type: y proximity;
      }
    </style>
    
    <style rel="stylesheet/scss" lang="scss" scoped>
    
      .user-avatar {
        width: 20px;
        height: 20px;
        border-radius: 4px;
        vertical-align: middle;
      }
    
      .msg-header {
        font-size: 12px;
        color: rgba(109, 114, 120, 1);
      }
    
      .avatar-name {
        vertical-align: middle;
      }
    
      .msg-body {
        text-align: center;
        max-width: 300px;
        min-width: 100px;
        word-wrap: break-word;
    
        margin: 4px 0;
        padding: 4px;
        line-height: 24px;
        border-radius: 4px;
        background-color: rgba(255, 255, 255, 1);
      }
    
      .chat-body {
        height: 100%;
        position: relative;
      }
    
      .chat-msg {
        padding: 10px;
    
        .chat-msg-item {
          margin-top: 10px;
          height: 65px;
        }
      }
    
      .chat-send {
        padding: 20px;
        background-color: rgba(255, 255, 255, 1);
        position: absolute;
        left: 50%;
        width: 100%;
        transform: translateX(-50%);
        bottom: 0px;
      }
    
      .chat-btns {
        text-align: center;
      }
    
      .action-item {
        margin-top: 10px;
      }
    </style>
    
    

    Nginx Proxy ConfigurationNginx.conf(can be added if needed)

    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    
    upstream websocket {
        server 127.0.0.1:8300;
    }
    
    server {
         server_name shop-web-mgt.onlythinking.com;
         listen 443 ssl;
         location / {
             proxy_pass http://websocket;
             proxy_read_timeout 300s;
             proxy_send_timeout 300s;
             
             proxy_set_header Host $host;
             proxy_set_header X-Real-IP $remote_addr;
             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
             
             proxy_http_version 1.1;
             proxy_set_header Upgrade $http_upgrade;
             proxy_set_header Connection $connection_upgrade;
         }
        ssl_certificate /etc/data/shop-web-mgt.onlythinking.com/full.pem;
        ssl_certificate_key /etc/data/shop-web-mgt.onlythinking.com/privkey.pem;
    }
    

    Realize Effect Graphics

    The interface is ugly, because it's not very good, please don't laugh!!

Project Address

https://github.com/cuteJ/shop-server (back-end)

https://github.com/cuteJ/shop-web-mgt (Front End)

Project presentation address

http://shop-web-mgt.onlythinking.com

Summary

Learn Websocket and write this Demo to impress you!

Tags: Java Session socket JSON Vue

Posted on Tue, 19 May 2020 12:21:49 -0400 by softnmedia