一、前言
MobileIMSDK 是什么?
MobileIMSDK 是一套专门为挪动端开发的开源 IM 即时通讯框架,超轻量级、高度提炼,一套 API 优雅反对 UDP、TCP、WebSocket 三种协定,反对 iOS、Android、H5、规范 Java 平台,服务端基于 Netty 编写。
工程地址是:
1)Gitee 码云地址:https://www.oschina.net/p/mob…
2)Github 托管地址:https://github.com/JackJiang2…
本文将实现:
1)基于 springboot 集成 MobileIMSDK;
2)开发 IM 服务端;
3)开发客户端;
4)实现 Java 客户端与客户端之间的通信。
- 补充阐明:本文所示 Demo 源码,请从文末“本文小结”的最初链接中下载!
二、SpringBoot 集成 MobileIMSDK 筹备
2.1 MobileIMSDK 下载
MobileIMSDK 下载地址:
1)国外地址:MobileIMSDK 的 Github 地址(最新版打包下载)
2)国内地址:MobileIMSDK 的码云 gitee 地址(访问速度快!,最新版打包下载)
须要用到的 lib 包:
1)服务端所需 jar 包:sdk_binary/Server/
2)客服端所需 jar 包:sdk_binary/Client_TCP/java/
如下图所示:
2.2 pom.xml 中引入相干依赖
因为这里是 maven 我的项目,其中一部分 jar 包可通过 maven 仓库间接引入,而其余的则通过内部 jar 包引入形式应用即可~
如下 4 个需作为内部 jar 包在 pom.xml 中引入:
<!– [url=https://mvnrepository.com/art…]https://mvnrepository.com/artifact/com.google.code.gson/gson[/url] –>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!– MobileIMSDK 所需 jar 包依赖[注:这里是在本地 lib 中引入,maven 地方仓库中暂无此 jar 包],要与 <includeSystemScope>true</includeSystemScope> 配合应用 –>
<dependency>
<groupId>com.zhengqing</groupId>
<artifactId>MobileIMSDK4j</artifactId>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/MobileIMSDK4j.jar</systemPath>
</dependency>
<dependency>
<groupId>com.zhengqing</groupId>
<artifactId>MobileIMSDKServerX_meta</artifactId>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/MobileIMSDKServerX_meta.jar</systemPath>
</dependency>
<dependency>
<groupId>com.zhengqing</groupId>
<artifactId>swing-worker-1.2(1.6-)</artifactId>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/swing-worker-1.2(1.6-).jar</systemPath>
</dependency>
<dependency>
<groupId>com.zhengqing</groupId>
<artifactId>MobileIMSDKServerX_netty</artifactId>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/MobileIMSDKServerX_netty.jar</systemPath>
</dependency>
<plugins>
<!-- maven 打包插件 -> 将整个工程打成一个 fatjar -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 作用: 我的项目打成 jar,同时把本地 jar 包也引入进去 -->
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
三、开发服务端
3.1 与客服端的所有数据交互事件(实现 ServerEventListener 类)
public class ServerEventListenerImpl implements ServerEventListener {private static Logger logger = LoggerFactory.getLogger(ServerEventListenerImpl.class);
/**
* 用户身份验证回调办法定义.
* <p>
* 服务端的应用层可在本办法中实现用户登陆验证。* <br>
* 留神:本回调在一种非凡状况下——即用户理论未退出登陆但再次发起来登陆包时,本回调是不会被调用的!* <p>
* 依据 MobileIMSDK 的算法实现,本办法中用户验证通过(即办法返回值 = 0 时)后
*,将立刻调用回调办法 {@link #onUserLoginAction_CallBack(int, String, IoSession)}。* 否则会将验证后果(本办法返回值错误码通过客户端的 ChatBaseEvent.onLoginMessage(int dwUserId, int dwErrorCode)
* 办法进行回调)告诉客户端)。*
* @param userId 传递过去的准一 id,保障惟一就能够通信,可能是登陆用户名、也可能是任意不反复的 id 等,具体意义由业务层决定
* @param token 用于身份甄别和合法性检查的 token,它可能是登陆密码,也可能是通过前置单点登陆接口拿到的 token 等,具体意义由业务层决定
* @param extra 额定信息字符串。本字段目前为保留字段,供下层利用自行搁置须要的内容
* @param session 此客户端连贯对应的 netty“会话”* @return 0 示意登陆验证通过,否则能够返回用户自已定义的错误码,错误码值应为:>=1025 的整数
*/
@Override
public int onVerifyUserCallBack(String userId, String token, String extra, Channel session) {logger.debug("【DEBUG_回调告诉】正在调用回调办法:OnVerifyUserCallBack...(extra="+ extra + ")");
return 0;
}
/**
* 用户登录验证胜利后的回调办法定义(可了解为上线告诉回调).
* <p>
* 服务端的应用层通常可在本办法中实现用户上线告诉等。* <br>
* 留神:本回调在一种非凡状况下——即用户理论未退出登陆但再次发起来登陆包时,回调也是肯定会被调用。*
* @param userId 传递过去的准一 id,保障惟一就能够通信,可能是登陆用户名、也可能是任意不反复的 id 等,具体意义由业务层决定
* @param extra 额定信息字符串。本字段目前为保留字段,供下层利用自行搁置须要的内容。为了丰盛应用层解决的伎俩,在本回调中也把此字段传进来了
* @param session 此客户端连贯对应的 netty“会话”*/
@Override
public void onUserLoginAction_CallBack(String userId, String extra, Channel session) {logger.debug("【IM_回调告诉 OnUserLoginAction_CallBack】用户:"+ userId + "上线了!");
}
/**
* 用户退出登录回调办法定义(可了解为下线告诉回调)。* <p>
* 服务端的应用层通常可在本办法中实现用户下线告诉等。*
* @param userId 下线的用户 user_id
* @param obj
* @param session 此客户端连贯对应的 netty“会话”*/
@Override
public void onUserLogoutAction_CallBack(String userId, Object obj, Channel session) {logger.debug("【DEBUG_回调告诉 OnUserLogoutAction_CallBack】用户:"+ userId + "离线了!");
}
/**
* 通用数据回调办法定义(客户端发给服务端的(即接管 user_id="0")).
* <p>
* MobileIMSDK 在收到客户端向 user_id=0(即接管指标是服务器)的状况下通过
* 本办法的回调告诉下层。下层通常可在本办法中实现如:增加好友申请等业务实现。*
* <p style="background:#fbf5ee;border-radius:4px;">
* <b><font color="#ff0000">【版本兼容性阐明】</font></b> 本办法用于代替 v3.x 中的以下办法:<br>
* <code>public boolean onTransBuffer_CallBack(String userId, String from_user_id
* , String dataContent, String fingerPrint, int typeu, Channel session);
* </code>
*
* @param userId 接管方的 user_id(本办法接管的是发给服务端的音讯,所以此参数的值必定 ==0)* @param from_user_id 发送方的 user_id
* @param dataContent 数据内容(文本模式)* @param session 此客户端连贯对应的 netty“会话”* @return true 示意本办法已胜利解决实现,否则示意未解决胜利。此返回值目前框架中并没有非凡意义,仅作保留吧
* @since 4.0
*/
@Override
public boolean onTransBuffer_C2S_CallBack(Protocal p, Channel session) {
// 接收者 uid
String userId = p.getTo();
// 发送者 uid
String from_user_id = p.getFrom();
// 音讯或指令内容
String dataContent = p.getDataContent();
// 音讯或指令指纹码(即惟一 ID)String fingerPrint = p.getFp();
//【重要】用户定义的音讯或指令协定类型(开发者可据此类型来辨别具体的音讯或指令)inttypeu = p.getTypeu();
logger.debug("【DEBUG_回调告诉】[typeu="+ typeu + "]收到了客户端"+ from_user_id + "发给服务端的音讯:str="+ dataContent);
returntrue;
}
/**
* 通道数据回调函数定义(客户端发给客户端的(即接管方 user_id 不为“0”的状况)).
* <p>
* <b> 留神:</b> 本办法当且仅当在数据被服务端胜利在线发送进来后被回调调用.
* <p>
* 下层通常可在本办法中实现用户聊天信息的收集,以便前期监控剖析用户的行为等 ^_^。* <p>
* 提醒:如果开启音讯 QoS 保障,因重传机制,本回调中的音讯实践上有反复的可能,请以参数 #fingerPrint
* 作为音讯的惟一标识 ID 进行去重解决。*
* <p style="background:#fbf5ee;border-radius:4px;">
* <b><font color="#ff0000">【版本兼容性阐明】</font></b> 本办法用于代替 v3.x 中的以下办法:<br>
* <code>public void onTransBuffer_C2C_CallBack(String userId, String from_user_id
* , String dataContent, String fingerPrint, int typeu);
*
* @param userId 接管方的 user_id(本办法接管的是客户端发给客户端的,所以此参数的值必定 >0)* @param from_user_id 发送方的 user_id
* @param dataContent
* @since 4.0
*/
@Override
public void onTransBuffer_C2C_CallBack(Protocal p) {
// 接收者 uid
String userId = p.getTo();
// 发送者 uid
String from_user_id = p.getFrom();
// 音讯或指令内容
String dataContent = p.getDataContent();
// 音讯或指令指纹码(即惟一 ID)String fingerPrint = p.getFp();
//【重要】用户定义的音讯或指令协定类型(开发者可据此类型来辨别具体的音讯或指令)inttypeu = p.getTypeu();
logger.debug("【DEBUG_回调告诉】[typeu="+ typeu + "]收到了客户端"+ from_user_id + "发给客户端"+ userId + "的音讯:str="+ dataContent);
}
/**
* 通用数据实时发送失败后的回调函数定义(客户端发给客户端的(即接管方 user_id 不为“0”的状况)).
* <p>
* 留神:本办法当且仅当在数据被服务端 <u> 在线发送 </u> 失败后被回调调用.
* <p>
* <b> 此办法存的意义何在?</b><br>
* 产生此种状况的场景可能是:对方的确不在线(那么此办法里就能够作为离线音讯解决了)、* 或者在发送时判断对方是在线的但服务端在发送时却没有胜利(这种状况就可能是通信谬误
* 或对方非正常通出但尚未达到会话超时时限)。<br><u> 应用层在此办法里实现离线音讯的解决即可!</u>
*
* <p style="background:#fbf5ee;border-radius:4px;">
* <b><font color="#ff0000">【版本兼容性阐明】</font></b> 本办法用于代替 v3.x 中的以下办法:<br>
* <code>public boolean onTransBuffer_C2C_RealTimeSendFaild_CallBack(String userId
* , String from_user_id, String dataContent, String fingerPrint, int typeu);
* </code>
*
* @param userId 接管方的 user_id(本办法接管的是客户端发给客户端的,所以此参数的值必定 >0),此 id 在本办法中不肯定保障有意义
* @param from_user_id 发送方的 user_id
* @param dataContent 音讯内容
* @param fingerPrint 该音讯对应的指纹(如果该音讯有 QoS 保障机制的话),用于在 QoS 重要机制下服务端离线存储时避免反复存储哦
* @return true 示意应用层曾经解决了离线音讯(如果该音讯有 QoS 机制,则服务端将代为发送一条伪应答包
*(伪应答仅意味着不是接管方的实时应答,而只是存储到离线 DB 中,但在发送方看来也算是被对方收到,只是延
* 迟收到而已(离线音讯嘛))),否则示意应用层没有解决(如果此音讯有 QoS 机制,则发送方在 QoS 重传机制超时
* 后报出音讯发送失败的提醒)* @see #onTransBuffer_C2C_CallBack(Protocal)
* @since 4.0
*/
@Override
public boolean onTransBuffer_C2C_RealTimeSendFaild_CallBack(Protocal p) {
// 接收者 uid
String userId = p.getTo();
// 发送者 uid
String from_user_id = p.getFrom();
// 音讯或指令内容
String dataContent = p.getDataContent();
// 音讯或指令指纹码(即惟一 ID)String fingerPrint = p.getFp();
//【重要】用户定义的音讯或指令协定类型(开发者可据此类型来辨别具体的音讯或指令)inttypeu = p.getTypeu();
logger.debug("【DEBUG_回调告诉】[typeu="+ typeu + "]客户端"+ from_user_id + "发给客户端"+ userId + "的音讯:str="+ dataContent
+ ",因实时发送没有胜利,须要下层利用作离线解决哦,否则此音讯将被抛弃.");
returnfalse;
}
}
3.2 服务端被动发动音讯的 QoS 回调告诉(实现 MessageQoSEventListenerS2C 类)
public class MessageQoSEventS2CListnerImpl implements MessageQoSEventListenerS2C {
private static Logger logger = LoggerFactory.getLogger(MessageQoSEventS2CListnerImpl.class);
@Override
public void messagesLost(ArrayList<Protocal> lostMessages) {
logger.debug("【DEBUG_QoS_S2C 事件】收到零碎的未实时送达事件告诉,以后共有"
+ lostMessages.size() + "个包 QoS 保障机制完结,断定为【无奈实时送达】!");
}
@Override
public void messagesBeReceived(String theFingerPrint) {if(theFingerPrint != null) {logger.debug("【DEBUG_QoS_S2C 事件】收到对方已收到音讯事件的告诉,fp="+ theFingerPrint);
}
}
}
3.3 服务端配置
public class ServerLauncherImpl extends ServerLauncher {
// 动态类办法:进行一些全局配置设置
static{
// 设置 MobileIMSDK 服务端的网络监听端口
ServerLauncherImpl.PORT = 7901;
// 开 / 关 Demog 日志的输入
QoS4SendDaemonS2C.getInstance().setDebugable(true);
QoS4ReciveDaemonC2S.getInstance().setDebugable(true);
ServerLauncher.debug = true;
// TODO 与客户端协商一致的心跳敏感模式设置
// ServerToolKits.setSenseMode(SenseMode.MODE_10S);
// 敞开与 Web 端的音讯互通桥接器(其实 SDK 中默认就是 false)ServerLauncher.bridgeEnabled = false;
// TODO 跨服桥接器 MQ 的 URI(本参数只在 ServerLauncher.bridgeEnabled 为 true 时有意义)
// BridgeProcessor.IMMQ_URI = “amqp://js:19844713@192.168.31.190”;
}
// 实例构造方法
public ServerLauncherImpl() throws IOException {super();
}
/**
* 初始化音讯处理事件监听者.
*/
@Override
protected void initListeners() {
// ** 设置各种回调事件处理实现类
this.setServerEventListener(newServerEventListenerImpl());
this.setServerMessageQoSEventListener(newMessageQoSEventS2CListnerImpl());
}
}
3.4 服务端启动类
舒适小提示:这里因为小编将服务端和客户端集成在同一个我的项目中,因而如下配置:
SpringBoot 的 CommandLineRunner 接口次要用于实现在服务初始化后,去执行一段代码块逻辑(run 办法),这段初始化代码在整个利用生命周期内只会执行一次!
@Order(value = 1):依照肯定的程序去执行,value 值越小越先执行
@Slf4j
@Component
@Order(value = 1)
public class ChatServerRunner implements CommandLineRunner {
@Override
public void run(String... strings) throws Exception {log.info("================= ↓↓↓↓↓↓ 启动 MobileIMSDK 服务端 ↓↓↓↓↓↓ =================");
// 实例化后记得 startup 哦,独自 startup()的目标是让调用者能够提早决定何时真正启动 IM 服务
final ServerLauncherImpl sli = new ServerLauncherImpl();
// 启动 MobileIMSDK 服务端的 Demo
sli.startup();
// 加一个钩子,确保在 JVM 退出时开释 netty 的资源
Runtime.getRuntime().addShutdownHook(newThread(sli::shutdown));
}
}
如果服务端与客户端不在同一个我的项目,服务端可间接通过如下形式启动即可~
四、开发客户端
4.1 客户端与 IM 服务端连贯事件
@Slf4j
public class ChatBaseEventImpl implements ChatBaseEvent {
@Override
public void onLoginMessage(int dwErrorCode) {if(dwErrorCode == 0) {log.debug("IM 服务器登录 / 连贯胜利!");
} else{log.error("IM 服务器登录 / 连贯失败,错误代码:"+ dwErrorCode);
}
}
@Override
public void onLinkCloseMessage(int dwErrorCode) {log.error("与 IM 服务器的网络连接出错敞开了,error:"+ dwErrorCode);
}
}
4.2 接管音讯事件
@Slf4j
public class ChatTransDataEventImpl implements ChatTransDataEvent {
@Override
public void onTransBuffer(String fingerPrintOfProtocal, String userid, String dataContent, inttypeu) {log.debug("[typeu="+ typeu + "]收到来自用户"+ userid + "的音讯:"+ dataContent);
}
@Override
public void onErrorResponse(int errorCode, String errorMsg) {log.debug("收到服务端谬误音讯,errorCode="+ errorCode + ", errorMsg="+ errorMsg);
}
}
4.3 音讯是否送达事件
@Slf4j
public class MessageQoSEventImpl implements MessageQoSEvent {
@Override// 对方未胜利接管音讯的回调事件 lostMessages:寄存音讯内容
public void messagesLost(ArrayList<Protocal> lostMessages) {log.debug("收到零碎的未实时送达事件告诉,以后共有"+ lostMessages.size() + "个包 QoS 保障机制完结,断定为【无奈实时送达】!");
}
@Override// 对方胜利接管到音讯的回调事件
public void messagesBeReceived(String theFingerPrint) {if(theFingerPrint != null) {log.debug("收到对方已收到音讯事件的告诉,fp="+ theFingerPrint);
}
}
}
4.4 MobileIMSDK 初始化配置
public class IMClientManager {
private static IMClientManager instance = null;
/**
* MobileIMSDK 是否已被初始化. true 示意已初化实现,否则未初始化.
*/
privatebooleaninit = false;
public static IMClientManager getInstance() {if(instance == null) {instance = new IMClientManager();
}
return instance;
}
private IMClientManager() {initMobileIMSDK();
}
public void initMobileIMSDK() {if(!init) {
// 设置服务器 ip 和服务器端口
ConfigEntity.serverIP = "127.0.0.1";
ConfigEntity.serverPort = 8901;
// MobileIMSDK 外围 IM 框架的敏感度模式设置
// ConfigEntity.setSenseMode(SenseMode.MODE_10S);
// 开启 / 敞开 DEBUG 信息输入
ClientCoreSDK.DEBUG = false;
// 设置事件回调
ClientCoreSDK.getInstance().setChatBaseEvent(newChatBaseEventImpl());
ClientCoreSDK.getInstance().setChatTransDataEvent(newChatTransDataEventImpl());
ClientCoreSDK.getInstance().setMessageQoSEvent(newMessageQoSEventImpl());
init = true;
}
}
}
4.5 连贯 IM 服务端,发送音讯
服务类:
public interface IChatService {
/**
* 登录连贯 IM 服务器申请
*
* @param username: 用户名
* @param password: 明码
* @return: void
*/
void loginConnect(String username, String password);
/**
* 发送音讯
*
* @param friendId: 接管音讯者 id
* @param msg: 音讯内容
* @return: void
*/
void sendMsg(String friendId, String msg);
}
服务实现类:
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class ChatServiceImpl implements IChatService {
@Override
public void loginConnect(String username, String password) {
// 确保 MobileIMSDK 被初始化哦(整个 APP 生生命周期中只需调用一次哦)// 提醒:在不退出 APP 的状况下退出登陆后再从新登陆时,请确保调用本办法一次,不然会报 code=203 谬误哦!IMClientManager.getInstance().initMobileIMSDK();
// * 异步提交登陆名和明码
new LocalUDPDataSender.SendLoginDataAsync(username, password) {
/**
* 登陆信息发送实现后将调用本办法(留神:此处仅是登陆信息发送实现,真正的登陆后果要在异步回调中解决哦)。* @param code 数据发送返回码,0 示意数据胜利收回,否则是错误码
*/
protected void fireAfterSendLogin(int code) {if(code == 0) {log.debug("数据发送胜利!");
} else{log.error("数据发送失败。错误码是:"+ code);
}
}
}.execute();}
@Override
public void sendMsg(String friendId, String msg) {// 发送音讯(异步晋升体验,你也可间接调用 LocalUDPDataSender.send(..)办法发送)new LocalUDPDataSender.SendCommonDataAsync(msg, friendId) {
@Override
protected void onPostExecute(Integer code) {if(code == 0) {log.debug("数据已胜利收回!");
} else{log.error("数据发送失败。错误码是:"+ code + "!");
}
}
}.execute();}
}
五、编写 Controller 进行测试
@RestController
@RequestMapping(“/api”)
@Api(tags = “ 聊天测试 - 接口 ”)
public class ChatController {
@Autowired
private IChatService chatService;
@PostMapping(value = "/loginConnect", produces = Constants.CONTENT_TYPE)
@ApiOperation(value = "登陆申请", httpMethod = "POST", response = ApiResult.class)
public ApiResult loginConnect(@RequestParamString username, @RequestParamString password) {chatService.loginConnect(username, password);
return ApiResult.ok();}
@PostMapping(value = "/sendMsg", produces = Constants.CONTENT_TYPE)
@ApiOperation(value = "发送音讯", httpMethod = "POST", response = ApiResult.class)
public ApiResult sendMsg(@RequestParam String friendId, @RequestParam String msg) {chatService.sendMsg(friendId, msg);
return ApiResult.ok();}
}
启动我的项目,拜访:http://127.0.0.1:8080/swagger…
1)loginConnect 接口:
任意输出一个账号密码登录连贯 IM 服务端:
控制台日志如下:
2)sendMsg 接口:
给指定用户发送音讯:这里因为只有一个客户端,上一步登录了一个 admin 账号,因而小编给 admin 账号(也就是本人) 发送音讯
控制台日志如下:
六、本文小结
对于集成可参考 MobileIMSDK 给出的文档一步一步实现。
该开源工程对应的官网文档比拟齐全,须要哪个端,就去看对应端的手册就好了。
1)Demo 装置和应用
客户端 Demo 装置和应用帮忙 (Android) [1]
客户端 Demo 装置和应用帮忙 (iOS) [2]
客户端 Demo 装置和应用帮忙 (Java) [3]
客户端 Demo 演示和阐明 (H5) [4]
服务端 Demo 装置和应用帮忙 [5] new
2)开发者指南
客户端开发指南 (Android)
客户端开发指南 (iOS)
客户端开发指南 (Java)
客户端开发指南 (H5)
服务端开发指南
3)API 文档
客户端 SDK API 文档 (Android):TCP 版、UDP 版
客户端 SDK API 文档 (iOS):TCP 版、UDP 版
客户端 SDK API 文档 (Java):TCP 版、UDP 版
客户端 SDK API 文档 (H5):点此进入
服务端 SDK API 文档
另外:作者给出了通过 Java GUI 编程实现的一个小 demo,咱们能够先将其运行起来,先体验一下性能,代码量也不是太多,咱们能够通过 debug 形式查看执行流程。
分明执行流程之后咱们就能够将 demo 中的代码移植到咱们本人的我的项目中加以批改使用于本人的业务中,切勿拿起就跑,否则一旦运气不好,将节约更多的工夫去集成,这样很不好!
最初:案例 demo 中相干代码正文都有,这里就简略说下整个流程吧:
1)首先启动 IM 服务端
2)用户在客户端登录一个用户与服务端建设连贯放弃通信(客户端 ChatServiceImpl 中 loginConnect 办法为登录连贯服务端事件;服务端 ServerEventListenerImpl 中 onUserLoginVerify 办法为服务端接管的上线告诉事件);
3)客户端通过 ChatServiceImpl 中 sendMsg 办法发送一条音讯,如果对方在线能接管音讯则走服务端 ServerEventListenerImpl 中 onTransferMessage4C2C 办法,否则走 onTransferMessage_RealTimeSendFaild 办法;如果对方胜利接管到音讯,客户端将走 MessageQoSEventImpl 中 messagesBeReceived 事件,否则走 messagesLost 事件;
4)客户端通过 ChatMessageEvent 中 onRecieveMessage 回调事件接管音讯。
附:本文案例 demo 源码下载:
1)主地址:https://gitee.com/zhengqingya…
2)备地址:https://gitee.com/instant_mes…
附录:更多 IM 聊天老手实际代码
《跟着源码学 IM(一):手把手教你用 Netty 实现心跳机制、断线重连机制》
《跟着源码学 IM(二):自已开发 IM 很难?手把手教你撸一个 Andriod 版 IM》
《跟着源码学 IM(三):基于 Netty,从零开发一个 IM 服务端》
《跟着源码学 IM(四):拿起键盘就是干,教你徒手开发一套分布式 IM 零碎》
《跟着源码学 IM(五):正确理解 IM 长连贯、心跳及重连机制,并入手实现》
《跟着源码学 IM(六):手把手教你用 Go 疾速搭建高性能、可扩大的 IM 零碎》
《跟着源码学 IM(七):手把手教你用 WebSocket 打造 Web 端 IM 聊天》
《跟着源码学 IM(八):万字长文,手把手教你用 Netty 打造 IM 聊天》
《跟着源码学 IM(九):基于 Netty 实现一套分布式 IM 零碎》
《跟着源码学 IM(十):基于 Netty,搭建高性能 IM 集群(含技术思路 + 源码)》