IM即时通讯系统[SpringBoot+Netty]——梳理(三)

项目源代码

目录
IM即时通讯系统[SpringBoot+Netty]——梳理(一)
IM即时通讯系统[SpringBoot+Netty]——梳理(二)
IM即时通讯系统[SpringBoot+Netty]——梳理(四)
IM即时通讯系统[SpringBoot+Netty]——梳理(五)

七、打通业务服务器与IM服务器多端同步


这里考虑到SDK如何获取到tcp服务的地址

  1. 可以在SDK上写死一个ip,让这个SDK一直去连接这个地址。如果这个地址挂了,这个服务就会挂掉,这个sdk就得进行修改
  2. 也可以在SDK上写多个ip,如果这四个ip都挂了,才算挂了
  3. 暴露一个http请求,每次用户登录的时候,都会往这个请求,拿一次tcp的连接地址,如果是web端获取到web的地址,如果是tcp就拿到tcp的地址

这里我们要使用上面的第三种方式来实现,所以要在逻辑层导入zk方面的东西

@Component
public class ZKit {
    
    

    private static Logger logger = LoggerFactory.getLogger(ZKit.class);

    @Autowired
    private ZkClient zkClient;
    /**
     * get all TCP server node from zookeeper
     *
     * @return
     */
    public List<String> getAllTcpNode() {
    
    
        List<String> children = zkClient.getChildren(Constants.ImCoreZkRoot + Constants.ImCoreZkRootTcp);
//        logger.info("Query all node =[{}] success.", JSON.toJSONString(children));
        return children;
    }

    /**
     * get all WEB server node from zookeeper
     *
     * @return
     */
    public List<String> getAllWebNode() {
    
    
        List<String> children = zkClient.getChildren(Constants.ImCoreZkRoot + Constants.ImCoreZkRootWeb);
//        logger.info("Query all node =[{}] success.", JSON.toJSONString(children));
        return children;
    }
}

在这里插入图片描述

通过配置文件动态注入zk的地址和超时间,然后就可以使用上面那个获取到tcp和web的服务地址了

1、负载均衡策略—随机模式

public interface RouteHandle {
    
    
    public String routeServer(List<String> values, String key);
}
public class RandomHandle implements RouteHandle {
    
    
    @Override
    public String routeServer(List<String> values, String key) {
    
    

        int size = values.size();

        if(size == 0){
    
    
            throw new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE);
        }
        // 随机获取一个im地址值
        int i = ThreadLocalRandom.current().nextInt(size);

        return values.get(i);
    }
}

在这里插入图片描述

将上面的随机算法注入到里面
在这里插入图片描述
当我们访问这个接口的时候,就可以随机的获取到地址和端口号

在这里插入图片描述

2、负载均衡策略—轮询模式


public class LoopHandle implements RouteHandle {
    
    

    private AtomicLong index = new AtomicLong();

    @Override
    public String routeServer(List<String> values, String key) {
    
    
        int size = values.size();
        if(size == 0){
    
    
            throw new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE);
        }
        Long l = index.incrementAndGet() % size;
        if(l < 0){
    
    
            l = 0L;
        }
        return values.get(l.intValue());
    }
}

这里使用的是一个原子类,每次放温暖的时候都去加1,然后再取模,达到轮询的目的去取地址

在这里插入图片描述

将轮询策略注入,测试的时候,每点击一次,就能看到轮询的结果
在这里插入图片描述

3、负载均衡策略—一致性Hash


实现一致性hash还需要实现不同的HashMap,这里使用了抽象类,来保证扩展性

public class ConsistentHashHandle implements RouteHandle {
    
    

    private AbstractConsistentHash abstractConsistentHash;

    public void setAbstractConsistentHash(AbstractConsistentHash abstractConsistentHash) {
    
    
        this.abstractConsistentHash = abstractConsistentHash;
    }

    // TreeMap实现一致性hash
    @Override
    public String routeServer(List<String> values, String key) {
    
    

        return abstractConsistentHash.process(values, key);
    }
}
public abstract class AbstractConsistentHash {
    
    

    // add
    protected abstract void add(long key, String value);
    // sort
    protected void sort(){
    
    };
    // 获取节点 get
    protected abstract String getFirstNodeValue(String value);

    // 处理之前事件
    protected abstract void processBefore();

    // 传入节点列表以及客户端信息获取一个服务节点
    public synchronized String process(List<String> values, String key){
    
    
        processBefore();
        for (String value : values) {
    
    
            add(hash(value), value);
        }
        sort();
        return getFirstNodeValue(key) ;
    }

    // hash运算
    public Long hash(String value){
    
    
        MessageDigest md5;
        try {
    
    
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
    
    
            throw new RuntimeException("MD5 not supported", e);
        }
        md5.reset();
        byte[] keyBytes = null;
        try {
    
    
            keyBytes = value.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
    
    
            throw new RuntimeException("Unknown string :" + value, e);
        }

        md5.update(keyBytes);
        byte[] digest = md5.digest();

        // hash code, Truncate to 32-bits
        long hashCode = ((long) (digest[3] & 0xFF) << 24)
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);

        long truncateHashCode = hashCode & 0xffffffffL;
        return truncateHashCode;
    }
}
public class TreeMapConsistentHash extends AbstractConsistentHash{
    
    
    // map
    private TreeMap<Long, String> treeMap = new TreeMap<>();

    //
    private static final int NODE_SIZE = 2;

    @Override
    protected void add(long key, String value) {
    
    
        for (int i = 0; i < NODE_SIZE; i++) {
    
    
            treeMap.put(super.hash("node" + key + i), value);
        }
        treeMap.put(key, value);
    }

    @Override
    protected String getFirstNodeValue(String value) {
    
    
        Long hash = super.hash(value);
        SortedMap<Long, String> last = treeMap.tailMap(hash);
        if(!last.isEmpty()){
    
    
            return last.get(last.firstKey());
        }

        if(treeMap.size() == 0){
    
    
            throw  new ApplicationException(UserErrorCode.SERVER_NOT_AVAILABLE);
        }

        return treeMap.firstEntry().getValue();
    }

    @Override
    protected void processBefore() {
    
    
        treeMap.clear();
    }
}

       这里实现的是TreeMap,也可以使用其他的Map去实现,这里一些方法要被重写,一些不用,这个TreeMap要重写add、get方法

       这里还要考虑一个问题,比如我们计算出来的hash值是一个10和一个1000000,两个数相差过大,当我们根据userId去获取服务地址的时候,通过调用tailMap可能总是会选择到10或100000,造成一种不均衡的状态,所以这里引入了一种添加虚拟节点的方式,每一个普通节点附加两个虚拟节点,虚拟节点的key是普通节点的key加一些东西,然后value和普通节点的一样,也就是最终比如之前就有10、1000000,但是添加了之后有了10、100、1000、10000、100060、100000,就可以在一定程度上解决老是获取到那固定的服务地址,避免造成涝的涝死,旱死的旱死的局面

在这里插入图片描述

测试的时候就可以通过不同的userId去尝试获取不同的服务地址了

4、配置负载均衡策略


之前我们使用的策略都是手动在BeabConfig中修改的,这样的肯定不使用,所以我们要把这个放到配置文件中去,下面的就是去做一些优化

@Configuration
public class BeanConfig {
    
    

    @Autowired
    private AppConfig appConfig;

