Java服务端集成环信IM

由于业务需求,需要在系统中集成环信,所以去看了下官方文档,写篇博客分享一下。

在开始之前,我们需要了解一下什么是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_nameHeader_valueDescription
Acceptapplication/json服务器端返回给客户端的数据类型
Content-Typeapplication/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
之后,将此 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应用也基本上都是这么来的。

以上仅供参考,如有不妥的地方请留下您的见解,谢谢。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值