1、即时通信
如果想简易打造一套聊天方案参照我的,websocket聊天室的制作:
1.1 什么是即时通信?
1.2 功能说明
聊天功能,用户可以和好友或陌生人聊天。
如果是陌生人,通过聊一下功能进行打招呼,如果对方同意后,就成为了好友,可以进行聊天了。
2 技术方案
对于高并发的即时通讯实现,还是很有挑战的,所需要考虑的点非常多,除了要实现功能,还要考虑并发、流量、负载、服务器、容灾等等。虽然有难度也并不是高不可攀。
对于现实即时通讯往往有两种方案:
- 方案一:
- 自主实现,从设计到架构,再到实现。
- 技术方面可以采用:Netty + WebSocket + RocketMQ + MongoDB + Redis + ZooKeeper + MySQL
- 方案二:
- 对接第三方服务完成。
- 这种方式简单,只需要按照第三方的api进行对接就可以了。
- 如:环信、网易、容联云通讯等。
如何选择呢?
如果是中大型企业做项目可以选择自主研发,如果是中小型企业研发中小型的项目,选择第二种方案即可。方案一需要有大量的人力、物力的支持,开发周期长,成本高,但可控性强。方案二,成本低,开发周期短,能够快速的集成起来进行功能的开发,只是在可控性方面来说就差了一些。
交友项目选择方案二进行实现。
3 环信
官网:https://www.easemob.com/ 稳定健壮,消息必达,亿级并发的即时通讯云
3.1 开发简介
集成:
环信和用户体系的集成主要发生在2个地方,服务器端集成和客户端集成。
3.2 环信Console
需要使用环信平台,那么必须要进行注册,登录之后即可创建应用。环信100以内的用户免费使用,100以上就要注册企业版了。
4 用户体系集成
4.1 Appkey 数据结构
当您申请了 AppKey 后,会得到一个 xxxx#xxxx 格式的字符串,字符串只能由小写字母数字组成,AppKey是环信应用的唯一标识。前半部分 org_name 是在多租户体系下的唯一租户标识,后半部分 app_name 是租户下的app唯一标识(在环信后台创建一个app时填写的应用 id 即是 app_name )。下述的 REST API 中,/{org_name}/{app_name}的请求,均是针对一个唯一的appkey进行的。目前环信注册的appkey暂不能由用户自己完成删除操作,如果对 APP 删除需要联系环信操作完成。
Appkey | xxxx | 分隔符 | xxxx |
环信应用的唯一标识 | org_name | # | app_name |
4.2 环信 ID 数据结构
环信作为一个聊天通道,只需要提供环信 ID (也就是 IM 用户名)和密码就够了。
名称 | 字段名 | 数据类型 | 描述 |
环信 ID | username | String | 在 AppKey 的范围内唯一用户名。 |
用户密码 | password | String | 用户登录环信使用的密码。 |
4.3 环信 ID 使用规则
当 APP 和环信集成的时候,需要把 APP 系统内的已有用户和新注册的用户和环信集成,为每个已有用户创建一个环信的账号(环信 ID),并且 APP 有新用户注册的时候,需要同步的在环信中注册。
在注册环信账户的时候,需要注意环信 ID 的规则:
使用英文字母和(或)数字的组合
不能使用中文
不能使用 email 地址
不能使用 UUID
用户ID的长度在255字节以内
中间不能有空格或者井号(#)等特殊字符
允许的用户名正则 “[a-zA-Z0-9_-.](a~z大小写字母/数字/下划线/横线/英文句号),其他都不允许如果是大写字母会自动转成小写`
不区分大小写。系统忽略大小写,认为 AA、Aa、aa、aA 都是一样的。如果系统已经存在了环信 ID 为 AA 的用户,再试图使用 aa 作为环信 ID 注册新用户,系统返回用户名重复,以此类推。但是请注意:环信 ID 在数据上的表现形式还是用户最初注册的形式,注册时候使用的大写就保存大写,是小写就保存小写。即:使用 AA 注册,环信保存的 ID 就是 AA;使用 Aa 注册,环信保存的 ID 就是 Aa,以此类推。
另:本文档中可能会交错使用“环信 ID”和“环信用户名”两个术语,但是请注意,这里两个的意思是一样的。
因为一个用户的环信 ID 和他的在 APP 中的用户名并不需要一致,只需要有一个明确的对应关系。例如,用户名是 example@easemob.com,当这个用户登录到 APP 的时候,可以登录成功之后,再登录环信的服务器,所以这时候,只需要能够从 example@easemob.com 推导出这个用户的环信 ID 即可。
4.4 获取管理员权限
环信提供的 REST API 需要权限才能访问,权限通过发送 HTTP 请求时携带 token 来体现,下面描述获取 token 的方式。说明:API 描述的时候使用到的 {APP 的 client_id} 之类的这种参数需要替换成具体的值。
重要提醒:获取 token 时服务器会返回 token 有效期,具体值参考接口返回的 expires_in 字段值。由于网络延迟等原因,系统不保证 token 在此值表示的有效期内绝对有效,如果发现 token 使用异常请重新获取新的 token,比如“http response code”返回 401。另外,请不要频繁向服务器发送获取 token 的请求,同一账号发送此请求超过一定频率会被服务器封号,切记,切记!!
client_id 和 client_secret 可以在环信管理后台的 [APP 详情页面看到。
Request Headers
参数 | 说明 |
Content-Type | application/json |
Request Body
Response Body
参数 | 说明 |
access_token | 有效的token字符串 |
expires_in | token 有效时间,以秒为单位,在有效期内不需要重复获取 |
application | 当前 App 的 UUID 值 |
4.4.1 配置
将用户体系集成的逻辑写入到sso系统中。
huanxin.properties
oldlu.huanxin.url=http://a1.easemob.com/ oldlu.huanxin.orgName=1105190515097562 oldlu.huanxin.appName=oldlu oldlu.huanxin.clientId=YXA67ZofwHblEems-_Fh-17T2g oldlu.huanxin.clientSecret=YXA60r45rNy2Ux5wQ7YYoEPwynHmUZk
说明:这配置在控制台可以找到。
package com.oldlu.sso.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; @Configuration @PropertySource("classpath:huanxin.properties") @ConfigurationProperties(prefix = "oldlu.huanxin") @Data public class HuanXinConfig { private String url; private String orgName; private String appName; private String clientId; private String clientSecret; }
4.4.2 获取token
package com.oldlu.sso.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.oldlu.sso.config.HuanXinConfig; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.Duration; import java.util.HashMap; import java.util.Map; @Service public class HuanXinTokenService { private static final ObjectMapper MAPPER = new ObjectMapper(); @Autowired private HuanXinConfig huanXinConfig; @Autowired private RestTemplate restTemplate; public static final String REDIS_KEY = "HX_TOKEN"; @Autowired private RedisTemplate<String, String> redisTemplate; private String refreshToken() { String targetUrl = this.huanXinConfig.getUrl() + this.huanXinConfig.getOrgName() + "/" + this.huanXinConfig.getAppName() + "/token"; Map<String, String> param = new HashMap<>(); param.put("grant_type", "client_credentials"); param.put("client_id", this.huanXinConfig.getClientId()); param.put("client_secret", this.huanXinConfig.getClientSecret()); //请求环信接口 ResponseEntity<String> responseEntity = this.restTemplate.postForEntity(targetUrl, param, String.class); if (responseEntity.getStatusCodeValue() != 200) { return null; } String body = responseEntity.getBody(); try { JsonNode jsonNode = MAPPER.readTree(body); String accessToken = jsonNode.get("access_token").asText(); if (StringUtils.isNotBlank(accessToken)) { // 将token保存到redis,有效期为5天,环信接口返回的有效期为6天 this.redisTemplate.opsForValue().set(REDIS_KEY, accessToken, Duration.ofDays(5)); return accessToken; } } catch (Exception e) { e.printStackTrace(); } return null; } public String getToken() { String token = this.redisTemplate.opsForValue().get(REDIS_KEY); if (StringUtils.isBlank(token)) { return this.refreshToken(); } return token; } }
4.5 注册环信用户
注册环信用户分为2种,开放注册、授权注册,区别在于开发注册不需要token,授权注册需要token。
我们使用的授权注册:
package com.oldlu.sso.vo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class HuanXinUser { private String username; private String password; }
package com.oldlu.sso.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.oldlu.sso.config.HuanXinConfig; import com.oldlu.sso.vo.HuanXinUser; import org.apache.commons.codec.digest.DigestUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.Arrays; @Service public class HuanXinService { private static final ObjectMapper MAPPER = new ObjectMapper(); @Autowired private HuanXinTokenService huanXinTokenService; @Autowired private RestTemplate restTemplate; @Autowired private HuanXinConfig huanXinConfig; /** * 注册环信用户 * * @param userId * @return */ public boolean register(Long userId) { String targetUrl = this.huanXinConfig.getUrl() + this.huanXinConfig.getOrgName() + "/" + this.huanXinConfig.getAppName() + "/users"; String token = this.huanXinTokenService.getToken(); try { // 请求体 HuanXinUser huanXinUser = new HuanXinUser(String.valueOf(userId), DigestUtils.md5Hex(userId + "_itlu_oldlu")); String body = MAPPER.writeValueAsString(huanXinUser); // 请求头 HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); headers.add("Authorization", "Bearer " + token); HttpEntity<String> httpEntity = new HttpEntity<>(body, headers); ResponseEntity<String> responseEntity = this.restTemplate.postForEntity(targetUrl, httpEntity, String.class); return responseEntity.getStatusCodeValue() == 200; } catch (Exception e) { e.printStackTrace(); } // 注册失败 return false; } }
加入到登录逻辑中:
4.6 测试
可以看到已经注册到了环信。
4.7 查询环信用户信息
在app中,用户登录后需要根据用户名密码登录环信,由于用户名密码保存在后台,所以需要提供接口进行返回。
实现:
package com.oldlu.server.controller; import com.oldlu.server.pojo.User; import com.oldlu.server.utils.UserThreadLocal; import com.oldlu.server.vo.HuanXinUser; import org.apache.commons.codec.digest.DigestUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("huanxin") public class HuanXinController { @GetMapping("user") public ResponseEntity<HuanXinUser> queryHuanXinUser(){ User user = UserThreadLocal.get(); HuanXinUser huanXinUser = new HuanXinUser(); huanXinUser.setUsername(user.getId().toString()); huanXinUser.setPassword(DigestUtils.md5Hex(user.getId() + "_itlu_oldlu")); return ResponseEntity.ok(huanXinUser); } }
4.8 发送消息给客户端
目前已经完成了用户体系的对接,下面我们进行测试发送消息,场景是这样的:
点击“聊一下”,就会给对方发送一条陌生人信息,这个消息由系统发送完成。
消息内容:
{"userId": "1","nickname":"老陆","strangerQuestion": "测试一下问题","reply": "aaaaa"}