注:如果您發現任何不正確的內容,或者您想要分享有關本文主題的更多信息,請撰寫評論或聯繫我 [email protected]。
如有侵權請聯繫我刪除。環信的API是公開的,應該沒啥事吧~~~
分享一次自實現IM的經驗。自己實現了一個簡略版本的IM。勉強可以用,就是頁面需要自己實現。雖然最終還是使用了環信的產品。因爲自己架構的不太成熟。沒有考慮到很多因素。果然啊自己造的就是沒有別人已經成熟的產品,使用的更堅挺、持久,圓潤~!
調用環信工具類
/**
* 環信常量類
*
* @Date 2019/1/8-15:24
* @Author Amarone
* @Description you should see
* <pre>
* 1. http://api-docs.easemob.com/#/%E8%8E%B7%E5%8F%96token
* 2. http://docs-im.easemob.com/im/100serverintegration/10intro
* </pre>
**/
public class EasemobConst {
public static String SEGMENTATION = "/";
//*************************************************** 基礎屬性配置 *******************************************
/**
* IM 登錄統一默認密碼
*/
public static String IM_DEFAULT_PASSWORD = "";
/**
* 企業的唯一標識 開發者在環信開發者管理後臺註冊賬號時填寫的企業 ID 此處從配置文件獲取
*/
public static String BASE_ORG_NAME = ****;
/**
* 同一“企業”下“APP”唯一標識,開發者在環信開發者管理後臺創建應用時填寫的“應用名稱”
*/
public static String BASE_APP_NAME = *****;
/**
* 請求環信接口TOKEN
*/
public static String BASE_TOKEN = "";
/**
* 基礎屬性 - 環信後臺配置
*/
public static String BASE_CLIENT_ID = ****;
/**
* 基礎屬性 - 環信後臺配置
*/
public static String BASE_CLIENT_SECRET = ****;
//*************************************************** API 接口URL 配置 ******************************************************
/**
* 環信公共請求URL、請求鏈接參考 http://api-docs.easemob.com/#!/%E7%94%A8%E6%88%B7%E4%BD%93%E7%B3%BB%E9%9B%86%E6%88%90/post_org_name_app_name_users
* 格式爲: ${COMMON_PATH}/{org_name}/{app_name}/users
*/
public static String API_BASE_PATH = "http://a1.easemob.com/" + BASE_ORG_NAME + SEGMENTATION + BASE_APP_NAME;
/**
* 獲取token
* POST /{org_name}/{app_name}/token
*/
public static String API_URL_GETTOKEN = "/token";
/**
* /{org_name}/{app_name}/users/{owner_username}/contacts/users/{friend_username}
*
* 向IM 用戶添加好友
*/
public static String API_URL_ADD_FRIEND = "/users/{owner_username}/contacts/users/{friend_username}";
/**
* API 接口 用戶體系集成
*/
public static String API_URL_USER_SYSTEM = "/users";
/**
* API 接口 查看一個用戶的在線狀態。 GET /{org_name}/{app_name}/users/{username}/status
*/
public static String API_URL_ONLINE_STATUS = "/users/{username}/status";
/**
* 離線消息數量 GET /{org_name}/{app_name}/users/{owner_username}/offline_msg_count
*/
public static String API_URL_OFFLINE_MSG = "/users/{owner_username}/offline_msg_count";
/**
* 解除好友關係 DELETE /{org_name}/{app_name}/users/{owner_username}/contacts/users/{friend_username}
*/
public static String API_URL_RELIEVE_FRIEND = "/users/{owner_username}/contacts/users/{friend_username}";
}
/**
* 獲取token
*
* @Date 2019/1/11-14:45
* @Author Amarone
* @Description
**/
public class GetToken {
private String grant_type = "client_credentials";
// 從配置文件獲取
private String client_id = ***;
// 從配置文件獲取
private String client_secret = ****;
// 省略get set
}
/**
* 註冊賬戶
*
* @Date 2019/1/15-10:57
* @Author Amarone
* @Description
**/
public class RegisterAcc {
/**
* 用戶名
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 暱稱
*/
private String nickname;
// 省略get set 有參 無參 構造
}
/**
* @Date 2019/1/12-18:04
* @Author Amarone
* @Description
**/
public class RspToken {
/**
* token 值
*/
private String access_token;
/**
* token 有效時間,以秒爲單位,在有效期內不需要重複獲取
*/
private String expires_in;
/**
* 當前 APP 的 UUID 值
*/
private String application;
// 省略get set 有參 無參 構造
}
import org.apache.commons.codec.Charsets;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.LinkedHashMap;
import reactor.core.publisher.Mono;
/**
* 環信工具類
*
* @Date 2019/1/8-15:23
* @Author Amarone
* @Description
**/
@Service
public class EasemobUtil extends BaseLogger {
/**
* 發送post 請求
*
* @param method 請求的方法
* @param url 請求的路徑
* @param para 發送請求的參數
* @param parameter 請求路徑中的參數
* @return rsp
*/
public LinkedHashMap prefixRequest(HttpMethod method, String url, Object para, Object... parameter) throws Exception {
if (method == null) {
throw new BizException("請求方法未識別!");
}
LinkedHashMap rsp = null;
try {
if (HttpMethod.POST.compareTo(method) == 0) {
rsp = postRequest(url, para, parameter);
} else if (HttpMethod.GET.compareTo(method) == 0) {
rsp = getRequest(url, parameter);
} else if (HttpMethod.DELETE.compareTo(method) == 0) {
rsp = deleteRequest(url, parameter);
} else {
throw new BizException("請求方法未識別!");
}
} catch (Exception e) {
if (e.getCause() instanceof EasemobException) {
EasemobException ex = (EasemobException) e.getCause();
if (String.valueOf(EasemobEnum.ANEW_TOKEN.getValue()).equals(ex.messageCode)) {
logger.warn("調用環信 401 異常,重新請求TOKEN");
// 401 錯誤爲 token過期
getEasemobToken();
// 重新發送請求
prefixRequest(method, url, para, parameter);
} else if (String.valueOf(EasemobEnum.BAD_REQ.getValue()).equals(ex.messageCode)) {
logger.warn("調用環信 400 異常," + e.getMessage());
} else {
e.printStackTrace();
throw ex;
}
} else {
e.printStackTrace();
throw e;
}
}
return rsp;
}
/**
* 刪除方法
*
* @param url 請求的路徑
* @param parameter 請求路徑中的參數
*/
public LinkedHashMap deleteRequest(String url, Object... parameter) throws Exception {
if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
getEasemobToken();
}
WebClient webClient = buildBase();
Mono<LinkedHashMap> result = webClient
.delete()
.uri(url, parameter)
.header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
.acceptCharset(Charsets.UTF_8)
.retrieve()
.onStatus(e -> e.is4xxClientError(), resp -> {
return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
})
.onStatus(e -> e.is5xxServerError(), resp -> {
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(LinkedHashMap.class)
.doOnError(onerror -> {
logger.error("請求IM服務失敗,請稍後重試!");
onerror.printStackTrace();
});
return result.block();
}
/**
* 發送post請求(本方法未處理異常)
*
* @param url 請求的路徑
* @param para 發送請求的參數
* @param parameter 請求路徑中的參數
* @return rsp
*/
public LinkedHashMap postRequest(String url, Object para, Object... parameter) throws Exception {
if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
getEasemobToken();
}
WebClient webClient = buildBase();
Mono<LinkedHashMap> result = null;
if (parameter != null) {
result = webClient
.post()
.uri(url, parameter)
.header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
.acceptCharset(Charsets.UTF_8)
.syncBody(para)
.retrieve()
.onStatus(e -> e.is4xxClientError(), resp -> {
return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
})
.onStatus(e -> e.is5xxServerError(), resp -> {
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(LinkedHashMap.class)
.doOnError(onerror -> {
logger.error("請求IM服務失敗,請稍後重試!");
onerror.printStackTrace();
});
} else {
result = webClient
.post()
.uri(url)
.header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
.acceptCharset(Charsets.UTF_8)
.syncBody(para)
.retrieve()
.onStatus(e -> e.is4xxClientError(), resp -> {
return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
})
.onStatus(e -> e.is5xxServerError(), resp -> {
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(LinkedHashMap.class);
}
if (result != null) {
return result.block();
} else {
return null;
}
}
/**
* get 類型請求,應調用{prefixGetRequest()} 方法 (本方法未處理異常)
*
* @param url 請求url
* @param parameter url中參數 使用 {}
* @return result
*/
public LinkedHashMap getRequest(String url, Object... parameter) throws Exception {
if (StringUtils.isEmpty(EasemobConst.BASE_TOKEN)) {
getEasemobToken();
}
WebClient webClient = buildBase();
Mono<LinkedHashMap> result = webClient
.get()
.uri(url, parameter)
.header(HttpHeaders.AUTHORIZATION, EasemobConst.BASE_TOKEN)
.acceptCharset(Charsets.UTF_8)
.retrieve()
.onStatus(e -> e.is4xxClientError(), resp -> {
return Mono.error(new EasemobException(String.valueOf(resp.statusCode().value()), resp.statusCode().getReasonPhrase()));
})
.onStatus(e -> e.is5xxServerError(), resp -> {
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(LinkedHashMap.class)
.doOnError(onerror -> {
logger.error("請求IM服務失敗,請稍後重試!");
onerror.printStackTrace();
});
return result.block();
}
/**
* 獲取環信token
*/
void getEasemobToken() {
WebClient webClient = buildBase();
Mono<RspToken> rspMono = webClient
.post()
.uri(EasemobConst.API_URL_GETTOKEN)
.syncBody(new GetToken())
.retrieve()
.onStatus(e -> e.is4xxClientError(), resp -> {
return Mono.error(new RuntimeException(resp.statusCode().value() + " : " + resp.statusCode().getReasonPhrase()));
})
.bodyToMono(RspToken.class);
rspMono.doOnError(onerror -> {
logger.error("請求IM服務失敗,請稍後重試!");
onerror.printStackTrace();
});
RspToken rspToken = rspMono.block();
// There are fucking Spaces
EasemobConst.BASE_TOKEN = "Bearer " + rspToken.getAccess_token();
}
/**
* 構建基礎請求
*/
WebClient buildBase() {
return WebClient
.builder()
.baseUrl(EasemobConst.API_BASE_PATH)
.defaultHeaders(header -> {
header.setContentType(MediaType.APPLICATION_JSON);
})
.build();
}
}
自實現IM
使用技術如下
- - JAVA 1.8
- - SpringBoot
- - Websocket
- - Netty
- - RabbitMQ
- - Redis
實現想法
總體流程:
- - 客戶端指用戶使用的客戶端。客戶端統一使用Websocket發起連接請求到後臺的Netty服務器
- - 客戶端連接成功之後,緊接着想服務器發起一段報文。報文內含 客戶端的用戶id等信息
- - Netty接收到請求之後將Channel與客戶id綁定,存儲到java.util.map 裏面
- - 每次接收到請求判斷map裏面是否存在消息接受者的id。如果沒有存到消息同步庫。待接收者上線後拉取信息
客服機器人:
- - 客服機器人由後臺寫netty客戶端連接到服務器,機器人與普通客戶之間以客戶id開頭字母匹配。
以上寫的信息有點不詳細。想請教一下各位大神指出不足之處。
- 暫不考慮傳輸 音頻、視頻等。目前只傳送純文本
- 暫不考慮報文安全性
- 暫不考慮報文大小、浪費的流量以及電量等
- 暫不考慮大量併發
- 暫不考慮分佈式部署
目前只是實現業務目標。
2018年12月20日
- 現在又多了一個問題。怎麼把這玩意弄成高可用的、、
2018年12月21日
- 如果管理channel等信息存儲在map裏面。這樣對於高可用的行不通的,一旦單點故障。整個IM就完蛋了。