前言
springboot整合netty实现一对一聊天室。
WebSocketHandler.java
核心功能由WebSocketHandler实现,发来的消息反序列化为MessageVO对象,通过type判断不同的功能。
package com.meeting.chatroom.handler;
import com.alibaba.fastjson.JSON;
import com.meeting.chatroom.entity.*;
import com.meeting.chatroom.service.ChatService;
import com.meeting.chatroom.service.FriendService;
import com.meeting.chatroom.util.SpringUtil;
import com.meeting.common.util.JwtTokenUtil;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import java.util.HashMap;
import java.util.Map;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
public class WebSocketHandler extends SimpleChannelInboundHandler<Object> {
private ChannelPromise handshakePromise;
private Channel channel = null;
private Long fromId = null;
private final JwtTokenUtil jwtTokenUtil = SpringUtil.getBean(JwtTokenUtil.class);
private final ChatService chatService = SpringUtil.getBean(ChatService.class);
private final FriendService friendService = SpringUtil.getBean(FriendService.class);
private final ChatChannelGroup chatChannelGroup = SpringUtil.getBean(ChatChannelGroup.class);
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
handshakePromise = ctx.newPromise();
}
/**
* 活跃的通道 也可以当作用户连接上客户端进行使用
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
}
/**
* 出现异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// todo 异常处理
System.out.println(cause.getClass());
System.out.println(cause.getMessage());
sendMessageToChannel(ctx.channel(), ResponseData.SERVER_PROBLEM);
}
/**
* 不活跃的通道 就说明用户失去连接
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 从channelGroup通道组中移除
chatChannelGroup.removeChannel(this.fromId, this.channel);
}
/**
* 服务器接受客户端的数据信息
* @param ctx
* @param data
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, Object data) throws Exception {
if (data instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) data);
} else if (data instanceof TextWebSocketFrame) {
handleWebSocketFrame((TextWebSocketFrame) data);
}
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
if (!GET.equals(req.method())) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN, ctx.alloc().buffer(0)));
return;
}
String token = req.headers().get("Sec-WebSocket-Protocol");
if (token == null || !jwtTokenUtil.validateToken(token)
|| (this.fromId = jwtTokenUtil.getUserIdFromToken(token)) == null) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED, ctx.alloc().buffer(0)));
return;
}
this.channel = ctx.channel();
this.chatChannelGroup.addChannel(this.fromId, this.channel);
final WebSocketServerHandshakerFactory wsFactory =
new WebSocketServerHandshakerFactory(getWebSocketLocation(ctx.pipeline(), req, "/ws"),
"WebSocket", true, 65536 * 10);
final WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req);
final ChannelPromise localHandshakePromise = handshakePromise;
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
// setHandshaker(ctx.channel(), handshaker);
HttpHeaders headers = new DefaultHttpHeaders();
headers.set("Access-Control-Allow-Methods", "*");
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Access-Control-Allow-Origin", "*");
headers.set("Access-Control-Allow-Headers", "*");
headers.set("Access-Control-Expose-Headers", "*");
headers.set("Sec-WebSocket-Protocol", token);
final ChannelFuture handshakeFuture = handshaker.handshake(ctx.channel(), req, headers, ctx.newPromise());
handshakeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
localHandshakePromise.tryFailure(future.cause());
ctx.fireExceptionCaught(future.cause());
} else {
localHandshakePromise.trySuccess();
}
}
});
}
}
private void handleWebSocketFrame(TextWebSocketFrame data) {
// 获取客户端发送的消息
String content = data.text();
MessageVO messageVO;
try {
messageVO = JSON.parseObject(content, MessageVO.class);
} catch (RuntimeException exception) {
ResponseData toSender = ResponseData.ILLEGAL_MESSAGE_FORMAT;
sendMessageToChannel(this.channel, toSender);
return;
}
if (messageVO.getType() == null) {
sendMessageToChannel(this.channel, ResponseData.ILLEGAL_MESSAGE_FORMAT);
};
int type = messageVO.getType();
// 根据type处理不同业务
if (type == MessageType.CHAT.getType()) {
// 聊天类型的消息
MessageDO messageDO = new MessageDO(messageVO, this.fromId);
// 发送消息
// 从全channelMap中获取接受方的channel
Channel sender = this.channel;
Channel receiver = chatChannelGroup.getChannelById(messageVO.getToId());
if (receiver == null) {
// 对方离线
sendToOfflineUser(sender, messageDO);
} else {
sendToOnlineUser(sender, receiver, messageDO);
}
} else if (type == MessageType.SIGNED.getType()) {
// 消息签收
if (messageVO.getId() == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
}
sign(channel, messageVO);
} else if (type == MessageType.REQUEST.getType()) {
// 好友添加请求
if (messageVO.getToId() == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleRequest(fromId, messageVO.getToId());
}
} else if (type == MessageType.REPLY.getType()) {
// 回复好友请求
Long id = messageVO.getId();
if (id == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleReply(id, messageVO.isAgree());
}
} else if (type == MessageType.PRIVATE_WEBRTC_OFFER.getType()) {
// 用户发起会话请求
Long sender = messageVO.getSender();
Long receiver = messageVO.getReceiver();
if (sender == null || receiver == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleWebRtcOffer(messageVO, receiver);
}
} else if (type == MessageType.PRIVATE_WEBRTC_ANSWER.getType()) {
// 响应会话请求
Long sender = messageVO.getSender();
Long receiver = messageVO.getReceiver();
if (sender == null || receiver == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleWebRtcAnswer(messageVO, sender);
}
} else if (type == MessageType.PRIVATE_WEBRTC_CANDIDATE.getType()) {
// ICE候选者
Long target = messageVO.getTarget();
if (target == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleWebRtcCandidate(messageVO, target);
}
} else if (type == MessageType.PRIVATE_WEBRTC_DISCONNECT.getType()) {
// 挂断电话
Long target = messageVO.getTarget();
if (target == null) {
sendMessageToChannel(this.channel, ResponseData.ID_NOT_FOUND);
} else {
handleWebRtcDisconnect(messageVO, target);
}
} else {
handleDefault(this.channel);
}
}
/**
* 发送消息给在线用户
* @param sender Channel对象 发送方
* @param receiver Channel对象 接收方
* @param message MessageDO对象 封装消息
*/
private void sendToOnlineUser(Channel sender, Channel receiver, MessageDO message) {
ResponseDataContainer container = chatService.sendToUser(message, true);
ResponseData toSender = container.getToSender();
ResponseData toReceiver = container.getToReceiver();
// 发送响应给发送方
sendMessageToChannel(sender, toSender);
if (toSender.isSuccess()) {
// 发送消息给接收方
sendMessageToChannel(receiver, toReceiver);
}
}
/**
* 发送消息给离线用户
* @param sender Channel对象 发送方
* @param message MessageDO对象 封装消息
*/
private void sendToOfflineUser(Channel sender, MessageDO message) {
ResponseDataContainer container = chatService.sendToUser(message, false);
ResponseData toSender = container.getToSender();
sendMessageToChannel(sender, toSender);
}
/**
* 接收方签收消息
* @param channel Channel对象 接收方
* @param message MessageVO对象 封装消息Id
*/
private void sign(Channel channel, MessageVO message) {
ResponseDataContainer container = chatService.sign(message.getId(), this.fromId);
sendMessageToChannel(channel, container.getToReceiver());
}
/**
* 处理添加好友
* @param fromId 发送方id
* @param toId 接受方id
*/
private void handleRequest(Long fromId, Long toId) {
ResponseDataContainer container = friendService.requestFriend(fromId, toId);
ResponseData toSender = container.getToSender();
ResponseData toReceiver = container.getToReceiver();
if (toSender.isSuccess()) {
// 请求成功
if (toSender != ResponseData.HAVE_ALREADY_REQUESTED) {
// 不是重复请求
Channel receiver = chatChannelGroup.getChannelById(toId);
if (receiver != null) {
// 对方在线
sendMessageToChannel(receiver, toReceiver);
}
}
sendMessageToChannel(this.channel, toSender);
} else {
sendMessageToChannel(this.channel, toSender);
}
}
/**
* 处理回复好友请求
* @param id 请求id
* @param agree 是否同意
*/
private void handleReply(Long id, boolean agree) {
ResponseDataContainer container = friendService.replyFriend(id, agree, this.fromId);
ResponseData toSender = container.getToSender();
ResponseData toReceiver = container.getToReceiver();
if (toSender.isSuccess()) {
if (agree) {
sendMessageToChannel(this.channel, toSender);
if (toReceiver != null) {
Long receiverId = (Long) toSender.getData().get("id");
Channel receiver = chatChannelGroup.getChannelById(receiverId);
if (receiver != null) {
sendMessageToChannel(receiver, toReceiver);
}
}
} else {
sendMessageToChannel(this.channel, toSender);
}
} else {
sendMessageToChannel(this.channel, toSender);
}
}
/**
* 处理用户发起会话请求
* @param messageVO messageVO对象,封装消息
* @param receiver 接收方的id
*/
private void handleWebRtcOffer(MessageVO messageVO, Long receiver) {
// 因为是发起请求,因此服务器应将该消息从 sender 向 receiver 转发
Channel receiverChannel = chatChannelGroup.getChannelById(receiver);
Map<String, Object> map = new HashMap<>();
if (receiverChannel != null) {
map.put("type", messageVO.getType());
map.put("sdp", messageVO.getSdp());
map.put("sender", messageVO.getSender());
map.put("receiver", messageVO.getReceiver());
sendMessageToChannel(receiverChannel, map);
} else {
map.put("accept", false);
sendMessageToChannel(receiverChannel, map);
}
}
/**
* 处理响应会话请求
* @param messageVO messageVO对象,封装消息
* @param sender 发送方用户id
*/
private void handleWebRtcAnswer(MessageVO messageVO, Long sender) {
// 因为是响应对方发起的请求,因此服务器应将该消息从 receiver 向 sender 转发。
Channel senderChannel = chatChannelGroup.getChannelById(sender);
if (senderChannel != null) {
Map<String, Object> map = new HashMap<>();
map.put("type", messageVO.getType());
map.put("accept", messageVO.getAccept());
map.put("sdp", messageVO.getSdp());
map.put("sender", messageVO.getSender());
map.put("receiver", messageVO.getReceiver());
sendMessageToChannel(senderChannel, map);
}
}
/**
* 处理ICE候选者
* @param messageVO messageVO对象,封装消息
* @param target 目标用户id
*/
private void handleWebRtcCandidate(MessageVO messageVO, Long target) {
Channel targetChannel = chatChannelGroup.getChannelById(target);
if (targetChannel != null) {
Map<String, Object> map = new HashMap<>();
map.put("type", messageVO.getType());
map.put("candidate", messageVO.getCandidate());
map.put("sdpMid", messageVO.getSdpMid());
map.put("sdpMLineIndex", messageVO.getSdpMLineIndex());
map.put("sender", messageVO.getSender());
map.put("receiver", messageVO.getReceiver());
map.put("target", messageVO.getTarget());
sendMessageToChannel(targetChannel, map);
}
}
/**
* 处理挂断电话
* @param messageVO messageVO对象,封装消息
* @param target 目标用户id
*/
private void handleWebRtcDisconnect(MessageVO messageVO, Long target) {
Channel targetChannel = chatChannelGroup.getChannelById(target);
if (targetChannel != null) {
Map<String, Object> map = new HashMap<>();
map.put("type", messageVO.getType());
map.put("sender", messageVO.getSender());
map.put("receiver", messageVO.getReceiver());
map.put("target", messageVO.getTarget());
sendMessageToChannel(targetChannel, map);
}
}
/**
* 默认处理位置消息类型
* @param channel
*/
private void handleDefault(Channel channel) {
sendMessageToChannel(channel, ResponseData.TYPE_NOT_ALLOWED);
}
private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private void sendMessageToChannel(Channel channel, ResponseData responseData) {
channel.writeAndFlush(
new TextWebSocketFrame(JSON.toJSONString(responseData))
);
}
private void sendMessageToChannel(Channel channel, Map<String, Object> responseData) {
channel.writeAndFlush(
new TextWebSocketFrame(JSON.toJSONString(responseData))
);
}
private String getWebSocketLocation(ChannelPipeline cp, HttpRequest req, String path) {
String protocol = "ws";
if (cp.get(SslHandler.class) != null) {
// SSL in use so use Secure WebSockets
protocol = "wss";
}
String host = req.headers().get(HttpHeaderNames.HOST);
return protocol + "://" + host + path;
}
private void setHandshaker(Channel channel, WebSocketServerHandshaker handshaker) {
channel.attr(AttributeKey.valueOf(WebSocketHandler.class, "HANDSHAKER")).set(handshaker);
}
}
http请求
部分功能经过考虑,由http请求实现更合理。
package com.meeting.chatroom.controller;
import com.meeting.chatroom.entity.ChatChannelGroup;
import com.meeting.chatroom.service.OnOpenService;
import com.meeting.common.entity.ResponseData;
import com.meeting.common.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 负责处理websocket连接后的一些请求
*/
@Controller
public class OnOpenController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private OnOpenService onOpenService;
/**
* 获取好友列表、好有请求、未签收的信息
* @param token jwt token
* @return ResponseData对象
*/
@ResponseBody
@GetMapping("/getFriendsAndMessages")
public ResponseData getFriendsAndMessages(@RequestHeader(value = "Authorization", required = false) String token) {
ResponseData responseData = null;
Long uid = null;
if (token == null || !jwtTokenUtil.validateToken(token)
|| (uid = jwtTokenUtil.getUserIdFromToken(token)) == null) {
return new ResponseData(401, "未登录");
}
return onOpenService.getFriendsAndMessage(uid);
}
}
package com.meeting.chatroom.controller;
import com.meeting.chatroom.service.ChatService;
import com.meeting.common.entity.ResponseData;
import com.meeting.common.exception.UnAuthorizedException;
import com.meeting.common.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
@CrossOrigin(origins = ("*"))
public class HistoryController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private ChatService chatService;
@ResponseBody
@GetMapping("/getHistoryMessage")
public ResponseData getHistoryMessage(@RequestHeader(value = "Authorization", required = false) String token,
@RequestParam("toId") long uid,
@RequestParam(value = "page", required = false) Integer page) {
Long id = null;
if (token == null || !jwtTokenUtil.validateToken(token)
|| (id = jwtTokenUtil.getUserIdFromToken(token)) == null) {
throw new UnAuthorizedException();
}
if (page == null || page < 1) {
page = 1;
}
int num = 10;
int start = num * (page - 1);
return chatService.selectHistoryMessage(id, uid, start, num);
}
}
心跳
关于心跳处理,等之后再做。