    @Bean
    public RouteHandle routeHandle() throws Exception {
    
    
        // 获取配置文件中使用的哪个路由策略
        Integer imRouteWay = appConfig.getImRouteWay();
        // 使用的路由策略的具体的路径
        String routWay = "";
        // 通过配置文件中的路由策略的代表值去Enum获取到具体路径的类
        ImUrlRouteWayEnum handler = ImUrlRouteWayEnum.getHandler(imRouteWay);
        // 赋值给具体路径
        routWay = handler.getClazz();
        // 通过反射拿到路由策略的类
        RouteHandle routeHandle = (RouteHandle) Class.forName(routWay).newInstance();

        // 如果是hash策略的话,还要搞一个具体的hash算法
        if (handler == ImUrlRouteWayEnum.HASH){
    
    
            // 通过反射拿到ConsistentHashHandle中的方法
            Method method = Class.forName(routWay).getMethod("setAbstractConsistentHash", AbstractConsistentHash.class);
            // 从配置文件中拿到指定hash算法的代表值
            Integer consistentHashWay = appConfig.getConsistentHashWay();
            // 具体hash算法的类的路径
            String hashWay = "";
            // 通过Enue拿到对象
            RouteHashMethodEnum handler1 = RouteHashMethodEnum.getHandler(consistentHashWay);
            // 赋值
            hashWay = handler1.getClazz();
            // 通过反射拿到hash算法
            AbstractConsistentHash abstractConsistentHash = (AbstractConsistentHash) Class.forName(hashWay).newInstance();
            method.invoke(routeHandle, abstractConsistentHash);
        }

        return routeHandle;
    }
}

在这里插入图片描述

配置

5、使用Apache—HttpClient封装http请求工具


@Configuration
@ConfigurationProperties(prefix = "httpclient")
public class GlobalHttpClientConfig {
    
    

    private Integer maxTotal; // 最大连接数
    private Integer defaultMaxPerRoute; // 最大并发链接数
    private Integer connectTimeout; // 创建链接的最大时间
    private Integer connectionRequestTimeout; // 链接获取超时时间
    private Integer socketTimeout; // 数据传输最长时间
    private boolean staleConnectionCheckEnabled; // 提交时检查链接是否可用

    PoolingHttpClientConnectionManager manager = null;
    HttpClientBuilder httpClientBuilder = null;

    // 定义httpClient链接池
    @Bean(name = "httpClientConnectionManager")
    public PoolingHttpClientConnectionManager getPoolingHttpClientConnectionManager() {
    
    
        return getManager();
    }

    private PoolingHttpClientConnectionManager getManager() {
    
    
        if (manager != null) {
    
    
            return manager;
        }
        manager = new PoolingHttpClientConnectionManager();
        manager.setMaxTotal(maxTotal); // 设定最大链接数
        manager.setDefaultMaxPerRoute(defaultMaxPerRoute); // 设定并发链接数
        return manager;
    }

    /**
     * 实例化连接池,设置连接池管理器。 这里需要以参数形式注入上面实例化的连接池管理器
     *
     * @Qualifier 指定bean标签进行注入
     */
    @Bean(name = "httpClientBuilder")
    public HttpClientBuilder getHttpClientBuilder(
            @Qualifier("httpClientConnectionManager") PoolingHttpClientConnectionManager httpClientConnectionManager) {
    
    

        // HttpClientBuilder中的构造方法被protected修饰,所以这里不能直接使用new来实例化一个HttpClientBuilder,可以使用HttpClientBuilder提供的静态方法create()来获取HttpClientBuilder对象
        httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setConnectionManager(httpClientConnectionManager);
        return httpClientBuilder;
    }


    /**
     * 注入连接池,用于获取httpClient
     *
     * @param httpClientBuilder
     * @return
     */
    @Bean
    public CloseableHttpClient getCloseableHttpClient(
            @Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) {
    
    

        return httpClientBuilder.build();
    }

    public CloseableHttpClient getCloseableHttpClient() {
    
    
        if (httpClientBuilder != null) {
    
    
            return httpClientBuilder.build();
        }
        httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setConnectionManager(getManager());
        return httpClientBuilder.build();
    }

    /**
     * Builder是RequestConfig的一个内部类 通过RequestConfig的custom方法来获取到一个Builder对象
     * 设置builder的连接信息
     *
     * @return
     */
    @Bean(name = "builder")
    public RequestConfig.Builder getBuilder() {
    
    
        RequestConfig.Builder builder = RequestConfig.custom();
        return builder.setConnectTimeout(connectTimeout).setConnectionRequestTimeout(connectionRequestTimeout)
                .setSocketTimeout(socketTimeout).setStaleConnectionCheckEnabled(staleConnectionCheckEnabled);
    }

    /**
     * 使用builder构建一个RequestConfig对象
     *
     * @param builder
     * @return
     */
    @Bean
    public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder) {
    
    
        return builder.build();
    }

    public Integer getMaxTotal() {
    
    
        return maxTotal;
    }

    public void setMaxTotal(Integer maxTotal) {
    
    
        this.maxTotal = maxTotal;
    }

    public Integer getDefaultMaxPerRoute() {
    
    
        return defaultMaxPerRoute;
    }

    public void setDefaultMaxPerRoute(Integer defaultMaxPerRoute) {
    
    
        this.defaultMaxPerRoute = defaultMaxPerRoute;
    }

    public Integer getConnectTimeout() {
    
    
        return connectTimeout;
    }

    public void setConnectTimeout(Integer connectTimeout) {
    
    
        this.connectTimeout = connectTimeout;
    }

    public Integer getConnectionRequestTimeout() {
    
    
        return connectionRequestTimeout;
    }

    public void setConnectionRequestTimeout(Integer connectionRequestTimeout) {
    
    
        this.connectionRequestTimeout = connectionRequestTimeout;
    }

    public Integer getSocketTimeout() {
    
    
        return socketTimeout;
    }

    public void setSocketTimeout(Integer socketTimeout) {
    
    
        this.socketTimeout = socketTimeout;
    }

    public boolean isStaleConnectionCheckEnabled() {
    
    
        return staleConnectionCheckEnabled;
    }

    public void setStaleConnectionCheckEnabled(boolean staleConnectionCheckEnabled) {
    
    
        this.staleConnectionCheckEnabled = staleConnectionCheckEnabled;
    }
}

在这里插入图片描述

这样就可以根据这个类上面的注解获取到httpclient中的配置文件的属性了

@Component
public class HttpRequestUtils {
    
    

    @Autowired
    private CloseableHttpClient httpClient;

    @Autowired
    private RequestConfig requestConfig;

    @Autowired
    GlobalHttpClientConfig httpClientConfig;

    public String doGet(String url, Map<String, Object> params, String charset) throws Exception {
    
    
        return doGet(url,params,null,charset);
    }

