【netty聊天室实战】用protobuf进行聊天室消息发送的前后端代码

聊天室管理后台:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

前端代码:

/*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());
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值