571
社区成员




WebSocket是一种在单个TCP连接上进行全双工通信的协议。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程.
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。
客户端利用websocket与服务端建立长连接,服务端判断客户端发送的消息类型,是注册,私聊还是群聊,根据不同的消息类型去执行相应的操作,如转发信息等.对所有建立连接的客户端将对应的管道加入一个常量onlineUserMap中,若一个用户A向另一个用户B私聊,若B在线,则将消息直接转发到管道中,若B不在线,则将对应的消息存放在缓存中,在每个用户上线时读取缓存中的数据看是否有发送给自己的信息.
id:主键
groupId:群组的id
groupName:群组的名字
userId:群组中包含的用户id
groupAvatarUrl:群组的头像
id:主键
userId:用户的id
username:用户名
password:用户密码
avatarUrl:用户头像
id:主键
userId:用户的id
friendId:用户的朋友的id
初始化时将webSocketChildChannelHandler设为默认的处理器,在webSocketChildChannelHandler中除了添加编码解码器外再先后添加httpRequestHandler和webSocketServerHandler.httpRequestHandler主要是对发送的http请求进行协议的升级,升级到websocket协议.
//默认的处理器为webSocketChildChannelHandler
@Resource(name = "webSocketChildChannelHandler")
private ChannelHandler childChannelHandler;
@Autowired
private EventLoopGroup bossGroup;
@Autowired
private EventLoopGroup workerGroup;
@Autowired
private ServerBootstrap serverBootstrap;
public WebSocketServer() {
}
@Override
public void run() {
try {
long begin = System.currentTimeMillis();
//option针对的是boss线程,childoption针对的是worker线程
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)//配置TCP,握手字符串长度设置
.option(ChannelOption.TCP_NODELAY, true)//使其尽可能的发送大块数据,提高效率
.childOption(ChannelOption.SO_KEEPALIVE, true)//开启心跳机制
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(592048))//配置固定长度接收缓存区分配器
.childHandler(childChannelHandler);
long end = System.currentTimeMillis();
logger.info("Netty Websocket服务器启动完成,耗时 " + (end - begin));
//绑定端口
serverChannelFuture = serverBootstrap.bind(port).sync();
//监听关闭事件
serverChannelFuture.channel().closeFuture().sync();
} catch (Exception e) {
logger.info(e.getMessage());
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
e.printStackTrace();
}
}
WebSocketServerHandler:主要是两个函数,一个是对用户发送的消息的处理,一个是去缓存中读取用户没有接收的信息并转发给用户.
WebSocketFrame是WebSocket服务在建立的时候,在通道中处理的数据类型.
/**
* 描述:读取完连接的消息后,对消息进行处理。
* 这里主要是处理WebSocket请求
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame webSocketFrame) throws Exception {
//关闭请求
if (webSocketFrame instanceof CloseWebSocketFrame) {
//
WebSocketServerHandshaker handshaker =
Constant.webSocketHandshakerMap.get(ctx.channel().id().asLongText());
if (handshaker == null) {
sendErrorMessage(ctx, "不存在的客户端连接!");
} else {
handshaker.close(ctx.channel(), ((CloseWebSocketFrame) webSocketFrame).retain());
}
return;
}
//ping请求
if (webSocketFrame instanceof PingWebSocketFrame) {
ctx.channel().write(new PongWebSocketFrame(webSocketFrame.content().retain()));
return;
}
//只支持文本格式,不支持二进制消息
if (!(webSocketFrame instanceof TextWebSocketFrame)) {
sendErrorMessage(ctx, "仅支持文本格式,不支持二进制消息");
return;
}
//服务端收到新消息
String request = ((TextWebSocketFrame) webSocketFrame).text();
LOGGER.info("服务端收到了信息消息: " + request);
JSONObject param = null;
try {
param = JSONObject.parseObject(request);
} catch (Exception e) {
sendErrorMessage(ctx, "JSON字符串转换出错!");
e.printStackTrace();
}
if (param == null) {
sendErrorMessage(ctx, "参数为空!");
return;
}
String type = (String) param.get("type");
switch (type) {
case "REGISTER":
chatService.register(param, ctx);
break;
case "SINGLE_SENDING":
chatService.singleSend(param, ctx);
break;
case "GROUP_SENDING":
chatService.groupSend(param, ctx);
break;
default:
chatService.typeError(ctx);
break;
}
readRedisCache();
}
public void readRedisCache() {
if (chatRedisUtils.hgetCacheChatMessage(Constant.USERS_ID) != null) {
Map<Object, Object> chatMap = chatRedisUtils.hgetCacheChatMessage(Constant.USERS_ID);
String msg;
for (Map.Entry<Object, Object> entry : chatMap.entrySet()) {
String item = entry.getKey() + "";
StringBuilder key = new StringBuilder();
for (int i = 0; i < item.length(); i++) {
if (item.charAt(i) != '-') {
key.append(item.charAt(i));
} else {
break;
}
}
msg = entry.getValue() + "";
String responseJson = new ResponseJson().success()
.setData("fromUserId", key)
.setData("content", msg)
.setData("type", ChatType.SINGLE_SENDING)
.toString();
sendMessage(Constant.USERS_ID, responseJson);
}
chatRedisUtils.deleteCacheChatMessage(Constant.USERS_ID);
}
}
私聊:根据客户端发送的消息获取消息发送者id,接受者id以及消息的内容,去onlineUserMap中获得接受者的管道,如果管道不存在,即说明接受者不在线,那么将消息暂存在缓存中,在接受者下次登录时再进行转发.其中对redis的操作使用了工具类chatRedisUtils.主要是基本redisUtil操作的抽取,在此不在赘述.
@Override
public void singleSend(JSONObject param, ChannelHandlerContext ctx) {
String fromUserId = (String) param.get("fromUserId");
String toUserId = (String) param.get("toUserId");
String content = (String) param.get("content");
ChannelHandlerContext toUserCtx = Constant.onlineUserMap.get(toUserId);
if (toUserCtx == null) {
System.out.println("toUserCtx:" + toUserCtx);
String responJson = new ResponseJson()
.error(MessageFormat.format("userId为 {0} 的用户没有登录!", toUserId))
.toString();
String key = toUserId;
String item = chatRedisUtils.createChatNumber(Integer.parseInt(fromUserId));
chatRedisUtils.saveCacheChatMessage(key, item, content);
sendMessage(ctx, responJson);
} else {
String responseJson = new ResponseJson().success()
.setData("fromUserId", fromUserId)
.setData("content", content)
.setData("type", ChatType.SINGLE_SENDING)
.toString();
sendMessage(toUserCtx, responseJson);
}
}
群聊:群聊实现思路与私聊差不多,只是少了一个接受者管道是否存在的判断环节,根据toGroupId去数据库中取得对应的群组信息,对群组中的每一个用户(管道)都执行转发消息操作.
@Override
public void groupSend(JSONObject param, ChannelHandlerContext ctx) {
String fromUserId = (String) param.get("fromUserId");
String toGroupId = (String) param.get("toGroupId");
String content = (String) param.get("content");
GroupInfo groupInfo = groupInfoDao.getByGroupId(toGroupId);
if (groupInfo == null) {
String responseJson = new ResponseJson().error("该群id不存在").toString();
sendMessage(ctx, responseJson);
} else {
String responseJson = new ResponseJson().success()
.setData("fromUserId", fromUserId)
.setData("content", content)
.setData("toGroupId", toGroupId)
.setData("type", ChatType.GROUP_SENDING)
.toString();
groupInfo.getMembers().stream()
.forEach(member -> {
ChannelHandlerContext toCtx = Constant.onlineUserMap.get(member.getUserId());
if (toCtx != null && !member.getUserId().equals(fromUserId)) {
sendMessage(toCtx, responseJson);
}
});
}
}
private void sendMessage(ChannelHandlerContext ctx, String responseJson) {
ctx.channel().writeAndFlush(new TextWebSocketFrame(responseJson));
}
利用原生websocket建立连接,socket.onmessage根据服务端回传的消息判断消息的类型并执行对应的操作.socket.onopen和socket.onclose分别是刚建立连接和结束连接时执行的操作.刚建立连接后,客户端会执行register函数向服务端注册连接管道.
socket = new WebSocket("ws://localhost:3333");
socket.onmessage = function (event) {
var json = JSON.parse(event.data);
if (json.status == 200) {
var type = json.data.type;
console.log("收到一条新信息,类型为:" + type);
switch (type) {
case "REGISTER":
ws.registerReceive();
break;
case "SINGLE_SENDING":
ws.singleReceive(json.data);
break;
case "GROUP_SENDING":
ws.groupReceive(json.data);
break;
default:
console.log("不正确的类型!");
}
socket.onopen = setTimeout(function (event) {
console.log("WebSocket已成功连接!");
ws.register();
}, 1000)
socket.onclose = function (event) {
console.log("WebSocket已关闭...");
};
ws.register: function () {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
var data = {
"userId": userId,
"type": "REGISTER"
};
socket.send(JSON.stringify(data));
} else {
alert("Websocket连接没有开启!");
}
}
发送消息主要分为私聊singleSend与群聊groupSend
singleSend: function (fromUserId, toUserId, content) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
var data = {
"fromUserId": fromUserId,
"toUserId": toUserId,
"content": content,
"type": "SINGLE_SENDING"
};
socket.send(JSON.stringify(data));
} else {
alert("Websocket连接没有开启!");
}
},
groupSend: function (fromUserId, toGroupId, content) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
var data = {
"fromUserId": fromUserId,
"toGroupId": toGroupId,
"content": content,
"type": "GROUP_SENDING"
};
socket.send(JSON.stringify(data));
} else {
alert("Websocket连接没有开启!");
}
}
主要是接受到数据后前端页面显示的处理,如对消息框及好友列表的处理,通过将消息拼接为原生html加入当前页面中显示给用户.
singleReceive: function (data) {
// 获取、构造参数
console.log("data", data);
var fromUserId = data.fromUserId;
var content = data.content;
var fromAvatarUrl;
var $receiveLi;
$('.conLeft').find('span.hidden-userId').each(function () {
if (this.innerHTML == fromUserId) {
fromAvatarUrl = $(this).parent(".liRight")
.siblings(".liLeft").children('img').attr("src");
$receiveLi = $(this).parent(".liRight").parent("li");
}
})
var answer = '';
answer += '<li>' +
'<div class="answers">' + content + '</div>' +
'<div class="answerHead"><img src="' + fromAvatarUrl + '"/></div>' +
'</li>';
// 消息框处理
processMsgBox.receiveSingleMsg(answer, fromUserId);
// 好友列表处理
processFriendList.receiving(content, $receiveLi);
},
groupReceive: function (data) {
// 获取、构造参数
console.log(data);
var fromUserId = data.fromUserId;
var content = data.content;
var toGroupId = data.toGroupId;
var fromAvatarUrl;
var $receiveLi;
$('.conLeft').find('span.hidden-userId').each(function () {
if (this.innerHTML == fromUserId) {
fromAvatarUrl = $(this).parent(".liRight")
.siblings(".liLeft").children('img').attr("src");
/* $receiveLi = $(this).parent(".liRight").parent("li"); */
}
})
$('.conLeft').find('span.hidden-groupId').each(function () {
if (this.innerHTML == toGroupId) {
$receiveLi = $(this).parent(".liRight").parent("li");
}
})
var answer = '';
answer += '<li>' +
'<div class="answers">' + content + '</div>' +
'<div class="answerHead"><img src="' + fromAvatarUrl + '"/></div>' +
'</li>';
// 消息框处理
processMsgBox.receiveGroupMsg(answer, toGroupId);
// 好友列表处理
processFriendList.receiving(content, $receiveLi);
}
作者:NP447