    /**
     * 通过给的url地址,获取服务器数据
     *
     * @param url     服务器地址
     * @param params  封装用户参数
     * @param charset 设定字符编码
     * @return
     */
    public String doGet(String url, Map<String, Object> params, Map<String, Object> header, String charset) throws Exception {
    
    

        if (StringUtils.isEmpty(charset)) {
    
    
            charset = "utf-8";
        }
        URIBuilder uriBuilder = new URIBuilder(url);
        // 判断是否有参数
        if (params != null) {
    
    
            // 遍历map,拼接请求参数
            for (Map.Entry<String, Object> entry : params.entrySet()) {
    
    
                uriBuilder.setParameter(entry.getKey(), entry.getValue().toString());
            }
        }
        // 声明 http get 请求
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        httpGet.setConfig(requestConfig);

        if (header != null) {
    
    
            // 遍历map,拼接header参数
            for (Map.Entry<String, Object> entry : header.entrySet()) {
    
    
                httpGet.addHeader(entry.getKey(),entry.getValue().toString());
            }
        }

        String result = "";
        try {
    
    
            // 发起请求
            CloseableHttpResponse response = httpClient.execute(httpGet);
            // 判断状态码是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
    
    
                // 返回响应体的内容
                result = EntityUtils.toString(response.getEntity(), charset);
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * GET请求, 含URL 参数
     *
     * @param url
     * @param params
     * @return 如果状态码为200,则返回body,如果不为200,则返回null
     * @throws Exception
     */
    public String doGet(String url, Map<String, Object> params) throws Exception {
    
    
        return doGet(url, params, null);
    }

    /**
     * GET 请求,不含URL参数
     *
     * @param url
     * @return
     * @throws Exception
     */
    public String doGet(String url) throws Exception {
    
    
        return doGet(url, null, null);
    }

    public String doPost(String url, Map<String, Object> params, String jsonBody, String charset) throws Exception {
    
    
        return doPost(url,params,null,jsonBody,charset);
    }

    /**
     * 带参数的post请求
     *
     * @param url
     * @return
     * @throws Exception
     */
    public String doPost(String url, Map<String, Object> params, Map<String, Object> header, String jsonBody, String charset) throws Exception {
    
    

        if (StringUtils.isEmpty(charset)) {
    
    
            charset = "utf-8";
        }
        URIBuilder uriBuilder = new URIBuilder(url);
        // 判断是否有参数
        if (params != null) {
    
    
            // 遍历map,拼接请求参数
            for (Map.Entry<String, Object> entry : params.entrySet()) {
    
    
                uriBuilder.setParameter(entry.getKey(), entry.getValue().toString());
            }
        }

        // 声明httpPost请求
        HttpPost httpPost = new HttpPost(uriBuilder.build());
        // 加入配置信息
        httpPost.setConfig(requestConfig);

        // 判断map是否为空,不为空则进行遍历,封装from表单对象
        if (StringUtils.isNotEmpty(jsonBody)) {
    
    
            StringEntity s = new StringEntity(jsonBody, charset);
            s.setContentEncoding(charset);
            s.setContentType("application/json");

            // 把json body放到post里
            httpPost.setEntity(s);
        }

        if (header != null) {
    
    
            // 遍历map,拼接header参数
            for (Map.Entry<String, Object> entry : header.entrySet()) {
    
    
                httpPost.addHeader(entry.getKey(),entry.getValue().toString());
            }
        }

        String result = "";
//		CloseableHttpClient httpClient = HttpClients.createDefault(); // 单个
        CloseableHttpResponse response = null;
        try {
    
    
            // 发起请求
            response = httpClient.execute(httpPost);
            // 判断状态码是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
    
    
                // 返回响应体的内容
                result = EntityUtils.toString(response.getEntity(), charset);
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        return result;
    }

    /**
     * 不带参数post请求
     * @param url
     * @return
     * @throws Exception
     */
    public String doPost(String url) throws Exception {
    
    
        return doPost(url, null,null,null);
    }

    /**
     * get 方法调用的通用方式
     * @param url
     * @param tClass
     * @param map
     * @param charSet
     * @return
     * @throws Exception
     */
    public <T> T doGet(String url, Class<T> tClass, Map<String, Object> map, String charSet) throws Exception {
    
    

        String result = doGet(url, map, charSet);
        if (StringUtils.isNotEmpty(result))
            return JSON.parseObject(result, tClass);
        return null;

    }

    /**
     * get 方法调用的通用方式
     * @param url
     * @param tClass
     * @param map
     * @param charSet
     * @return
     * @throws Exception
     */
    public <T> T doGet(String url, Class<T> tClass, Map<String, Object> map, Map<String, Object> header, String charSet) throws Exception {
    
    

        String result = doGet(url, map, header, charSet);
        if (StringUtils.isNotEmpty(result))
            return JSON.parseObject(result, tClass);
        return null;

    }

    /**
     * post 方法调用的通用方式
     * @param url
     * @param tClass
     * @param map
     * @param jsonBody
     * @param charSet
     * @return
     * @throws Exception
     */
    public <T> T doPost(String url, Class<T> tClass, Map<String, Object> map, String jsonBody, String charSet) throws Exception {
    
    

        String result = doPost(url, map,jsonBody,charSet);
        if (StringUtils.isNotEmpty(result))
            return JSON.parseObject(result, tClass);
        return null;

    }

    public <T> T doPost(String url, Class<T> tClass, Map<String, Object> map, Map<String, Object> header, String jsonBody, String charSet) throws Exception {
    
    

        String result = doPost(url, map, header,jsonBody,charSet);
        if (StringUtils.isNotEmpty(result))
            return JSON.parseObject(result, tClass);
        return null;

    }

    /**
     * post 方法调用的通用方式
     * @param url
     * @param map
     * @param jsonBody
     * @param charSet
     * @return
     * @throws Exception
     */
    public String  doPostString(String url, Map<String, Object> map, String jsonBody, String charSet) throws Exception {
    
    
        return doPost(url, map,jsonBody,charSet);
    }

    /**
     * post 方法调用的通用方式
     * @param url
     * @param map
     * @param jsonBody
     * @param charSet
     * @return
     * @throws Exception
     */
    public String  doPostString(String url, Map<String, Object> map, Map<String, Object> header, String jsonBody, String charSet) throws Exception {
    
    
        return doPost(url, map, header, jsonBody,charSet);
    }
}

然后用封装的http请求工具封装一个回调的类

@Component
public class CallbackService {
    
    

    private Logger logger = LoggerFactory.getLogger(CallbackService.class);

    @Autowired
    private HttpRequestUtils httpRequestUtils;

    @Autowired
    private AppConfig appConfig;

    @Autowired
    private ShareThreadPool shareThreadPool;

    public void callback(Integer appId, String callbackCommand, String jsonBody){
    
    
        shareThreadPool.submit(()->{
    
    
            try {
    
    
                httpRequestUtils.doPost(appConfig.getCallbackUrl(), Object.class, builderUrlParams(appId, callbackCommand),
                        jsonBody, null);
            } catch (Exception e) {
    
    
                logger.error("callback 回调{} : {}出现异常 : {} ",callbackCommand , appId, e.getMessage());
            }
        });
    }

    public ResponseVO beforecallback(Integer appId, String callbackCommand, String jsonBody){
    
    
        try {
    
    
            ResponseVO responseVO
                    = httpRequestUtils.doPost("", ResponseVO.class, builderUrlParams(appId, callbackCommand)
                    , jsonBody, null);
            return responseVO;
        } catch (Exception e) {
    
    
            logger.error("callback 之前 回调{} : {}出现异常 : {} ",callbackCommand , appId, e.getMessage());
            return ResponseVO.successResponse();
        }
    }

    public Map builderUrlParams(Integer appId, String command){
    
    
        Map map = new HashMap();
        map.put("appId", appId);
        map.put("command", command);
        return map;
    }
}

       这个回调的地址也是可以配置在配置文件中的,当你需要得知服务端的状态的时候就可以设置这个回调地址,添加了回调逻辑的操作,就会给你发送一条消息,以便你知晓请求的怎么样了

6、用户资料变更、群组模块回调


在这里插入图片描述

这些回调函数的开关都放在配置文件中了,当我们这个修改用户信息成功了以后,调用这个回调函数,往设置好回调地址的地方用http发一条信息,修改群组模块也如此。

7、数据多端同步


在这里插入图片描述

技术选取

  1. 轮询拉取:两台设备每隔一段时间就去拉取,这样做性能有很大的浪费,尤其是移动端,非电费流量
  2. 业务回调:之前实现了这个,让业务服务器知道谁加谁为好友,这时候调用一个专门用来发消息的接口,告诉A的所有客户端,我加了一次好友,然后去拉取一次好友列表

缺点:一是与Im服务端增加了交互,并且数据的同步强依赖于业务服务器,如果回调的不同,两面的数据还是不同步的;二是如果是客户端通过sdk去拉取好友列表的话,那是一次全量拉取,改变一个好友,就重新拉取所有的列表,又点浪费了

  1. TCP通知:到收到好友请求后,并且处理成功后,就主动的去发送特定的指令给这个用户的其他端,并且将新添加的好友信息也附带过去,这时候收到这条特色树消息的用户,不需要请求服务器,只是根据这条消息进行更本地好友列表即可,这样既解决了空轮训,也解决了和业务系统强依赖的问题

8、封装查询用户Session工具类


因为要给多端进行同步,所以就要获取到userSession列表,才能给他们去做同步,所以要在service加入redis。

@Component
public class UserSessionUtils {
    
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 1、获取用户所有的session
    public List<UserSession> getUserSession(Integer appId, String userId){
    
    
        // 获取session的key
        String userSessionKey = appId + Constants.RedisConstants.UserSessionConstants + userId;
        // 获取到这个map
        Map<Object, Object> entries =
                stringRedisTemplate.opsForHash().entries(userSessionKey);
        List<UserSession> list = new ArrayList<>();
        Collection<Object> values = entries.values();
        for (Object value : values) {
    
    
            String str = (String)value;
            UserSession userSession
                    = JSONObject.parseObject(str, UserSession.class);

            // 只获取在线的
            if(userSession.getConnectState() == ImConnectStatusEnum.ONLINE_STATUS.getCode()){
    
    
                list.add(userSession);
            }
        }
        return list;
    }

    // 获取指定端的session

    public UserSession getUserSession(Integer appId, String userId, Integer clientType, String imei){
    
    
        // 获取session的key
        String userSessionKey = appId + Constants.RedisConstants.UserSessionConstants + userId;
        String hashKey = clientType + ":" + imei;
        Object o = stringRedisTemplate.opsForHash().get(userSessionKey, hashKey);
        UserSession userSession
                = JSONObject.parseObject(o.toString(), UserSession.class);

        return userSession;
    }
}

9、封装MessageProducer给用户发送消息


       上一个是封装的获取Session的,这个封装的是给提取出的userSession发送消息的工具类,逻辑层不能给客户端发消息,所以要通过rabbitmq将要发送的消息扔到tcp层,然后再发送给客户端,完成一次发送消息的逻辑

@Service
public class MessageProducer {
    
    

    private static Logger logger = LoggerFactory.getLogger(MessageProducer.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private UserSessionUtils userSessionUtils;

    private String queueName = Constants.RabbitConstants.MessageService2Im;

    // 将要发送的信息弄到rabbitmq中去
    public boolean sendMessage(UserSession userSession, Object message){
    
    
        try {
    
    
            logger.info("send message == " + message);
            rabbitTemplate.convertAndSend(queueName, userSession.getBrokerId() + "", message);
            return true;
        }catch (Exception e){
    
    
            logger.error("send error :" + e.getMessage());
            return false;
        }
    }

    // 发送数据报包,包装数据,调用sendMessage
    public boolean sendPack(String toId, Command command, Object msg, UserSession userSession){
    
    
        MessagePack messagePack = new MessagePack();

        messagePack.setCommand(command.getCommand());
        messagePack.setToId(toId);
        messagePack.setClientType(userSession.getClientType());
        messagePack.setAppId(userSession.getAppId());
        messagePack.setImei(userSession.getImei());

        JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(msg));
        messagePack.setData(jsonObject);
        String body = JSONObject.toJSONString(messagePack);

        return sendMessage(userSession, body);
    }

    // 发送给某个用户所有端
    public List<ClientInfo> sendToUser(String toId, Command command, Object msg, Integer appId){
    
    
        List<UserSession> userSession
                = userSessionUtils.getUserSession(appId, toId);
        List<ClientInfo> list = new ArrayList<>();
        for (UserSession session : userSession) {
    
    
            boolean b = sendPack(toId, command, msg, session);
            if(b){
    
    
                list.add(new ClientInfo(session.getAppId()
                        , session.getClientType(), session.getImei()));
            }
        }
        return list;
    }

    // 发送给除了某一端的其他端(这个相当于是对下面那个方法做了一个再封装)
    public void sendToUser(String toId, Integer clientType, String imei,
                           Command command, Object msg, Integer appId){
    
    
        // 如果imei好和clientType不为空的话,说明就是正常的用户,那就把这个信息发送给除了这个端的其他用户
        if(clientType != null && StringUtils.isNotBlank(imei)){
    
    
            ClientInfo clientInfo = new ClientInfo(appId, clientType, imei);
            sendToUserExceptClient(toId, command, msg, clientInfo);
        }else{
    
    
            sendToUser(toId, command, msg, appId);
        }
    }

    // 发送给除了某一端的其他端
    public void sendToUserExceptClient(String toId, Command command, Object msg, ClientInfo clientInfo){
    
    
        List<UserSession> userSession
                = userSessionUtils.getUserSession(clientInfo.getAppId(), toId);
        for (UserSession session : userSession) {
    
    
            if(!isMatch(session, clientInfo)){
    
    
                sendPack(toId, command, msg, session);
            }
        }
    }

    // 发送给某个用户的指定客户端
    public void sendToUser(String toId, Command command, Object data, ClientInfo clientInfo){
    
    
        UserSession userSession = userSessionUtils.getUserSession(clientInfo.getAppId(),
                toId, clientInfo.getClientType(), clientInfo.getImei());
        sendPack(toId, command, data, userSession);
    }

    private boolean isMatch(UserSession sessionDto, ClientInfo clientInfo) {
    
    
        return Objects.equals(sessionDto.getAppId(), clientInfo.getAppId())
                && Objects.equals(sessionDto.getImei(), clientInfo.getImei())
                && Objects.equals(sessionDto.getClientType(), clientInfo.getClientType());
    }
}

10、编写用户资料、好友模块变更通知


大致意思就是这样,要加的模块按照功能的需求加就完事了

在这里插入图片描述

11、封装GroupMessageProducer给用户发送消息


这个和普通的消息还不太一样,因为群组的特殊性,有的操作只用通知群主管理员和被操作人,有的需要都告诉

@Component
public class GroupMessageProducer {
    
    

    @Autowired
    private MessageProducer messageProducer;

    @Autowired
    private ImGroupMemberService imGroupMemberService;

    public void producer(String userId, Command command, Object data, ClientInfo clientInfo){
    
    
        JSONObject o = (JSONObject) JSONObject.toJSON(data);
        String groupId = o.getString("groupId");

        // 获取群内的所有群成员的id
        List<String> groupMemberId
                = imGroupMemberService.getGroupMemberId(groupId, clientInfo.getAppId());

        // 加人的时候的TCP通知,只用告诉管理员和本人即可
        if(command.equals(GroupEventCommand.ADDED_MEMBER)){
    
    
            // 发送给管理员和被加入人本身
            List<GroupMemberDto> groupManager
                    = imGroupMemberService.getGroupManager(groupId, clientInfo.getAppId());
            AddGroupMemberPack addGroupMemberPack = o.toJavaObject(AddGroupMemberPack.class);
            List<String> members = addGroupMemberPack.getMembers();
            // 发送给管理员
            for (GroupMemberDto groupMemberDto : groupManager) {
    
    
                if(clientInfo.getClientType() != ClientType.WEBAPI.getCode()
                        && groupMemberDto.getMemberId().equals(userId)){
    
    
                    messageProducer.sendToUserExceptClient(groupMemberDto.getMemberId(), command, data, clientInfo);
                }else{
    
    
                    messageProducer.sendToUser(groupMemberDto.getMemberId(), command, data, clientInfo.getAppId());
                }
            }

            // 发送给本人的其他端
            for (String member : members) {
    
    
                if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){
    
    
                    messageProducer.sendToUserExceptClient(member, command, data, clientInfo);
                }else{
    
    
                    messageProducer.sendToUser(member, command, data, clientInfo.getAppId());
                }
            }
        }
        // 踢人出群的时候的tcp通知
        else if(command.equals(GroupEventCommand.DELETED_MEMBER)){
    
    
            // 获取
            RemoveGroupMemberPack pack = o.toJavaObject(RemoveGroupMemberPack.class);
            // 删除哪个成员id
            String member = pack.getMember();
            // 走到这步骤的时候,这个已经被删除了,所以这里查出所有的成员id没有,哪个删除的人
            List<String> members
                    = imGroupMemberService.getGroupMemberId(groupId, clientInfo.getAppId());
            // 这里加一下
            members.add(member);
            // 然后全部通知一下
            for (String memberId : members) {
    
    
                if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){
    
    
                    messageProducer.sendToUserExceptClient(memberId,command,data,clientInfo);
                }else{
    
    
                    messageProducer.sendToUser(memberId,command,data,clientInfo.getAppId());
                }
            }
        }
        // 修改成员信息的时候的tcp通知,通知所有管理员
        else if(command.equals(GroupEventCommand.UPDATED_MEMBER)){
    
    
            UpdateGroupMemberPack pack = o.toJavaObject(UpdateGroupMemberPack.class);
            // 被修改人的id
            String memberId = pack.getGroupId();
            // 获取到所有的管理员
            List<GroupMemberDto> groupManager
                    = imGroupMemberService.getGroupManager(groupId, clientInfo.getAppId());
            // 将被修改人也要通知到,所以搞一个dto
            GroupMemberDto groupMemberDto = new GroupMemberDto();
            groupMemberDto.setMemberId(memberId);
            groupManager.add(groupMemberDto);
            // 全发一遍
            for (GroupMemberDto member : groupManager) {
    
    
                if(clientInfo.getClientType() != ClientType.WEBAPI.getCode() && member.equals(userId)){
    
    
                    messageProducer.sendToUserExceptClient(member.getMemberId(),command,data,clientInfo);
                }else{
    
    
                    messageProducer.sendToUser(member.getMemberId(),command,data,clientInfo.getAppId());
                }
            }
        }else{
    
    
            for (String memberId : groupMemberId) {
    
    
                // 如果clientType不为空,并且类型不是Web,那么一定就是app端发送的
                if(clientInfo.getClientType() != null
                        && clientInfo.getClientType() != ClientType.WEBAPI.getCode() && memberId.equals(userId)){
    
    
                    // 发送给除了本端的其他端
                    messageProducer.sendToUserExceptClient(memberId, command, data, clientInfo);
                }else{
    
    
                    // 全发
                    messageProducer.sendToUser(memberId, command, data, clientInfo.getAppId());
                }
            }
        }
    }
}

12、编写群组模块TCP通知


在这里插入图片描述

其他就省略了!

13、TCP服务处理逻辑层投递的MQ消息


从mq中获取到消息,然后处理消息,这里用到了工厂模式

public class ProcessFactory {
    
    

