Springboot+netty+websocket实现在线聊天
1:导入netty依赖包
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.2.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.7</version>
</dependency>
2:添加通道组池,管理所有websocket连接
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
/**
* MyChannelHandlerPool
* 通道组池,管理所有websocket连接
* @author zhengkai.blog.csdn.net
* @date 2019-06-12
*/
public class MyChannelHandlerPool {
public MyChannelHandlerPool(){}
public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}
2:添加Netty服务器配置类
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* NettyServer Netty服务器配置
* @author zhengkai.blog.csdn.net
* @date 2019-06-12
*/
public class NettyServer {
private final int port;
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
ch.pipeline().addLast(new HttpServerCodec());
//以块的方式来写的处理器
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new MyWebSocketHandler());
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536*10));
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
}
3:添加通道实体和消息实体
import io.netty.channel.Channel;
import org.jboss.logging.Logger;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class UserChannelRel {
private static Logger logger = Logger.getLogger(UserChannelRel.class);
private static ConcurrentHashMap<String, Channel> idChannelMap = new ConcurrentHashMap<>();
private static ConcurrentHashMap<Channel,String> channelIdMap = new ConcurrentHashMap<>();
public synchronized static void put (String senderId, Channel channel){
idChannelMap.put(senderId,channel);
channelIdMap.put(channel,senderId);
}
/**
* 根据id获取channel
* @param senderId
* @return
*/
public static Channel getById(String senderId){
return idChannelMap.get(senderId);
}
/**
* 根据channel获取id
* @param channel
* @return
*/
public static String getByChannel(Channel channel){
return channelIdMap.get(channel);
}
public static void output(){
for(Map.Entry<String,Channel> entry : idChannelMap.entrySet()){
Channel value = entry.getValue();
logger.info("——————UserId: "+entry.getKey() +",ChannelId: "+entry.getValue().id().asShortText());
}
}
}
Data
@ApiModel("消息实体")
public class MessageEntity implements Serializable {
//消息id
@ApiModelProperty(value = "id")
public Integer id;
/**
* 发送者id
*/
@ApiModelProperty(value = "发送者id")
public String fromId;
/**
* 发送者name
*/
@ApiModelProperty(value = "发送者name")
public String from;
/**
* 接收者id
*/
@ApiModelProperty(value = "接收者id")
public String toId;
/**
* 接收者name
*/
@ApiModelProperty(value = "接收者name")
public String to;
/**
* 发送的文本
*/
@ApiModelProperty(value = "发送的文本")
public String title;
/**
* 发送的消息标签
*/
@ApiModelProperty(value = "发送的消息标签")
public String label;
/**
* 详情id
*/
@ApiModelProperty(value = "详情id")
public Integer entityId;
/**
* 发送时间
*/
@ApiModelProperty(value = "发送时间")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
public Date sendingTime;
/**
* 状态
*/
@ApiModelProperty(value = "状态")
public String state;
/**
* 类别
*/
@ApiModelProperty(value = "类别")
public String type;
}
4:添加响应类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.hrkj.coll.dmsjk.securityKnowledge.entity.MessageEntity;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.jboss.logging.Logger;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static Logger logger = Logger.getLogger(UserChannelRel.class);
/**
* 用于存储群聊房间号和群聊成员的channel信息
*/
public static ConcurrentHashMap<String, ChannelGroup> groupMap = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启!");
//添加到channelGroup通道组
MyChannelHandlerPool.channelGroup.add(ctx.channel());
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭!");
//添加到channelGroup 通道组
MyChannelHandlerPool.channelGroup.remove(ctx.channel());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次连接是FullHttpRequest,处理参数 by zhengkai.blog.csdn.net
if (null != msg && msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
String uri = request.uri();
Map paramMap=getUrlParams(uri);
String userId = paramMap.get("userId").toString();
UserChannelRel.put(userId,ctx.channel());
System.out.println("登录的用户id是:{}"+new String[]{userId});
//如果url包含参数,需要处理
if(uri.contains("?")){
String newUri=uri.substring(0,uri.indexOf("?"));
request.setUri(newUri);
}
super.channelRead(ctx, msg);
}else if(msg instanceof TextWebSocketFrame){
//正常的TEXT消息类型
TextWebSocketFrame frame=(TextWebSocketFrame)msg;
channelRead0(ctx,frame);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String content = msg.text();
System.out.println("客户端收到服务器数据:"+content);
MessageEntity messageEntity = JSON.parseObject(content, MessageEntity.class);
//处理群聊任务
if ("2".equals(messageEntity.getType())) {
//推送群聊信息
groupMap.get(messageEntity.getToId()).writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageEntity)));
} else {
//获取当前被发送消息的用户id
String userId = messageEntity.getToId();
Channel channel = UserChannelRel.getById(userId);
// Channel channel = ctx.channel();
System.out.println("当前channel:"+channel);
if (channel != null) {
channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageEntity)));
}
}
}
/**
* 发送给所有的成员
* @param message
*/
private void sendAllMessage(String message){
//收到信息后,群发给所有channel
MyChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));
}
/**
* 发送指定用户
* @param messageEntity
*/
public static void sendOne(MessageEntity messageEntity){
//获取当前被发送消息的用户id
String userId = messageEntity.getToId();
Channel channel = UserChannelRel.getById(userId);
// Channel channel = ctx.channel();
System.out.println("当前channel:"+channel);
if (channel != null) {
channel.writeAndFlush(new TextWebSocketFrame(JSONObject.toJSONString(messageEntity)));
}
}
private static Map getUrlParams(String url){
Map<String,String> map = new HashMap<>();
url = url.replace("?",";");
if (!url.contains(";")){
return map;
}
if (url.split(";").length > 0){
String[] arr = url.split(";")[1].split("&");
for (String s : arr){
String key = s.split("=")[0];
String value = s.split("=")[1];
map.put(key,value);
}
return map;
}else{
return map;
}
}
}
4:接口测试
/**
* 用户-用户之间相互之间发送消息
* @param messageEntity 消息参数
*/
@ApiOperation("用户-用户之间相互之间发送消息")
@GetMapping("/sendOne")
public Object sendOne(MessageEntity messageEntity) {
//获取当前登录用户信息
User user = getUser();
//保存消息信息
messageEntity.setFromId(user.get_id());
messageEntity.setFrom(user.getUsername());
messageEntity.setState("0");
messageEntity.setType("1");
messageEntity.setSendingTime(new Date());
boolean b = messageEntityService.insert(messageEntity);
if(b){
//通过参数调用netty发送消息方法
MyWebSocketHandler.sendOne(messageEntity);
}
return success();
}
5:网址测试
测试网址:http://coolaf.com/tool/chattest