本项目开发周期比较短 大部分都是围绕业务来去实现 很多需要完善地方这里只是出了一个简短的例子
ps:在此之前没有搞过即时通讯,这个项目也算是入门大佬勿喷
所使用的技术:
netty,rabbitMQ,redis,mongoDB,mysql
先上java依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--引入 redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
<!-- spring-boot-starter-data-mongodb -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
以上基本满足了 还有一些第三方推送和oss储存就不一一放出来了,不会用oss和推送的可以去看看博客
下面开始详细介绍:
1.定义前后端通用数据结构
package com.yj.im.project.nettyServer;
import com.yj.im.project.entity.ChatRecord;
import java.io.Serializable;
public class Message implements Serializable {
/**
* 服务端接受参数
* type 消息类型 0.连接 1.发送消息 2.接收消息 3.客户端保持心跳 4.群聊 500.异常
* chatRecord {
* * @param {Object} msgType 消息类型 0.文字 1.图片,2.视频,
* * @param {Object} userId 发送的id
* * @param {Object} recipientId 接收者id
* * @param {Object} msg 消息
* * @param {Object} msgid 对应的消息id (读取是需要加入)
* }
*/
/**
* 客户端
* type 消息类型 0.连接 1.客户端向服务器发送消息(发消息) 2.服务端向客户端发送消息(接收消息) 4.群聊 3.客户端保持心跳 500.异常
* 备注:即: 客户端发送的统一为 1
* 服务端发送的统一为 2
* 前后端心跳包发送统一为 3
* <p>
* chatRecord {
* * @param {Object} msgType 消息类型 0.文字 1.图片,2.视频,
* * @param {Object} userId 发送的id
* * @param {Object} recipientId 接收者id
* * @param {Object} msg 消息(如果msgType为1,2此字段为文件路径)
* * @param {Object} msgid 对应的消息id (读取是需要加入)
* }
*/
private Integer type;//消息类型
private ChatRecord chatRecord;//聊天消息
private Object ext; //附加消息
public Integer getType() {
return type;
}
public Message setType(Integer type) {
this.type = type;
return this;
}
public ChatRecord getChatRecord() {
return chatRecord;
}
public Message setChatRecord(ChatRecord chatRecord) {
this.chatRecord = chatRecord;
return this;
}
public Object getExt() {
return ext;
}
public Message setExt(Object ext) {
this.ext = ext;
return this;
}
}
package com.yj.im.project.entity;
import java.util.Date;
import java.io.Serializable;
/**
* 聊天记录表(ChatRecord)实体类
*
* @author makejava
* @since 2020-04-07 16:14:52
*/
public class ChatRecord implements Serializable {
private static final long serialVersionUID = 489748139527994068L;
/**
* 记录表id
*/
private Long id;
private String mongoId;
/**
* 用户ID
*/
private Long userId;
/**
* 接收者用户ID
*/
private Long recipientId;
/**
* 用户在群里的昵称
*/
private String nickName;
/**
* 头像
*/
private String img;
/**
* 消息名称
*/
private String msgName;
/**
* 是否已读(0.已读 1.未读)
*/
private Integer hasRead;
/**
* 是否删除(0.已删除 1.未删除)
*/
private Integer hasDelete;
/**
* 消息类型(0.文字 1.图片,3.视频)
*/
private Integer msgType;
/**
* 区分用户消息还是系统消息
*/
private Integer sysMsgType;
/**
* 消息内容(如果是图片则为文件路径)
*/
private String message;
/**
* 状态(0逻辑删除、1数据有效)
*/
private Integer status;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
public String getMongoId() {
return mongoId;
}
public ChatRecord setMongoId(String mongoId) {
this.mongoId = mongoId;
return this;
}
public String getImg() {
return img;
}
public ChatRecord setImg(String img) {
this.img = img;
return this;
}
public String getMsgName() {
return msgName;
}
public ChatRecord setMsgName(String msgName) {
this.msgName = msgName;
return this;
}
public String getNickName() {
return nickName;
}
public ChatRecord setNickName(String nickName) {
this.nickName = nickName;
return this;
}
public Integer getSysMsgType() {
return sysMsgType;
}
public ChatRecord setSysMsgType(Integer sysMsgType) {
this.sysMsgType = sysMsgType;
return this;
}
public Long getId() {
return id;
}
public ChatRecord setId(Long id) {
this.id = id;
return this;
}
public Long getUserId() {
return userId;
}
public ChatRecord setUserId(Long userId) {
this.userId = userId;
return this;
}
public Long getRecipientId() {
return recipientId;
}
public ChatRecord setRecipientId(Long recipientId) {
this.recipientId = recipientId;
return this;
}
public Integer getHasRead() {
return hasRead;
}
public ChatRecord setHasRead(Integer hasRead) {
this.hasRead = hasRead;
return this;
}
public Integer getHasDelete() {
return hasDelete;
}
public ChatRecord setHasDelete(Integer hasDelete) {
this.hasDelete = hasDelete;
return this;
}
public Integer getMsgType() {
return msgType;
}
public ChatRecord setMsgType(Integer msgType) {
this.msgType = msgType;
return this;
}
public String getMessage() {
return message;
}
public ChatRecord setMessage(String message) {
this.message = message;
return this;
}
public Integer getStatus() {
return status;
}
public ChatRecord setStatus(Integer status) {
this.status = status;
return this;
}
public Date getCreateTime() {
return createTime;
}
public ChatRecord setCreateTime(Date createTime) {
this.createTime = createTime;
return this;
}
public Date getUpdateTime() {
return updateTime;
}
public ChatRecord setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
return this;
}
}
2.后续跟上netty代码:
package com.yj.im.project.nettyServer;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
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;
import io.netty.handler.timeout.IdleStateHandler;
public class WebSocketInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//-------------
//用于支持 Http协议
//-----------------
//websocket基于 http协议,需要有 http 的编解码器
pipeline.addLast(new HttpServerCodec())
//对于大数据流的支持
.addLast(new ChunkedWriteHandler())
//添加对HTTP请求和响应的聚合器:只要使用Netty进行 http编码都需要使用到
//对HttpMessage进行聚合,聚合成FullHttpRequest或者FullHttpResponse
//在 netty 编程总都会使用到Handler
.addLast(new HttpObjectAggregator(1024 * 64))
.addLast(new WebSocketServerProtocolHandler("/ws"))
//添加Netty空闲超时检查的支持
//4:读空闲超时,8:写空闲超时,12: 读写空闲超时
.addLast(new IdleStateHandler(4, 8, 12))
.addLast(new HearBeatHandler())
//添加自定有的 handler
.addLast(new ChatHandler());
}
}
package com.yj.im.project.nettyServer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class WebSocketServer {
private EventLoopGroup bossGroup;//主线程
private EventLoopGroup workerGroup;//工作线程
private ServerBootstrap server;//服务器
private ChannelFuture future; //回调
@PostConstruct
public void start() {
future = server.bind(8089);
BaseNettyServer.nettyLog().warn("-----------------------------------------------------------------------------------------");
BaseNettyServer.nettyLog().warn("---------------------------------netty server 开始启动---------------------------------");
BaseNettyServer.nettyLog().warn("-----------------------------------------------------------------------------------------");
BaseNettyServer.nettyLog().warn("-----------------------------------------------------------------------------------------");
BaseNettyServer.nettyLog().warn("---------------------------------netty server - 启动成功---------------------------------");
BaseNettyServer.nettyLog().warn("-----------------------------------------------------------------------------------------");
}
public WebSocketServer() {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketInitializer());
}
}
netty是需要两个线程来工作的具体的可以去百度看看这里不做介绍,然后定义端口加上@PostConstruct启动的时候执行一次就好了WebSocketServer()是当前的构造器,容器启动的时候会加载这个构造器。
3.这里就是自定义的消息处理handler,在这之前先把一些其他类粘出来
package com.yj.im.project.nettyServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BaseNettyServer {
private final static Logger log = LoggerFactory.getLogger(HearBeatHandler.class);
public static Logger nettyLog(){
return log;
}
}
package com.yj.im.project.nettyServer;
import com.yj.im.project.entity.ChatFriend;
import io.netty.channel.Channel;
import java.util.HashMap;
import java.util.Map;
public class UserChannelMap {
//保存用户id和通道的map对象
private static Map<String, Channel> userChannelMap;
static {
userChannelMap = new HashMap<>();
}
/**
* 建立用户和通道直接的关联
*
* @param userId
* @param channel
*/
public static void put(Long userId, Channel channel) {
userChannelMap.put("im" + userId, channel);
}
public static Channel getChannel(Long userId) {
return userChannelMap.get("im" + userId);
}
/**
* 解除用户和通道直接的关系
*
* @param userid
*/
public static void remove(String userid) {
userChannelMap.remove