    private static BaseProcess defaultProcess;

    static {
    
    
        defaultProcess = new BaseProcess() {
    
    
            @Override
            public void processBefore() {
    
    

            }

            @Override
            public void processAfter() {
    
    

            }
        };
    }

    public static BaseProcess getMessageProcess(Integer command) {
    
    
        return defaultProcess;
    }
}

在这里插入图片描述

public abstract class BaseProcess {
    
    

    public abstract void processBefore();

    public void process(MessagePack messagePack){
    
    
        processBefore();
        // 通过从rabbitmq中拿到的数据报,找到我们要发送给哪个客户端的channel
        NioSocketChannel channel = SessionScoketHolder.get(messagePack.getAppId(), messagePack.getToId()
                , messagePack.getClientType(), messagePack.getImei());
        if(channel != null){
    
    
            // 如果不为空的话
            channel.writeAndFlush(messagePack);
        }
        processAfter();
    }

    public abstract void processAfter();
}

在这里插入图片描述
在这里插入图片描述

14、接口调用鉴权加密—加解密算法HMAC-SHA256


因为调用接口的人,可能是app的用户,也有可能是后台管理员,所以要选择一个可逆的加密

加密

public class Base64URL {
    
    
    public static byte[] base64EncodeUrl(byte[] input) {
    
    
        byte[] base64 = new BASE64Encoder().encode(input).getBytes();
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
    
    
                case '+':
                    base64[i] = '*';
                    break;
                case '/':
                    base64[i] = '-';
                    break;
                case '=':
                    base64[i] = '_';
                    break;
                default:
                    break;
            }
        return base64;
    }

