前言

最近在做一个聊天性能,具体需要:相似微信,在一个好友列表中,点击某个好友就能够建设与该好友的聊天连贯,向该好友发送音讯,对方可能实时显示进去,进行真正意义上的聊天。
在做之前,不论在界面布局,还是性能实现方面都下了一点功夫,最终还是一点点实现了,当初就记录一下。

在编码之前得先理解一下WebSocket

  1. 什么是WebSocket?
  • WebSocket,即Web浏览器与Web服务器之间全双工通信规范;是HTML5中的协定,反对长久间断,http协定不反对持久性连贯。Http1.0和HTTP1.1都不反对持久性的链接,HTTP1.1中的keep-alive,将多个http申请合并为1个
  • 一旦确立WebSocket通信连贯,不管服务器还是客户端,任意一方都可间接向对方发送报文
  1. WebSocket特点?
  • 推送性能:反对由服务器向客户端推送数据的推送性能,这样,服务器可间接发送数据,而不用期待客户端的申请
  • 缩小通信量:只有建设起WebSocket,就能够始终放弃连贯状态

头部字段多了上面2个属性:

Upgrade:webSocketConnection:Upgrade

1、实现成果

点击左侧好友列表时,会建设websocket连贯,把以后发消息的用户发送给websocket服务器

输出音讯

2、前端实现

