由于业务需求,需要在系统中集成环信,所以去看了下官方文档,写篇博客分享一下。
在开始之前,我们需要了解一下什么是REST。
REST(Representational State Transfer)是一种轻量级的 Web Service 架构风格,可以翻译成“表述性状态转移”,实现和操作明显比 SOAP 和 XML-RPC 更为简洁,可以完全通过 HTTP 协议实现,还可以利用缓存 Cache 来提高响应速度,性能、效率和易用性上都优于 SOAP 协议。
REST 架构遵循了 CRUD 原则,CRUD原则对于资源只需要四种行为:Create(创建)、Read(读取)、Update(更新)和Delete(删除)就可以完成对其操作和处理。这四个操作是一种原子操作,对资源的操作包括获取、创建、修改和删除资源的操作正好对应HTTP 协议提供的 GET、POST、PUT 和 DELETE 方法,因此 REST 把 HTTP 对一个 URL 资源的操作限制在POST、GET、PUT 和 DELETE 这四个之内。这种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。
我看到一篇关于RESTful的很不错的文章:RESTful API 设计指南,有兴趣的可以看下,讲的很好。
环信 REST 平台
环信 REST 平台提供的是一个多租户用户体系,资源以集合(Collection)的形式来描述,这里所说的 Collection 包括 DataBase、企业(orgs)、应用(apps)、IM用户(users)、群组(chatgroups)、消息(chatmessages)、文件(chatfiles)等等,之间的包含关系是:
DB = {org1, org2, …}
org = {app1, app2, …}
app = {users, messages, chatfiles, chatmessages, chatgroups, …}
users = {user1, user2, …}
messages = {message1, message2, …}
chatfiles = {chatfile1, chatfile2, …}
chatmessages = {chatmessage1, chatmessage2, …}
chatgroups = {group1, group2, …}
多租户是指软件架构支持一个实例服务多个用户(Customer),每一个用户被称之为租户(Tenant),软件给予租户可以对系统进行部分定制的能力,如用户界面颜色或业务规则,但是他们不能定制修改软件的代码。详情可查看官方文档:服务端集成。
在环信服务体系中,不同org之间的用户数据相互隔离,同一个 org 下不同 APP 之间的用户数据相互隔离。
REST server
环信的服务器端接口都是通过REST服务方式提供的,REST API基于最简单的HTTP协议,在各个编程语言中都提供了良好的支持。
REST client
REST client 就是调用 REST API 的程序端,调用方式有多种:Linux curl、浏览器、编程语言 HTTP 请求访问实现等。
调用 REST API,本质就是发送 HTTP 请求,只不过大家常用的可能是 HTTP GET 和 HTTP POST 请求,但是在 REST 里面还经常用到 HTTP PUT 和 HTTP DELETE。在 REST 中,把这四种操作称之为动词,可以(但不是特别准确)想象成增删改查。
而动词所操作的对象,在 REST 中,被称之为“资源”,也就是 URL,而这些也都是标准的 HTTP 协议的内容。实际上,当我们在浏览器中打开一个网站的时候,例如,打开环信官网,浏览器实际上发送给网站服务器的,就是一个 HTTP GET 的请求。
需要注意的是,环信的 REST API 都是基于 JSON 的,所以在构造 HTTP 请求的时候,需要在 HTTP HEADER 中指明:
Header_name | Header_value | Description |
---|---|---|
Accept | application/json | 服务器端返回给客户端的数据类型 |
Content-Type | application/json | 客户端发送到服务器端的数据类型 |
JAVA
在 Java 中,REST client 实现方式有多种,比如 JBOss RestEasy、Sun Jersey、Dropwizard、Apache HTTPClient。
本文用的是Spring的RestTemplate。有一篇不错的博客:Spring RestTemplate中几种常见的请求方式,可以去看看。
在集成之前,我们得在环信上注册并创建一个账号。
注册开发者账号
第 1 步:进入环信官网,选择“注册 > 注册即时通讯云”,进入环信管理后台的注册页面。
第 2 步:在注册页面中,填写详细资料,并点击“注册”按钮。
注册成功后,我们会向您填写的邮箱中发送验证信息,请前往邮箱进行账号激活。
创建应用
账号激活成功后,回到控制台登录页面登录到开发者后台。
第 1 步:在我的应用中,点击创建应用按钮,如下图:
第 2 步:填写创建应用的名称(内容只限于数字、大小写字母),如下图:
(应用名称会存在于你生成的 AppKey 中,如:测试 Demo 中 AppKey 为 easemob-demo#chatdemo,则 chatdemo 为填写的应用名称。注册授权根据需要自行选择,AppKey的长度限制为1k字节以内。)
第 3 步:填写好应用名称后,点确定。创建成功,系统会为你生成 AppKey 以及相关配置信息,如下图:
之后,将此 AppKey 配置到 Android、iOS、Web IM 上即可。
名词 | 解释 |
---|---|
org_name | 企业的唯一标识,开发者在环信开发者管理后台注册账号时填写的企业 ID |
app_name | 同一“企业”下“APP”唯一标识,开发者在环信开发者管理后台创建应用时填写的“应用名称” |
org_admin | 开发者在环信开发者管理后台注册时填写的“用户名”,企业管理员拥有对该企业账号下所有资源的操作权限 |
AppKey | 一个 APP 的唯一标识,规则是 ${org_name} # ${app_name} |
这些在官方文档上都是有的,可点击查看:注册并创建应用
编写代码
接下来就开始用户集成了,详细的就不说了,可点击查看用户体系集成。
主要把写好的工具类分享一下。在对应的场景,只需调用对应的方法即可。
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 环信工具类
*/
public class HXUtil {
private static RestTemplate restTemplate = new RestTemplate();
// 企业的唯一标识,开发者在环信开发者管理后台注册账号时填写的企业 ID
private static final String ORG_NAME = "1122161011178276";
// App的client_id
private static final String CLIENT_ID = "YXA6Irz_oI-GEead-FFvbfaMbQ";
// App的client_secret
private static final String CLIENT_SECRET = "YXA6VsR5JypETS3iPFvNNxYklmho0Vw";
// 同一“企业”下“APP”唯一标识,开发者在环信开发者管理后台创建应用时填写的“应用名称”
private static final String APP_NAME = "testapp";
// 链接前缀
private static final String URL_PREFIX = "http://a1.easemob.com/" + ORG_NAME + "/" + APP_NAME + "/";
// 缓存的token
private static Token token;
// token的失效时间
private static long expiresTime;
public enum HXMessageType {
txt,// 文本
img,// 图片
loc,// 位置
audio,// 音频
video,// 视频
file// 文件
}
/**
* 获取Token
* 注意:关于有效时间,我在网上找过,说的是7天,但是返回的是5184000,
* 但是官网上说是以秒为单位,这么算下来就是60天了,
* 觉得不太对,就先将有效时间设为了7天
* @return token
*/
public static Token getToken() {
// 判断Token是否已经过期,如果过期需要重新获取
if (token == null || expiresTime < new Date().getTime()) {
try {
JSONObject body = new JSONObject();
body.put("grant_type", "client_credentials");
body.put("client_id", CLIENT_ID );
body.put("client_secret", CLIENT_SECRET );
HttpEntity httpEntity = new HttpEntity(body.toString(), null);
ResponseEntity<Token> tokenResponseEntity = restTemplate.postForEntity(URL_PREFIX + "token", httpEntity, Token.class);
token = tokenResponseEntity.getBody();
// 设置7天后过期
Calendar c = Calendar.getInstance();
c.add(Calendar.DATE, 7);
expiresTime = c.getTime().getTime();
} catch (RestClientException e) {
e.printStackTrace();
}
}
return token;
}
/**
* 添加用户
*
* @param username 用户名(唯一非空)
* @param password 密码
* @return 是否成功
*/
public static boolean addUser(String username, String password) {
try {
JSONArray body = new JSONArray();
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", username);
jsonObject.put("password", password);
body.add(jsonObject);
HttpEntity httpEntity = new HttpEntity(body.toString(), null);
ResponseEntity responseEntity = restTemplate.postForEntity(URL_PREFIX + "users", httpEntity, null);
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 修改用户密码
*
* @param username 用户名
* @param newpassword 新密码
* @return 是否成功
*/
public static boolean updatePassword(String username, String newpassword) {
try {
JSONObject body = new JSONObject();
body.put("newpassword", newpassword);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + getToken().getAccess_token());
HttpEntity httpEntity = new HttpEntity(body.toString(), headers);
ResponseEntity responseEntity = restTemplate.postForEntity(URL_PREFIX + "users/{username}/password", httpEntity, null, username);
System.out.println(responseEntity.getStatusCodeValue());
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 删除用户
*
* @param username 用户名
*/
public static boolean deleteUser(String username) {
try {
HttpEntity httpEntity = new HttpEntity(null, getHttpHeaders(MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON));
ResponseEntity<HXUser> responseEntity = restTemplate.exchange(URL_PREFIX + "users/{username}", HttpMethod.DELETE, httpEntity, HXUser.class, username);
System.out.println(responseEntity.getStatusCodeValue());
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 添加好友
*
* @param ownerUsername 用户名
* @param friendName 好友用户名
* @return 是否成功
*/
public static boolean addFriend(String ownerUsername, String friendName) {
try {
HttpEntity httpEntity = new HttpEntity(null, getHttpHeaders(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON));
ResponseEntity responseEntity = restTemplate.postForEntity(URL_PREFIX + "users/{owner_username}/contacts/users/{friend_username}", httpEntity, HXUser.class, ownerUsername, friendName);
System.out.println(responseEntity.getStatusCodeValue());
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 删除好友
*
* @param ownerUsername 用户名
* @param friendName 好友用户名
* @return 是否成功
*/
public static boolean deleteFriend(String ownerUsername, String friendName) {
try {
HttpEntity httpEntity = new HttpEntity(null, getHttpHeaders(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON));
ResponseEntity responseEntity = restTemplate.exchange(URL_PREFIX + "users/{owner_username}/contacts/users/{friend_username}", HttpMethod.DELETE, httpEntity, HXUser.class, ownerUsername, friendName);
System.out.println(responseEntity.getStatusCodeValue());
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 发送消息
*
* @param sendUser 发送用户
* @param targetUser 接收用户
* @param msg 发送消息
* @return 是否成功
*/
public static boolean sendToUser(String sendUser, String targetUser, String msg) {
try {
JSONObject body = new JSONObject();
body.put("target_type", "users");
JSONArray targetUserjson = new JSONArray();
targetUserjson.add(targetUser);
body.put("target", targetUserjson);
JSONObject msgJson = new JSONObject();
msgJson.put("type", HXMessageType.txt.name());
msgJson.put("msg", msg);
body.put("msg", msgJson);
body.put("from", sendUser);
HttpEntity httpEntity = new HttpEntity(body, getHttpHeaders(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON));
ResponseEntity responseEntity = restTemplate.postForEntity(URL_PREFIX + "messages", httpEntity, null);
System.out.println(responseEntity.getStatusCodeValue());
return responseEntity.getStatusCodeValue() == 200;
} catch (RestClientException e) {
e.printStackTrace();
return false;
}
}
/**
* 获取HttpHeaders
*
* @param contentType 客户端发送类型
* @param accept 响应类型
* @return HttpHeaders
*/
private static HttpHeaders getHttpHeaders(MediaType contentType, MediaType... accept) {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + getToken().getAccess_token());
headers.setContentType(contentType != null ? contentType : MediaType.APPLICATION_JSON);
headers.setAccept(Arrays.asList((accept != null && accept.length > 0) ? accept : new MediaType[]{MediaType.APPLICATION_JSON}));
return headers;
}
}
这里用到了两个实体类:HXUser、Token。
HXUser:
public class HXUser {
private String uuid; // 用户的UUID,标识字段
private String type; // 类型,“user”用户类型
private Long created;
private Long modified;
private String username; // 用户名,也就是环信 ID,(唯一,非空)
private String nickName; // 昵称
private boolean activated; // 用户是否已激活,“true”已激活,“false“封禁,封禁需要通过解禁接口进行解禁,才能正常登录
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
}
Token:
public class Token {
private String access_token; // 有效的token字符串
private String expires_in; // token 有效时间,以秒为单位,在有效期内不需要重复获取
private String application; // 当前 App 的 UUID 值
// getter and setter
}
本来还是想把上传文件和发送图片等写上的,后来由于需求,决定把聊天这部分交由前端直接访问环信了,所以写完发送消息就没再写了。
其实聊天这部分本人也是推荐由前端来写的。即时通信讲究的就是一个即时性,如果由后端来的话,就不能保证这个即时性了,因为如果前端走后端,后端再走环信,需要两步,而前端直接走环信,则只需要一步,就算网络等外界因素的存在,也不会太慢。而且前端也可以对消息进行一个缓存,从而降低网络访问,而且现在的IM应用也基本上都是这么来的。
以上仅供参考,如有不妥的地方请留下您的见解,谢谢。