    public static byte[] base64EncodeUrlNotReplace(byte[] input) {
    
    
        byte[] base64 = new BASE64Encoder().encode(input).getBytes(Charset.forName("UTF-8"));
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
    
    
                case '+':
                    base64[i] = '*';
                    break;
                case '/':
                    base64[i] = '-';
                    break;
                case '=':
                    base64[i] = '_';
                    break;
                default:
                    break;
            }
        return base64;
    }

    public static byte[] base64DecodeUrlNotReplace(byte[] input) throws IOException {
    
    
        for (int i = 0; i < input.length; ++i)
            switch (input[i]) {
    
    
                case '*':
                    input[i] = '+';
                    break;
                case '-':
                    input[i] = '/';
                    break;
                case '_':
                    input[i] = '=';
                    break;
                default:
                    break;
            }
        return new BASE64Decoder().decodeBuffer(new String(input,"UTF-8"));
    }

    public static byte[] base64DecodeUrl(byte[] input) throws IOException {
    
    
        byte[] base64 = input.clone();
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
    
    
                case '*':
                    base64[i] = '+';
                    break;
                case '-':
                    base64[i] = '/';
                    break;
                case '_':
                    base64[i] = '=';
                    break;
                default:
                    break;
            }
        return new BASE64Decoder().decodeBuffer(base64.toString());
    }
}

public class SigAPI {
    
    
    final private long appId;
    final private String key;

    public SigAPI(long appId, String key) {
    
    
        this.appId = appId;
        this.key = key;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        SigAPI asd = new SigAPI(10000, "123456");
        String sign = asd.genUserSig("lld", 1000000);
//        Thread.sleep(2000L);
        JSONObject jsonObject = decodeUserSig(sign);
        System.out.println("sign:" + sign);
        System.out.println("decoder:" + jsonObject.toString());
    }

    /**
     * @description: 解密方法
     * @param
     * @return com.alibaba.fastjson.JSONObject
     * @author lld
     */
    public static JSONObject decodeUserSig(String userSig) {
    
    
        JSONObject sigDoc = new JSONObject(true);
        try {
    
    
            byte[] decodeUrlByte = Base64URL.base64DecodeUrlNotReplace(userSig.getBytes());
            byte[] decompressByte = decompress(decodeUrlByte);
            String decodeText = new String(decompressByte, "UTF-8");

            if (StringUtils.isNotBlank(decodeText)) {
    
    
                sigDoc = JSONObject.parseObject(decodeText);

            }

        } catch (Exception ex) {
    
    
            ex.printStackTrace();
        }

        return sigDoc;
    }

    /**
     * 解压缩
     *
     * @param data 待压缩的数据
     * @return byte[] 解压缩后的数据
     */
    public static byte[] decompress(byte[] data) {
    
    
        byte[] output = new byte[0];

        Inflater decompresser = new Inflater();
        decompresser.reset();
        decompresser.setInput(data);

        ByteArrayOutputStream o = new ByteArrayOutputStream(data.length);
        try {
    
    
            byte[] buf = new byte[1024];
            while (!decompresser.finished()) {
    
    
                int i = decompresser.inflate(buf);
                o.write(buf, 0, i);
            }
            output = o.toByteArray();
        } catch (Exception e) {
    
    
            output = data;
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                o.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

        decompresser.end();
        return output;
    }


    /**
     * 【功能说明】用于签发 IM 服务中必须要使用的 UserSig 鉴权票据
     * <p>
     * 【参数说明】
     */
    public String genUserSig(String userid, long expire) {
    
    
        return genUserSig(userid, expire, null);
    }


    private String hmacsha256(String identifier, long currTime, long expire, String base64Userbuf) {
    
    
        String contentToBeSigned = "TLS.identifier:" + identifier + "\n"
                + "TLS.appId:" + appId + "\n"
                + "TLS.expireTime:" + currTime + "\n"
                + "TLS.expire:" + expire + "\n";
        if (null != base64Userbuf) {
    
    
            contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n";
        }
        try {
    
    
            byte[] byteKey = key.getBytes(StandardCharsets.UTF_8);
            Mac hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256");
            hmac.init(keySpec);
            byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes(StandardCharsets.UTF_8));
            return (Base64.getEncoder().encodeToString(byteSig)).replaceAll("\\s*", "");
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
    
    
            return "";
        }
    }

