websocket聊天室(简版在线聊天WebSocket)
websocket聊天室(简版在线聊天WebSocket)后端(shop-server)聊天主动轮询其实会产生大量无效请求,增加了服务器压力。由此,websocket 协议的补充,为我们带来了新的解决思路。利用Websocket 实现一个简陋群聊功能,加深一下Websocket 理解。
序言- What is Webscoket ?
- websocket 应用场景
- 简版群聊实现
- 代码例子
- 小结
Websokcet 是一种单个TCP连接上进行全双工通信的协议,通过HTTP/1.1 协议的101状态码进行握手。
http://websocket.org
Websocket 应用场景Websocket 和 http 协议都是web通讯协议,两者有何区别?先说Http,它是一种请求响应协议,这种模型决定了,只能客户端请求,服务端被动回答。如果我们有服务端主动推送给客户端的需求怎么办?比如一个股票网站,我们会选择主动轮询,也就是”拉模式“。
大家可以思考下主动轮询带来的问题是什么?
主动轮询其实会产生大量无效请求,增加了服务器压力。
由此,websocket 协议的补充,为我们带来了新的解决思路。
简版群聊实现利用Websocket 实现一个简陋群聊功能,加深一下Websocket 理解。
- 假设李雷和韩梅梅都登录在线;
- 李雷通过浏览器发送消息转nginx 代理到Ws服务器;
- Ws服务器加载所有在线会话广播消息;
- 韩梅梅接受到消息。
聊天
代码例子后端(shop-server)
引入pom.xml 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类
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();
}
}
接受请求端点
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);
}
}
}
聊天业务处理器
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) {
// 广播消息
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;
}
}
前端(shop-web-mgt)
引入依赖
npm install vue-native-websocket --save
添加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
编写组件
<template>
<div>
<ot-drawer
title="聊天"
: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>
<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>
Nginx 代理配置 nginx.conf (如有需要可添加)
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;
}
实现效果图
界面比较丑,因为不太擅长,请大家别见笑!!
聊天界面
聊天界面
聊天界面
聊天界面
项目地址
https://github.com/cuteJ/shop-server (后端)
https://github.com/cuteJ/shop-web-mgt (前端)
项目演示地址
http://shop-web-mgt.onlythinking.com
小结该篇学习Websocket,写此Demo加深印象!