聊天室管理后台:
前端代码:
/*CIM服务器IP*/
const CIM_HOST = window.location.hostname;
/*
*服务端 websocket端口
*/
const CIM_PORT = 45678;
const CIM_URI = "ws://" + CIM_HOST + ":" + CIM_PORT;
/*
*特殊的消息类型,代表被服务端强制下线
*/
const ACTION_999 = "999";
const DATA_HEADER_LENGTH = 1;
const MESSAGE = 2;
const REPLY_BODY = 4;
const SENT_BODY = 3;
const PING = 1;
const PONG = 0;
/**
* PONG字符串转换后
* @type {Uint8Array}
*/
const PONG_BODY = new Uint8Array([80,79,78,71]);
let socket;
let manualStop = false;
const CIMPushManager = {};
let roomId;
CIMPushManager.connect = function (id) {
roomId = id;
let isJoined = window.sessionStorage.getItem(roomId);
if (isJoined !== 'true'){
quitRoom();
return;
}
manualStop = false;
socket = new WebSocket(CIM_URI);
socket.cookieEnabled = false;
socket.binaryType = 'arraybuffer';
socket.onopen = CIMPushManager.innerOnConnectFinished;
socket.onmessage = CIMPushManager.innerOnMessageReceived;
socket.onclose = CIMPushManager.innerOnConnectionClosed;
};
CIMPushManager.bind = function (roomId) {
let body = new proto.com.farsunset.cim.sdk.web.model.SentBody();
body.setKey("client_bind");
body.setTimestamp(new Date().getTime());
body.getDataMap().set("uid", window.localStorage.uid);
body.getDataMap().set("roomId", roomId);
body.getDataMap().set("name", window.localStorage.name);
CIMPushManager.sendRequest(body);
};
CIMPushManager.setChatting = function (value) {
isChatting = value;
};
CIMPushManager.stop = function () {
manualStop = true;
socket.close();
};
CIMPushManager.resume = function () {
manualStop = false;
CIMPushManager.connect(roomId);
};
CIMPushManager.innerOnConnectFinished = function () {
onConnectFinished();
};
CIMPushManager.innerOnMessageReceived = function (e) {
let data = new Uint8Array(e.data);
let type = data[0];
let body = data.subarray(DATA_HEADER_LENGTH, data.length);
if (type === PING) {
CIMPushManager.pong();
return;
}
if (type === MESSAGE) {
let message = proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinary(body);
onInterceptMessageReceived(message.toObject(false));
return;
}
if (type === REPLY_BODY) {
let message = proto.com.farsunset.cim.sdk.web.model.ReplyBody.deserializeBinary(body);
/**
* 将proto对象转换成json对象,去除无用信息
*/
let reply = {};
reply.code = message.getCode();
reply.key = message.getKey();
reply.message = message.getMessage();
reply.timestamp = message.getTimestamp();
reply.data = {};
/**
* 注意,遍历map这里的参数 value在前key在后
*/
message.getDataMap().forEach(function (v, k) {
reply.data[k] = v;
});
onReplyReceived(reply);
}
};
CIMPushManager.innerOnConnectionClosed = function (e) {
if (!manualStop) {
setTimeout(function () {
CIMPushManager.connect(roomId);
}, 1);
}
};
CIMPushManager.sendRequest = function (body) {
let data = body.serializeBinary();
let protobuf = new Uint8Array(data.length + 1);
protobuf[0] = SENT_BODY;
protobuf.set(data, 1);
socket.send(protobuf);
};
CIMPushManager.pong = function () {
let pong = new Uint8Array(PONG_BODY.byteLength + 1);
pong[0] = PONG;
pong.set(PONG_BODY,1);
socket.send(pong);
};
function onInterceptMessageReceived(message) {
/*
*收到消息后,将消息发送给页面
*/
if (onMessageReceived instanceof Function) {
onMessageReceived(message);
}
}
发送消息的js函数:
CIMPushManager.sendRequest = function (body) {
let data = body.serializeBinary();
let protobuf = new Uint8Array(data.length + 1);
protobuf[0] = SENT_BODY;
protobuf.set(data, 1);
socket.send(protobuf);
};
聊天室Socket连接成功时回调bind方法:(在room.html中)
/** 当socket连接成功回调 **/
function onConnectFinished(){
let roomId = "${roomId!}";
CIMPushManager.bind(roomId);
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(reply){
hideProcess();
}
其中room.html全部代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Chat room</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
<!-- rel="stylesheet"就是声明此文件为样式表文件,告诉浏览器你link过来的是一个样式。-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" >
<link href="/resource/css/app.css?v=1" rel="stylesheet" >
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/viewerjs@1.10.1/dist/viewer.min.css">
<script type="text/javascript" src="/resource/js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/resource/js/common.js" ></script>
<script type="text/javascript" src="/resource/cim/message.js"></script>
<script type="text/javascript" src="/resource/cim/replybody.js"></script>
<script type="text/javascript" src="/resource/cim/sentbody.js"></script>
<script type="text/javascript" src="/resource/cim/cim.web.sdk.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/viewerjs@1.10.1/dist/viewer.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-viewer@1.0.1/dist/jquery-viewer.min.js"></script>
<script>
let ACTION_CHAT = "0";
let ACTION_JOIN_ROOM = "1";
let ACTION_LEAVE_ROOM = "2";
let ACTION_OUT_ROOM = "3";
let toTextView;
let toImageView;
let fromTextView;
let fromImageView;
let memberView;
let tipsView;
function init(){
let uid = window.localStorage.uid;
if (uid === undefined) {
window.location.href="/app";
// window.location.href 是一个跳转,它可以在动态,静态页面中 都可以实现 跳转。
// windows.location.href="/url" 当前页面打开URL页面,前面三个用法相同。
return;
}
// localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。
//
// localStorage 用于长久保存整个网站的数据,保存的数据没有过期时间,直到手动去删除。
//
// localStorage 属性是只读的。
CIMPushManager.connect('${roomId!}');
$(".window").css({"top":$(".appbar").innerHeight(),bottom:$(".input-panel").innerHeight()});
toTextView = $("#to_text_view");
toTextView.remove();
// remove() 方法移除被选元素,包括所有的文本和子节点。
//
// 该方法也会移除被选元素的数据和事件。
fromTextView = $("#from_text_view");
fromTextView.remove();
toImageView = $("#to_image_view");
toImageView.remove();
fromImageView = $("#from_image_view");
fromImageView.remove();
tipsView = $("#tips");
tipsView.remove();
memberView = $("#item_member_view");
memberView.remove();
$.get({
url: '/api/room/${roomId!}',
success: function (data) {
$(document).attr("title",data.data.name);
$("#roomName").text(data.data.name);
}
});
$("#input-text").keyup(function(event){
//keyup:当用户释放键盘上的按键时触发event.keyCode==13代表的就是回车键Enter,意思就是点击回车,
if(event.keyCode === 13){
onMessageSend();
}
});
}
/***********************************推送配置开始**************************/
/** 当socket连接成功回调 **/
function onConnectFinished(){
let roomId = "${roomId!}";
CIMPushManager.bind(roomId);
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(reply){
hideProcess();
}
function quitRoom(){
window.sessionStorage.removeItem('${roomId!}');
window.location.href="/home?id=${roomId!}"
}
function onClickQuitRoom(){
showProcess("Loading......");
let uid = window.localStorage.uid;
let name = window.localStorage.name;
$.post({
url: '/api/room/leave',
data:{uid:uid,name:name,roomId:${roomId!}},
success: function (data) {
hideProcess();
if (data.code === 200){
quitRoom();
}
}
});
}
/** 当收到消息时候回调 **/
function onMessageReceived(message)
{
console.log(message);
if(message.action === ACTION_CHAT && message.format == 0){
let view = fromTextView.clone();
view.find(".time").text(toDatetime(message.timestamp));
view.find(".message").html(message.content);
view.find(".message").css({"max-width":$(window).width() / 1.8});
view.find(".name").text(message.extra);
view.find(".icon").attr("src","/api/file/user-icon/"+message.sender +"?version="+message.title);
this.addWindowVew(view);
}
if(message.action === ACTION_CHAT && message.format == 1){
let view = fromImageView.clone();
view.find(".time").text(toDatetime(message.timestamp));
view.find(".name").text(message.extra);
view.find(".icon").attr("src","/api/file/user-icon/"+message.sender +"?version="+message.title);
let json = $.parseJSON( message.content );
addImageView(json,view);
}
if(message.action === ACTION_JOIN_ROOM){
let view = tipsView.clone();
view.find(".name").text(message.extra);
view.find(".action").text("entered the room.");
addWindowVew(view);
return
}
if(message.action === ACTION_LEAVE_ROOM){
let view = tipsView.clone();
view.find(".name").text(message.extra);
view.find(".action").text("left the room.");
addWindowVew(view);
}
if(message.action === ACTION_OUT_ROOM){
quitRoom();
}
}
function onImageSelected() {
showProcess("Image uploading.....");
$("#imageForm").find("input[name=key]").val(generateUUID());
document.getElementById('imageForm').submit();
}
function onMessageSend(){
$(".emoji-panel").hide();
let content = $("#input-text").html().replaceAll("<div><br></div>","");
if ($.trim(content) === ''){
return;
}
let uid = window.localStorage.uid;
let name = window.localStorage.name;
let icon = window.localStorage.icon;
$.post({
url: '/api/message/send',
data:{uid:uid,name:name,icon:icon,roomId:${roomId!},format:0,content:content},
success: function (data) {
if (data.code === 200){
$("#input-text").empty();
let view = toTextView.clone();
view.find(".time").text(toDatetime(data.data));
view.find(".message").html(content);
view.find(".name").text(name);
view.find(".icon").attr("src","/api/file/user-icon/"+uid +"?version="+icon);
view.find(".message").css({"max-width":$(window).width() / 1.8});
addWindowVew(view);
}
}
});
}
其中点击回车发送消息的部分:
$("#input-text").keyup(function(event){
//keyup:当用户释放键盘上的按键时触发event.keyCode==13代表的就是回车键Enter,意思就是点击回车,
if(event.keyCode === 13){
onMessageSend();
}
});
后端聊天室代码:
实体类:
@Data
@Entity
@Table(name = "t_chat_room_member")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "room_id")
private Long roomId;
@Column(name = "name", length = 16,nullable = false)
private String name;
@Column(name = "uid", length = 32,nullable = false)
private String uid;
@JsonIgnore
@Column(name = "create_time",nullable = false)
private Date createTime;
}
控制器:
@RestController("apiRoomController")
@RequestMapping("/api/room")
public class RoomController {
@Resource
private MemberService memberService;
@Resource
private RoomService roomService;
@PostMapping(value = "/join")
public @ResponseBody ResponseEntity<Void> join(@RequestParam long roomId,
@RequestParam String name,
@RequestParam String uid,
@RequestParam String password) {
if (!roomService.checkPassword(roomId,password)){
return ResponseEntity.make(HttpStatus.FORBIDDEN);
}
Member member = new Member();
member.setUid(uid);
member.setName(name);
member.setRoomId(roomId);
memberService.pushEvent(member, MessageAction.ACTION_JOIN_ROOM);
return ResponseEntity.make();
}
@PostMapping(value = "/leave")
public @ResponseBody ResponseEntity<Void> leave(@RequestParam long roomId,
@RequestParam String name,
@RequestParam String uid) {
Member member = new Member();
member.setUid(uid);
member.setName(name);
member.setRoomId(roomId);
memberService.remove(member);
memberService.pushEvent(member, MessageAction.ACTION_LEAVE_ROOM);
return ResponseEntity.make();
}
@GetMapping(value = "/{id}")
public @ResponseBody ResponseEntity<Room> get(@PathVariable long id) {
return ResponseEntity.ok(roomService.findOne(id));
}
}
其中pushevent进行推送事件的代码:
public void pushEvent(Member member,String action){
Message message = new Message();
message.setId(System.currentTimeMillis());
message.setAction(action);
message.setSender(member.getUid());
message.setReceiver(String.valueOf(member.getRoomId()));
message.setExtra(member.getName());
message.setTimestamp(System.currentTimeMillis());
groupMessagePusher.push(message);
}
推送消息类:
/**
* 推送群消息
*/
@Component
public class GroupMessagePusher {
@Resource
private TagSessionGroup tagSessionGroup;
public void push(final Message message) {
String roomId = message.getReceiver();
tagSessionGroup.write(roomId,message , channel -> !Objects.equals(message.getSender(),channel.attr(ChannelAttr.UID).get()));
}
public void push(final Message message,String uid) {
String roomId = message.getReceiver();
tagSessionGroup.write(roomId,message , channel -> Objects.equals(uid,channel.attr(ChannelAttr.UID).get()));
}
}
taggroup定义:
public class TagSessionGroup extends SessionGroup {
public TagSessionGroup() {
}
protected String getKey(Channel channel) {
return (String)channel.attr(ChannelAttr.TAG).get();
}
}
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SessionGroup extends ConcurrentHashMap<String, Collection<Channel>> {
private static final Collection<Channel> EMPTY_LIST = new LinkedList();
private final transient ChannelFutureListener remover = new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
future.removeListener(this);
SessionGroup.this.remove(future.channel());
}
};
public SessionGroup() {
}
protected String getKey(Channel channel) {
return (String)channel.attr(ChannelAttr.UID).get();
}
public void remove(Channel channel) {
String uid = this.getKey(channel);
if (uid != null) {
Collection<Channel> collections = (Collection)this.getOrDefault(uid, EMPTY_LIST);
collections.remove(channel);
if (collections.isEmpty()) {
this.remove(uid);
}
}
}
public void add(Channel channel) {
String uid = this.getKey(channel);
if (uid != null && channel.isActive()) {
channel.closeFuture().addListener(this.remover);
Collection<Channel> collections = (Collection)this.putIfAbsent(uid, new ConcurrentLinkedQueue(Collections.singleton(channel)));
if (collections != null) {
collections.add(channel);
}
if (!channel.isActive()) {
this.remove(channel);
}
}
}
public void write(String key, Message message) {
this.find(key).forEach((channel) -> {
channel.writeAndFlush(message);
});
}
public void write(String key, Message message, Predicate<Channel> matcher) {
this.find(key).stream().filter(matcher).forEach((channel) -> {
channel.writeAndFlush(message);
});
}
public void write(String key, Message message, Collection<String> excludedSet) {
this.find(key).stream().filter((channel) -> {
return excludedSet == null || !excludedSet.contains(channel.attr(ChannelAttr.UID).get());
}).forEach((channel) -> {
channel.writeAndFlush(message);
});
}
public void write(Message message) {
this.write(message.getReceiver(), message);
}
public Collection<Channel> find(String key) {
return (Collection)this.getOrDefault(key, EMPTY_LIST);
}
public Collection<Channel> find(String key, String... channel) {
List<String> channels = Arrays.asList(channel);
return (Collection)this.find(key).stream().filter((item) -> {
return channels.contains(item.attr(ChannelAttr.CHANNEL).get());
}).collect(Collectors.toList());
}
}