    private String genUserSig(String userid, long expire, byte[] userbuf) {
    
    

        long currTime = System.currentTimeMillis() / 1000;

        JSONObject sigDoc = new JSONObject();
        sigDoc.put("TLS.identifier", userid);
        sigDoc.put("TLS.appId", appId);
        sigDoc.put("TLS.expire", expire);
        sigDoc.put("TLS.expireTime", currTime);

        String base64UserBuf = null;
        if (null != userbuf) {
    
    
            base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", "");
            sigDoc.put("TLS.userbuf", base64UserBuf);
        }
        String sig = hmacsha256(userid, currTime, expire, base64UserBuf);
        if (sig.length() == 0) {
    
    
            return "";
        }
        sigDoc.put("TLS.sig", sig);
        Deflater compressor = new Deflater();
        compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8));
        compressor.finish();
        byte[] compressedBytes = new byte[2048];
        int compressedBytesLength = compressor.deflate(compressedBytes);
        compressor.end();
        return (new String(Base64URL.base64EncodeUrl(Arrays.copyOfRange(compressedBytes,
                0, compressedBytesLength)))).replaceAll("\\s*", "");
    }

    public String genUserSig(String userid, long expire, long time,byte [] userbuf) {
    
    

        JSONObject sigDoc = new JSONObject();
        sigDoc.put("TLS.identifier", userid);
        sigDoc.put("TLS.appId", appId);
        sigDoc.put("TLS.expire", expire);
        sigDoc.put("TLS.expireTime", time);

        String base64UserBuf = null;
        if (null != userbuf) {
    
    
            base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", "");
            sigDoc.put("TLS.userbuf", base64UserBuf);
        }
        String sig = hmacsha256(userid, time, expire, base64UserBuf);
        if (sig.length() == 0) {
    
    
            return "";
        }
        sigDoc.put("TLS.sig", sig);
        Deflater compressor = new Deflater();
        compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8));
        compressor.finish();
        byte[] compressedBytes = new byte[2048];
        int compressedBytesLength = compressor.deflate(compressedBytes);
        compressor.end();
        return (new String(Base64URL.base64EncodeUrl(Arrays.copyOfRange(compressedBytes,
                0, compressedBytesLength)))).replaceAll("\\s*", "");
    }
}

Handler

@Component
public class GateWayInterceptor implements HandlerInterceptor {
    
    

    @Autowired
    private IdentityCheck identityCheck;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    

        // 方便测试
        if(1 == 1){
    
    
            return true;
        }

        // 获取appid 操作人 usersign
        String appIdStr = request.getParameter("appId");
        if(StringUtils.isBlank(appIdStr)){
    
    
            resp(ResponseVO.errorResponse(GateWayErrorCode.APPID_NOT_EXIST), response);
            return false;
        }

        String identifier = request.getParameter("identifier");
        if(StringUtils.isBlank(identifier)){
    
    
            resp(ResponseVO.errorResponse(GateWayErrorCode.OPERATER_NOT_EXIST), response);
            return false;
        }

        String userSign = request.getParameter("userSign");
        if(StringUtils.isBlank(userSign)){
    
    
            resp(ResponseVO.errorResponse(GateWayErrorCode.USERSIGN_IS_ERROR), response);
            return false;
        }

        // 校验签名和操作人和appId是否匹配
        ApplicationExceptionEnum applicationExceptionEnum
                = identityCheck.checkUserSign(identifier, appIdStr, userSign);

        if(applicationExceptionEnum != BaseErrorCode.SUCCESS){
    
    
            resp(ResponseVO.errorResponse(applicationExceptionEnum), response);
            return false;
        }
        return true;
    }

    private void resp(ResponseVO responseVO, HttpServletResponse response){
    
    
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");

        try {
    
    
            String resp = JSONObject.toJSONString(responseVO);
            writer = response.getWriter();
            writer.write(resp);
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            if(writer != null){
    
    
                writer.close();
            }
        }
    }
}

IdentityCheck

@Component
public class IdentityCheck {
    
    

    private static Logger logger = LoggerFactory.getLogger(IdentityCheck.class);

    @Autowired
    private ImUserService imUserService;

    @Autowired
    private AppConfig appConfig;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public ApplicationExceptionEnum checkUserSign(String identifier, String appId, String userSign){
    
    

        String cacheUserSig
                = stringRedisTemplate.opsForValue().get(appId + ":"
                + Constants.RedisConstants.userSign + ":" + identifier + userSign);
        if(!StringUtils.isBlank(cacheUserSig) && Long.valueOf(cacheUserSig) > System.currentTimeMillis() / 1000){
    
    
            return BaseErrorCode.SUCCESS;
        }

        // 获取秘钥
        String privatekey = appConfig.getPrivatekey();

        // 根据appId + 秘钥 创建 signApi
        SigAPI sigAPI = new SigAPI(Long.valueOf(appId), privatekey);

        // 调用 signApi 对 userSign解密
        JSONObject jsonObject = sigAPI.decodeUserSig(userSign);

        // 取出解密后的appId, 和操作人  和过期时间 做匹配,不通过则提示错误
        Long expireTime = 0L;
        Long expireSec = 0L;
        Long time = 0L;
        String decoderAppId = "";
        String decoderIdentifier = "";

        try {
    
    
            // 取出解密后的数据
            decoderAppId = jsonObject.getString("TLS.appId");
            decoderIdentifier = jsonObject.getString("TLS.identifier");
            String expireStr = jsonObject.get("TLS.expire").toString();
            String expireTimeStr = jsonObject.get("TLS.expireTime").toString();
            time =  Long.valueOf(expireTimeStr);
            expireSec = Long.valueOf(expireStr);
            expireTime = time + expireSec;
        }catch (Exception e){
    
    
            logger.error("checkUserSig-error: {}", e.getMessage());
            e.printStackTrace();
        }

        // 进行比对
        // 用户签名和操作人不匹配
            if(!decoderIdentifier.equals(identifier)){
    
    
            return GateWayErrorCode.USERSIGN_OPERATE_NOT_MATE;
        }

        // 用户签名不正确
        if(!decoderAppId.equals(appId)){
    
    
            return GateWayErrorCode.USERSIGN_IS_ERROR;
        }

        // 过期时间
        if(expireSec == 0){
    
    
            return GateWayErrorCode.USERSIGN_IS_EXPIRED;
        }

        if(expireTime < System.currentTimeMillis() / 1000){
    
    
            return GateWayErrorCode.USERSIGN_IS_EXPIRED;
        }

        // 把userSign存储到redis中去
        // appid + "xxx" + "userId" + sign
        String genSig = sigAPI.genUserSig(identifier, expireSec,time,null);
        if (genSig.toLowerCase().equals(userSign.toLowerCase())) {
    
    
            String key = appId + ":" + Constants.RedisConstants.userSign + ":" + identifier + userSign;
            Long etime = expireTime - System.currentTimeMillis() / 1000;
            stringRedisTemplate.opsForValue().set(
                    key, expireTime.toString(), etime, TimeUnit.SECONDS);
            this.setIsAdmin(identifier,Integer.valueOf(appId));
            return BaseErrorCode.SUCCESS;
        }
        return BaseErrorCode.SUCCESS;
    }

    private void setIsAdmin(String identifier, Integer appId) {
    
    
        //去DB或Redis中查找, 后面写
        ResponseVO<ImUserDataEntity> singleUserInfo = imUserService.getSingleUserInfo(identifier, appId);
        if(singleUserInfo.isOk()){
    
    
            RequestHolder.set(singleUserInfo.getData().getUserType() == ImUserTypeEnum.APP_ADMIN.getCode());
        }else{
    
    
            RequestHolder.set(false);
        }
    }
}

