一、前言
MobileIMSDK是什麼?
一個專爲移動端開發的開源原創即時通訊框架
,超輕量級、高度提煉,完全基於UDP協議
,支持iOS
、Android
、標準Java
平臺,服務端基於Mina
和Netty
編寫。MobileIMSDK還可與姊妹工程 MobileIMSDK-Web
無縫互通,從而實現Web網頁端聊天
或推送
等。
本文將實現
- 基於
springboot2.1.8.RELEASE
集成MobileIMSDK
- 開發IM
服務端
- 開發
客戶端
- 實現Java
客戶端與客戶端
之間的通信
二、SpringBoot
集成 MobileIMSDK
準備
1、MobileIMSDK下載:https://gitee.com/jackjiang/MobileIMSDK
- 服務端所需jar包:
dist/server-xxx
- 客服端所需jar包:
dist/client/java
2、pom.xml
中引入相關依賴
由於這裏是maven項目,其中一部分jar包可通過maven倉庫直接引入,而其餘的則通過外部jar包引入方式使用即可~
如下5個需作爲外部jar包在pom.xml中引入
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<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>
<dependency>
<groupId>com.zhengqing</groupId>
<artifactId>netty-all-4.1.17.Final</artifactId>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/netty-all-4.1.17.Final.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>
三、開發服務端
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();
// 【重要】用戶定義的消息或指令協議類型(開發者可據此類型來區分具體的消息或指令)
int typeu = p.getTypeu();
logger.debug("【DEBUG_回調通知】[typeu=" + typeu + "]收到了客戶端" + from_user_id + "發給服務端的消息:str=" + dataContent);
return true;
}
/**
* 通道數據回調函數定義(客戶端發給客戶端的(即接收方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();
// 【重要】用戶定義的消息或指令協議類型(開發者可據此類型來區分具體的消息或指令)
int typeu = 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();
// 【重要】用戶定義的消息或指令協議類型(開發者可據此類型來區分具體的消息或指令)
int typeu = p.getTypeu();
logger.debug("【DEBUG_回調通知】[typeu=" + typeu + "]客戶端" + from_user_id + "發給客戶端" + userId + "的消息:str=" + dataContent
+ ",因實時發送沒有成功,需要上層應用作離線處理哦,否則此消息將被丟棄.");
return false;
}
}
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、服務端配置
public class ServerLauncherImpl extends ServerLauncher {
// 靜態類方法:進行一些全局配置設置
static {
// 設置AppKey(此key目前爲保留字段,請忽略之)
ServerLauncher.appKey = "5418023dfd98c579b6001741";
// 設置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:[email protected]";
}
// 實例構造方法
public ServerLauncherImpl() throws IOException {
super();
}
/**
* 初始化消息處理事件監聽者.
*/
@Override
protected void initListeners() {
// ** 設置各種回調事件處理實現類
this.setServerEventListener(new ServerEventListenerImpl());
this.setServerMessageQoSEventListener(new MessageQoSEventS2CListnerImpl());
}
}
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(new Thread(sli::shutdown));
}
}
如果服務端與客戶端不在同一個項目 ,服務端可直接通過如下方式啓動即可~
四、開發客戶端
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);
}
}
2、接收消息事件
@Slf4j
public class ChatTransDataEventImpl implements ChatTransDataEvent {
@Override
public void onTransBuffer(String fingerPrintOfProtocal, String userid, String dataContent, int typeu) {
log.debug("[typeu=" + typeu + "]收到來自用戶" + userid + "的消息:" + dataContent);
}
@Override
public void onErrorResponse(int errorCode, String errorMsg) {
log.debug("收到服務端錯誤消息,errorCode=" + errorCode + ", errorMsg=" + errorMsg);
}
}
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、MobileIMSDK初始化配置
public class IMClientManager {
private static IMClientManager instance = null;
/**
* MobileIMSDK是否已被初始化. true表示已初化完成,否則未初始化.
*/
private boolean init = false;
public static IMClientManager getInstance() {
if (instance == null) {
instance = new IMClientManager();
}
return instance;
}
private IMClientManager() {
initMobileIMSDK();
}
public void initMobileIMSDK() {
if (!init) {
// 設置AppKey
ConfigEntity.appKey = "5418023dfd98c579b6001741";
// 設置服務器ip和服務器端口
ConfigEntity.serverIP = "127.0.0.1";
ConfigEntity.serverUDPPort = 7901;
// MobileIMSDK核心IM框架的敏感度模式設置
// ConfigEntity.setSenseMode(SenseMode.MODE_10S);
// 開啓/關閉DEBUG信息輸出
ClientCoreSDK.DEBUG = false;
// 設置事件回調
ClientCoreSDK.getInstance().setChatBaseEvent(new ChatBaseEventImpl());
ClientCoreSDK.getInstance().setChatTransDataEvent(new ChatTransDataEventImpl());
ClientCoreSDK.getInstance().setMessageQoSEvent(new MessageQoSEventImpl());
init = true;
}
}
}
6、連接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(@RequestParam String username, @RequestParam String 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給出的文檔一步一步實現,其中給出了通過Java GUI編程實現的一個小demo,我們可以先將其運行起來,先體驗一下功能,代碼量也不是太多,我們可以通過debug方式查看執行流程,清楚執行流程之後我們就可以將demo中的代碼移植到我們自己的項目中加以修改運用於自己的業務中,切勿拿起就跑,否則一旦運氣不好,將浪費更多的時間去集成,這樣很不好!
案例demo中相關代碼註釋都有,這裏就簡單說下整個流程吧:
- 首先啓動IM服務端
- 用戶在客戶端登錄一個用戶與服務端建立連接保持通信( 客戶端
ChatServiceImpl
中loginConnect
方法爲登錄連接服務端事件;服務端ServerEventListenerImpl
中onUserLoginAction_CallBack
方法爲服務端接收的上線通知事件) - 客戶端通過
ChatServiceImpl
中sendMsg
方法發送一條消息,如果對方在線能接收消息則走服務端ServerEventListenerImpl
中onTransBuffer_C2C_CallBack
方法,否則走onTransBuffer_C2C_RealTimeSendFaild_CallBack
方法;如果對方成功接收到消息,客戶端將走MessageQoSEventImpl
中messagesBeReceived
事件,否則走messagesLost
事件 - 客戶端通過
ChatTransDataEventImpl
中onTransBuffer
回調事件接收消息