前言
最近在做一个聊天功能,具体需求:类似微信,在一个好友列表中,点击某个好友就可以建立与该好友的聊天连接,向该好友发送消息,对方能够实时显示出来,进行真正意义上的聊天。
在做之前,不管在界面布局,还是功能实现方面都下了一点功夫,最终还是一点点实现了,现在就记录一下。
在编码之前得先了解一下WebSocket
- 什么是
WebSocket
?
WebSocket
,即Web浏览器与Web服务器之间全双工通信标准;是HTML5中的协议,支持持久连续,http协议不支持持久性连接。Http1.0和HTTP1.1都不支持持久性的链接,HTTP1.1中的keep-alive,将多个http请求合并为1个- 一旦确立
WebSocket
通信连接,不论服务器还是客户端,任意一方都可直接向对方发送报文
WebSocket
特点?
- 推送功能:支持由服务器向客户端推送数据的推送功能,这样,服务器可直接发送数据,而不必等待客户端的请求
- 减少通信量:只要建立起
WebSocket
,就可以一直保持连接状态头部字段多了下面2个属性:
Upgrade:webSocket Connection: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);
}
}