把Handler加到spring中

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    

    @Autowired
    private GateWayInterceptor gateWayInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(gateWayInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/v1/user/login")
                .excludePathPatterns("/v1/message/checkSend");
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
    
    
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

这样我们在访问接口的时候,就要带上appId、identifier、userSign,就可以了

八、消息业务的流程之打通消息收发核心

1、消息收发的核心流程


       ​在我们发消息的时候,一条消息前面就会出现一个转圈圈的东西,也就是此时将这个消息发送到了tcp层,然后tcp层再去投递到逻辑层,最终实现消息处理的还是逻辑层,当逻辑层处理完成之后,再投递到tcp层,最终tcp层将消息返回给sdk,app收到这条消息后,将那个圈圈去掉

       还有其他的情况,消息前有感叹号,也就是逻辑层判断了这条消息不能被发送,然后就要通过tcp层去告知sdk他不能被发送,sdk告诉app这条消息不能发送,就搞一个红色的感叹号

2、单聊消息分发逻辑—RabbitMQ连接tcp层和网关层


      先搞清楚,业务回调是因为连接客户端的是Tcp层,而service层不会直接连接到客户端,所以要通过一个http的业务回调机制,可以让客户端和service层进行感知,而数据多端同步是通过向tcp层的队列中投递消息,然后再由tcp层去分发到其他的客户端上,做数据同步,而这里只是单方面的service层连接到tcp层,service层没有接收到tcp层的rabbitmq的消息,所以这里要打通这个关系

下面的就是service层接受tcp层投递给service层的消息

@Component
public class ChatOperateReceiver {
    
    

    private static Logger logger = LoggerFactory.getLogger(ChatOperateReceiver.class);

    @Autowired
    private P2PMessageService p2PMessageService;

    @Autowired
    private MessageSyncService messageSyncService;

    // 这个注解就是消费者获取rabbitmq中的信息
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(value = Constants.RabbitConstants.Im2MessageService, durable = "true"),
                    exchange = @Exchange(value = Constants.RabbitConstants.Im2MessageService, durable = "true")
            ),concurrency = "1"
    )
    public void onChatMessage(@Payload Message message, @Headers Map<String, Object> headers,
                              Channel channel) throws IOException {
    
    
        String msg = new String(message.getBody(), "utf-8");
        logger.info("CHAT MSG FROM QUEUE ::: {}", msg);
        
        long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);

        try {
    
    
            JSONObject jsonObject = JSONObject.parseObject(msg);
            Integer command = jsonObject.getInteger("command");
            if(command.equals(MessageCommand.MSG_P2P.getCommand())){
    
    
                // 处理消息
                MessageContent messageContent
                        = jsonObject.toJavaObject(MessageContent.class);
                p2PMessageService.process(messageContent);
            }else if(command.equals(MessageCommand.MSG_RECIVE_ACK.getCommand())){
    
    
                // 消息接受确认
                MessageReciveAckContent messageContent
                        = jsonObject.toJavaObject(MessageReciveAckContent.class);
                messageSyncService.receiveMark(messageContent);
            }else if(command.equals(MessageCommand.MSG_READED.getCommand())){
    
    
                MessageReadedContent messageReadedContent
                        = jsonObject.toJavaObject(MessageReadedContent.class);
                messageSyncService.readMark(messageReadedContent);
            }else if(Objects.equals(command, MessageCommand.MSG_RECALL.getCommand())){
    
    
                RecallMessageContent messageContent = JSON.parseObject(msg, new TypeReference<RecallMessageContent>() {
    
    
                }.getType());
                messageSyncService.recallMessage(messageContent);
            }
            channel.basicAck(deliveryTag, false);
        }catch (Exception e){
    
    
            logger.error("处理消息出现异常:{}", e.getMessage());
            logger.error("RMQ_CHAT_TRAN_ERROR", e);
            logger.error("NACK_MSG:{}", msg);
            //第一个false 表示不批量拒绝,第二个false表示不重回队列
            channel.basicNack(deliveryTag, false, false);
        }
    }
}

通过解析出消息中的comman命令,然后做进一步的处理

在这里插入图片描述

然后在tcp层向service层投递,这样当客户端发送过来消息的时候,就会投递到service层中去

3、单聊消息分发逻辑—发送消息前置校验


前置校验要校验的是双方是否被禁用或者禁言,双方是否是好友关系

@Service
public class CheckSendMessageService {
    
    

    @Autowired
    private ImUserService imUserService;

    @Autowired
    private ImFriendShipService imFriendShipService;

    @Autowired
    private AppConfig appConfig;

    @Autowired
    private ImGroupService imGroupService;

    @Autowired
    private ImGroupMemberService imGroupMemberService;

    // 判断发送发是否被禁用或者禁言
    public ResponseVO checkSenderForvidAndMute(String fromId, Integer appId){
    
    
        // 获取单个用户
        ResponseVO<ImUserDataEntity> singleUserInfo
                = imUserService.getSingleUserInfo(fromId, appId);
        if(!singleUserInfo.isOk()){
    
    
            return singleUserInfo;
        }

        // 取出用户
        ImUserDataEntity user = singleUserInfo.getData();
        // 是否被禁用
        if(user.getForbiddenFlag() == UserForbiddenFlagEnum.FORBIBBEN.getCode()){
    
    
            return ResponseVO.errorResponse(MessageErrorCode.FROMER_IS_FORBIBBEN);
        }
        // 是否被禁言
        else if(user.getSilentFlag() == UserSilentFlagEnum.MUTE.getCode()){
    
    
            return ResponseVO.errorResponse(MessageErrorCode.FROMER_IS_MUTE);
        }
        return ResponseVO.successResponse();
    }

    // 判断好友关系
    public ResponseVO checkFriendShip(String fromId, String toId, Integer appId){
    
    
        if(appConfig.isSendMessageCheckFriend()){
    
    
            // 判断双方好友记录是否存在
            GetRelationReq fromReq = new GetRelationReq();
            fromReq.setFromId(fromId);
            fromReq.setToId(toId);
            fromReq.setAppId(appId);
            ResponseVO<ImFriendShipEntity> fromRelation = imFriendShipService.getRelation(fromReq);
            if(!fromRelation.isOk()){
    
    
                return fromRelation;
            }
            GetRelationReq toReq = new GetRelationReq();
            toReq.setFromId(toId);
            toReq.setToId(fromId);
            toReq.setAppId(appId);
            ResponseVO<ImFriendShipEntity> toRelation = imFriendShipService.getRelation(toReq);
            if(!toRelation.isOk()){
    
    
                return toRelation;
            }

            // 判断好友关系记录是否正常
            if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() != fromRelation.getData().getStatus()){
    
    
                return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_DELETED);
            }
            if(FriendShipStatusEnum.FRIEND_STATUS_NORMAL.getCode() != toRelation.getData().getStatus()){
    
    
                return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_DELETED);
            }

            // 判断是否在黑名单里面
            if(appConfig.isSendMessageCheckBlack()){
    
    
                if(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode()
                        != fromRelation.getData().getBlack()){
    
    
                    return ResponseVO.errorResponse(FriendShipErrorCode.FRIEND_IS_BLACK);
                }
                if(FriendShipStatusEnum.BLACK_STATUS_NORMAL.getCode()
                        != toRelation.getData().getBlack()){
    
    
                    return ResponseVO.errorResponse(FriendShipErrorCode.TARGET_IS_BLACK_YOU);
                }
            }
        }
        return ResponseVO.successResponse();
    }
}

然后就可以去调用前置校验在真正处理消息的之前了

4、单聊消息分发逻辑—消息分发的主流程


在这里插入图片描述

  1. 前置校验
  2. 回复ack
  3. 同步我方在线对端
  4. 分发给对方所有端

在这里插入图片描述

