一、前言
MobileIMSDK 是什么?
MobileIMSDK  是一套专门为移动端开发的开源 IM 即时通讯框架,超轻量级、高度提炼,一套 API 优雅支持 UDP 、TCP 、WebSocket 三种协议,支持 iOS、Android、H5、标准 Java 平台,服务端基于 Netty 编写。
工程地址是:
本文将实现:
(本文已同步发布于:http://www.52im.net/thread-3894-1-1.html)
二、SpringBoot 集成 MobileIMSDK 准备
2.1 MobileIMSDK 下载
MobileIMSDK 下载地址:
1)国外地址:MobileIMSDK 的 Github 地址(最新版打包下载)
2)国内地址:MobileIMSDK 的码云 gitee 地址(访问速度快!,最新版打包下载)
需要用到的 lib 包:
如下图所示:
2.2 pom.xml 中引入相关依赖
由于这里是 maven 项目,其中一部分 jar 包可通过 maven 仓库直接引入,而其余的则通过外部 jar 包引入方式使用即可~
如下 4 个需作为外部 jar 包在 pom.xml 中引入 :
<dependency>
 <groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
       复制代码
 </dependency>
<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 服务端启动类
温馨小提示:这里由于小编将服务端和客户端集成在同一个项目中,因此如下配置:
@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-ui.html
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 源码下载:
附录:更多 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 集群(含技术思路+源码)》
(本文已同步发布于:http://www.52im.net/thread-3894-1-1.html)
评论