1.7.1 客户端核心SDK
客户端核心SDK分为两个版本,一个是Web版,使用Typescript 语言编写;一个是原生版,使用c++11 语言。两者的架构跟业务功能是一致的,因此只要了解其中一版即可。下面以Web版(源码版本v1.10)为例,理解客户端SDK的设计。
1.源码架构
|-- echatim
| |-- common
| | |-- Topic.ts # 社区版Topic的定义, api规约定义
| | |-- TopicProfessional.ts # 专业版Topic的定义, api规约定义
| | |-- UUID.ts
| | |-- WebToolkit.ts # 一些Web常用工具类
| | `-- utils.ts # js 通用方法
| |-- core
| | |-- apis # 以下是http api与socket.io api的接口方法定义以及实现细节
| | | |-- ApiResponse.ts
| | | |-- EChatIMApis.ts
| | | |-- HttpApi.ts
| | | |-- IApi.ts
| | | |-- IHttpConfig.ts
| | | `-- SocketIOApi.ts
| | |-- cache
| | | `-- jwt.ts # 登录用户的数字签名(jwt)
| | |-- fileclient # 文件服务器客户端功能
| | | |-- FileServerClient.ts # 文件服务器客户端工厂
| | | `-- support # 以下针对各种不同平台的实现方式, 如Web, ReactNative, 微信小程序
| | | |-- AbstractClient.ts
| | | |-- AbstractXMLHttpClient.ts
| | | |-- BrowserXMLHttpClient.ts
| | | |-- Fetch.ts
| | | |-- PluploadClient.ts
| | | |-- ReactNativeFetchClient.ts
| | | |-- ReactNativeXMLHttpClient.ts
| | | |-- SuffixMime.ts
| | | `-- WxClient.ts
| | `-- sdk
| | |-- EChatIMSDK.ts # 主要sdk功能
| | |-- Errors.ts # sdk 错误码
| | |-- Socket.ts # 实现了socket.io 的代理, 连接,监听等
| | `-- SocketStatusManage.ts # socket.io 连接状态管理器
| |-- log
| | `-- Logger.ts # sdk日志方法
| `-- model # sdk中用到的模型结构
| |-- dto
... 省略 ...
| |-- form
... 省略 ...
| `-- protocol
... 省略 ...
|-- echatim.ts # 入口文件, 对引用模块导出EChatIMSDK实例
`-- version.js # 记录sdk的版本信息
2.引用客户端sdk
在Web html中, 只需要以下的一行代码:
<script src="echatim-sdk.js"></script>
即可实现引入sdk, 这一过程是在echatim.ts中处理的:
if(typeof window !== 'undefined'){
window['im'] = echatIMSDK;
window['im_webtoolkit'] = WebToolkit;
window['imsdk_version'] = version;
}
在加载echatim-sdk.js后, 使用window.im 即可访问sdk 实例echatIMSDK。
3.使用客户端sdk
echatIMSDK是单例的,是EChatIMSDK的一个对象。在Web Demo中,可见以下使用sdk的代码片段:
var im = window.im;
im.init(sdkConfig, function (sdk) {
if (sdk) {
console.info('echatIMSDK 成功连接, 可以使用 EChatIMApis 请求数据了.');
} else {
throw Error("echatIMSDK 初始化失败");
}
});
初始化时调用了echatIMSDK.init 方法, 来看看echatIMSDK.init 方法中做了什么事情:
init(config:EChatIMSDKConfig, callback: (sdk:EChatIMSDK) => void): EChatIMSDK {
const self = this;
// 1. 保存配置信息
self.config.host = config.host ? config.host : 'localhost';
self.config.socketPort = config.socketPort ? config.socketPort : 80;
self.config.httpPort = config.httpPort ? config.httpPort : 80;
self.transport = config.apiTransport ? config.apiTransport : 'HTTP';
self.config.key = config.key;
self.config.secret = config.secret;
self.config.loginToken = config.loginToken;
self.config.loginAuid = config.loginAuid;
self.config.listeners = config.listeners;
self.config.fileServerConfig = config.fileServerConfig;
if(config.autoLogin !== undefined){
self.config.autoLogin = config.autoLogin;
}
Logger.info(`ready to connect:${'http://' + self.config.host + ':' + self.config.socketPort}`);
// 2. 开启socket.io 连接
socket.connect('http://' + self.config.host + ':' + self.config.socketPort);
if(!firstMonitorSocket){
firstMonitorSocket = true;
// 加入socket连接事件监听(可选)
if(typeof self.config.listeners.onSocketConnectEvent === 'function'){
const socketManage = new SocketStatusManage(socket['socket']);
socketManage.addListener((status, data)=>{
self.config.listeners.onSocketConnectEvent(status, data);
})
}
}
// 3. 监听socket.io connect事件
socket.listen('connect', function(msg: any) {
Logger.info('apiSocket.io connected!');
self.socketConnected = true;
if(!firstInit && !self.config.autoLogin){
callback(self);
firstInit = true;
}
// 连接时自动登录
if(self.config.autoLogin){
self.autoLogin(function () {
self.logined = true;
callback(self); // 仅用户登录成功时, 初始化才完成.
if(self.config.listeners){
self.initConfigSocketioMessageListener();
self.initConfigImListener();
}
});
}
// sdk 初始化时不自动登录, 需要外部应用自行管理用户的登录状态.
else {
// 仅初始化socketio 相关的消息监听即可
self.initConfigSocketioMessageListener();
}
if(self.config.listeners && typeof self.config.listeners.onConnected === 'function'){
self.config.listeners.onConnected();
}
});
// 3. 监听socket.io disconnect事件
socket.listen('disconnect', function(msg: any) {
Logger.info('apiSocket.io disconnected!');
self.socketConnected = false;
self.logined = false;
if(self.config.listeners && typeof self.config.listeners.onDisConnected === 'function'){
self.config.listeners.onDisConnected();
}
});
return this;
}
在echatIMSDK.init 方法中,主要做了三件事情:
- 保存配置信息到sdk中;
- 开启socket.io 的连接;
- 监听socket.io 的connect, disconnect事件;
socket.io 的连接成功的事件监听中,sdk还处理了用户的自动登录(实现用户身份与socket.io通道的绑定); 自动请求部分api, 如: 获取好友列表,获取会话列表等,请求结果通过回调函数返回给应用层。
4.文件服务器客户端
在echatIMSDK 中,实现了newFileClient方法,通过该方法可以实例化一个文件上传实例。
public newFileClient():IFileServerClient {
// 根据sdk传入的配置信息,调用工厂方法,创建对应的客户端
if(this.config.fileServerConfig){
return FileServerClientFactory.create(this.config.fileServerConfig, this.config.key);
}
else {
throw new Error('没有fileServerConfig配置, 无法创建file client 实例');
}
}
在业务系统中,有两种方式使用该文件上传功能:
// 方式一: 直接绑定到一个dom节点, 适用于Web版
html:
<button id="sendFileMessage" />
js:
window.im.newFileClient().init({
domId:domId, // 绑定的dom节点
maxFileSize: 100,// 文件大小上限. 100mb
type:'FILE',
allowSuffix:[]// 允许上传的文件后缀名
},{
beforeUploadCallback:function (fileInfo) {
return fileInfo;// 1.可以自定义fileInfo内容; 2.返回'cancel'时可以取消上传;
},
progressCallback:function (fileInfo) {
},
uploadedCallback:function (fileInfo) {
},
errorCallback:function (fileInfo, errInfo) {
},
});
// 方式二: 使用返回的IFileServerClient实例, 手动调用upload方法
// 1.创建实例
this.fileClient = im.newFileClient();
// 2.初始化实例
this.fileClient.init({
maxFileSize: 100,// 文件大小上限. 100mb
type:'FILE',
allowSuffix:[]// 允许上传的文件后缀名
},{
beforeUploadCallback:function (fileInfo) {
return fileInfo;// 1.可以自定义fileInfo内容; 2.返回'cancel'时可以取消上传;
},
progressCallback:function (fileInfo) {
},
uploadedCallback:function (fileInfo) {
},
errorCallback:function (fileInfo, errInfo) {
},
});
// 3.手动上传
this.fileClient.upload(filePath);// filePath: 文件的物理路径
5.拓展现有的api
在Web版源码v1.10后,echatIMSDK提供了httpApiCall, socketioApiCall 两个高级方法,可用于http api 和socket.io 的API拓展。
5.1 相关源码
// http常规调用接口, 使用http POST方式访问后台api
// url: 请求完整路径; headers: 请求头, json格式; body: 请求体, json格式
public httpApiCall(url:string, headers:any, body: any): Promise<ApiResponse<any>> {
const api = new HttpApi<any, any>(url, "通用http api");
return api.rawCall(headers, body);
}
// socket.io常规调用接口, 使用socket.io方式访问后台api
// url: 请求完整路径; body: 请求体, json格式
public socketioApiCall(url:string, body: any): Promise<ApiResponse<any>> {
const api = new SocketIOApi<any, any>(url, "通用socket.io api");
return api.rawCall(body);
}
5.2 一个例子
// http api 例子
const httpcall = window.im.httpApiCall.bind(window.im);
function httpFetch(url, request) {
return httpcall(url, request.headers, request.body)
}
// 使用: 定义header, body
const headers = {
'Content-Type': 'application/json',
'Request-Id': new Date().getTime() + '' + parseInt(Math.random() * 1000 + ''),
'timestamp': new Date().getTime(),
};
if (needAuth) {
headers.authorization = 'client ' + jwt;
}
const params = {};
const request = {headers: headers, body: params};
httpFetch(url, request);
// socket api 例子
const socketcall = window.im.socketioApiCall.bind(window.im);
function socketFetch(url, body) {
return socketcall(url, body)
}
// 使用: socketFetch('topic.你的TOPIC定义' + '/' + '你的方法名', {})