I. Introduction
What is MobileIMSDK?
MobileIMSDK is a set of open source IM instant messaging framework specially developed for mobile terminals. It is ultra-lightweight and highly refined. A set of API elegantly supports three protocols: UDP, TCP and WebSocket, and supports iOS, Android, H5, standard Java platforms, and services. The terminal is written based on Netty.
The project address is:
1) Gitee code cloud address: https://www.oschina.net/p/mobileimsdk
2) Github hosting address: https://github.com/JackJiang2011/MobileIMSDK
This article will achieve:
1) Integrate MobileIMSDK based on springboot;
2) Develop IM server;
3) Develop the client;
4) Realize the communication between the Java client and the client.
- Supplementary note: The Demo source code shown in this article, please download it from the last link of "Summary of this article" at the end of the article!
Second, SpringBoot integrated MobileIMSDK preparation
2.1 MobileIMSDK download
MobileIMSDK download address:
1) Foreign address: Github address of MobileIMSDK (the latest version is packaged and downloaded)
2) Domestic address: Code cloud gitee address of MobileIMSDK (access speed is fast! The latest version is packaged and downloaded)
Required lib packages:
1) The jar package required by the server: sdk_binary/Server/
2) The jar package required by the client: sdk_binary/Client_TCP/java/
As shown below:
2.2 Introducing relevant dependencies into pom.xml Since this is a maven project, some of the jar packages can be imported directly through the maven repository, while the rest can be used through the introduction of external jar packages~
The following four need to be introduced in pom.xml as external jar packages:
<!-- [url= https://mvnrepository.com/artifact/com.google.code.gson/gson ]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>
<!-- The jar package required by MobileIMSDK depends [Note: This is introduced in the local lib, there is no such jar package in the maven central warehouse], it should be used in conjunction with <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. Development of the server
3.1 All data interaction events with the client (implementing the ServerEventListener class)
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 The server actively initiates the QoS callback notification of the message (implementing the MessageQoSEventListenerS2C class)
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 Server configuration
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 Warm tip for the server startup class: Here, since Xiaobian integrates the server and the client in the same project, the configuration is as follows:
SpringBoot's CommandLineRunner interface is mainly used to implement a piece of code block logic (run method) after the service is initialized. This initialization code will only be executed once in the entire application life cycle!
@Order(value = 1) : execute in a certain order, the smaller the value, the first
@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));
}
}
If the server and the client are not in the same project, the server can be started directly in the following way~
Fourth, develop the client
4.1 Client and IM server connection events
@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 Receive message events
@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 Whether the message is delivered to the event
@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 Initial configuration of 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 Connect to IM server and send message service class:
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);
}
Service implementation class:
@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();
}
}
5. Write Controller for testing
@RestController
@RequestMapping("/api")
@Api(tags = "Chat Test-Interface")
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();
}
}
Start the project and visit: http://127.0.0.1:8080/swagger-ui.html
1) loginConnect interface:
Enter any account and password to log in and connect to the IM server:
The console log is as follows:
2) sendMsg interface:
Send a message to the specified user: Since there is only one client here, an admin account was logged in in the previous step, so the editor sends a message to the admin account (that is, myself)
The console log is as follows:
6. Summary of this paper
For the integration, please refer to the document given by MobileIMSDK for step-by-step implementation.
The official documents corresponding to the open source project are relatively complete. For which end you need, just read the manual of the corresponding end.
1) Demo installation and use
Client Demo installation and use help (Android) [1]
Client Demo installation and use help (iOS) [2]
Client Demo installation and use help (Java) [3]
Client Demo Demonstration and Instructions (H5) [4]
Server-side Demo installation and use help[5] new
2) Developer Guide
Client Development Guide (Android)
Client Development Guide (iOS)
Client Development Guide (Java)
Client Development Guide (H5)
Server Development Guide
3) API Documentation
Client SDK API Documentation (Android): TCP Version, UDP Version Client SDK API Documentation (iOS): TCP Version, UDP Version Client SDK API Documentation (Java): TCP Version, UDP Version Client SDK API Documentation (H5) : Click here to enter the server SDK API documentation
In addition: the author gave a small demo implemented by Java GUI programming. We can run it first and experience the functions first. The amount of code is not too much. We can view the execution process through debug.
After the execution process is clear, we can transplant the code in the demo into our own project, modify it and apply it to our own business. Do not pick it up and run, otherwise, if you are unlucky, you will waste more time on integration. This is very bad!
Finally: there are relevant code comments in the case demo, here is a brief description of the whole process:
1) First start the IM server
2) The user logs in a user on the client to establish a connection with the server to maintain communication (the loginConnect method in the client ChatServiceImpl is the login connection server event; the onUserLoginVerify method in the server ServerEventListenerImpl is the online notification event received by the server);
3) The client sends a message through the sendMsg method in ChatServiceImpl. If the other party can receive the message online, it will go to the onTransferMessage4C2C method in the server ServerEventListenerImpl, otherwise it will go to the onTransferMessage_RealTimeSendFaild method; if the other party successfully receives the message, the client will go to the messagesBeReceived event in MessageQoSEventImpl, otherwise Go messagesLost event;
4) The client receives messages through the onRecieveMessage callback event in ChatMessageEvent.
Attachment: Download the demo source code of this case:
1) Main address: https://gitee.com/zhengqingya/java-workspace
2) Backup address: https://gitee.com/instant_messaging_network/java-workspace
Appendix: More IM chat novice practice code
"Learn IM from the source code (1): teach you to use Netty to implement the heartbeat mechanism, disconnection and reconnection mechanism"
"Learn IM with source code (2): Is it difficult to develop IM by yourself? Teach you to play an Android version of IM"
"Learn IM with the source code (3): Based on Netty, develop an IM server from scratch"
"Learn IM with the source code (4): Pick up the keyboard and do it, teach you to develop a distributed IM system with your bare hands"
"Learn IM from the source code (5): correctly understand the IM long connection, heartbeat and reconnection mechanism, and implement it by hand"
"Learn IM from the source code (6): teach you how to quickly build a high-performance and scalable IM system with Go"
"Learn IM from the source code (7): teach you how to use WebSocket to create web-side IM chat"
"Learn IM from the source code (8): 10,000-character long text, teach you how to use Netty to create IM chat"
"Learn IM with Source Code (9): Implementing a Distributed IM System Based on Netty"
"Learn IM with the source code (10): Based on Netty, build a high-performance IM cluster (including technical ideas + source code)"
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。