简版在线聊天Websocket
序言
What is Webscoket ?
websocket 应用场景
简版群聊实现
代码例子
小结
Webscoket
Websokcet 是一种单个TCP连接上进行全双工通信的协议,通过HTTP/1.1 协议的101状态码进行握手。
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>
<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="请输入你想说的内容..."
@keyup.enter.native="handleSendMsg"
></el-input>
<div class="chat-btns"> <el-button
class="action-item"
@click="handleClearMsg"
>清空
</el-button>
<el-button
type="success"
class="action-item"
@click="handleSendMsg"
v-scroll-to="{ el: '#msgList', offset: 140 }"
>发送
</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
})
}
// 监听消息接收
this.$options.sockets.onmessage = (res) => {
const data = JSON.parse(res.data);
console.log('收到消息', data);
if (data.code === 0) {
// 连接建立成功
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: '请输入内容'
});
return;
}
this.$store.dispatch('SendChatMsg', this.text).then(data => {
this.text = ''
})
},
handleClearMsg() {
this.msgList = [];
Cookies.remove('chatMsg');
// 删除
},
// 聊天关闭前
handleClose() {
// 缓存消息到本地
Cookies.set('chatMsg', JSON.stringify(this.msgList));
this.$emit('update:visible', false)
}
},
created() {
// 加载缓存数据
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 代理配置 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;
}
实现效果图
界面比较丑,因为不太擅长,请大家别见笑!!



项目地址
项目演示地址
小结
该篇学习Websocket,写此Demo加深印象!
简版在线聊天Websocket的更多相关文章
- 在线聊天室的实现(1)--websocket协议和javascript版的api
前言: 大家刚学socket编程的时候, 往往以聊天室作为学习DEMO, 实现简单且上手容易. 该Demo被不同语言实现和演绎, 网上相关资料亦不胜枚举. 以至于很多技术书籍在讲解网络相关的编程时, ...
- 使用WebSocket实现简单的在线聊天室
前言:我自已在网上找好了好多 WebSocket 制作 在线聊天室的案列,发现大佬们写得太高深了 我这种新手看不懂,所以就自已尝试写了一个在线简易聊天室 (我只用了js 可以用jq ) 话不多说,直接 ...
- javascript版QQ在线聊天挂件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- 使用websocket实现在线聊天功能
很早以前为了快速达到效果,使用轮询实现了在线聊天功能,后来无意接触了socket,关于socket我的理解是进程间通信,首先要有服务器跟客户端,服务的启动监听某ip端口定位该进程,客户端开启socke ...
- java版的类似飞秋的局域网在线聊天项目
原文链接:http://www.cnblogs.com/wangleiblog/articles/5323305.html 转载请注明 最近在弄一个java版的局域网在线聊天项目,功能跟飞秋差不多.p ...
- Spring Websocket实现简易在线聊天功能
针对Spring Websocket的实现,我参照了其他博主的文章https://www.cnblogs.com/leechenxiang/p/5306372.html 下面直接给出实现: 一.引入相 ...
- java在线聊天项目1.3版 ——设计好友列表框功能
设计好友列表框功能,思路—— 1.当客户端成功登陆后,则客户端把成功登陆信息发送给服务端, 2.由服务端将接收到来自各个成功登陆的客户端的用户信息添加进好友列表, 3.每当有成功登陆的用户就向各个客户 ...
- Swoole跟thinkphp5结合开发WebSocket在线聊天通讯系统
ThinkPHP使用Swoole需要安装 think-swoole Composer包,前提系统已经安装好了Swoole PECL 拓展* tp5的项目根目录下执行composer命令安装think- ...
- SpringBoot+Vue+WebSocket 实现在线聊天
一.前言 本文将基于 SpringBoot + Vue + WebSocket 实现一个简单的在线聊天功能 页面如下: 在线体验地址:http://www.zhengqingya.com:8101 二 ...
随机推荐
- git在push时候出现timeout的解决方法
1.先对照一下公钥是否正确:linux下生成的.ssh在~/.ssh,命令行直接cd ~/.ssh即可:2.本次出现的问题如下: 出现了连接不上的情况,传输协议用的是ssh,此时需要进行如下操作: 进 ...
- 数据挖掘入门系列教程(十一)之keras入门使用以及构建DNN网络识别MNIST
简介 在上一篇博客:数据挖掘入门系列教程(十点五)之DNN介绍及公式推导中,详细的介绍了DNN,并对其进行了公式推导.本来这篇博客是准备直接介绍CNN的,但是想了一下,觉得还是使用keras构建一个D ...
- 非阻塞算法(Lock-Free)的实现
目录 非阻塞的栈 非阻塞的链表 非阻塞算法(Lock-Free)的实现 上篇文章我们讲到了使用锁会带来的各种缺点,本文将会讲解如何使用非阻塞算法.非阻塞算法一般会使用CAS来协调线程的操作. 虽然非阻 ...
- if __name__ == '__main__'到底是什么?
引子 要搞清楚这个问题,可以先听一个故事~~~ 像我们做事一样,都需要一个起始点,终点存在与否无关紧要.编程也是一样,任何程序都有一个入口,在所谓的静态编译语言中,如Java的入口是一个名字叫做Mai ...
- c语言----实战植物大战僵尸
1. 原理 通过指针先找到阳光的地址,然后修改地址对应的值即修改阳光值. 2. 工具 CheatEngine --- 查询进程中变量的地址 Dll注入工具 ----- 注入 VS2017 3. ...
- python 列表加法"+"和"extend"的区别
相同点 : "+"和"extend"都能将两个列表成员拼接到到一起 不同点 : + : 生成的是一个新列表(id改变) extend : 是将一个列表的成员 ...
- python操作ftp文件
from ftplib import FTP ftp = FTP('ftp.abc.com') ftp.login(user='username', passwd='********') ftp.cw ...
- spring security learning(spring in action)
1.使用Spring Security配置命名空间 spring securtiy 提供了安全性相关的命名空间,我们可以将spring security的命名空间声明添加到spring公用的配置xml ...
- bzoj 4152[AMPPZ2014]The Captain
bzoj 4152[AMPPZ2014]The Captain 给定平面上的n个点,定义(x1,y1)到(x2,y2)的费用为min(|x1-x2|,|y1-y2|),求从1号点走到n号点的最小费用. ...
- Redux在项目中的文件结构
React + Redux 今天我们来唠唠在React一般项目中,使用Redux进行状态管理的时候,相对的如何存放reducer.action.api之类文件的结构与使用时机吧.本章默认看官们已经 ...