<!-- Chat.vue页面 --><template>  <div id="chat">    <!-- 聊天音讯治理 -->    <el-container style="height: 620px; border: 1px solid #eee">      <el-aside width="250px">        <user-list          :friendList="this.friendList"          @set-contact="getcontact"          ref="friends"          :activeIndex="activeIndex"        ></user-list>      </el-aside>      <el-container>        <el-header style="text-align: right; font-size: 12px">          <span>            <h2>{{this.username}}</h2>          </span>        </el-header>        <el-main style="height:400px;" class="msg-main">          <chat-msg ref="msg" :user="this.username" id="msg-box"></chat-msg>        </el-main>        <div class="m-text">          <textarea            placeholder="按 Ctrl + Enter 发送"            ref="sendMsg"            v-model="contentText"            @keyup.enter="sendText()"          ></textarea>          <div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">发送</div>        </div>      </el-container>    </el-container>  </div></template><script>import UserList from "../../components/chat/friendList";import ChatMsg from "../../components/chat/message";import InputText from "../../components/chat/text";export default {  data() {    return {      //好友列表      friendList: [],      activeIndex: null,      //以后聊天好友      activeFriend: [],      friends: "",      ws: null,      count: 0,      userId: this.$store.state.userInfo.uid, // 以后用户ID      username: this.$store.state.userInfo.username, // 以后用户昵称      avatar: this.$store.state.userInfo.uavatar, // 以后用户头像      msgList: [], // 聊天记录的数组      contentText: "" // input输出的值    };  },  components: {    UserList,    ChatMsg,    InputText  },  mounted() {    this.getFridends(this.userId);  },  destroyed() {    // 来到页面时敞开websocket连贯    this.ws.onclose(undefined);  },  methods: {    select(value) {      this.activeIndex = this.friendList.indexOf(value);    },    getcontact(list) {      console.log(this.$store.state.userInfo);      this.activeFriend = list;      //保留以后聊天好友      this.friends = list.ffriendName;      this.activeIndex = this.friendList.indexOf(this.friends);      this.getFridendInfo(this.friends);      this.initWebSocket(this.username);      this.getFridendMsg(this.friends);    },    // 发送聊天信息    sendText() {      let _this = this;      _this.$refs["sendMsg"].focus();      if (!_this.contentText) {        return;      }      let params = {        mfromuser: _this.username,        mtouser: _this.activeFriend[0].uusername,        mfromavatar: _this.activeFriend[0].uavatar,        mtoavatar: _this.avatar,        mmsg: _this.contentText,        mtype: 1      };      _this.ws.send(JSON.stringify(params)); //调用WebSocket send()发送信息的办法      _this.contentText = "";      setTimeout(() => {        _this.scrollToBottom();      }, 200);    },    // 进入页面创立websocket连贯    initWebSocket(id) {      let _this = this;      // 判断页面有没有存在websocket连贯      if (window.WebSocket) {        var serverHot = window.location.hostname;        // 填写本地IP地址,此处的 :8021端口号要与后端配置的统一!        var url = "ws://" + serverHot + ":8021" + "/websocket/" + id;         // `ws://127.0.0.1/9101/websocket/10086/老王`        let ws = new WebSocket(url);        _this.ws = ws;        ws.onopen = function(e) {          console.log("服务器连贯胜利: " + url);        };        ws.onclose = function(e) {          console.log("服务器连贯敞开: " + url);        };        ws.onerror = function() {          console.log("服务器连贯出错: " + url);        };        ws.onmessage = function(e) {          //接管服务器返回的数据          console.log(e);          let resData = e.data.split("|");          console.log(resData);          //更新音讯          if (resData[0] == _this.username) {            _this.getFridendMsg(_this.friends);          } else {            _this.$message.error("发送音讯失败,以后用户不存在");          }        };      }    },    //获取好友列表    async getFridends(id) {      console.log(id);      const { data: res } = await this.$http.get(        "api/business/chat/getfriends/" + id      );      if (res.success) {        // console.log(res);        this.friendList = res.data;      } else {        this.$message.error("获取好友列表失败:" + res.data.errorMsg);      }    },    //获取好友音讯列表    async getFridendMsg(name) {      const { data: res } = await this.$http.get(        "api/business/chat/getFriendMsg/" + name      );      if (res.success) {        // console.log(res);        this.msgList = res.data;        //把好友聊天记录传给音讯组件        this.$refs.msg.getMsgList(this.msgList);      } else {        this.$message.error("获取好友音讯失败:" + res.data.errorMsg);      }    },    //获取好友信息    async getFridendInfo(name) {      const { data: res } = await this.$http.get(        "api//system/user/getFriendInfo/" + name      );      if (res.success) {        console.log(res);        //好友信息        this.activeFriend = res.data;      } else {        this.$message.error("获取好友信息失败:" + res.data.errorMsg);      }    },    // 滚动条到底部    scrollToBottom() {      this.$nextTick(() => {        var container = this.$el.querySelector(".msg-main");        // console.log(container);        // console.log(container.scrollTop);        container.scrollTop = container.scrollHeight;      });    }  }};</script><style scoped lang="less">#chat {  overflow: hidden;}.el-header {  background-color: #b3c0d1;  color: #333;  line-height: 60px;}.el-aside {  color: #fff;  background-color: #2e3238;}.m-list {  li {    padding: 12px 15px;    border-bottom: 1px solid #292c33;    cursor: pointer;    transition: background-color 0.1s;    &:hover {      background-color: rgba(255, 255, 255, 0.03);    }    &.active {      background-color: rgba(255, 255, 255, 0.1);    }  }  .avatar,  .name {    vertical-align: middle;  }  .avatar {    border-radius: 2px;  }  .name {    display: inline-block;    margin: 0 0 0 15px;  }}.m-message {  padding: 10px 15px;  li {    margin-bottom: 15px;  }  .time {    margin: 7px 0;    text-align: center;    > span {      display: inline-block;      padding: 3px 20px;      font-size: 12px;      color: #fff;      border-radius: 2px;      background-color: #dcdcdc;    }  }  .avatar {    float: left;    margin: 0 15px 0 0;    border-radius: 3px;  }  .text {    display: inline-block;    position: relative;    padding: 3px 17px;    max-width: ~"calc(100% - 40px)";    min-height: 30px;    line-height: 2.5;    font-size: 12px;    text-align: left;    word-break: break-all;    background-color: #fafafa;    border-radius: 4px;    &:before {      content: " ";      position: absolute;      top: 9px;      right: 100%;      border: 6px solid transparent;      border-right-color: #fafafa;    }  }  .self {    text-align: right;    .avatar {      float: right;      margin: 0 0 0 10px;    }    .text {      background-color: #b2e281;      &:before {        right: inherit;        left: 100%;        border-right-color: transparent;        border-left-color: #b2e281;      }    }  }}.m-text {  display: flex;  height: 90px;  border-top: solid 1px #ddd;  textarea {    padding-left: 10px;    height: 100%;    width: 100%;    border: none;    outline: none;    font-family: "Micrsofot Yahei";    resize: none;  }  .btn {    height: 2.3rem;    min-width: 4rem;    background: #e0e0e0;    padding: 0.5rem;    font-size: 0.88rem;    color: white;    text-align: center;    border-radius: 0.2rem;    margin-left: 0.5rem;    transition: 0.5s;  }  .btn-active {    background: #409eff;  }}</style>
<!-- friendList.vue --><template>  <div class="m-list">    <ul v-for="(friend, i) in friendList">      <li @click="setContact(i)" :class="{'active':currentIndex===i}">        <img class="avatar" width="50" height="50" />        <p class="name">{{friend.ffriendName}}</p>      </li>    </ul>  </div></template><script>export default {  data() {    return {      currentIndex: this.activeIndex    };  },  props: ["friendList", "activeIndex"],  methods: {    setContact(index) {      this.currentIndex = index;      console.log(this.currentIndex);      //与父组件进行通信。把以后点击好友的用户信息传给父组件      this.$emit("set-contact", this.friendList[index]);    }  }};</script><style lang="less" scoped>.m-list {  li {    padding: 12px 15px;    border-bottom: 1px solid #292c33;    cursor: pointer;    transition: background-color 0.1s;    &:hover {      background-color: rgba(255, 255, 255, 0.03);    }    &.active {      background-color: rgba(255, 255, 255, 0.1);    }  }  .avatar,  .name {    vertical-align: middle;  }  .avatar {    border-radius: 2px;  }  .name {    display: inline-block;    margin: 0 0 0 15px;  }}</style>
<!-- message.vue --><template>  <div class="m-message">    <ul v-for="item in msgList">      <li>        <p class="time">          <span>{{item.mcreateTime | time}}</span>        </p>        <div class="main" :class="{self:item.mfromUser===username}">          <div class="title">{{item.mfromUser}}</div>          <img class="avatar" width="40" height="40" />          <div class="text">{{item.mmsg}}</div>        </div>      </li>    </ul>  </div></template><script>export default {  data() {    return {      msgList: this.userList,      username: this.user    };  },  props: ["user", "userList"],  methods: {    getMsgList(list) {      console.log(list);      this.msgList = list;    },  },  filters: {    // 将日期过滤为 yy-mm-dd hh:mm:ss    time(date) {      if (typeof date === "string") {        date = new Date(date); //把定义的工夫赋值进来进行上面的转换        let year = date.getFullYear();        let month = date.getMonth() + 1;        let day = date.getDate();        let hour = date.getHours();        let minute = date.getMinutes();        let second = date.getSeconds();        return (          year +          "-" +          month +          "-" +          day +          " " +          hour +          ":" +          minute +          ":" +          second        );      }    }  }};</script><style lang="less" scoped>.m-message {  padding: 10px 15px;  // overflow-y: scroll;  li {    margin-bottom: 15px;  }  .time {    margin: 7px 0;    text-align: center;    > span {      display: inline-block;      padding: 3px 20px;      font-size: 12px;      color: #fff;      border-radius: 2px;      background-color: #dcdcdc;    }  }  .avatar {    float: left;    margin: 0 15px 0 0;    border-radius: 3px;  }  .text {    display: inline-block;    position: relative;    padding: 3px 17px;    max-width: ~"calc(100% - 40px)";    min-height: 30px;    line-height: 2.5;    font-size: 12px;    text-align: left;    word-break: break-all;    background-color: #fafafa;    border-radius: 4px;    &:before {      content: " ";      position: absolute;      top: 9px;      right: 100%;      border: 6px solid transparent;      border-right-color: #fafafa;    }  }  .self {    text-align: right;    .avatar {      float: right;      margin: 0 0 0 10px;    }    .text {      background-color: #b2e281;      &:before {        right: inherit;        left: 100%;        border-right-color: transparent;        border-left-color: #b2e281;      }    }  }}</style>

3、后端实现

增加依赖

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-websocket</artifactId></dependency>
// WebSocketController.java@Controller@ServerEndpoint(value = "/websocket/{user}")@Api(tags = "业务模块-websocket连贯接口")public class WebSocketController {    // 这里应用动态,让 service 属于类    private static ChatMsgService chatMsgService;    // 注入的时候,给类的 service 注入    @Resource    public void setChatService(ChatMsgService chatService) {        WebSocketController.chatMsgService = chatService;    }    // 动态变量,用来记录以后在线连接数。应该把它设计成线程平安的。    private static int onlineCount = 0;    // concurrent包的线程平安Set,用来寄存每个客户端对应的MyWebSocket对象。若要实现服务端与繁多客户端通信的话,能够应用Map来寄存,其中Key能够为用户标识    private static ConcurrentHashMap<String, WebSocketController> webSocketSet = new ConcurrentHashMap<String, WebSocketController>();    // 与某个客户端的连贯会话,须要通过它来给客户端发送数据    private Session WebSocketSession;    // 记录以后发消息的用户    private String user = "";    /**     * 连贯建设胜利调用的办法     *     * session 可选的参数。session为与某个客户端的连贯会话,须要通过它来给客户端发送数据     */    @OnOpen    public void onOpen(@PathParam(value = "user") String param, Session WebSocketsession) {        user = param;        // System.out.println(user);        this.WebSocketSession = WebSocketsession;        webSocketSet.put(param, this);// 退出map中        addOnlineCount();     // 在线数加1        // System.out.println("有新连贯退出!以后在线人数为" + getOnlineCount());    }    /**     * 连贯敞开调用的办法     */    @OnClose    public void onClose() {        if (!user.equals("")) {            webSocketSet.remove(user); // 从set中删除            subOnlineCount();     // 在线数减1            // System.out.println("有一连贯敞开!以后在线人数为" + getOnlineCount());        }    }    /**     * 收到客户端音讯后调用的办法     *     * @param chatmsg 客户端发送过去的音讯     * @param session 可选的参数     */    @OnMessage    public void onMessage(String chatmsg, Session session)  throws SystemException{        JSONObject jsonObject = JSONObject.parseObject(chatmsg);        //给指定的人发消息        sendToUser(jsonObject.toJavaObject(ChatMsgVO.class));        //sendAll(message);    }    /**     * 给指定的人发送音讯     *     * @param chatMsg 音讯对象     */    public void sendToUser(ChatMsgVO chatMsg)  throws SystemException{        String fromUser = chatMsg.getMFromUser();        String mMsg = chatMsg.getMMsg();        System.out.println(fromUser);        mMsg= EmojiFilter.filterEmoji(mMsg);//过滤输入法输出的表情        chatMsgService.InsertChatMsg(chatMsg);        try {            if (webSocketSet.get(fromUser) != null) {                webSocketSet.get(fromUser).sendMessage(chatMsg.getMFromUser()+"|"+mMsg);            }else{                webSocketSet.get(chatMsg.getMFromUser()).sendMessage("0"+"|"+"以后用户不在线");            }        } catch (IOException e) {            throw new SystemException(SystemCodeEnum.PARAMETER_ERROR,e.getMessage());        }    }    /**     * 给所有人发消息     *     * @param message     */    private void sendAll(String message) {        String sendMessage = message.split("[|]")[1];        //遍历HashMap        for (String key : webSocketSet.keySet()) {            try {                //判断接管用户是否是以后发消息的用户                if (!user.equals(key)) {                    webSocketSet.get(key).sendMessage(sendMessage);                    System.out.println("key = " + key);                }            } catch (IOException e) {                e.printStackTrace();            }        }    }    /**     * 产生谬误时调用     *     * @param session     * @param error     */    @OnError    public void onError(Session session, Throwable error) {        error.printStackTrace();    }    /**     * 这个办法与下面几个办法不一样。没有用注解,是依据本人须要增加的办法。     *     * @param message     * @throws IOException     */    public void sendMessage(String message) throws IOException {        this.WebSocketSession.getBasicRemote().sendText(message);        //this.session.getAsyncRemote().sendText(message);    }    public static synchronized int getOnlineCount() {        return onlineCount;    }    public static synchronized void addOnlineCount() {        WebSocketController.onlineCount++;    }    public static synchronized void subOnlineCount() {        WebSocketController.onlineCount--;    }}
//ChatController.java@RestController@RequestMapping("business/chat")public class ChatMsgController {    @Resource    private ApiUserService apiUserService;    @Resource    private ChatMsgService chatMsgService;        @Resource    private ChatFriendsService chatFriendsService;    @ApiOperation(value = "获取好友聊天记录", notes = "依据以后用户查问好友聊天记录")    @GetMapping("/getFriendMsg/{username}")    public ResponseBean<List<ChatMsg>> getFriendMsg(@PathVariable String username) throws SystemException {        UserInfoVO userInfoVO = apiUserService.info();        List<ChatMsg> chatMsgs = chatMsgService.getFriendMsg(userInfoVO.getUsername(), username);        return ResponseBean.success(chatMsgs);    }    @ApiOperation(value = "获取用户好友", notes = "依据用户id查问用户好友")    @GetMapping("/getfriends/{id}")    public ResponseBean<List<ChatFriends>> getFriends(@PathVariable Long id) throws SystemException {        List<ChatFriends> chatFriendsList = chatFriendsService.getFriends(id);        return ResponseBean.success(chatFriendsList);    } }