5、详细分析群聊业务


在这里插入图片描述

6、发送群聊消息前置校验


和上面单聊的差不多

// 前置校验群组消息
public ResponseVO checkGroupMessage(String fromId, String groupId, Integer appId){
    
    
    // 发送方是否被禁言
    ResponseVO responseVO = checkSenderForvidAndMute(fromId, appId);
    if(!responseVO.isOk()){
    
    
        return responseVO;
    }

    // 判断群逻辑
    ResponseVO<ImGroupEntity> group = imGroupService.getGroup(groupId, appId);
    if(!group.isOk()){
    
    
        return group;
    }

    // 判断群成员是否在群内
    ResponseVO<GetRoleInGroupResp> roleInGroupOne
            = imGroupMemberService.getRoleInGroupOne(groupId, fromId, appId);
    if(!roleInGroupOne.isOk()){
    
    
        return roleInGroupOne;
    }
    GetRoleInGroupResp data = roleInGroupOne.getData();


    // 判断群是否被禁言
    //如果禁言,只有群管理和群主可以发言
    ImGroupEntity groupdata = group.getData();
    // 如果群组已经禁言并且 发言人不是群管理或者群主
    if(groupdata.getMute() == GroupMuteTypeEnum.MUTE.getCode()
            && (data.getRole() != GroupMemberRoleEnum.MAMAGER.getCode() ||
            data.getRole() != GroupMemberRoleEnum.OWNER.getCode())){
    
    
        return ResponseVO.errorResponse(GroupErrorCode.THIS_GROUP_IS_MUTE);
    }
    // 如果是个人禁言,并且还在禁言时长中
    if(data.getSpeakDate() != null && data.getSpeakDate() > System.currentTimeMillis()){
    
    
        return ResponseVO.errorResponse(GroupErrorCode.GROUP_MEMBER_IS_SPEAK);
    }
    return ResponseVO.successResponse();
}

7、群聊消息的分发逻辑


  1. 前置校验
  2. 回复ack
  3. 同步我方在线对端
  4. 分发给对方所有端

在这里插入图片描述

8、聊天记录存储结构单聊群聊读扩散、写扩散选型


读扩散

在这里插入图片描述
      举一个微博大V的例子,如果大V发一条消息,那么关注了大V的用户,就会从大V的队列中倒序拉取就可以获取到大V的消息了

写扩散

在这里插入图片描述

      也举一个微博大V的例子,如果大V发一条消息,每个用户都有自己的一个队列,大V会将消息写到所有订阅他的用户的队列中

       从这上面看的话,要是查询聊天记录的话,如果面对好多好多的用户来说的,写扩散要写好多的东西,读扩散反而只需要去读取即可,看起来读扩散比写扩散好一些

基础数据

在这里插入图片描述

这样使用读扩散似乎是减轻了写操作的压力,但是增加了读操作的压力,这样子的查询语句根本不好去建立索引

在这里插入图片描述

使用写扩散的话,写起来会些麻烦,但是查询聊天记录很快速

      如果说聊天记录很多,我们上上升到分库分表的场景,读扩散没有给合适的分片键,没有像写扩散那样的ownerId那样的标识。


存储结构

在这里插入图片描述

在这里插入图片描述

拆分开来

选型:

  1. 单聊服务建议使用写扩散,也就是写操作X2,不会一下子给服务器太多的压力,而且比较好查询
  2. 群聊服务建议使用读扩散,群聊有群id作为分片键,在群id上建立索引,就可以查询出这个群的所有的消息

9、 IM消息ID专题—分布式自增id解决方案介绍


  1. UUID:id长、无序、字符串
  2. 时间戳:存在重复性问题,而且不能保证唯一性
  3. 雪花算法:用一个64位的长整型作为全局唯一id,基于时间戳实现,性能高,但是基于时间戳会依赖于机器时钟,如果时间回拨会导致号段重复
  4. 自定义算法:像私有协议一样

10、如何将单聊、群聊消息持久化到DB


将私聊消息转换成对应的实体,然后分别存储到body和history中去

// 3、转化成 MessageHistory
public List<ImMessageHistoryEntity> extractToP2PMessageHistory(MessageContent messageContent
        , ImMessageBodyEntity imMessageBodyEntity){
    
    
    List<ImMessageHistoryEntity> list = new ArrayList<>();

    ImMessageHistoryEntity fromHistory = new ImMessageHistoryEntity();
    BeanUtils.copyProperties(messageContent, fromHistory);
    fromHistory.setOwnerId(messageContent.getFromId());
    // 雪花算法生成
    fromHistory.setMessageKey(imMessageBodyEntity.getMessageKey());
    fromHistory.setCreateTime(System.currentTimeMillis());

    ImMessageHistoryEntity toHistory = new ImMessageHistoryEntity();
    BeanUtils.copyProperties(messageContent, toHistory);
    toHistory.setOwnerId(messageContent.getToId());
    toHistory.setMessageKey(imMessageBodyEntity.getMessageKey());
    toHistory.setCreateTime(System.currentTimeMillis());

    list.add(fromHistory);
    list.add(toHistory);
    return list;
}
public ImMessageBody extractMessageBody(MessageContent messageContent){
    
    
    ImMessageBody imMessageBodyEntity = new ImMessageBody();
    imMessageBodyEntity.setAppId(messageContent.getAppId());
    imMessageBodyEntity.setMessageKey(snowflakeIdWorker.nextId());
    imMessageBodyEntity.setCreateTime(System.currentTimeMillis());
    imMessageBodyEntity.setSecurityKey("");
    imMessageBodyEntity.setExtra(messageContent.getExtra());
    imMessageBodyEntity.setDelFlag(DelFlagEnum.NORMAL.getCode());
    imMessageBodyEntity.setMessageTime(messageContent.getMessageTime());
    imMessageBodyEntity.setMessageBody(messageContent.getMessageBody());
    return imMessageBodyEntity;
}
 // 转化成 MessageHistory
public ImGroupMessageHistoryEntity extractToGroupMessageHistory(GroupChatMessageContent groupChatMessageContent,
                                                                ImMessageBodyEntity imMessageBodyEntity){
    
    
    ImGroupMessageHistoryEntity result
            = new ImGroupMessageHistoryEntity();
    BeanUtils.copyProperties(groupChatMessageContent, result);
    result.setGroupId(groupChatMessageContent.getGroupId());
    // 雪花算法生成
    result.setMessageKey(imMessageBodyEntity.getMessageKey());
    result.setCreateTime(System.currentTimeMillis());

    return result;
}

在这里插入图片描述

在这里插入图片描述

11、实现发送单聊和群聊的接口


// 发送群聊消息
public SendMessageResp send(SendGroupMessageReq req) {
    
    
    SendMessageResp sendMessageResp = new SendMessageResp();
    GroupChatMessageContent message = new GroupChatMessageContent();
    BeanUtils.copyProperties(req, message);
    // 插入
    messageStoreService.storeGroupMessage(message);

    sendMessageResp.setMessageKey(message.getMessageKey());
    sendMessageResp.setMessageTime(System.currentTimeMillis());
    // 我方同步在线端
    syncToSender(message, message);
    // 对方同步
    dispatchMessage(message);
    return sendMessageResp;
}
// 发送单聊消息
public SendMessageResp send(SendMessageReq req) {
    
    
    SendMessageResp sendMessageResp = new SendMessageResp();
    MessageContent message = new MessageContent();
    BeanUtils.copyProperties(req, message);
    // 插入数据
    messageStoreService.storeP2PMessage(message);
    sendMessageResp.setMessageKey(message.getMessageKey());
    sendMessageResp.setMessageTime(System.currentTimeMillis());
    // 同步我方在线端
    syncToSender(message, message);
    // 同步对方在线端
    dispatchMessage(message);
    return sendMessageResp;
}

猜你喜欢

转载自blog.csdn.net/weixin_52487106/article/details/130654139