Compare commits

...

156 Commits

Author SHA1 Message Date
xia jun
e6d50f36cb android sdk 支持15 2025-06-09 12:26:19 +08:00
flash
3634392e7f android sdk适配android 15 2025-06-09 12:19:24 +08:00
xia jun
4a68b47fdb 1、服务端SDK支持IP黑名单拦截
2、优化代码注释和升级依赖组件版本
2025-03-29 13:19:39 +08:00
xia jun
965abd1467 1、服务端SDK支持IP黑名单拦截
2、优化代码注释和升级依赖组件版本
2025-03-29 13:14:55 +08:00
远方夕阳
e8c87358ac
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2025-02-09 08:32:24 +00:00
xia jun
fe788c50c0 修改SDK文档描述,删除无用的参数 2025-01-06 11:45:35 +08:00
xia jun
b02d43af32 修复uni微信小程序模式下,Uint8Array转换的问题
参见https://blog.csdn.net/cnzzs/article/details/143497925
2025-01-01 10:24:11 +08:00
xia jun
c4a90251db 提供微信小程序客户端代码示例 2024-11-25 17:11:04 +08:00
xia jun
0d6ff67f08 新增长连接 接收发送消息实现 2024-11-03 13:48:21 +08:00
xia jun
626ee06c21 服务端SDK可以设置自定义日志处理器,可自行过滤不需要打印的日志 2024-10-18 14:04:27 +08:00
xia jun
eed7ffb747 服务端SDK可以设置自定义日志处理器,可自行过滤不需要打印的日志 2024-10-18 14:03:37 +08:00
远方夕阳
bdd66ce133
update README.md.
修改README

Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-07-17 04:53:50 +00:00
xia jun
7175585d37 优化解码socket数据 2024-06-11 12:53:52 +08:00
xia jun
baa536f442 Server SDK 优化Websocket鉴权机制,在握手之前鉴权。 2024-06-05 14:13:56 +08:00
远方夕阳
62fc15b33b
!20 ws鉴权问题
Merge pull request !20 from changhao.ni/master
2024-06-05 04:25:06 +00:00
远方夕阳
53fd3aa005
update cim-use-examples/cim-client-android/app/src/main/AndroidManifest.xml.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-06-05 03:59:12 +00:00
changhao.ni
b126dfc198 webscoket 鉴权方式修改 2024-05-29 10:29:21 +08:00
远方夕阳
ec130c97b4 update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-05-29 10:19:34 +08:00
远方夕阳
a4b401e16b update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-05-29 10:19:34 +08:00
LAPTOP-8KGKDQIB\39794
5d22b80a37 no message 2024-03-22 18:33:28 +08:00
LAPTOP-8KGKDQIB\39794
7e7b4c03da 修改https://gitee.com/farsunset/cim/issues/I9ACDM
session表channel字段加长到16位
2024-03-22 18:33:04 +08:00
远方夕阳
4098e564f4
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-03-20 07:06:47 +00:00
远方夕阳
570b977910
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-03-20 07:06:13 +00:00
远方夕阳
66c4ed8398
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-03-18 13:13:18 +00:00
远方夕阳
e5ba2ca32e
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-03-18 13:12:35 +00:00
远方夕阳
36270254b0
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-03-15 01:02:28 +00:00
远方夕阳
789c89185a Merge branch 'master' of https://gitee.com/farsunset/cim 2024-01-26 16:34:28 +08:00
远方夕阳
d189d4cca4 更新android demo工程 2024-01-26 16:34:17 +08:00
远方夕阳
a572ccc9a3
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2024-01-26 08:31:47 +00:00
远方夕阳
c1df188360 修改redis消息队列配置问题 2024-01-03 18:12:06 +08:00
远方夕阳
e60f64ec55 修改Android sdk 替换protobuf-lite为protobuf-javalite 2023-12-04 19:02:08 +08:00
zhuhailiang
43308baa5f sdk接口更新 2023-11-01 10:51:12 +08:00
zhuhailiang
a9909a4039 sdk封装接口调整、接口文档调整 2023-11-01 10:23:40 +08:00
zhuhailiang
ce2377c72b README.md文档更新 2023-10-25 11:34:55 +08:00
zhuhailiang
c9177612ea README.md说明文档提交 2023-10-25 11:32:02 +08:00
zhuhailiang
5cdb08306f cim客户端uniapp-sdk更新 2023-10-24 15:36:02 +08:00
远方夕阳
cfac26b7ad android sdk 适配 android14 2023-10-05 16:40:35 +08:00
远方夕阳
6a02c58d4e Merge branch 'master' of https://gitee.com/farsunset/cim 2023-10-05 11:46:01 +08:00
远方夕阳
9856788ed1 android sdk 适配 android14 2023-10-05 11:45:51 +08:00
远方夕阳
1c9efbbaf0
!19 flutter sdk 长连接登录信息 改为缺省参数 可自行填写
Merge pull request !19 from 杨杰/master
2023-07-30 02:18:22 +00:00
yangjiejie12308
37bb7a06bc flutter sdk 长连接登录信息 改为缺省参数 可自行填写 2023-07-17 09:32:34 +08:00
远方夕阳
fdf435a873
!18 flutter sdk 新增全平台 websocket 连接通道 支持 web ios android windows mac linux
Merge pull request !18 from 杨杰/master
2023-07-14 03:30:23 +00:00
yangjiejie12308
df3385151a 新增 flutter_websocket_sdk 支持所有平台 2023-07-14 11:22:34 +08:00
yangjiejie12308
2a3d315d75 更新sdk websocket sdk 增加 2023-07-14 10:33:26 +08:00
远方夕阳
c8f45c9a97
!17 Flutter SDK 连接uid 改为字符串类型
Merge pull request !17 from 杨杰/master
2023-07-13 09:53:53 +00:00
yangjiejie12308
03a309d8ee fix 允许UID 为字符串 2023-07-13 17:25:37 +08:00
远方夕阳
5252c00a8f
!16 修复 重复收到消息的bug
Merge pull request !16 from 杨杰/master
2023-07-10 10:35:08 +00:00
yangjiejie12308
d7e8649c17 fix 重复收到消息的bug 2023-07-10 10:46:43 +08:00
远方夕阳
8a494b9c02
!15 修复 flutter sdk 中 osversion 过长的错误
Merge pull request !15 from 杨杰/master
2023-07-06 09:32:05 +00:00
yangjiejie12308
2a8ec5865e fix osversion is too long 2023-07-06 11:43:52 +08:00
远方夕阳
2ba80c85cf
!14 upload sdk to pub.dev
Merge pull request !14 from 杨杰/master
2023-07-06 02:59:05 +00:00
yangjiejie12308
457eb21769 删除自动生成的文件 2023-07-04 16:01:30 +08:00
yangjiejie12308
f3a7ab7cdf 合并 2023-07-04 15:59:33 +08:00
yangjiejie12308
c4b30acef3 flutter public to pub.dev 2023-07-04 15:58:32 +08:00
杨杰
c55c3d5012
update cim-client-sdk/cim-flutter-sdk/README.md.
Signed-off-by: 杨杰 <5301327+smebclub@user.noreply.gitee.com>
2023-07-04 06:25:46 +00:00
杨杰
721839d981
update cim-client-sdk/cim-flutter-sdk/README.md.
Signed-off-by: 杨杰 <5301327+smebclub@user.noreply.gitee.com>
2023-07-04 06:25:18 +00:00
杨杰
c5947f718f
update cim-client-sdk/cim-flutter-sdk/README.md.
Signed-off-by: 杨杰 <5301327+smebclub@user.noreply.gitee.com>
2023-07-04 06:15:21 +00:00
远方夕阳
f798e8a6fb
!13 update flutter sdk
Merge pull request !13 from 杨杰/master
2023-06-14 03:34:55 +00:00
yangjiejie12308
1d8bf474f9 update flutter sdk 2023-06-14 09:36:31 +08:00
远方夕阳
68b3ebd5c4 android sdk更新
适配targetSdkVersion 33
2023-02-15 12:18:41 +08:00
远方夕阳
046bfd8ade android sdk更新
适配targetSdkVersion 33
2023-02-15 11:15:43 +08:00
远方夕阳
97fe0a2721 服务端sdk升级
1、服务端新增 配置长连接心跳间隔和允许超时次数相关参数
2、com.farsunset.cim.acceptor.config.AppSocketConfig修改为com.farsunset.cim.acceptor.config.SocketConfig
2023-02-06 18:22:21 +08:00
远方夕阳
adf4f827e6 android sdk 监听器容器修改为CopyOnWriteArrayList 2022-11-04 16:21:26 +08:00
远方夕阳
11f40a0f51 android sdk 监听器容器修改为CopyOnWriteArrayList 2022-11-04 16:19:52 +08:00
远方夕阳
40a4353d21 修复android sdk 一处bug
频发切换wifi和移动网络,概率出现无法重连的问题
2022-09-29 18:40:27 +08:00
远方夕阳
e89a3918c5 修复android sdk 一处bug
频发切换wifi和移动网络,概率出现无法重连的问题
2022-09-29 18:23:12 +08:00
远方夕阳
35ab7cc72c 迁移基于oc的ios sdk 2022-09-17 15:25:39 +08:00
Chentao
2a2613b5cc [cim-ios-oc-sdk]增加重构代码 2022-09-16 13:35:24 +08:00
远方夕阳
560da9bebe 修改android版sdk Not allowed to delete channel with a foreground service 的问题 2022-09-07 17:10:50 +08:00
远方夕阳
538f80cee1 1、优化服务端session管理实现
2、android优化sdk通知管理
2022-09-04 22:47:38 +08:00
远方夕阳
ea9efc8594 新增服务端sdk功能接口,和代码注释 2022-09-01 13:51:38 +08:00
远方夕阳
c93ac73e80 服务端demo新增webrtc信令接口示例 2022-08-30 11:50:26 +08:00
远方夕阳
a0ddbe47c5 升级组件版本 2022-08-29 20:24:42 +08:00
远方夕阳
7a9a369960 修改android12中可能出现ForegroundServiceStartNotAllowedException的问题 2022-08-27 23:01:15 +08:00
远方夕阳
69cbb31d9e sdk切换为maven依赖 2022-08-27 17:35:05 +08:00
远方夕阳
c62d646cfc sdk切换为maven依赖 2022-08-27 17:34:32 +08:00
远方夕阳
bb7266e170
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2022-08-18 07:49:43 +00:00
远方夕阳
40a32b8887
update README.md.
Signed-off-by: 远方夕阳 <xj753277@126.com>
2022-08-18 07:49:11 +00:00
远方夕阳
d2e9727df7 添加构建maven配置 2022-08-18 15:40:17 +08:00
远方夕阳
822bc6f104
!12 修复iOS消息监听的BUG
Merge pull request !12 from ZBNever/master
2022-06-24 01:39:33 +00:00
周博
e54f90cb04 ios修复消息监听的BUG 2022-06-24 09:03:15 +08:00
远方夕阳
0d45aac81b
update README.md. 2022-05-17 07:25:30 +00:00
远方夕阳
994b78272d
update README.md. 2022-05-17 07:24:52 +00:00
远方夕阳
2996271f83
update README.md. 2022-05-17 07:24:34 +00:00
远方夕阳
c5bed5b689
update README.md. 2022-05-17 07:17:21 +00:00
远方夕阳
8cd43fdf9c 修改json序列化ReplyBody 丢失data字段的问题 2022-05-11 11:10:54 +08:00
远方夕阳
cba314151d 修改json序列化ReplyBody 丢失data字段的问题 2022-05-11 11:02:33 +08:00
远方夕阳
461be0fab1 1、websocket鉴权失败时响应ReplyBody,告知客户端鉴权失败 2022-05-10 17:07:39 +08:00
远方夕阳
7579845369 需改Sentbody.toString() 可能出现空指针的问题 2022-05-06 15:19:27 +08:00
远方夕阳
ec94bfcdf9
update README.md. 2022-04-27 07:49:56 +00:00
远方夕阳
b9afa1ca08
update README.md. 2022-04-27 07:45:41 +00:00
远方夕阳
585f3c4c80
update README.md. 2022-04-27 07:45:03 +00:00
远方夕阳
bffe162516
update README.md. 2022-04-27 07:44:09 +00:00
远方夕阳
a339e2640c
update README.md. 2022-04-23 05:18:03 +00:00
远方夕阳
c9552347a4
update README.md. 2022-04-23 05:17:40 +00:00
远方
ac32108750 优化服务端异常打印日志 2022-04-16 12:29:14 +08:00
远方
4051660957 服务端sdk 优化日志打印,线程名加入UID信息 2022-04-16 12:07:03 +08:00
远方夕阳
0e8a6aba18
update cim-boot-server/src/main/resources/application.properties. 2022-04-16 03:47:12 +00:00
远方夕阳
8f9d420ba2 服务端sdk日志优化 2022-04-14 17:13:45 +08:00
远方夕阳
16d5738451
update cim-server-sdk/src/main/java/com/farsunset/cim/acceptor/NioSocketAcceptor.java. 2022-04-07 09:21:11 +00:00
远方夕阳
42ba6e0cd4
update cim-server-sdk/src/main/java/com/farsunset/cim/acceptor/NioSocketAcceptor.java. 2022-04-07 09:20:34 +00:00
远方夕阳
62107d5b44
update README.md. 2022-04-07 03:43:57 +00:00
远方
be301d95c9 服务端sdk 优化日志打印,线程名加入UID信息 2022-04-04 14:01:10 +08:00
远方
6c61a09682 修改dome说明文件 2022-04-04 13:13:56 +08:00
远方
ad60928b54 修改Android sdk 网络断开 没有回调的问题 2022-03-28 21:26:57 +08:00
远方
2563c3fc12 1、调整Android sdk 连接超时为5秒(原10秒)
1、修复Android sdk 延时重连机制 时间不准确的问题
2022-03-27 21:46:44 +08:00
远方
bf7b6e79ae SentBody新增参数默认值 2022-03-20 14:25:08 +08:00
远方
eadac1b887 Android sdk 兼容targetSdkVersion 31 2022-03-20 14:14:51 +08:00
远方夕阳
4cc4d1d3b4
add cim-boot-server/init.sql. 2022-03-17 10:31:33 +00:00
远方夕阳
0263f804bb
update cim-boot-server/src/main/resources/application.properties. 2022-03-15 01:02:52 +00:00
远方夕阳
1cadbd167c 1.websocket新增支持json序列化方式,助力于小程序开发
2.服务端sdk优化包路径结构
3.修改部分问题
2022-03-14 18:57:29 +08:00
远方夕阳
91a890396d 兼容ws链接uri后面拼接参数 2022-02-16 14:31:08 +08:00
远方
fbe5e24e41 websocket支持握手时鉴权 2022-02-15 20:52:27 +08:00
远方夕阳
ec8b4a8392 websocket支持在握手时鉴权
文档地址:https://www.yuque.com/yuanfangxiyang/ma4ytb/vvy3iz#mmdUX
2022-02-15 19:52:32 +08:00
远方夕阳
9247b3daa5
update README.md. 2022-02-09 07:38:11 +00:00
远方夕阳
5ad68b833b
update README.md. 2022-01-17 13:10:24 +00:00
远方夕阳
8739fb87eb
update README.md. 2022-01-17 13:09:33 +00:00
远方夕阳
39a4ae2485
update README.md. 2022-01-17 13:08:52 +00:00
远方夕阳
4e98a6e571
update README.md. 2022-01-17 13:04:54 +00:00
远方夕阳
bf134137ee
update README.md. 2022-01-17 13:04:26 +00:00
远方夕阳
a505d3f06c !11 dotNet sdk 以及示例
Merge pull request !11 from 杨杰/dotnet
2021-12-27 10:09:21 +00:00
smeb_yangjie
e19c6424f2 dotnet_sdk说明 2021-12-24 19:40:51 +08:00
smeb_yangjie
a901e2b1f6 cim-dotnet-examples 2021-12-24 19:32:45 +08:00
35762f5ec9 fix 2021-12-24 18:12:37 +08:00
远方夕阳
7275b4b5ea !10 在flutter中测试通过的dart示例
Merge pull request !10 from 杨杰/cim-flutter-sdk
2021-12-24 03:24:52 +00:00
bf9b2860df cim for flutter sdk 2021-12-24 11:10:15 +08:00
远方夕阳
84786e6fdb update README.md. 2021-10-30 10:24:22 +00:00
远方夕阳
f6699c039e !8 弃用Cocoapods,支持SPM
Merge pull request !8 from 飞鱼log/master
2021-10-21 01:59:40 +00:00
飞鱼
4e0520b2b5 弃用Cocoapods, 支持SPM。SwiftUI Hello world! 2021-10-20 20:43:08 +08:00
远方夕阳
1faf67a977 1、修改APNs三方包为pushy
2、修改注释和增强SessionGroup
2021-10-18 22:07:25 +08:00
远方夕阳
1c1480ef2c !7 CIM Swift SDK
Merge pull request !7 from 飞鱼log/master
2021-10-04 04:38:04 +00:00
飞鱼
77ad75cbba Swift SDK 2021-10-01 19:00:04 +08:00
远方夕阳
0ce42c6901 update README.md. 2021-09-27 03:52:52 +00:00
远方夕阳
3d7ffd9038 update README.md. 2021-09-24 07:24:51 +00:00
远方夕阳
418c26f6f9 修改ChannelAttr.LANGUAGE找不到的问题 2021-09-23 20:05:10 +08:00
远方夕阳
562508d5a2 update README.md. 2021-09-13 07:23:22 +00:00
远方夕阳
98c04c936d update README.md. 2021-09-13 07:22:27 +00:00
远方夕阳
eec65bcfe1 update README.md. 2021-09-13 07:20:58 +00:00
远方夕阳
5af799d808 update README.md. 2021-09-11 10:56:18 +00:00
远方夕阳
abc3300f9d update README.md. 2021-09-11 10:56:07 +00:00
远方夕阳
ddd7e741b8 TagSessionGroup新增过滤器参数 2021-09-08 21:48:21 +08:00
远方夕阳
bfa0b5b8ea TagSessionGroup新增过滤器参数 2021-09-08 21:46:51 +08:00
远方夕阳
bcb7a59ef4 update README.md. 2021-08-17 08:32:30 +00:00
远方夕阳
919df56402 update README.md. 2021-08-17 08:31:36 +00:00
远方夕阳
15885dbee5 update README.md. 2021-08-17 08:29:42 +00:00
xiajun
673bb6bbc0 服务端IM端口和通过配置启用和禁用 2021-07-28 18:27:26 +08:00
xiajun
6baf846b4c 建立连接时新增客户端language参数 2021-06-17 20:10:47 +08:00
远方夕阳
c9e0d15bd1 update cim-use-examples/cim-client-web/index.html. 2021-06-08 10:52:01 +08:00
远方夕阳
c9c317d6c8 修改服务端sdk一处可能出现空指针的问题 2021-06-02 21:29:07 +08:00
远方夕阳
cada6b890e !5 修复在linux下启动报错
Merge pull request !5 from ilaotan/master
2021-05-25 20:38:11 +08:00
tanliansheng
fd4b0eff90 修复在linux下启动报错 issue https://gitee.com/farsunset/cim/issues/I3SOLE 2021-05-24 19:24:15 +08:00
远方夕阳
7c46874e44 update README.md. 2021-05-09 11:08:32 +08:00
远方夕阳
19f634e31e update README.md. 2021-05-09 11:08:10 +08:00
远方夕阳
7bb5ae30ec update README.md. 2021-05-09 11:07:57 +08:00
远方夕阳
4bf070fa0d Merge branch 'master' of https://gitee.com/farsunset/cim
# Conflicts:
#	README.md
2021-05-09 10:42:50 +08:00
远方夕阳
756ff96b8a 版本升级到4.0.0
参见更新日志
2021-05-09 10:41:44 +08:00
605 changed files with 19011 additions and 26141 deletions

30
.gitignore vendored
View File

@ -1,2 +1,28 @@
target/
app/build/
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
/target/
/.idea/
.idea
*.class
*/local.properties

100
README.md
View File

@ -1,97 +1,69 @@
#### 项目介绍
CIM项目是基于mina或者netty框架下的推送系统我们平常使用第三方的推送SDK如极光推送百度推送小米推送以及腾讯信鸽等来支撑自己的移动端的业务或许有一些用户自己实现即时通讯系统的需求那么CIM为您提供了一个解决方案或者思路目前CIM支持 websocketandroidios桌面应用系统应用等多端接入支持目前CIM服务端使用springboot搭建仅仅拥有消息推送的功能可以理解为是一个消息队列方案因此不包含任何业务功能。关于数据缓存与持久化都需要使用者自己开发但是配备了比较完整的使用文档。最后希望CIM能为您带来一些价值。
---
## 以下是基于CIM开发的2款产品并不开源!!!
## [http://farsunset.com](http://farsunset.com)
#### 和信
和信是基于CIM组件开发的一整套完整的产品,面向所有人开放注册的试用场景。具有丰富的功能,聊天、群组、好友列表、黑名单、公众号、朋友圈等功能。不依赖任何第三方服务,可以私有化部署。
## 收费产品介绍
#### 侣信
侣信是基于CIM组件开发的一整套完整的产品,面向中小企业和者各类团队组织内部交流使用工具。具有丰富的功能,聊天、群组、部门组织、公众号、内部朋友圈等功能。不依赖任何第三方服务,可以私有化部署。
#### 布咕(https://farsunset.com/)
布咕是基于CIM组件开发的一整套完整的产品,面向所有人开放注册的试用场景。具有丰富的功能,音视频会议,聊天、群组、好友,组织架构、公众号、朋友圈等功能。不依赖任何第三方云服务,可以私有化部署。
<div align="center">
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/hoxin/call_video_incoming.jpg" width="24%" />
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/hoxin/group_video_calling.jpg" width="24%" />
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/hoxin/single_chatting_light.jpg" width="24%" />
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/hoxin/single_chatting_dark.jpg" width="24%" />
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/hoxin/moment_timeline_light.jpg" width="24%" />
</div>
---
#### 目录说明
## 项目介绍
1.cim-use-examples是各个客户端使用示例
[在线文档](https://www.yuque.com/yuanfangxiyang/ma4ytb)(https://www.yuque.com/yuanfangxiyang/ma4ytb)
2.cim-client-sdk 是各个客户端的SDK源码
CIM是一套完善的消息推送框架可应用于信令推送即时聊天移动设备指令推送等领域。开发者可沉浸于业务开发不用关心消息通道长连接、消息编解码协议等繁杂处理。
3.cim-server-sdk 是服务端SDK源码,分为 mina和netty 两个版本,二者任选其一
CIM采用业内主流开源技术构建易于扩展和使用并完美支持集群部署支持海量链接目前支持websocketandroidios桌面应用系统应用等多端接入持,可应用于移动应用物联网智能家居嵌入式开发桌面应用WEB应用即时消服务。
4.cim-boot-server是springboot服务端工程源码,使用Idea工具开发
历时10年基于CIM的项目已经运行在全国各个地方包括上市公司各地政务系统警务系统等服务于上百家客户希望CIM也能为您带来价值如果您也希望加入项目成为贡献者请联系我。如果觉得有用欢迎打赏。
其中所有的sdk均为IntelliJ IDEA工程Maven打包成jar导出引入到对应的客户端或服务端工程
如果对您有价值请送一个star和Fork喔~
<div align="center">
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/qcode/ali_pay.jpg" width="30%" />
<img src="http://staticres.oss-cn-hangzhou.aliyuncs.com/qcode/wechat_pay.jpg" width="30%" />
</div>
#### 功能预览
1.控制台页面[http://127.0.0.1:8080](http://127.0.0.1:8080)
## 功能预览
### 控制台页面[http://127.0.0.1:8080](http://127.0.0.1:8080)
![image](https://images.gitee.com/uploads/images/2019/0315/165050_9e269c1c_58912.png)
2.Android客户端
### Android客户端
![image](https://images.gitee.com/uploads/images/2019/0315/165050_6f20f69e_58912.png)
3.Web客户端
### Web客户端
![image](https://images.gitee.com/uploads/images/2019/0315/165050_dfc33c18_58912.png)
#### 更新日志
-------------------------------------------------------------------------------------------
版本:3.5.0/时间:2018-08-22
1.服务端由原来的 spring+struts2修改为springboot工程
2.全面重写websocket的实现全面拥抱protobuf替换json序列化方式更加高效
-------------------------------------------------------------------------------------------
版本:3.6.0/时间:2019-04-17
1.服务端springboot升级2.1.4,protobuf升级3.7.0
2.android sdk升级适配android8.0+,修复一些之前的兼容性问题
3.消息的id字段名由mid修改为id类型由String修改为long;
-------------------------------------------------------------------------------------------
版本:3.7.0/时间:2019-05-13
1.服务端cim-boot-server修改为idea maven工程
2.android sdk优化升级去除mina或netty相关包的依赖
3.java sdk优化升级去除mina或netty相关包的依赖
4.新增web sdk可以由index.html快速启动demo
5.修正文档中一些疏漏
-------------------------------------------------------------------------------------------
版本:3.7.5/时间:2019-11-13
1.android sdk 优化使用protobuf-lite版本替代较为臃肿的protobut-java版本
-------------------------------------------------------------------------------------------
版本:3.8.0/时间:2020-01-17
## Maven Gradle
1.服务端sdk将websocket的服务端口和原生socket的端口分离可以禁用其中一个或者同时启
服务端sdk引用
2.web端的sdk简化流程不再需要心跳响应,修改了连接成功回调方法名称和创建连接方法名
```
3.andoid sdk修改几个广播action的名称以及回调方法名称详见cim-client-android工程
<dependency>
<groupId>com.farsunset</groupId>
<artifactId>cim-server-sdk-netty</artifactId>
<version>4.2.10</version>
</dependency>
4.所有sdk均使用maven构建idea工具开发发现多处代码单词拼写错误使用阿里语法检测组件优化了部分代码
```
5.同步修改了文档
android端sdk引用
```
implementation "com.farsunset:cim-android-sdk:4.2.15"
```

View File

@ -0,0 +1,2 @@
如果你不想搭建服务端也可以使用,公共的服务器做测试喔
具体信息参见https://www.yuque.com/yuanfangxiyang/ma4ytb/vvy3iz#yC5Vq

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="Spring" name="Spring">
<configuration />
</facet>
<facet type="web" name="Web">
<configuration>
<webroots />
<sourceRoots>
<root url="file://$MODULE_DIR$/src/main/java" />
<root url="file://$MODULE_DIR$/src/main/resources" />
</sourceRoots>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
<orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.12.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.12.1" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.30" level="project" />
<orderEntry type="library" name="Maven: jakarta.annotation:jakarta.annotation-api:1.3.5" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.2.4.RELEASE" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.yaml:snakeyaml:1.25" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.10.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.10.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.10.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.2" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.2" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-tomcat:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-core:9.0.31" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-el:9.0.31" level="project" />
<orderEntry type="library" name="Maven: org.apache.tomcat.embed:tomcat-embed-websocket:9.0.31" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-validation:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" />
<orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.0.18.Final" level="project" />
<orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final" level="project" />
<orderEntry type="library" name="Maven: com.fasterxml:classmate:1.5.1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-web:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-beans:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-webmvc:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-aop:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-expression:5.2.4.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-freemarker:2.2.5.RELEASE" level="project" />
<orderEntry type="library" name="Maven: org.freemarker:freemarker:2.3.29" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context-support:5.2.4.RELEASE" level="project" />
<orderEntry type="module-library">
<library name="Maven: com.farsunset:cim-server-sdk:3.8.0">
<CLASSES>
<root url="jar://$MODULE_DIR$/libs/cim-server-sdk-netty-3.8.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="Maven: org.apache.mina:mina-core:2.1.3" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-handler:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-buffer:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-codec-http:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-common:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-transport:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: io.netty:netty-resolver:4.1.35.Final" level="project" />
<orderEntry type="library" name="Maven: com.google.protobuf:protobuf-java:3.11.1" level="project" />
<orderEntry type="library" name="Maven: cn.teaey.apns4j:apns4j:1.1.4" level="project" />
<orderEntry type="library" name="Maven: commons-io:commons-io:2.6" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-lang3:3.8.1" level="project" />
</component>
</module>

View File

@ -1,671 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="cim-boot-server" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="cim-boot-server" target="1.8" />
</bytecodeTargetLevel>
</component>
<component name="Encoding">
<file url="file://$PROJECT_DIR$" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ContextJavaBeanUnresolvedMethodsInspection" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
<option name="TOP_LEVEL_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="INNER_CLASS_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="METHOD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
</value>
</option>
<option name="FIELD_OPTIONS">
<value>
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
<option name="REQUIRED_TAGS" value="" />
</value>
</option>
<option name="IGNORE_DEPRECATED" value="false" />
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
<option name="myAdditionalJavadocTags" value="date" />
</inspection_tool>
<inspection_tool class="SpringJavaInjectionPointsAutowiringInspection" enabled="false" level="ERROR" enabled_by_default="false" />
</profile>
<version value="1.0" />
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="cim-boot-server" options="-parameters" />
</option>
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/cim-boot-server.iml" filepath="$PROJECT_DIR$/cim-boot-server.iml" />
</modules>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
<component name="libraryTable">
<library name="Maven: ch.qos.logback:logback-classic:1.2.3">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: ch.qos.logback:logback-core:1.2.3">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: cn.teaey.apns4j:apns4j:1.1.4">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/cn/teaey/apns4j/apns4j/1.1.4/apns4j-1.1.4.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/cn/teaey/apns4j/apns4j/1.1.4/apns4j-1.1.4-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/cn/teaey/apns4j/apns4j/1.1.4/apns4j-1.1.4-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.10.2/jackson-annotations-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.10.2/jackson-annotations-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.10.2/jackson-annotations-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.core:jackson-core:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.10.2/jackson-core-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.10.2/jackson-core-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.10.2/jackson-core-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.core:jackson-databind:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.10.2/jackson-databind-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.10.2/jackson-databind-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.10.2/jackson-databind-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.10.2/jackson-datatype-jdk8-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.10.2/jackson-datatype-jdk8-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.10.2/jackson-datatype-jdk8-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.10.2/jackson-datatype-jsr310-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.10.2/jackson-datatype-jsr310-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.10.2/jackson-datatype-jsr310-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/module/jackson-module-parameter-names/2.10.2/jackson-module-parameter-names-2.10.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/module/jackson-module-parameter-names/2.10.2/jackson-module-parameter-names-2.10.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/jackson/module/jackson-module-parameter-names/2.10.2/jackson-module-parameter-names-2.10.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.fasterxml:classmate:1.5.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/fasterxml/classmate/1.5.1/classmate-1.5.1-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: com.google.protobuf:protobuf-java:3.11.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.11.1/protobuf-java-3.11.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.11.1/protobuf-java-3.11.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/com/google/protobuf/protobuf-java/3.11.1/protobuf-java-3.11.1-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: commons-io:commons-io:2.6">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/commons-io/commons-io/2.6/commons-io-2.6.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/commons-io/commons-io/2.6/commons-io-2.6-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/commons-io/commons-io/2.6/commons-io-2.6-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-buffer:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-buffer/4.1.35.Final/netty-buffer-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-buffer/4.1.35.Final/netty-buffer-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-buffer/4.1.35.Final/netty-buffer-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-codec-http:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec-http/4.1.35.Final/netty-codec-http-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec-http/4.1.35.Final/netty-codec-http-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec-http/4.1.35.Final/netty-codec-http-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-codec:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec/4.1.35.Final/netty-codec-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec/4.1.35.Final/netty-codec-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-codec/4.1.35.Final/netty-codec-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-common:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-common/4.1.35.Final/netty-common-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-common/4.1.35.Final/netty-common-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-common/4.1.35.Final/netty-common-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-handler:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-handler/4.1.35.Final/netty-handler-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-handler/4.1.35.Final/netty-handler-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-handler/4.1.35.Final/netty-handler-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-resolver:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-resolver/4.1.35.Final/netty-resolver-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-resolver/4.1.35.Final/netty-resolver-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-resolver/4.1.35.Final/netty-resolver-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: io.netty:netty-transport:4.1.35.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport/4.1.35.Final/netty-transport-4.1.35.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport/4.1.35.Final/netty-transport-4.1.35.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/io/netty/netty-transport/4.1.35.Final/netty-transport-4.1.35.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: jakarta.annotation:jakarta.annotation-api:1.3.5">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: jakarta.validation:jakarta.validation-api:2.0.2">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/2.0.2/jakarta.validation-api-2.0.2.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/2.0.2/jakarta.validation-api-2.0.2-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/jakarta/validation/jakarta.validation-api/2.0.2/jakarta.validation-api-2.0.2-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.commons:commons-lang3:3.8.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/commons/commons-lang3/3.8.1/commons-lang3-3.8.1-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.logging.log4j:log4j-api:2.12.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-api/2.12.1/log4j-api-2.12.1-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.12.1">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/logging/log4j/log4j-to-slf4j/2.12.1/log4j-to-slf4j-2.12.1-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.mina:mina-core:2.1.3">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/mina/mina-core/2.1.3/mina-core-2.1.3.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/mina/mina-core/2.1.3/mina-core-2.1.3-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/mina/mina-core/2.1.3/mina-core-2.1.3-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.tomcat.embed:tomcat-embed-core:9.0.31">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-core/9.0.31/tomcat-embed-core-9.0.31.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-core/9.0.31/tomcat-embed-core-9.0.31-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-core/9.0.31/tomcat-embed-core-9.0.31-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.tomcat.embed:tomcat-embed-el:9.0.31">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-el/9.0.31/tomcat-embed-el-9.0.31.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-el/9.0.31/tomcat-embed-el-9.0.31-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-el/9.0.31/tomcat-embed-el-9.0.31-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.apache.tomcat.embed:tomcat-embed-websocket:9.0.31">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.31/tomcat-embed-websocket-9.0.31.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.31/tomcat-embed-websocket-9.0.31-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.31/tomcat-embed-websocket-9.0.31-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.freemarker:freemarker:2.3.29">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/freemarker/freemarker/2.3.29/freemarker-2.3.29.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/freemarker/freemarker/2.3.29/freemarker-2.3.29-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/freemarker/freemarker/2.3.29/freemarker-2.3.29-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.hibernate.validator:hibernate-validator:6.0.18.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.4.1.Final/jboss-logging-3.4.1.Final.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.4.1.Final/jboss-logging-3.4.1.Final-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jboss/logging/jboss-logging/3.4.1.Final/jboss-logging-3.4.1.Final-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.slf4j:jul-to-slf4j:1.7.30">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.slf4j:slf4j-api:1.7.30">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-autoconfigure/2.2.5.RELEASE/spring-boot-autoconfigure-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-autoconfigure/2.2.5.RELEASE/spring-boot-autoconfigure-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-autoconfigure/2.2.5.RELEASE/spring-boot-autoconfigure-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-freemarker:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-freemarker/2.2.5.RELEASE/spring-boot-starter-freemarker-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-freemarker/2.2.5.RELEASE/spring-boot-starter-freemarker-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-freemarker/2.2.5.RELEASE/spring-boot-starter-freemarker-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-json:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-json/2.2.5.RELEASE/spring-boot-starter-json-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-json/2.2.5.RELEASE/spring-boot-starter-json-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-json/2.2.5.RELEASE/spring-boot-starter-json-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-logging:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-logging/2.2.5.RELEASE/spring-boot-starter-logging-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-logging/2.2.5.RELEASE/spring-boot-starter-logging-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-logging/2.2.5.RELEASE/spring-boot-starter-logging-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-tomcat:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-tomcat/2.2.5.RELEASE/spring-boot-starter-tomcat-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-tomcat/2.2.5.RELEASE/spring-boot-starter-tomcat-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-tomcat/2.2.5.RELEASE/spring-boot-starter-tomcat-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-validation:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-validation/2.2.5.RELEASE/spring-boot-starter-validation-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-validation/2.2.5.RELEASE/spring-boot-starter-validation-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-validation/2.2.5.RELEASE/spring-boot-starter-validation-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter-web:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-web/2.2.5.RELEASE/spring-boot-starter-web-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-web/2.2.5.RELEASE/spring-boot-starter-web-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter-web/2.2.5.RELEASE/spring-boot-starter-web-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot-starter:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter/2.2.5.RELEASE/spring-boot-starter-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter/2.2.5.RELEASE/spring-boot-starter-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot-starter/2.2.5.RELEASE/spring-boot-starter-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework.boot:spring-boot:2.2.5.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot/2.2.5.RELEASE/spring-boot-2.2.5.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot/2.2.5.RELEASE/spring-boot-2.2.5.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/boot/spring-boot/2.2.5.RELEASE/spring-boot-2.2.5.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-aop:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-aop/5.2.4.RELEASE/spring-aop-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-aop/5.2.4.RELEASE/spring-aop-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-aop/5.2.4.RELEASE/spring-aop-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-beans:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-beans/5.2.4.RELEASE/spring-beans-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-beans/5.2.4.RELEASE/spring-beans-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-beans/5.2.4.RELEASE/spring-beans-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-context-support:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context-support/5.2.4.RELEASE/spring-context-support-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context-support/5.2.4.RELEASE/spring-context-support-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context-support/5.2.4.RELEASE/spring-context-support-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-context:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context/5.2.4.RELEASE/spring-context-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context/5.2.4.RELEASE/spring-context-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-context/5.2.4.RELEASE/spring-context-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-core:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-core/5.2.4.RELEASE/spring-core-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-core/5.2.4.RELEASE/spring-core-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-core/5.2.4.RELEASE/spring-core-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-expression:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-expression/5.2.4.RELEASE/spring-expression-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-expression/5.2.4.RELEASE/spring-expression-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-expression/5.2.4.RELEASE/spring-expression-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-jcl:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-jcl/5.2.4.RELEASE/spring-jcl-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-jcl/5.2.4.RELEASE/spring-jcl-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-jcl/5.2.4.RELEASE/spring-jcl-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-web:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-web/5.2.4.RELEASE/spring-web-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-web/5.2.4.RELEASE/spring-web-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-web/5.2.4.RELEASE/spring-web-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.springframework:spring-webmvc:5.2.4.RELEASE">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-webmvc/5.2.4.RELEASE/spring-webmvc-5.2.4.RELEASE.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-webmvc/5.2.4.RELEASE/spring-webmvc-5.2.4.RELEASE-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/springframework/spring-webmvc/5.2.4.RELEASE/spring-webmvc-5.2.4.RELEASE-sources.jar!/" />
</SOURCES>
</library>
<library name="Maven: org.yaml:snakeyaml:1.25">
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/yaml/snakeyaml/1.25/snakeyaml-1.25.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/yaml/snakeyaml/1.25/snakeyaml-1.25-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/yaml/snakeyaml/1.25/snakeyaml-1.25-sources.jar!/" />
</SOURCES>
</library>
</component>
</project>

View File

@ -1,238 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AnalysisUIOptions">
<option name="GROUP_BY_SEVERITY" value="true" />
</component>
<component name="ChangeListManager">
<list default="true" id="bed47126-03b3-4370-a6c1-08503492974f" name="Default Changelist" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="DefaultGradleProjectSettings">
<option name="isMigrated" value="true" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Class" />
</list>
</option>
</component>
<component name="ProjectId" id="1PxWQToGq56jcmMz176UjXWLSaU" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RequestMappingsPanelOrder0" value="0" />
<property name="RequestMappingsPanelOrder1" value="1" />
<property name="RequestMappingsPanelWidth0" value="75" />
<property name="RequestMappingsPanelWidth1" value="75" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="aspect.path.notification.shown" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.path.for.package.eslint" value="project" />
<property name="node.js.path.for.package.tslint" value="project" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
<property name="settings.editor.selected.configurable" value="configurable.group.appearance" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\Administrator\Documents\gitee-cim\cim-boot-server\lib" />
<recent name="$PROJECT_DIR$/lib" />
<recent name="C:\Users\Administrator\Documents\cim\cim-boot-server\cim-boot-server\cim-boot-server" />
<recent name="C:\Users\Administrator\Documents\cim\cim-boot-server\cim-boot-server\cim-boot-server\src" />
</key>
</component>
<component name="RunManager">
<configuration name="ServerLauncher" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" temporary="true" nameIsGenerated="true">
<module name="cim-boot-server" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.farsunset.cim.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="SPRING_BOOT_MAIN_CLASS" value="com.farsunset.cim.ServerLauncher" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<recent_temporary>
<list>
<item itemvalue="Spring Boot.ServerLauncher" />
</list>
</recent_temporary>
</component>
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="bed47126-03b3-4370-a6c1-08503492974f" name="Default Changelist" comment="" />
<created>1557734989980</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1557734989980</updated>
<workItem from="1557734991214" duration="96000" />
<workItem from="1557735150926" duration="502000" />
<workItem from="1557738240090" duration="289000" />
<workItem from="1557740865016" duration="686000" />
<workItem from="1558432010952" duration="352000" />
<workItem from="1558596242648" duration="1398000" />
<workItem from="1559009816995" duration="1293000" />
<workItem from="1559110709907" duration="1801000" />
<workItem from="1559198165156" duration="1213000" />
<workItem from="1559269742769" duration="598000" />
<workItem from="1562899329294" duration="2642000" />
<workItem from="1563179336728" duration="715000" />
<workItem from="1563191907066" duration="17000" />
<workItem from="1563245165817" duration="382000" />
<workItem from="1566816214034" duration="698000" />
<workItem from="1567410257659" duration="115000" />
<workItem from="1568597528002" duration="523000" />
<workItem from="1579076046499" duration="14355000" />
<workItem from="1579226829168" duration="8852000" />
<workItem from="1583472574255" duration="526000" />
<workItem from="1583473153031" duration="15009000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="2" />
</component>
<component name="WindowStateProjectService">
<state x="609" y="345" key="#com.intellij.fileTypes.FileTypeChooser" timestamp="1583567210548">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state x="609" y="345" key="#com.intellij.fileTypes.FileTypeChooser/0.23.1920.1057@0.23.1920.1057" timestamp="1583567210548" />
<state x="645" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog" timestamp="1583472646530">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state x="645" y="287" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog/0.23.1920.1057@0.23.1920.1057" timestamp="1583472646530" />
<state x="627" y="209" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog" timestamp="1579149988674">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="627" y="209" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog/0.0.1920.1040@0.0.1920.1040" timestamp="1579149988674" />
<state x="690" y="218" key="FileChooserDialogImpl" timestamp="1579230010253">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="690" y="218" key="FileChooserDialogImpl/0.0.1920.1040@0.0.1920.1040" timestamp="1579230010253" />
<state width="1870" height="403" key="GridCell.Tab.0.bottom" timestamp="1583567568644">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.0.bottom/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696749" />
<state width="1870" height="403" key="GridCell.Tab.0.bottom/0.23.1920.1057@0.23.1920.1057" timestamp="1583567568644" />
<state width="1870" height="403" key="GridCell.Tab.0.center" timestamp="1583567568644">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.0.center/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696749" />
<state width="1870" height="403" key="GridCell.Tab.0.center/0.23.1920.1057@0.23.1920.1057" timestamp="1583567568644" />
<state width="1870" height="403" key="GridCell.Tab.0.left" timestamp="1583567568643">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.0.left/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696749" />
<state width="1870" height="403" key="GridCell.Tab.0.left/0.23.1920.1057@0.23.1920.1057" timestamp="1583567568643" />
<state width="1870" height="403" key="GridCell.Tab.0.right" timestamp="1583567568644">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.0.right/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696749" />
<state width="1870" height="403" key="GridCell.Tab.0.right/0.23.1920.1057@0.23.1920.1057" timestamp="1583567568644" />
<state width="1870" height="565" key="GridCell.Tab.1.bottom" timestamp="1583567566487">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.1.bottom/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696751" />
<state width="1870" height="565" key="GridCell.Tab.1.bottom/0.23.1920.1057@0.23.1920.1057" timestamp="1583567566487" />
<state width="1870" height="565" key="GridCell.Tab.1.center" timestamp="1583567566486">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.1.center/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696750" />
<state width="1870" height="565" key="GridCell.Tab.1.center/0.23.1920.1057@0.23.1920.1057" timestamp="1583567566486" />
<state width="1870" height="565" key="GridCell.Tab.1.left" timestamp="1583567566486">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.1.left/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696749" />
<state width="1870" height="565" key="GridCell.Tab.1.left/0.23.1920.1057@0.23.1920.1057" timestamp="1583567566486" />
<state width="1870" height="565" key="GridCell.Tab.1.right" timestamp="1583567566486">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state width="1867" height="615" key="GridCell.Tab.1.right/0.0.1920.1040@0.0.1920.1040" timestamp="1579243696751" />
<state width="1870" height="565" key="GridCell.Tab.1.right/0.23.1920.1057@0.23.1920.1057" timestamp="1583567566486" />
<state width="1867" height="386" key="GridCell.Tab.2.bottom" timestamp="1579156159339">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1867" height="386" key="GridCell.Tab.2.bottom/0.0.1920.1040@0.0.1920.1040" timestamp="1579156159339" />
<state width="1867" height="386" key="GridCell.Tab.2.center" timestamp="1579156159339">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1867" height="386" key="GridCell.Tab.2.center/0.0.1920.1040@0.0.1920.1040" timestamp="1579156159339" />
<state width="1867" height="386" key="GridCell.Tab.2.left" timestamp="1579156159339">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1867" height="386" key="GridCell.Tab.2.left/0.0.1920.1040@0.0.1920.1040" timestamp="1579156159339" />
<state width="1867" height="386" key="GridCell.Tab.2.right" timestamp="1579156159339">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1867" height="386" key="GridCell.Tab.2.right/0.0.1920.1040@0.0.1920.1040" timestamp="1579156159339" />
<state x="574" y="219" width="771" height="670" key="find.popup" timestamp="1583473246286">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state x="574" y="193" width="771" height="654" key="find.popup/0.0.1920.1040@0.0.1920.1040" timestamp="1579242964438" />
<state x="574" y="219" width="771" height="670" key="find.popup/0.23.1920.1057@0.23.1920.1057" timestamp="1583473246286" />
<state x="161" y="163" key="new project wizard" timestamp="1579242569030">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="161" y="163" key="new project wizard/0.0.1920.1040@0.0.1920.1040" timestamp="1579242569030" />
<state x="539" y="28" width="840" height="1051" key="search.everywhere.popup" timestamp="1583567214095">
<screen x="0" y="23" width="1920" height="1057" />
</state>
<state x="539" y="5" width="840" height="1034" key="search.everywhere.popup/0.0.1920.1040@0.0.1920.1040" timestamp="1579235575382" />
<state x="539" y="28" width="840" height="1051" key="search.everywhere.popup/0.23.1920.1057@0.23.1920.1057" timestamp="1583567214095" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" type="java-line">
<url>jar://$PROJECT_DIR$/lib/cim-server-sdk-mina-3.7.0.jar!/com/farsunset/cim/sdk/server/handler/CIMNioSocketAcceptor.class</url>
<line>72</line>
<option name="timeStamp" value="5" />
</line-breakpoint>
<line-breakpoint enabled="true" type="java-line">
<url>jar://$PROJECT_DIR$/lib/cim-server-sdk-netty-3.7.0.jar!/com/farsunset/cim/sdk/server/coder/WebMessageEncoder.class</url>
<line>22</line>
<option name="timeStamp" value="10" />
</line-breakpoint>
<line-breakpoint enabled="true" type="java-line">
<url>file://$PROJECT_DIR$/src/main/java/com/farsunset/cim/handler/SessionClosedHandler.java</url>
<line>48</line>
<option name="timeStamp" value="12" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>1.8</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project>

4
cim-boot-server/init.sql Normal file
View File

@ -0,0 +1,4 @@
create database cim;
#只需要创建库即可,服务器启动会自动创建表,基于application.properties中
#spring.jpa.hibernate.ddl-auto = update

View File

View File

@ -6,20 +6,22 @@
<groupId>com.farsunset</groupId>
<artifactId>cim-boot-server</artifactId>
<version>1.0.0</version>
<version>4.2.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<version>2.5.14</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<netty.version>4.1.35.Final</netty.version>
<mina.version>2.1.3</mina.version>
<protobuf.version>3.11.1</protobuf.version>
<cim.server.sdk.version>4.2.10</cim.server.sdk.version>
<protobuf.version>3.25.0</protobuf.version>
<mysql.jdbc.version>9.2.0</mysql.jdbc.version>
<common.pool.version>2.12.1</common.pool.version>
<swagger.version>3.0.0</swagger.version>
</properties>
<dependencies>
<dependency>
@ -36,59 +38,49 @@
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>com.farsunset</groupId>
<artifactId>cim-server-sdk</artifactId>
<version>3.8.0</version>
<scope>system</scope>
<!-- mina 、netty版本 sdk任选其一 -->
<systemPath>${project.basedir}/libs/cim-server-sdk-netty-3.8.0.jar</systemPath>
<artifactId>cim-server-sdk-netty</artifactId>
<version>${cim.server.sdk.version}</version>
</dependency>
<!--- ##################使用mina版本SDK时的配置 start ##################-->
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>${mina.version}</version>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${common.pool.version}</version>
</dependency>
<!--- ##################使用mina版本SDK时的配置 end ##################-->
<!--- ##################使用netty本SDK时的配置 start ##################-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>${netty.version}</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.jdbc.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-http</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>${netty.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
<version>${netty.version}</version>
</dependency>
<!--- ##################使用netty本SDK时的配置 end ##################-->
<dependency>
<groupId>com.google.protobuf</groupId>
@ -97,23 +89,21 @@
</dependency>
<dependency>
<groupId>cn.teaey.apns4j</groupId>
<artifactId>apns4j</artifactId>
<version>1.1.4</version>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>0.15.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
@ -128,4 +118,4 @@
</plugin>
</plugins>
</build>
</project>
</project>

1
cim-boot-server/run.bat Normal file
View File

@ -0,0 +1 @@
java -Dcom.sun.akuma.Daemon=daemonized -Dspring.profiles.active=pro -jar ./cim-boot-server-4.2.0.jar

3
cim-boot-server/run.sh Executable file
View File

@ -0,0 +1,3 @@
#! /bin/bash
java -Dcom.sun.akuma.Daemon=daemonized -Dspring.profiles.active=dev -Dserver.port=9090 -jar ./cim-boot-server-4.2.0.jar &

View File

@ -0,0 +1,32 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BootApplication {
public static void main(String[] args) {
SpringApplication.run(BootApplication.class, args);
}
}

View File

@ -1,42 +0,0 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
import org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;
@SpringBootApplication
public class ServerLauncher {
public static void main(String[] args) {
SpringApplication.run(ServerLauncher.class, args);
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 用户标注通过 token查到的用户账号注入到Controller参数里面
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessToken {
}

View File

@ -0,0 +1,4 @@
package com.farsunset.cim.annotation;
public @interface CreateAction {
}

View File

@ -19,16 +19,17 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.handler;
package com.farsunset.cim.annotation;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.SentBody;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 心跳handler主要是让netty重置cheannel的空闲时间
* 用户标注通过 token查到的用户账号注入到Controller参数里面
*/
public class HeartbeatHandler implements CIMRequestHandler {
@Override
public void process(CIMSession session, SentBody body) {}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UID {
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,18 +19,19 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.handler;
package com.farsunset.cim.component.event;
import com.farsunset.cim.model.Message;
import org.springframework.context.ApplicationEvent;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.SentBody;
public class MessageEvent extends ApplicationEvent {
public MessageEvent(Message message) {
super(message);
}
public interface CIMRequestHandler {
@Override
public Message getSource() {
return (Message) source;
}
/**
* 处理长连接发送的请求
* @param session
* @param message
*/
void process(CIMSession session, SentBody message);
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.event;
import com.farsunset.cim.entity.Session;
import org.springframework.context.ApplicationEvent;
public class SessionEvent extends ApplicationEvent {
public SessionEvent(Session session) {
super(session);
}
@Override
public Session getSource() {
return (Session) source;
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.handler;
import com.farsunset.cim.component.handler.annotation.CIMHandler;
import com.farsunset.cim.component.redis.SignalRedisTemplate;
import com.farsunset.cim.constant.ChannelAttr;
import com.farsunset.cim.constants.Constants;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.handler.CIMRequestHandler;
import com.farsunset.cim.model.ReplyBody;
import com.farsunset.cim.model.SentBody;
import com.farsunset.cim.service.SessionService;
import io.netty.channel.Channel;
import org.springframework.http.HttpStatus;
import javax.annotation.Resource;
/**
* 客户长连接 账户绑定实现
*/
@CIMHandler(key = "client_bind")
public class BindHandler implements CIMRequestHandler {
@Resource
private SessionService sessionService;
@Resource
private SessionGroup sessionGroup;
@Resource
private SignalRedisTemplate signalRedisTemplate;
@Override
public void process(Channel channel, SentBody body) {
if (sessionGroup.isManaged(channel)){
return;
}
ReplyBody reply = new ReplyBody();
reply.setKey(body.getKey());
reply.setCode(HttpStatus.OK.value());
reply.setTimestamp(System.currentTimeMillis());
String uid = body.get("uid");
Session session = new Session();
session.setUid(uid);
session.setNid(channel.attr(ChannelAttr.ID).get());
session.setDeviceId(body.get("deviceId"));
session.setChannel(body.get("channel"));
session.setDeviceName(body.get("deviceName"));
session.setAppVersion(body.get("appVersion"));
session.setOsVersion(body.get("osVersion"));
session.setLanguage(body.get("language"));
channel.attr(ChannelAttr.UID).set(uid);
channel.attr(ChannelAttr.CHANNEL).set(session.getChannel());
channel.attr(ChannelAttr.DEVICE_ID).set(session.getDeviceId());
channel.attr(ChannelAttr.LANGUAGE).set(session.getLanguage());
/*
*存储到数据库
*/
sessionService.add(session);
channel.attr(Constants.SESSION_ID).set(session.getId());
/*
* 添加到内存管理
*/
sessionGroup.add(channel);
/*
*向客户端发送bind响应
*/
channel.writeAndFlush(reply);
/*
* 发送上线事件到集群中的其他实例控制其他设备下线
*/
signalRedisTemplate.bind(session);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.handler;
import com.farsunset.cim.component.handler.annotation.CIMHandler;
import com.farsunset.cim.constant.ChannelAttr;
import com.farsunset.cim.constants.Constants;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.handler.CIMRequestHandler;
import com.farsunset.cim.model.SentBody;
import com.farsunset.cim.service.SessionService;
import io.netty.channel.Channel;
import javax.annotation.Resource;
import java.util.Objects;
/**
* 连接断开时更新用户相关状态
*/
@CIMHandler(key = "client_closed")
public class ClosedHandler implements CIMRequestHandler {
@Resource
private SessionService sessionService;
@Override
public void process(Channel channel, SentBody message) {
Long sessionId = channel.attr(Constants.SESSION_ID).get();
if (sessionId == null){
return;
}
/*
* ios开启了apns也需要显示在线因此不删记录
*/
if (Objects.equals(channel.attr(ChannelAttr.CHANNEL).get(), Session.CHANNEL_IOS)){
sessionService.updateState(sessionId, Session.STATE_INACTIVE);
return;
}
sessionService.delete(sessionId);
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.handler;
import com.farsunset.cim.component.handler.annotation.CIMHandler;
import com.farsunset.cim.component.push.DefaultMessagePusher;
import com.farsunset.cim.component.redis.SignalRedisTemplate;
import com.farsunset.cim.constant.ChannelAttr;
import com.farsunset.cim.constants.Constants;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.handler.CIMRequestHandler;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.model.ReplyBody;
import com.farsunset.cim.model.SentBody;
import com.farsunset.cim.service.SessionService;
import io.netty.channel.Channel;
import org.springframework.http.HttpStatus;
import javax.annotation.Resource;
/**
* 客户端长连接通道发消息
*/
@CIMHandler(key = "client_message")
public class MessageHandler implements CIMRequestHandler {
@Resource
private DefaultMessagePusher defaultMessagePusher;
@Override
public void process(Channel channel, SentBody body) {
Message message = new Message();
/*
获取到当前链接的UID
*/
message.setSender(channel.attr(ChannelAttr.UID).get());
message.setReceiver(body.get("receiver"));
message.setAction(body.get("action"));
message.setContent(body.get("content"));
message.setFormat(body.get("format"));
message.setTitle(body.get("title"));
message.setExtra(body.get("extra"));
message.setId(System.currentTimeMillis());
defaultMessagePusher.push(message);
/*
* 将发送的消息ID 通过Replay异步发送给客户端
* 可通过reqId来对应上多个消息的各自ID
*/
ReplyBody reply = new ReplyBody();
reply.setKey(body.getKey());
reply.setCode(HttpStatus.OK.value());
reply.put("messageId",String.valueOf(message.getId()));
reply.put("requestId",body.get("requestId"));
reply.setTimestamp(System.currentTimeMillis());
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.handler.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface CIMHandler {
String key();
}

View File

@ -0,0 +1,65 @@
package com.farsunset.cim.component.logger;
import com.farsunset.cim.handler.LoggingHandler;
import com.farsunset.cim.model.Ping;
import com.farsunset.cim.model.Pong;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import org.springframework.stereotype.Component;
/**
* 自定义 CIM事件日志打印重写方法时注意需要有以下2种之一
* 1调用对应的ctx.fireXX(msg)方法;
* 2调用supper
*/
@ChannelHandler.Sharable
@Component
public class CIMEventLogger extends LoggingHandler {
/**
* 不打印客户端发送的Pong日志
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Pong){
ctx.fireChannelRead(msg);
return;
}
super.channelRead(ctx,msg);
}
/**
* 不打印服务端发送的Ping日志
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof Ping){
ctx.write(msg, promise);
return;
}
super.write(ctx,msg,promise);
}
/**
* 不打长连接空闲事件日志
* @param ctx
* @param evt
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
ctx.fireUserEventTriggered(evt);
}
}

View File

@ -0,0 +1,185 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.message;
import com.farsunset.cim.component.event.SessionEvent;
import com.farsunset.cim.constant.ChannelAttr;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.util.JSONUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOutboundInvoker;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* 集群环境下监控多设备登录情况控制是否其余终端下线的逻辑
*/
@Component
public class BindMessageListener implements MessageListener {
private static final String FORCE_OFFLINE_ACTION = "999";
private static final String SYSTEM_ID = "0";
/*
一个账号只能在同一个类型的终端登录
: 多个android或ios不能同时在线
一个android或ios可以和web桌面同时在线
*/
private final Map<String,String[]> conflictMap = new HashMap<>();
/*
* web可能同一个终端 打开多 tab页面可以同时保持连接
*/
private final Set<String> keepLiveChannels = new HashSet<>();
@Resource
private SessionGroup sessionGroup;
public BindMessageListener(){
conflictMap.put(Session.CHANNEL_ANDROID,new String[]{Session.CHANNEL_ANDROID,Session.CHANNEL_IOS});
conflictMap.put(Session.CHANNEL_IOS,new String[]{Session.CHANNEL_ANDROID,Session.CHANNEL_IOS});
conflictMap.put(Session.CHANNEL_WINDOWS,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
conflictMap.put(Session.CHANNEL_WEB,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
conflictMap.put(Session.CHANNEL_MAC,new String[]{Session.CHANNEL_WINDOWS,Session.CHANNEL_WEB,Session.CHANNEL_MAC});
keepLiveChannels.add(Session.CHANNEL_WEB);
}
@EventListener
public void onMessage(SessionEvent event) {
this.handle(event.getSource());
}
@Override
public void onMessage(org.springframework.data.redis.connection.Message redisMessage, byte[] bytes) {
Session session = JSONUtils.fromJson(redisMessage.getBody(), Session.class);
this.handle(session);
}
private void handle(Session session){
String uid = session.getUid();
String[] conflictChannels = conflictMap.get(session.getChannel());
if (ArrayUtils.isEmpty(conflictChannels)){
return;
}
Collection<Channel> channelList = sessionGroup.find(uid,conflictChannels);
channelList.removeIf(new KeepLivePredicate(session));
/*
* 同设备仅关闭连接无需通知客户端
*/
channelList.stream().filter(new SameDevicePredicate(session)).forEach(ChannelOutboundInvoker::close);
/*
* 不同设备关闭连接 通知客户端账号在其他设备登录
*/
channelList.stream().filter(new DifferentDevicePredicate(session)).forEach(new BreakOffMessageConsumer(uid,session.getDeviceName()));
}
private static class BreakOffMessageConsumer implements Consumer<Channel> {
private final Message message;
private BreakOffMessageConsumer(String uid,String deviceName) {
message = new Message();
message.setAction(FORCE_OFFLINE_ACTION);
message.setReceiver(uid);
message.setSender(SYSTEM_ID);
message.setContent(deviceName);
}
@Override
public void accept(Channel channel) {
channel.writeAndFlush(message).addListener(ChannelFutureListener.CLOSE);
}
}
private static class SameDevicePredicate implements Predicate<Channel> {
private final String deviceId;
private SameDevicePredicate(Session session) {
this.deviceId = session.getDeviceId();
}
@Override
public boolean test(Channel channel) {
return Objects.equals(this.deviceId,channel.attr(ChannelAttr.DEVICE_ID).get());
}
}
private static class DifferentDevicePredicate implements Predicate<Channel>{
private final SameDevicePredicate predicate;
private DifferentDevicePredicate(Session session) {
this.predicate = new SameDevicePredicate(session);
}
@Override
public boolean test(Channel channel) {
return !predicate.test(channel);
}
}
private class KeepLivePredicate implements Predicate<Channel>{
private final Session session;
private KeepLivePredicate(Session session) {
this.session = session;
}
@Override
public boolean test(Channel ioChannel) {
if (Objects.equals(session.getNid(),ioChannel.attr(ChannelAttr.ID).get())){
return true;
}
String deviceId = ioChannel.attr(ChannelAttr.DEVICE_ID).toString();
String channel = ioChannel.attr(ChannelAttr.CHANNEL).toString();
return keepLiveChannels.contains(channel) && Objects.equals(session.getDeviceId(),deviceId);
}
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.message;
import com.farsunset.cim.component.event.MessageEvent;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.util.JSONUtils;
import org.springframework.context.event.EventListener;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 集群环境下监听redis队列广播消息到每个实例进行推送
* 如果使用MQ的情况也最好替换为MQ消息队列
*/
@Component
public class PushMessageListener implements MessageListener {
@Resource
private SessionGroup sessionGroup;
@Override
public void onMessage(org.springframework.data.redis.connection.Message redisMessage, byte[] bytes) {
Message message = JSONUtils.fromJson(redisMessage.getBody(), Message.class);
this.onMessage(message);
}
@EventListener
public void onMessage(MessageEvent event) {
this.onMessage(event.getSource());
}
public void onMessage(Message message) {
String uid = message.getReceiver();
if (uid == null){
return;
}
sessionGroup.write(uid,message);
}
}

View File

@ -0,0 +1,28 @@
package com.farsunset.cim.component.predicate;
import com.farsunset.cim.auth.AuthPredicateInfo;
import org.springframework.stereotype.Component;
import java.util.function.Predicate;
/**
* WS 鉴权验证
*/
@Component
public class AuthPredicate implements Predicate<AuthPredicateInfo> {
@Override
public boolean test(AuthPredicateInfo auth) {
/*
可通过header或者uri传递参数
String token = auth.getHeader("token");
String token = auth.getParameter("token");
User user = doAuth(token);
auth.getCtx().attr(AttributeKey.valueOf("user_id")).set(user.getId());
*/
return true;
}
}

View File

@ -0,0 +1,25 @@
package com.farsunset.cim.component.predicate;
import org.springframework.stereotype.Component;
import java.util.function.Predicate;
@Component
public class BlacklistPredicate implements Predicate<String> {
/**
* IP黑名单拦截处理
* @param remoteAddress IP地址
* @return true:不拦截 false:拦截
*/
@Override
public boolean test(String remoteAddress) {
/*
* 自行根据IP判断是否需要拦截
*/
return true;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,9 +19,10 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.push;
package com.farsunset.cim.component.push;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.model.Message;
/*
* 消息发送实接口
@ -34,6 +35,6 @@ public interface CIMMessagePusher {
*
* @param msg
*/
public void push(Message msg);
void push(Message msg);
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.push;
import com.farsunset.cim.component.redis.KeyValueRedisTemplate;
import com.farsunset.cim.component.redis.SignalRedisTemplate;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.service.APNsService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/*
* 消息发送实现类
*
*/
@Component
public class DefaultMessagePusher implements CIMMessagePusher {
@Resource
private APNsService apnsService;
@Resource
private SignalRedisTemplate signalRedisTemplate;
@Resource
private KeyValueRedisTemplate keyValueRedisTemplate;
/**
* 向用户发送消息
*
* @param message
*/
public final void push(Message message) {
String uid = message.getReceiver();
/*
* 说明iOS客户端开启了apns
*/
String deviceToken = keyValueRedisTemplate.getDeviceToken(uid);
if(deviceToken != null) {
apnsService.push(message,deviceToken);
return;
}
/*
* 通过发送redis广播到集群中的每台实例获得当前UID绑定了连接并推送
* @see com.farsunset.cim.component.message.PushMessageListener
*/
signalRedisTemplate.push(message);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,44 +19,40 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.handler;
package com.farsunset.cim.component.redis;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.service.CIMSessionService;
import com.farsunset.cim.constants.Constants;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
/*
* 断开连接清除session
*
*/
@Component
public class SessionClosedHandler implements CIMRequestHandler {
public class KeyValueRedisTemplate extends StringRedisTemplate {
@Resource
private CIMSessionService cimSessionService;
@Override
public void process(CIMSession ios, SentBody message) {
Object quietly = ios.getAttribute(CIMConstant.KEY_QUIETLY_CLOSE);
if (Objects.equals(quietly, true)) {
return;
}
Object account = ios.getAttribute(CIMConstant.KEY_ACCOUNT);
if (account == null) {
return;
}
cimSessionService.remove(account.toString());
public KeyValueRedisTemplate(RedisConnectionFactory connectionFactory) {
super(connectionFactory);
}
public void set(String key ,String value) {
super.boundValueOps(key).set(value);
}
public String get(String key) {
return super.boundValueOps(key).get();
}
public String getDeviceToken(String uid){
return super.boundValueOps(String.format(Constants.APNS_DEVICE_TOKEN,uid)).get();
}
public void openApns(String uid,String deviceToken){
super.boundValueOps(String.format(Constants.APNS_DEVICE_TOKEN,uid)).set(deviceToken);
}
public void closeApns(String uid){
super.delete(String.format(Constants.APNS_DEVICE_TOKEN,uid));
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.component.redis;
import com.farsunset.cim.component.event.MessageEvent;
import com.farsunset.cim.component.event.SessionEvent;
import com.farsunset.cim.constants.Constants;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.util.JSONUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
@Component
public class SignalRedisTemplate extends StringRedisTemplate {
@Value("${spring.profiles.active}")
private String env;
@Resource
private ApplicationContext applicationContext;
public SignalRedisTemplate(LettuceConnectionFactory connectionFactory) {
super(connectionFactory);
connectionFactory.setValidateConnection(true);
}
/**
* 消息发送到 集群中的每个实例获取对应长连接进行消息写入
* @param message
*/
public void push(Message message) {
if (isDev()){
applicationContext.publishEvent(new MessageEvent(message));
return;
}
super.convertAndSend(Constants.PUSH_MESSAGE_INNER_QUEUE, JSONUtils.toJSONString(message));
}
/**
* 消息发送到 集群中的每个实例解决多终端在线冲突问题
* @param session
*/
public void bind(Session session) {
if (isDev()){
applicationContext.publishEvent(new SessionEvent(session));
return;
}
super.convertAndSend(Constants.BIND_MESSAGE_INNER_QUEUE, JSONUtils.toJSONString(session));
}
/**
* 本地调试环境下不走redis避免lettuce 经常command timeout
* @return
*/
private boolean isDev(){
return Objects.equals(env,"dev");
}
}

View File

@ -19,52 +19,42 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.client.model;
package com.farsunset.cim.component.redis;
import java.io.Serializable;
import java.util.HashMap;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* java |android 客户端请求结构
*
*/
public class Intent implements Serializable {
@Component
public class TokenRedisTemplate extends StringRedisTemplate {
private static final long serialVersionUID = 1L;
private static final String TOKEN_CACHE_PREFIX = "TOKEN_%s";
private String action;
private final HashMap<String, Object> data = new HashMap<String, Object>();
public Intent() {
public TokenRedisTemplate(RedisConnectionFactory connectionFactory) {
super(connectionFactory);
}
public Intent(String action) {
this.action = action;
public void save(String token, String uid) {
String key = String.format(TOKEN_CACHE_PREFIX,token);
super.boundValueOps(key).set(uid);
}
public String getAction() {
return action;
public String get(String token) {
String key = String.format(TOKEN_CACHE_PREFIX,token);
return super.boundValueOps(key).get();
}
public void setAction(String action) {
this.action = action;
public void remove(String token) {
String key = String.format(TOKEN_CACHE_PREFIX,token);
super.delete(key);
}
public void putExtra(String key, Object value) {
data.put(key, value);
}
public Object getExtra(String key) {
return data.get(key);
}
public long getLongExtra(String key, long defValue) {
Object v = getExtra(key);
try {
return Long.parseLong(v.toString());
} catch (Exception e) {
return defValue;
}
}
}

View File

@ -1,13 +1,46 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config;
import com.farsunset.cim.handler.BindHandler;
import com.farsunset.cim.handler.SessionClosedHandler;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.SentBody;
import org.springframework.beans.factory.annotation.Value;
import com.farsunset.cim.acceptor.AppSocketAcceptor;
import com.farsunset.cim.acceptor.WebsocketAcceptor;
import com.farsunset.cim.acceptor.config.SocketConfig;
import com.farsunset.cim.acceptor.config.WebsocketConfig;
import com.farsunset.cim.component.handler.annotation.CIMHandler;
import com.farsunset.cim.component.logger.CIMEventLogger;
import com.farsunset.cim.component.predicate.AuthPredicate;
import com.farsunset.cim.component.predicate.BlacklistPredicate;
import com.farsunset.cim.config.properties.APNsProperties;
import com.farsunset.cim.config.properties.CIMAppSocketProperties;
import com.farsunset.cim.config.properties.CIMWebsocketProperties;
import com.farsunset.cim.group.SessionGroup;
import com.farsunset.cim.group.TagSessionGroup;
import com.farsunset.cim.handler.CIMRequestHandler;
import com.farsunset.cim.model.SentBody;
import com.farsunset.cim.service.SessionService;
import io.netty.channel.Channel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
@ -15,36 +48,79 @@ import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@EnableConfigurationProperties({
APNsProperties.class,
CIMWebsocketProperties.class,
CIMAppSocketProperties.class})
@Configuration
public class CIMConfig implements CIMRequestHandler, ApplicationListener<ApplicationStartedEvent> {
@Resource
private ApplicationContext applicationContext;
private final HashMap<String,CIMRequestHandler> appHandlerMap = new HashMap<>();
@Resource
private SessionService sessionService;
private final HashMap<String,CIMRequestHandler> handlerMap = new HashMap<>();
@Bean
public SessionGroup sessionGroup() {
return new SessionGroup();
}
@Bean
public TagSessionGroup tagSessionGroup() {
return new TagSessionGroup();
}
@Bean(destroyMethod = "destroy")
public CIMNioSocketAcceptor getNioSocketAcceptor(@Value("${cim.app.port}") int port,
@Value("${cim.websocket.port}") int websocketPort) {
@Bean(destroyMethod = "destroy",initMethod = "bind")
@ConditionalOnProperty(name = {"cim.websocket.enable"},matchIfMissing = true)
public WebsocketAcceptor websocketAcceptor(CIMWebsocketProperties properties, AuthPredicate authPredicate, BlacklistPredicate blacklistPredicate, CIMEventLogger cimEventLogger) {
WebsocketConfig config = new WebsocketConfig();
config.setBlacklistPredicate(blacklistPredicate);
config.setAuthPredicate(authPredicate);
config.setPath(properties.getPath());
config.setPort(properties.getPort());
config.setProtocol(properties.getProtocol());
config.setOuterRequestHandler(this);
config.setEnable(properties.isEnable());
return new CIMNioSocketAcceptor.Builder()
.setAppPort(port)
.setWebsocketPort(websocketPort)
.setOuterRequestHandler(this)
.build();
config.setWriteIdle(properties.getWriteIdle());
config.setReadIdle(properties.getReadIdle());
config.setMaxPongTimeout(properties.getMaxPongTimeout());
config.setLoggingHandler(cimEventLogger);
return new WebsocketAcceptor(config);
}
@Bean(destroyMethod = "destroy",initMethod = "bind")
@ConditionalOnProperty(name = {"cim.app.enable"},matchIfMissing = true)
public AppSocketAcceptor appSocketAcceptor(CIMAppSocketProperties properties,BlacklistPredicate blacklistPredicate, CIMEventLogger cimEventLogger) {
SocketConfig config = new SocketConfig();
config.setPort(properties.getPort());
config.setOuterRequestHandler(this);
config.setEnable(properties.isEnable());
config.setWriteIdle(properties.getWriteIdle());
config.setReadIdle(properties.getReadIdle());
config.setMaxPongTimeout(properties.getMaxPongTimeout());
config.setLoggingHandler(cimEventLogger);
config.setBlacklistPredicate(blacklistPredicate);
return new AppSocketAcceptor(config);
}
@Override
public void process(CIMSession session, SentBody body) {
public void process(Channel channel, SentBody body) {
CIMRequestHandler handler = appHandlerMap.get(body.getKey());
CIMRequestHandler handler = handlerMap.get(body.getKey());
if(handler == null) {return ;}
handler.process(session, body);
handler.process(channel, body);
}
/*
@ -53,9 +129,19 @@ public class CIMConfig implements CIMRequestHandler, ApplicationListener<Applica
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
appHandlerMap.put("client_bind",applicationContext.getBean(BindHandler.class));
appHandlerMap.put("client_closed",applicationContext.getBean(SessionClosedHandler.class));
Map<String, CIMRequestHandler> beans = applicationContext.getBeansOfType(CIMRequestHandler.class);
applicationContext.getBean(CIMNioSocketAcceptor.class).bind();
for (Map.Entry<String, CIMRequestHandler> entry : beans.entrySet()) {
CIMRequestHandler handler = entry.getValue();
CIMHandler annotation = handler.getClass().getAnnotation(CIMHandler.class);
if (annotation != null){
handlerMap.put(annotation.key(),handler);
}
}
sessionService.deleteLocalhost();
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config;
import com.farsunset.cim.mvc.resolver.TokenArgumentResolver;
import com.farsunset.cim.mvc.resolver.UidArgumentResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.util.List;
@Configuration
public class MvcConfig implements WebMvcConfigurer{
@Resource
private HandlerInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/webrtc/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UidArgumentResolver());
argumentResolvers.add(new TokenArgumentResolver());
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config;
import com.farsunset.cim.constants.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import java.util.Objects;
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
@Autowired
public RedisConfig(LettuceConnectionFactory connectionFactory, @Value("${spring.profiles.active}") String profile){
if (Objects.equals("dev",profile)){
connectionFactory.setValidateConnection(true);
}
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,
MessageListener pushMessageListener,
MessageListener bindMessageListener){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(pushMessageListener,new ChannelTopic(Constants.PUSH_MESSAGE_INNER_QUEUE));
container.addMessageListener(bindMessageListener,new ChannelTopic(Constants.BIND_MESSAGE_INNER_QUEUE));
return container;
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.List;
@EnableOpenApi
@Configuration
public class SwaggerConfig {
@Bean
public Docket messageApiDocket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(new ApiInfoBuilder()
.title("CIM Push Service APIs.")
.description("消息发送相关接口")
.version("3.0")
.build())
.groupName("1、消息相关接口")
.select()
.apis(RequestHandlerSelectors.basePackage("com.farsunset.cim.mvc.controller.message"))
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
@Bean
public Docket webrtcApiDocket() {
return new Docket(DocumentationType.OAS_30)
.apiInfo(new ApiInfoBuilder()
.title("CIM Push Service APIs.")
.description("可用于webrtc通话信令接口")
.version("3.0")
.build())
.groupName("2、Webrtc相关接口")
.select()
.apis(RequestHandlerSelectors.basePackage("com.farsunset.cim.mvc.controller.webrtc"))
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private List<SecurityScheme> securitySchemes() {
List<SecurityScheme> schemeList = new ArrayList<>();
schemeList.add(new ApiKey("access-token", "access-token", "header"));
return schemeList;
}
private List<SecurityContext> securityContexts() {
List<SecurityContext> securityContextList = new ArrayList<>();
List<SecurityReference> securityReferenceList = new ArrayList<>();
securityReferenceList.add(new SecurityReference("access-token", new AuthorizationScope[]{new AuthorizationScope("global", "accessAnything")}));
securityContextList.add(SecurityContext
.builder()
.securityReferences(securityReferenceList)
.operationSelector(operationContext -> true)
.build()
);
return securityContextList;
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "cim.apns")
public class APNsProperties {
private boolean debug;
private String appId;
private final P12 p12 = new P12();
public static class P12 {
private String file;
private String password;
public void setFile(String file) {
this.file = file;
}
public void setPassword(String password) {
this.password = password;
}
public String getFile() {
return file;
}
public String getPassword() {
return password;
}
}
public P12 getP12() {
return p12;
}
public boolean isDebug() {
return debug;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
public String getP12File() {
return p12.file;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getP12Password() {
return p12.password;
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
@ConfigurationProperties(prefix = "cim.app")
public class CIMAppSocketProperties {
private boolean enable;
private Integer port;
/**
长链接写空闲时间触发时间(s)
心跳发送定时每当x秒无数据下发写入触发 服务端-->客户端 心跳事件
*/
private Duration writeIdle = Duration.ofSeconds(45);
/**
长链接读空闲时间触发时间(s)
心跳响应定时每当readIdle - writeIdle秒无数据接收触发心跳超时计数
*/
private Duration readIdle = Duration.ofSeconds(60);
/**
长链接最大允许心跳响应超时次数
达到该次数则 服务端断开链接
*/
private int maxPongTimeout = 1;
public void setPort(Integer port) {
this.port = port;
}
public Integer getPort() {
return port;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public Duration getWriteIdle() {
return writeIdle;
}
public void setWriteIdle(Duration writeIdle) {
if (writeIdle == null || writeIdle.getSeconds() <= 0){
return;
}
this.writeIdle = writeIdle;
}
public Duration getReadIdle() {
return readIdle;
}
public void setReadIdle(Duration readIdle) {
if (readIdle == null || readIdle.getSeconds() <= 0){
return;
}
this.readIdle = readIdle;
}
public int getMaxPongTimeout() {
return maxPongTimeout;
}
public void setMaxPongTimeout(int maxPongTimeout) {
if (maxPongTimeout <= 0){
return;
}
this.maxPongTimeout = maxPongTimeout;
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.config.properties;
import com.farsunset.cim.constant.WebsocketProtocol;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
@ConfigurationProperties(prefix = "cim.websocket")
public class CIMWebsocketProperties {
private boolean enable;
private Integer port;
private String path;
private WebsocketProtocol protocol;
/**
长链接写空闲时间触发时间(s)
心跳发送定时每当x秒无数据下发写入触发 服务端-->客户端 心跳事件
*/
private Duration writeIdle = Duration.ofSeconds(45);
/**
长链接读空闲时间触发时间(s)
心跳响应定时每当readIdle - writeIdle秒无数据接收触发心跳超时计数
*/
private Duration readIdle = Duration.ofSeconds(60);
/**
长链接最大允许心跳响应超时次数
达到该次数则 服务端断开链接
*/
private int maxPongTimeout = 1;
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public WebsocketProtocol getProtocol() {
return protocol;
}
public void setProtocol(WebsocketProtocol protocol) {
this.protocol = protocol;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public Duration getWriteIdle() {
return writeIdle;
}
public void setWriteIdle(Duration writeIdle) {
if (writeIdle == null || writeIdle.getSeconds() <= 0){
return;
}
this.writeIdle = writeIdle;
}
public Duration getReadIdle() {
return readIdle;
}
public void setReadIdle(Duration readIdle) {
if (readIdle == null || readIdle.getSeconds() <= 0){
return;
}
this.readIdle = readIdle;
}
public int getMaxPongTimeout() {
return maxPongTimeout;
}
public void setMaxPongTimeout(int maxPongTimeout) {
if (maxPongTimeout <= 0){
return;
}
this.maxPongTimeout = maxPongTimeout;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.constants;
import io.netty.util.AttributeKey;
public interface Constants {
String PUSH_MESSAGE_INNER_QUEUE = "signal/channel/PUSH_MESSAGE_INNER_QUEUE";
String BIND_MESSAGE_INNER_QUEUE = "signal/channel/BIND_MESSAGE_INNER_QUEUE";
String APNS_DEVICE_TOKEN = "APNS_OPEN_%s";
AttributeKey<Long> SESSION_ID = AttributeKey.valueOf("session_id");
}

View File

@ -19,44 +19,58 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.client.model;
package com.farsunset.cim.constants;
import java.io.Serializable;
public interface MessageAction {
import com.farsunset.cim.sdk.client.constant.CIMConstant;
/*
* 系统定制消息---语音通话请求
*/
String ACTION_900 = "900";
/**
* 客户端心跳响应
*/
public class HeartbeatResponse implements Serializable, Protobufable {
/*
* 系统定制消息---视频通话请求
*/
String ACTION_901 = "901";
private static final long serialVersionUID = 1L;
private static final String TAG = "CLIENT_HEARTBEAT_RESPONSE";
private static final String CMD_HEARTBEAT_RESPONSE = "CR";
/*
* 系统定制消息---通话接受
*/
String ACTION_902 = "902";
private static final HeartbeatResponse object = new HeartbeatResponse();
/*
* 系统定制消息---通话拒绝
*/
String ACTION_903 = "903";
private HeartbeatResponse() {
/*
* 系统定制消息---对方正忙
*/
String ACTION_904 = "904";
}
/*
* 系统定制消息---对方挂断
*/
String ACTION_905 = "905";
public static HeartbeatResponse getInstance() {
return object;
}
/*
* 系统定制消息---取消呼叫
*/
String ACTION_906 = "906";
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
/*
* 系统定制消息---同步ICE SPD
*/
String ACTION_907 = "907";
@Override
public String toString() {
return TAG;
}
/*
* 系统定制消息---同步Offer
*/
String ACTION_908 = "908";
@Override
public byte getType() {
return CIMConstant.ProtobufType.C_H_RS;
}
/*
* 系统定制消息---同步Answer
*/
String ACTION_909 = "909";
}

View File

@ -0,0 +1,210 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.entity;
import javax.persistence.*;
@Entity
@Table(name = "t_cim_session")
public class Session{
public static final int STATE_ACTIVE = 0;
public static final int STATE_APNS = 1;
public static final int STATE_INACTIVE = 2;
public static final String CHANNEL_IOS = "ios";
public static final String CHANNEL_ANDROID = "android";
public static final String CHANNEL_WINDOWS = "windows";
public static final String CHANNEL_MAC = "mac";
public static final String CHANNEL_WEB = "web";
/**
* 数据库主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
/**
* session绑定的用户账号
*/
@Column(name = "uid")
private String uid;
/**
* session在本台服务器上的ID
*/
@Column(name = "nid",length = 32,nullable = false)
private String nid;
/**
* 客户端ID (设备号码+应用包名),ios为deviceToken
*/
@Column(name = "device_id",length = 64,nullable = false)
private String deviceId;
/**
* 终端设备型号
*/
@Column(name = "device_name")
private String deviceName;
/**
* session绑定的服务器IP
*/
@Column(name = "host",length = 15,nullable = false)
private String host;
/**
* 终端设备类型
*/
@Column(name = "channel",length = 16,nullable = false)
private String channel;
/**
* 终端应用版本
*/
@Column(name = "app_version")
private String appVersion;
/**
* 终端系统版本
*/
@Column(name = "os_version")
private String osVersion;
/**
* 终端语言
*/
@Column(name = "language")
private String language;
/**
* 登录时间
*/
@Column(name = "bind_time")
private Long bindTime;
/**
* 状态
*/
private int state;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getNid() {
return nid;
}
public void setNid(String nid) {
this.nid = nid;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getAppVersion() {
return appVersion;
}
public void setAppVersion(String appVersion) {
this.appVersion = appVersion;
}
public String getOsVersion() {
return osVersion;
}
public void setOsVersion(String osVersion) {
this.osVersion = osVersion;
}
public Long getBindTime() {
return bindTime;
}
public void setBindTime(Long bindTime) {
this.bindTime = bindTime;
}
public String getLanguage() {
return language;
}
public void setLanguage(String language) {
this.language = language;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}

View File

@ -1,141 +0,0 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.handler;
import com.farsunset.cim.push.CIMMessagePusher;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.sdk.server.handler.CIMRequestHandler;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.sdk.server.model.ReplyBody;
import com.farsunset.cim.sdk.server.model.SentBody;
import com.farsunset.cim.service.CIMSessionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
/*
* 账号绑定实现
*
*/
@Component
public class BindHandler implements CIMRequestHandler {
private final Logger logger = LoggerFactory.getLogger(BindHandler.class);
@Resource
private CIMSessionService cimSessionService;
@Value("${server.host}")
private String host;
@Resource
private CIMMessagePusher defaultMessagePusher;
@Override
public void process(CIMSession newSession, SentBody body) {
ReplyBody reply = new ReplyBody();
reply.setKey(body.getKey());
reply.setCode(HttpStatus.OK.value());
reply.setTimestamp(System.currentTimeMillis());
try {
String account = body.get("account");
newSession.setAccount(account);
newSession.setDeviceId(body.get("deviceId"));
newSession.setHost(host);
newSession.setChannel(body.get("channel"));
newSession.setDeviceModel(body.get("device"));
newSession.setClientVersion(body.get("appVersion"));
newSession.setSystemVersion(body.get("osVersion"));
newSession.setBindTime(System.currentTimeMillis());
/*
* 由于客户端断线服务端可能会无法获知的情况客户端重连时需要关闭旧的连接
*/
CIMSession oldSession = cimSessionService.get(account);
/*
* 如果是账号已经在另一台终端登录则让另一个终端下线
*/
if (oldSession != null && fromOtherDevice(newSession,oldSession) && oldSession.isConnected()) {
sendForceOfflineMessage(oldSession, account, newSession.getDeviceModel());
}
/*
* 有可能是同一个设备重复连接则关闭旧的链接这种情况一般是客户端断网联网又重新链接上来之前的旧链接没有来得及通过心跳机制关闭在这里手动关闭
* 条件1连接来自是同一个设备
* 条件2.2个连接都是同一台服务器
*/
if (oldSession != null && !fromOtherDevice(newSession,oldSession) && Objects.equals(oldSession.getHost(),host)) {
closeQuietly(oldSession);
}
cimSessionService.save(newSession);
} catch (Exception exception) {
reply.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
logger.error("Bind has error",exception);
}
newSession.write(reply);
}
private boolean fromOtherDevice(CIMSession oldSession ,CIMSession newSession) {
return !Objects.equals(oldSession.getDeviceId(), newSession.getDeviceId());
}
private void sendForceOfflineMessage(CIMSession oldSession, String account, String deviceModel) {
Message msg = new Message();
msg.setAction(CIMConstant.MessageAction.ACTION_OFFLINE);
msg.setReceiver(account);
msg.setSender("system");
msg.setContent(deviceModel);
msg.setId(System.currentTimeMillis());
defaultMessagePusher.push(msg);
closeQuietly(oldSession);
}
private void closeQuietly(CIMSession oldSession) {
if (oldSession.isConnected() && Objects.equals(host, oldSession.getHost())) {
oldSession.setAttribute(CIMConstant.KEY_QUIETLY_CLOSE,true);
oldSession.closeOnFlush();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,15 +19,15 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.admin.controller;
package com.farsunset.cim.mvc.controller.admin;
import io.swagger.annotations.Api;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
@Api(produces = "application/json", tags = "页面导航",hidden = true)
public class NavigationController {
@GetMapping(value = "/")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,9 +19,10 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.admin.controller;
package com.farsunset.cim.mvc.controller.admin;
import com.farsunset.cim.service.CIMSessionService;
import com.farsunset.cim.service.SessionService;
import io.swagger.annotations.Api;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -30,15 +31,18 @@ import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
@Controller
@Api(produces = "application/json", tags = "在线用户页面",hidden = true)
@RequestMapping("/console/session")
public class SessionController {
@Resource
private CIMSessionService cimSessionService;
private SessionService sessionService;
@GetMapping(value = "/list")
public String list(Model model) {
model.addAttribute("sessionList", cimSessionService.list());
model.addAttribute("sessionList", sessionService.findAll());
return "console/session/manage";
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.controller.message;
import com.farsunset.cim.service.SessionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/apns")
@Api(produces = "application/json", tags = "APNs推送相关")
public class APNsController {
@ApiOperation(httpMethod = "POST", value = "开启apns")
@ApiImplicitParams({
@ApiImplicitParam(name = "deviceToken", value = "APNs的deviceToken", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
@ApiImplicitParam(name = "uid", value = "用户ID", paramType = "query", dataTypeClass = String.class,example = "0")
})
@PostMapping(value = "/open")
public ResponseEntity<Void> open(@RequestParam String uid , @RequestParam String deviceToken) {
sessionService.openApns(uid,deviceToken);
return ResponseEntity.ok().build();
}
@Resource
private SessionService sessionService;
@ApiOperation(httpMethod = "POST", value = "关闭apns")
@ApiImplicitParam(name = "uid", value = "用户ID", paramType = "query", dataTypeClass = String.class,example = "0")
@PostMapping(value = "/close")
public ResponseEntity<Void> close(@RequestParam String uid) {
sessionService.closeApns(uid);
return ResponseEntity.ok().build();
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.controller.message;
import com.farsunset.cim.component.push.DefaultMessagePusher;
import com.farsunset.cim.model.Message;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/message")
@Api(produces = "application/json", tags = "消息相关接口" )
public class MessageController {
@Resource
private DefaultMessagePusher defaultMessagePusher;
@ApiOperation(httpMethod = "POST", value = "发送消息")
@ApiImplicitParams({
@ApiImplicitParam(name = "sender", value = "发送者UID", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
@ApiImplicitParam(name = "receiver", value = "接收者UID", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
@ApiImplicitParam(name = "action", value = "消息动作", paramType = "query", dataTypeClass = String.class, required = true, example = ""),
@ApiImplicitParam(name = "title", value = "消息标题", paramType = "query", dataTypeClass = String.class, example = ""),
@ApiImplicitParam(name = "content", value = "消息内容", paramType = "query", dataTypeClass = String.class, example = ""),
@ApiImplicitParam(name = "format", value = "消息格式", paramType = "query", dataTypeClass = String.class, example = ""),
@ApiImplicitParam(name = "extra", value = "扩展字段", paramType = "query", dataTypeClass = String.class, example = ""),
})
@PostMapping(value = "/send")
public ResponseEntity<Long> send(@RequestParam String sender ,
@RequestParam String receiver ,
@RequestParam String action ,
@RequestParam(required = false) String title ,
@RequestParam(required = false) String content ,
@RequestParam(required = false) String format ,
@RequestParam(required = false) String extra) {
Message message = new Message();
message.setSender(sender);
message.setReceiver(receiver);
message.setAction(action);
message.setContent(content);
message.setFormat(format);
message.setTitle(title);
message.setExtra(extra);
message.setId(System.currentTimeMillis());
defaultMessagePusher.push(message);
return ResponseEntity.ok(message.getId());
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.controller.webrtc;
import com.farsunset.cim.annotation.AccessToken;
import com.farsunset.cim.mvc.response.ResponseEntity;
import com.farsunset.cim.service.AccessTokenService;
import io.swagger.annotations.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Api(produces = "application/json", tags = "用户登录接口" )
@Validated
public class UserController {
@Resource
private AccessTokenService accessTokenService;
@ApiOperation(httpMethod = "POST", value = "模拟登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "telephone", value = "手机号码", paramType = "query", dataTypeClass = String.class),
@ApiImplicitParam(name = "password", value = "密码", paramType = "query", dataTypeClass = String.class),
})
@PostMapping(value = "/login")
public ResponseEntity<?> login(@RequestParam String telephone) {
Map<String,Object> body = new HashMap<>();
body.put("id",Long.parseLong(telephone));
body.put("name","测试用户");
body.put("telephone","telephone");
ResponseEntity<Map<String,Object>> result = new ResponseEntity<>();
result.setData(body);
result.setToken(accessTokenService.generate(telephone));
result.setTimestamp(System.currentTimeMillis());
return result;
}
@ApiOperation(httpMethod = "GET", value = "退出登录")
@GetMapping(value = "/logout")
public ResponseEntity<Void> logout(@ApiParam(hidden = true) @AccessToken String token) {
accessTokenService.delete(token);
return ResponseEntity.make();
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.controller.webrtc;
import com.farsunset.cim.annotation.UID;
import com.farsunset.cim.component.push.DefaultMessagePusher;
import com.farsunset.cim.constants.MessageAction;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.mvc.request.WebrtcRequest;
import com.farsunset.cim.mvc.response.ResponseEntity;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/webrtc")
@Api(produces = "application/json", tags = "单人通话信令推送接口" )
public class WebrtcController {
@Resource
private DefaultMessagePusher defaultMessagePusher;
@ApiOperation(httpMethod = "POST", value = "发起单人语音通话")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/voice"})
public ResponseEntity<Void> voice(@ApiParam(hidden = true) @UID String uid,@RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_900);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "发起单人视频通话")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/video"})
public ResponseEntity<Void> video(@ApiParam(hidden = true) @UID String uid,@RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_901);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "接受通话")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/accept"})
public ResponseEntity<Void> accept(@ApiParam(hidden = true) @UID String uid,@RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_902);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "拒绝通话")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/reject"})
public ResponseEntity<Void> reject(@ApiParam(hidden = true) @UID String uid,@RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_903);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "反馈正忙")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/busy"})
public ResponseEntity<Void> busy(@ApiParam(hidden = true) @UID String uid, @RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_904);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "挂断通话")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/hangup"})
public ResponseEntity<Void> hangup(@ApiParam(hidden = true) @UID String uid,@RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_905);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "取消呼叫")
@ApiImplicitParam(name = "targetId", value = "对方用户ID", paramType = "query", dataTypeClass = Long.class)
@PostMapping(value = {"/cancel"})
public ResponseEntity<Void> cancel(@ApiParam(hidden = true) @UID String uid, @RequestParam String targetId) {
Message message = new Message();
message.setAction(MessageAction.ACTION_906);
message.setSender(uid);
message.setReceiver(targetId);
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "同步IceCandidate")
@PostMapping(value = {"/transmit/ice"})
public ResponseEntity<Void> ice(@ApiParam(hidden = true) @UID String uid,
@RequestBody WebrtcRequest request
) {
Message message = new Message();
message.setAction(MessageAction.ACTION_907);
message.setSender(uid);
message.setContent(request.getContent());
message.setReceiver(request.getUid());
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "同步offer")
@PostMapping(value = {"/transmit/offer"})
public ResponseEntity<Void> offer(@ApiParam(hidden = true) @UID String uid,
@RequestBody WebrtcRequest request
) {
Message message = new Message();
message.setAction(MessageAction.ACTION_908);
message.setSender(uid);
message.setContent(request.getContent());
message.setReceiver(request.getUid());
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
@ApiOperation(httpMethod = "POST", value = "同步answer")
@PostMapping(value = {"/transmit/answer"})
public ResponseEntity<Void> answer(@ApiParam(hidden = true) @UID String uid,
@RequestBody WebrtcRequest request
) {
Message message = new Message();
message.setAction(MessageAction.ACTION_909);
message.setSender(uid);
message.setContent(request.getContent());
message.setReceiver(request.getUid());
defaultMessagePusher.push(message);
return ResponseEntity.make();
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.interceptor;
import com.farsunset.cim.annotation.AccessToken;
import com.farsunset.cim.annotation.UID;
import com.farsunset.cim.service.AccessTokenService;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 在此鉴权获得UID
*/
@Component
public class TokenInterceptor implements HandlerInterceptor {
private static final String HEADER_TOKEN = "access-token";
@Resource
private AccessTokenService accessTokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader(HEADER_TOKEN);
String uid = accessTokenService.getUid(token);
/*
* 直接拒绝无token的接口调用请求或者token没有查询到对应的登录用户
*/
if (uid == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
request.setAttribute(UID.class.getName(), uid);
request.setAttribute(AccessToken.class.getName(), token);
return true;
}
}

View File

@ -19,45 +19,40 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.client.model;
package com.farsunset.cim.mvc.request;
import com.farsunset.cim.annotation.CreateAction;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import com.farsunset.cim.sdk.client.constant.CIMConstant;
@ApiModel("单人通话ice、offer、answer同步请求体")
public class WebrtcRequest implements Serializable {
/**
* 服务端心跳请求
*
*/
public class HeartbeatRequest implements Serializable, Protobufable {
@NotNull(message = "UID不能为空",groups = CreateAction.class)
@ApiModelProperty("对方UID")
private String uid;
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
private static final String CMD_HEARTBEAT_RESPONSE = "SR";
private static final HeartbeatRequest object = new HeartbeatRequest();
private HeartbeatRequest() {
@NotEmpty(message = "content不能超过2000个字符",groups = CreateAction.class)
@ApiModelProperty("ice信息json、offer或者answer的sdp")
private String content;
public String getUid() {
return uid;
}
public static HeartbeatRequest getInstance() {
return object;
public void setUid(String uid) {
this.uid = uid;
}
@Override
public byte[] getByteArray() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
public String getContent() {
return content;
}
@Override
public String toString() {
return TAG;
public void setContent(String content) {
this.content = content;
}
@Override
public byte getType() {
return CIMConstant.ProtobufType.S_H_RQ;
}
}

View File

@ -19,43 +19,28 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
package com.farsunset.cim.mvc.resolver;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.annotation.AccessToken;
import com.farsunset.cim.annotation.UID;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.io.Serializable;
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 服务端心跳请求
*/
public class HeartbeatRequest implements Serializable, Transportable {
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
private static final String CMD_HEARTBEAT_RESPONSE = "SR";
private static HeartbeatRequest object = new HeartbeatRequest();
private HeartbeatRequest() {
}
public static HeartbeatRequest getInstance() {
return object;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AccessToken.class);
}
@Override
public String toString() {
return TAG;
}
@Override
public byte[] getBody() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
@Override
public byte getType() {
return CIMConstant.ProtobufType.S_H_RQ;
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return webRequest.getAttribute(AccessToken.class.getName(),RequestAttributes.SCOPE_REQUEST);
}
}

View File

@ -19,45 +19,27 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.server.model;
package com.farsunset.cim.mvc.resolver;
import com.farsunset.cim.sdk.server.constant.CIMConstant;
import com.farsunset.cim.annotation.UID;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import java.io.Serializable;
public class UidArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 服务端心跳请求
*
*/
public class HeartbeatRequest implements Serializable, Transportable {
private static final long serialVersionUID = 1L;
private static final String TAG = "SERVER_HEARTBEAT_REQUEST";
private static final String CMD_HEARTBEAT_RESPONSE = "SR";
private static HeartbeatRequest object = new HeartbeatRequest();
private HeartbeatRequest() {
}
public static HeartbeatRequest getInstance() {
return object;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UID.class);
}
@Override
public byte[] getBody() {
return CMD_HEARTBEAT_RESPONSE.getBytes();
}
@Override
public String toString() {
return TAG;
}
@Override
public byte getType() {
return CIMConstant.ProtobufType.S_H_RQ;
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return webRequest.getAttribute(UID.class.getName(),RequestAttributes.SCOPE_REQUEST);
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.mvc.response;
import org.springframework.http.HttpStatus;
public class ResponseEntity<T> {
private int code = HttpStatus.OK.value();
private String message;
private T data;
private String token;
private Long timestamp;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getToken() {
return token;
}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public void setToken(String token) {
this.token = token;
}
public static ResponseEntity<Void> make(){
return new ResponseEntity<>();
}
public static ResponseEntity<Void> make(int code){
return make(code,null);
}
public static <T> ResponseEntity<T> make(int code,String message){
ResponseEntity<T> result = new ResponseEntity<>();
result.setCode(code);
result.setMessage(message);
return result;
}
public static ResponseEntity<Void> make(HttpStatus status){
ResponseEntity<Void> result = new ResponseEntity<>();
result.setCode(status.value());
result.setMessage(status.getReasonPhrase());
return result;
}
public static <Q> ResponseEntity<Q> make(HttpStatus status,String message){
ResponseEntity<Q> result = new ResponseEntity<>();
result.setCode(status.value());
result.setMessage(message);
return result;
}
public static <Q> ResponseEntity<Q> ok(Q data){
ResponseEntity<Q> result = new ResponseEntity<>();
result.setData(data);
return result;
}
}

View File

@ -1,96 +0,0 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.push;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.service.ApnsService;
import com.farsunset.cim.service.CIMSessionService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Objects;
/*
* 消息发送实现类
*
*/
@Component
public class DefaultMessagePusher implements CIMMessagePusher {
@Value("${server.host}")
private String host;
@Resource
private CIMSessionService cimSessionService;
@Resource
private ApnsService apnsService;
/*
* 向用户发送消息
*
* @param message
*/
@Override
public void push(Message message) {
CIMSession session = cimSessionService.get(message.getReceiver());
if(session == null) {
return;
}
/*
* IOS设备如果开启了apns则使用apns推送
*/
if (session.isIOSChannel() && session.isApnsEnable()) {
apnsService.push(message, session.getDeviceId());
return;
}
/*
* 服务器集群时判断当前session是否连接于本台服务器
* 如果连接到了其他服务器则转发请求到目标服务器
*/
if (session.isConnected() && !Objects.equals(host, session.getHost())) {
/*
* @TODO
* 在此调用目标服务器接口来发送如session.host = 123.123.123.123
* 调用目标服务器的消息发送接口http://123.123.123.123:8080/message/send
*/
return;
}
/*
* 如果是Android浏览器或者windows客户端则直接发送
*/
if (session.isConnected() && Objects.equals(host, session.getHost())) {
session.write(message);
}
}
}

View File

@ -1,34 +1,51 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.repository;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.entity.Session;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/*
* 正式场景下使用redis或者数据库来存储session信息
*/
@Repository
public class SessionRepository {
@Transactional(rollbackFor = Exception.class)
public interface SessionRepository extends JpaRepository<Session, Long> {
private ConcurrentHashMap<String, CIMSession> map = new ConcurrentHashMap<>();
@Modifying
@Query("delete from Session where host = ?1 ")
void deleteAll(String host);
@Modifying
@Query("update Session set state = :state where id = :id")
void updateState(long id,int state);
public void save(CIMSession session){
map.put(session.getAccount(),session);
}
@Modifying
@Query("update Session set state = " + Session.STATE_APNS + " where uid = ?1 and channel = ?2")
void openApns(String uid,String channel);
public CIMSession get(String account){
return map.get(account);
}
public void remove(String account){
map.remove(account);
}
public List<CIMSession> findAll(){
return new LinkedList<>(map.values());
}
@Modifying
@Query("update Session set state = " + Session.STATE_ACTIVE + " where uid = ?1 and channel = ?2")
void closeApns(String uid,String channel);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,9 +21,10 @@
*/
package com.farsunset.cim.service;
import com.farsunset.cim.sdk.server.model.Message;
public interface ApnsService {
import com.farsunset.cim.model.Message;
public interface APNsService {
void push(Message message, String deviceToken);
}

View File

@ -19,14 +19,14 @@
* *
***************************************************************************************
*/
package com.farsunset.cim.sdk.client.model;
package com.farsunset.cim.service;
/**
* 需要向另一端发送的结构体
*/
public interface Protobufable {
byte[] getByteArray();
public interface AccessTokenService {
byte getType();
String generate(String uid);
String getUid(String token);
void delete(String value);
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service;
import com.farsunset.cim.entity.Session;
import java.util.List;
/**
* 存储连接信息便于查看用户的链接信息
*/
public interface SessionService {
void add(Session session);
void delete(long id);
/**
* 删除本机的连接记录
*/
void deleteLocalhost();
void updateState(long id,int state);
void openApns(String uid,String deviceToken);
void closeApns(String uid);
List<Session> findAll();
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientBuilder;
import com.eatthepath.pushy.apns.ApnsPushNotification;
import com.eatthepath.pushy.apns.util.ApnsPayloadBuilder;
import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder;
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
import com.eatthepath.pushy.apns.util.TokenUtil;
import com.farsunset.cim.config.properties.APNsProperties;
import com.farsunset.cim.model.Message;
import com.farsunset.cim.service.APNsService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
@Service
public class APNsServiceImpl implements APNsService {
private static final Logger LOGGER = LoggerFactory.getLogger(APNsServiceImpl.class);
private final ApnsClient apnsClient;
private final APNsProperties properties;
@Autowired
public APNsServiceImpl(APNsProperties properties) throws IOException {
this.properties = properties;
InputStream stream = getClass().getResourceAsStream(properties.getP12File());
apnsClient = new ApnsClientBuilder()
.setApnsServer(properties.isDebug() ? ApnsClientBuilder.DEVELOPMENT_APNS_HOST : ApnsClientBuilder.PRODUCTION_APNS_HOST)
.setClientCredentials(stream, properties.getP12Password())
.build();
}
@SuppressWarnings("deprecation")
@Override
public void push(Message message, String deviceToken) {
if(StringUtils.isBlank(deviceToken)) {
return ;
}
ApnsPayloadBuilder payloadBuilder = new SimpleApnsPayloadBuilder();
payloadBuilder.setAlertTitle("您有一条新的消息");
payloadBuilder.setSound("default");
payloadBuilder.setBadgeNumber(1);
payloadBuilder.addCustomProperty("id",message.getId());
payloadBuilder.addCustomProperty("action",message.getAction());
payloadBuilder.addCustomProperty("content",message.getContent());
payloadBuilder.addCustomProperty("sender",message.getSender());
payloadBuilder.addCustomProperty("receiver",message.getReceiver());
payloadBuilder.addCustomProperty("format",message.getFormat());
payloadBuilder.addCustomProperty("extra",message.getExtra());
payloadBuilder.addCustomProperty("timestamp",message.getTimestamp());
String token = TokenUtil.sanitizeTokenString(deviceToken);
String payload = payloadBuilder.build();
ApnsPushNotification notification = new SimpleApnsPushNotification(token, properties.getAppId(), payload);
apnsClient.sendNotification(notification).whenComplete((response, cause) -> {
if (response != null) {
LOGGER.info("APNs push done.\ndeviceToken : {} \napnsPayload : {}",deviceToken,payload);
} else {
LOGGER.error("APNs push failed",cause);
}
});
}
}

View File

@ -21,56 +21,42 @@
*/
package com.farsunset.cim.service.impl;
import com.farsunset.cim.repository.SessionRepository;
import com.farsunset.cim.sdk.server.handler.CIMNioSocketAcceptor;
import com.farsunset.cim.sdk.server.model.CIMSession;
import com.farsunset.cim.service.CIMSessionService;
import com.farsunset.cim.component.redis.TokenRedisTemplate;
import com.farsunset.cim.service.AccessTokenService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.UUID;
@Service
public class CIMSessionServiceImpl implements CIMSessionService {
@Resource
private CIMNioSocketAcceptor nioSocketAcceptor;
@Resource
private SessionRepository sessionRepository;
public class AccessTokenServiceImpl implements AccessTokenService {
@Resource
private TokenRedisTemplate tokenRedisTemplate;
@Override
public void save(CIMSession session) {
sessionRepository.save(session);
public String getUid(String token) {
if (StringUtils.isBlank(token)){
return null;
}
return tokenRedisTemplate.get(token);
}
/*
*
* @param account 用户id
@Override
public void delete(String token) {
tokenRedisTemplate.delete(token);
}
/**
* 方便调试这里生成token为永不过期
* @param uid
* @return
*/
@Override
public CIMSession get(String account) {
CIMSession session = sessionRepository.get(account);
if (session != null){
session.setSession(nioSocketAcceptor.getManagedSession(session.getNid()));
}
return session;
}
@Override
public void remove(String account) {
sessionRepository.remove(account);
public String generate(String uid) {
String newToken = UUID.randomUUID().toString().replace("-","");
tokenRedisTemplate.save(newToken, uid);
return newToken;
}
@Override
public List<CIMSession> list() {
return sessionRepository.findAll();
}
}

View File

@ -1,86 +0,0 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import cn.teaey.apns4j.Apns4j;
import cn.teaey.apns4j.network.ApnsChannel;
import cn.teaey.apns4j.network.ApnsChannelFactory;
import cn.teaey.apns4j.network.ApnsGateway;
import com.farsunset.cim.sdk.server.model.Message;
import com.farsunset.cim.service.ApnsService;
import com.farsunset.cim.util.ApnsPayloadCompat;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.InputStream;
@Service
public class JavaApnsServiceImpl implements ApnsService {
private static final Logger LOGGER = LoggerFactory.getLogger(JavaApnsServiceImpl.class);
@Value("${apple.apns.p12.password}")
private String password;
@Value("${apple.apns.p12.file}")
private String p12Path;
@Value("${apple.apns.debug}")
private boolean isDebug;
@SuppressWarnings("deprecation")
@Override
public void push(Message message, String deviceToken) {
if(StringUtils.isBlank(deviceToken)) {
return ;
}
InputStream stream = getClass().getResourceAsStream(p12Path);
ApnsChannelFactory apnsChannelFactory = Apns4j.newChannelFactoryBuilder()
.keyStoreMeta(stream)
.keyStorePwd(password)
.apnsGateway(isDebug ? ApnsGateway.DEVELOPMENT : ApnsGateway.PRODUCTION)
.build();
ApnsChannel apnsChannel = apnsChannelFactory.newChannel();
try {
ApnsPayloadCompat apnsPayload = new ApnsPayloadCompat();
apnsPayload.setAction(message.getAction());
apnsPayload.setContent(message.getContent());
apnsPayload.setSender(message.getSender());
apnsPayload.setFormat(message.getFormat());
apnsPayload.setReceiver(message.getReceiver());
apnsChannel.send(deviceToken, apnsPayload);
LOGGER.info(deviceToken +"\r\ndata: {}",apnsPayload.toJsonString());
}catch(Exception exception) {
LOGGER.error("Apns has error",exception);
}finally {
apnsChannel.close();
IOUtils.closeQuietly(stream);
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.service.impl;
import com.farsunset.cim.component.redis.KeyValueRedisTemplate;
import com.farsunset.cim.entity.Session;
import com.farsunset.cim.repository.SessionRepository;
import com.farsunset.cim.service.SessionService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class SessionServiceImpl implements SessionService {
@Resource
private SessionRepository sessionRepository;
@Resource
private KeyValueRedisTemplate keyValueRedisTemplate;
private final String host;
public SessionServiceImpl() throws UnknownHostException {
host = InetAddress.getLocalHost().getHostAddress();
}
@Override
public void add(Session session) {
session.setBindTime(System.currentTimeMillis());
session.setHost(host);
sessionRepository.save(session);
}
@Override
public void delete(long id) {
sessionRepository.deleteById(id);
}
@Override
public void deleteLocalhost() {
sessionRepository.deleteAll(host);
}
@Override
public void updateState(long id, int state) {
sessionRepository.updateState(id,state);
}
@Override
public void openApns(String uid,String deviceToken) {
keyValueRedisTemplate.openApns(uid,deviceToken);
sessionRepository.openApns(uid,Session.CHANNEL_IOS);
}
@Override
public void closeApns(String uid) {
keyValueRedisTemplate.closeApns(uid);
sessionRepository.closeApns(uid,Session.CHANNEL_IOS);
}
@Override
public List<Session> findAll() {
return sessionRepository.findAll()
.stream()
.filter(session -> session.getState() == Session.STATE_ACTIVE || session.getState() == Session.STATE_APNS)
.collect(Collectors.toList());
}
}

View File

@ -1,81 +0,0 @@
/*
* Copyright 2013-2019 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.util;
import cn.teaey.apns4j.protocol.ApnsPayload;
public class ApnsPayloadCompat extends ApnsPayload {
private static final String DATA_FORMAT = "{\"aps\": {\"message\": {\"action\":\"%s\",\"content\":\"%s\",\"sender\":\"%s\",\"receiver\":\"%s\",\"format\":\"%s\"},\"content-available\": 1}}";
private String action;
private String content;
private String sender;
private String format;
private String receiver;
public String getReceiver() {
return receiver;
}
public void setReceiver(String receiver) {
this.receiver = receiver;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
@Override
public String toJsonString() {
return String.format(DATA_FORMAT, action, content, sender,receiver,format);
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2013-2022 Xia Jun(3979434@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
***************************************************************************************
* *
* Website : http://www.farsunset.com *
* *
***************************************************************************************
*/
package com.farsunset.cim.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.io.IOException;
import java.util.List;
public final class JSONUtils {
private static final JsonMapper OBJECT_MAPPER = JsonMapper.builder()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
.enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
public static String toJSONString(Object data) {
try {
return OBJECT_MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
}
public static <T> T fromJson(String str, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(str, clazz);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
public static <T> T fromJson(byte[] data, Class<T> clazz) {
try {
return OBJECT_MAPPER.readValue(data, clazz);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
public static <T> List<T> parseList(String str, Class<T> clazz) {
JavaType javaType = OBJECT_MAPPER.getTypeFactory().constructCollectionLikeType(List.class,clazz);
try {
return OBJECT_MAPPER.readValue(str, javaType);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}

Binary file not shown.

View File

@ -1,6 +1,56 @@
server.port=8080
server.host=127.0.0.1
# 服务端文档地址
# https://www.yuque.com/yuanfangxiyang/ma4ytb/vvy3iz#pW3DQ
server.port=8080
spring.jackson.default-property-inclusion=non_empty
#单台服务器可设置为dev广播消息走本地消息事件(参见SignalRedisTemplate.java)
#多台服务器集群环境设置为prd广播消息走三方消息队列
spring.profiles.active=dev
##################################################################
# JDBC Config #
##################################################################
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/cim?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username = cim
spring.datasource.password = f8HYPmssXL6XmZeK
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=MASTER_HIKARI_POOL
spring.datasource.hikari.max-lifetime=120000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=600000
##################################################################
# JPA Config #
##################################################################
spring.jpa.database = MYSQL
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto = update
spring.jpa.open-in-view = false
spring.jpa.hibernate.naming.implicit-strategy= org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
spring.jpa.hibernate.naming.physical-strategy= org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
##################################################################
# Redis Config #
##################################################################
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=12
spring.redis.lettuce.pool.max-active=10
spring.redis.lettuce.pool.max-wait= 10s
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=1
spring.redis.timeout=10s
##################################################################
# Freemarker Config #
@ -18,9 +68,26 @@ spring.messages.basename=i18n/messages
##################################################################
# CIM Config #
##################################################################
cim.app.port=23456
cim.websocket.port=34567
apple.apns.debug=false
apple.apns.p12.password= your p12 password
apple.apns.p12.file= /apns/lvxin.p12
#commented to disable this port.
cim.app.port=23456
cim.app.enable=true
cim.app.write-idle=45s
cim.app.read-idle=60s
cim.app.max-pong-timeout=3
cim.websocket.enable=true
cim.websocket.port=34567
cim.websocket.path=/
## json or protobuf
cim.websocket.protocol=protobuf
cim.websocket.write-idle=45s
cim.websocket.read-idle=60s
cim.websocket.max-pong-timeout=3
#please setting your p12 info and appId.
cim.apns.p12.file=/apns/app.p12
cim.apns.p12.password=123
cim.apns.debug=false
cim.apns.app-id=com.xxx.xxx.ios

View File

@ -32,6 +32,7 @@ module.common.headlogo = 头像
module.common.homepage = 主页
module.common.website = 网址
module.common.text = 文字
module.common.language = 语言
module.global.error.500.hint = 服务程序发生内部错误
module.global.error.400.hint = 请求参数类型不正确
@ -57,7 +58,7 @@ module.console.cimsession.channel = 终端
module.console.cimsession.app.version =应用版本
module.console.cimsession.os.version =系统版本
module.console.cimsession.deviceid =设备编号
module.console.cimsession.device.model =终端型号
module.console.cimsession.device.name =终端型号
module.console.cimsession.online.time =在线时长(秒)
module.console.cimsession.time.format ={0}秒

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %cyan(%logger{32}) - %msg %n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
</root>
</configuration>

View File

@ -3,9 +3,14 @@
<ul class="ui-nav-inner">
<li style="height: 50px;text-align: center;margin-top: 10px;">
<a type="button" target="_blank" href="/webclient" class="btn btn-danger" >
<span class="glyphicon glyphicon-globe"></span> WEB版本
<span class="glyphicon glyphicon-globe"></span> 网页终端
</a>
</li>
<li style="height: 50px;text-align: center;margin-top: 10px;">
<a type="button" target="_blank" href="/swagger-ui/index.html" class="btn btn-success" >
<span class="glyphicon glyphicon-leaf"></span> 接口文档
</a>
</li>
<li style="border-bottom: 1px solid #D1D6DA;"></li>
<li class="ui-item" id="sessionMenu">
<a href="/console/session/list">

View File

@ -20,8 +20,7 @@ function showMessageDialog(account){
function doSendMessage(){
var message = $('#message').val();
var account = $('#Saccount').val();
if($.trim(message)=='')
{
if($.trim(message)===''){
return;
}
showProcess("<@spring.message 'module.console.cimsession.sending'/>");
@ -50,34 +49,34 @@ function doSendMessage(){
<thead>
<tr class="tableHeader">
<th width="8%"><@spring.message 'module.console.cimsession.account'/></th>
<th width="6%"> <@spring.message 'module.console.cimsession.nid'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.channel'/></th>
<th width="14%"><@spring.message 'module.console.cimsession.deviceid'/></th>
<th width="10%"><@spring.message 'module.console.cimsession.device.model'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.app.version'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.os.version'/></th>
<th width="6%"><@spring.message 'module.console.cimsession.online.time'/></th>
<th width="25%"><@spring.message 'module.common.location'/></th>
<th width="8%"><@spring.message "module.common.operation"/></th>
<th width="8%"><@spring.message 'module.console.cimsession.nid'/></th>
<th width="8%"><@spring.message 'module.console.cimsession.channel'/></th>
<th width="15%"><@spring.message 'module.console.cimsession.deviceid'/></th>
<th width="15%"><@spring.message 'module.console.cimsession.device.name'/></th>
<th width="8%"><@spring.message 'module.console.cimsession.app.version'/></th>
<th width="8%"><@spring.message 'module.console.cimsession.os.version'/></th>
<th width="10%"><@spring.message 'module.common.language'/></th>
<th width="10%"><@spring.message 'module.console.cimsession.online.time'/></th>
<th width="10%"><@spring.message "module.common.operation"/></th>
</tr>
</thead>
<tbody>
<#list sessionList as cimsession>
<tr style="height: 50px;">
<td>${cimsession.account! }</td>
<td>${cimsession.uid! }</td>
<td><#if cimsession.nid??>${cimsession.nid}</#if></td>
<td>${cimsession.channel! }</td>
<td>${cimsession.deviceId! }</td>
<td>${cimsession.deviceModel! }</td>
<td>${cimsession.clientVersion! }</td>
<td>${cimsession.systemVersion! }</td>
<td>${cimsession.deviceName! }</td>
<td>${cimsession.appVersion! }</td>
<td>${cimsession.osVersion! }</td>
<td>${cimsession.language! }</td>
<td>
<@spring.messageArgs "module.console.cimsession.time.format",[((.now?long - cimsession.bindTime)/1000)?round?c] />
</td>
<td>${cimsession.location!}</td>
<td>
<div class="btn-group btn-group-xs">
<button type="button" class="btn btn-primary" style="padding: 5px;" onclick="showMessageDialog('${cimsession.account!}')">
<button type="button" class="btn btn-primary" style="padding: 5px;" onclick="showMessageDialog('${cimsession.uid!}')">
<span class="glyphicon glyphicon-send" style="top:2px;"></span>
<@spring.message 'module.console.cimsession.send.message'/>
</button>

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8"/>
<title>CIM Webclient </title>
<title>CIM Webclient for protobuf</title>
<link rel="shortcut icon" href="/image/favicon.ico" type="image/x-icon">
<link charset="utf-8" rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" />
<link charset="utf-8" rel="stylesheet" href="/css/common.css" />
@ -22,25 +22,32 @@
/** 当socket连接成功回调 **/
function onConnectFinished(){
CIMPushManager.bindAccount($('#account').val());
CIMPushManager.bind($('#account').val());
}
/** 当收到请求回复时候回调 **/
function onReplyReceived(reply)
{
console.log(reply);
if(reply.key=='client_bind' && reply.code==200)
{
hideProcess();
$('#LoginDialog').fadeOut();
if (reply.key === KEY_CLIENT_BIND && reply.code === CODE_OK) {
hideProcess();
$('#LoginDialog').fadeOut();
$('#MessageDialog').fadeIn();
$('#MessageDialog').addClass("in");
$("#current_account").text($('#account').val());
}
/**
* 链接鉴权失败
*/
if(reply.key === KEY_HANDSHAKE && reply.code === CODE_UNAUTHORIZED){
hideProcess();
showETip("鉴权失败");
}
$('#MessageDialog').fadeIn();
$('#MessageDialog').addClass("in");
$("#current_account").text($('#account').val());
}
}
/** 当收到消息时候回调 **/
@ -48,7 +55,10 @@
function onMessageReceived(message)
{
console.log(message);
if(message.action == ACTION_999){
/*
账户在其他地方登录了
*/
if(message.action === ACTION_999){
$('#MessageDialog').fadeOut();
$('#LoginDialog').fadeIn();
$('#LoginDialog').addClass("in");
@ -56,9 +66,8 @@
}
showNotification(message.content);
$("#messageList").append("<h6 style='text-align: center;'>时间:"+new Date(message.timestamp).toLocaleString()+"</h6>");
$("#messageList").append("<div class='alert alert-info' >"+message.content+"</div>");
var time = new Date(message.timestamp).toLocaleString();
$("#messageList").prepend("<div class='alert alert-info' >"+time+"</p></p>"+message.content+"</div>");
}
@ -83,8 +92,9 @@
$(document).ready(function(){
$('#LoginDialog').fadeIn();
$('#LoginDialog').addClass("in");
initNotification();
$('#host').val(CIM_HOST);
$('#port').val(CIM_PORT);
initNotification();
});
@ -130,12 +140,28 @@
</div>
<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
<span class="input-group-addon"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></span>
<span class="input-group-addon">账号</span>
<input type="text" class="form-control" id="account" maxlength="32" placeholder="帐号(数字或者英文字母)"
style="display: inline; width: 100%; height: 50px;" />
</div>
</div>
</div>
<div class="alert alert-success" role="alert" style="margin: 0 10px;">
cim.web.sdk.js中设置cim服务的IP(域名)和端口
</div>
<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" id="host" maxlength="32" readonly = "readonly"
style="display: inline; width: 100%; height: 50px;" />
</div>
<div class="input-group" style="margin-top: 30px;margin-left:10px;margin-right:10px;margin-bottom:30px;">
<span class="input-group-addon">port</span>
<input type="text" class="form-control" id="port" maxlength="32" readonly = "readonly"
style="display: inline; width: 100%; height: 50px;" />
</div>
</div>
<div class="modal-footer" style="text-align: center;">
<a type="button" class="btn btn-success btn-lg" onclick="doLogin()"
style="width: 300px;">登录</a>
@ -146,10 +172,13 @@
<!-- 消息提示页面 -->
<div class="modal fade" data-backdrop="static" id="MessageDialog" tabindex="-1" role="dialog" >
<div class="modal-dialog" style="width: 600px;margin: 30px auto;">
<div class="alert alert-success" role="alert">
通过<a href="/console/session/list" class="alert-link" target="_blank" >控制台</a>或者<a href="/swagger-ui/index.html" target="_blank" class="alert-link">调用接口</a>发送消息
</div>
<div class="modal-dialog" style="width: 720px;margin: 30px auto;">
<div class="modal-content" >
<div class="modal-header" style="text-align: center;">
<span style="float: left;">请在管理页面推送一条消息</span>
<span style="float: left;">消息显示面板</span>
<span style="float: right;color: #4caf50;">当前帐号:<span id="current_account"></span></span>
</div>
<div class="modal-body" id="messageList" style="min-height: 600px;" >

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
/*CIM服务器IP*/
const CIM_HOST = "127.0.0.1";
const CIM_HOST = window.location.hostname;
/*
*服务端 websocket端口
*/
@ -7,8 +7,7 @@ const CIM_PORT = 34567;
const CIM_URI = "ws://" + CIM_HOST + ":" + CIM_PORT;
const APP_VERSION = "1.0.0";
const APP_CHANNEL = "browser";
const APP_PACKAGE = "com.farsunset.cim";
const APP_CHANNEL = "web";
/*
*特殊的消息类型代表被服务端强制下线
@ -18,6 +17,25 @@ const DATA_HEADER_LENGTH = 1;
const MESSAGE = 2;
const REPLY_BODY = 4;
const SENT_BODY = 3;
const PING = 1;
const PONG = 0;
/*
* 握手鉴权常量
*/
const KEY_HANDSHAKE = "client_handshake";
const CODE_UNAUTHORIZED = "401";
const CODE_OK = "200";
const KEY_CLIENT_BIND = "client_bind";
/**
* PONG字符串转换后
* @type {Uint8Array}
*/
const PONG_BODY = new Uint8Array([80,79,78,71]);
let socket;
let manualStop = false;
@ -33,27 +51,27 @@ CIMPushManager.connect = function () {
socket.onclose = CIMPushManager.innerOnConnectionClosed;
};
CIMPushManager.bindAccount = function (account) {
CIMPushManager.bind = function (account) {
window.localStorage.account = account;
let deviceId = window.localStorage.deviceIddeviceId;
if (deviceId == '' || deviceId == undefined) {
let deviceId = window.localStorage.deviceId;
if (deviceId === '' || deviceId === undefined) {
deviceId = generateUUID();
window.localStorage.deviceId = deviceId;
}
let browser = getBrowser();
let body = new proto.com.farsunset.cim.sdk.web.model.SentBody();
body.setKey("client_bind");
body.setKey(KEY_CLIENT_BIND);
body.setTimestamp(new Date().getTime());
body.getDataMap().set("account", account);
body.getDataMap().set("uid", account);
body.getDataMap().set("channel", APP_CHANNEL);
body.getDataMap().set("appVersion", APP_VERSION);
body.getDataMap().set("osVersion", browser.version);
body.getDataMap().set("packageName", APP_PACKAGE);
body.getDataMap().set("deviceId", deviceId);
body.getDataMap().set("device", browser.name);
body.getDataMap().set("deviceName", browser.name);
body.getDataMap().set("language", navigator.language);
CIMPushManager.sendRequest(body);
};
@ -83,15 +101,21 @@ CIMPushManager.innerOnMessageReceived = function (e) {
let type = data[0];
let body = data.subarray(DATA_HEADER_LENGTH, data.length);
if (type == MESSAGE) {
if (type === PING) {
CIMPushManager.pong();
return;
}
if (type === MESSAGE) {
let message = proto.com.farsunset.cim.sdk.web.model.Message.deserializeBinary(body);
onInterceptMessageReceived(message.toObject(false));
return;
}
if (type == REPLY_BODY) {
if (type === REPLY_BODY) {
let message = proto.com.farsunset.cim.sdk.web.model.ReplyBody.deserializeBinary(body);
/**
/*
* 将proto对象转换成json对象去除无用信息
*/
let reply = {};
@ -101,13 +125,21 @@ CIMPushManager.innerOnMessageReceived = function (e) {
reply.timestamp = message.getTimestamp();
reply.data = {};
/**
/*
* 注意遍历map这里的参数 value在前key在后
*/
message.getDataMap().forEach(function (v, k) {
reply.data[k] = v;
});
/*
* 判断是否是握手鉴权失败
* 终止后续自动重连
*/
if(reply.key === KEY_HANDSHAKE && reply.code === CODE_UNAUTHORIZED){
manualStop = true;
}
onReplyReceived(reply);
}
};
@ -123,16 +155,24 @@ CIMPushManager.innerOnConnectionClosed = function (e) {
CIMPushManager.sendRequest = function (body) {
let data = body.serializeBinary();
let protobuf = new Uint8Array(data.length);
protobuf.set(data, 0);
let protobuf = new Uint8Array(data.length + 1);
protobuf[0] = SENT_BODY;
protobuf.set(data, 1);
socket.send(protobuf);
};
CIMPushManager.pong = function () {
let pong = new Uint8Array(PONG_BODY.byteLength + 1);
pong[0] = PONG;
pong.set(PONG_BODY,1);
socket.send(pong);
};
function onInterceptMessageReceived(message) {
/*
*被强制下线之后不再继续连接服务端
*/
if (message.action == ACTION_999) {
if (message.action === ACTION_999) {
manualStop = true;
}
/*
@ -174,7 +214,7 @@ function generateUUID() {
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid.replace(/-/g, '');
}

View File

@ -1,23 +0,0 @@
//
// CIMHeader.h
// CIMKit
//
// Created by mason on 2020/11/13.
//
#ifndef CIMHeader_h
#define CIMHeader_h
#import "GCDAsyncSocket.h"
#import "SentBody.pbobjc.h"
#import "Message.pbobjc.h"
#import "NSData+IM.h"
#import "NSString+IM.h"
#import "CIMSendMessageData.h"
#import "CIMService.h"
#endif /* CIMHeader_h */

View File

@ -1,103 +0,0 @@
//
// CIMMessageObserver.h
// CIMKit
//
// Created by mason on 2020/11/18.
//
#import <Foundation/Foundation.h>
#import "GCDAsyncSocket.h"
#import "CIMMessageModel.h"
@class CIMService;
/// 消息回调
@protocol CIMPeerMessageObserver <NSObject>
/// 接受到消息
/// @param msg msg description
-(void)cimHandleMessage:(CIMMessageModel * _Nonnull)msg;
/// 消息解析失败
/// @param data data description
-(void)cimHandleMessageError:(NSData * _Nonnull)data;
@end
/// 服务器连接回调
@protocol CIMConnectionObserver <NSObject>
@optional
/// 用户绑定成功
/// @param bindSuccess bindSuccess description
-(void)cimDidBindUserSuccess:(BOOL)bindSuccess;
/// 连接成功
-(void)cimDidConnectSuccess;
/// 断开连接
-(void)cimDidConnectClose;
/// 连接失败
/// @param error res description
-(void)cimDidConnectError:(NSError *_Nullable)error;
@end
NS_ASSUME_NONNULL_BEGIN
@interface CIMService : NSObject
+(CIMService*)instance;
/// 配置IM服务器
/// @param host host description
/// @param port port description
-(void)configHost:(NSString *)host onPort:(NSInteger)port;
/// 连接服务器并绑定用户
/// @param userId userId description
-(void)connectionBindUserId:(NSString *)userId;
/// 添加消息监听回调
/// @param observer observer description (可添加多个)不同时记得Remove
-(void)addMessageObserver:(id<CIMPeerMessageObserver>)observer;
/// 添加连接状态监听回调
/// @param observer observer description (可添加多个)不同时记得Remove
-(void)addConnectionObserver:(id<CIMConnectionObserver>)observer;
/// 移除监听
/// @param observer observer description
-(void)removeMessageObserver:(id<CIMPeerMessageObserver>)observer;
/// 移除监听回调
/// @param observer observer description
-(void)removeConnectionObserver:(id<CIMConnectionObserver>)observer;
/// 退出后台 断开连接
-(void)enterBackground;
/// 进入前台重新连接
-(void)enterForeground;
/// 重新连接
-(void)reconnect;
/// 断开连接
-(void)disconnect;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,5 +0,0 @@
#### iOS版本SDK介绍
iOS版本SDK是由开发者Siter(siterwu@gmail.com)贡献提供的感谢Siter在百忙之中做出的贡献。
---
## 源码和集成方式参见下面地址
## [https://gitee.com/Siter/cimkit](https://gitee.com/Siter/cimkit)

View File

@ -1 +0,0 @@
集成方式请参考文档和cim-use-examples中对应的demo

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: com.google.protobuf:protobuf-lite:3.0.1" level="project" />
<orderEntry type="module-library">
<library name="Maven: android:android:8.0.0">
<CLASSES>
<root url="jar://$MODULE_DIR$/libs/android.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
</component>
</module>

View File

@ -6,46 +6,151 @@
<groupId>com.farsunset</groupId>
<artifactId>cim-android-sdk</artifactId>
<version>3.8.2</version>
<version>4.2.15</version>
<packaging>jar</packaging>
<name>${project.groupId}:${project.artifactId}</name>
<description>Netty based instant messaging android sdk</description>
<url>http://farsunset.com</url>
<licenses>
<license>
<name>Apache License</name>
<url>http://www.apache.org/licenses/</url>
</license>
</licenses>
<scm>
<connection>https://github.com/farsunset/cim.git</connection>
<url>https://github.com/farsunset/cim</url>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<protobuf.lite.version>3.0.1</protobuf.lite.version>
<google.protobuf.version>3.25.4</google.protobuf.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-lite</artifactId>
<version>${protobuf.lite.version}</version>
<artifactId>protobuf-javalite</artifactId>
<version>${google.protobuf.version}</version>
</dependency>
<dependency>
<groupId>android</groupId>
<artifactId>android</artifactId>
<scope>system</scope>
<version>8.0.0</version>
<version>15.0.0</version>
<systemPath>${project.basedir}/libs/android.jar</systemPath>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<!-- Source -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Javadoc -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<show>private</show>
<nohelp>true</nohelp>
<charset>UTF-8</charset>
<encoding>UTF-8</encoding>
<docencoding>UTF-8</docencoding>
<additionalparam>-Xdoclint:none</additionalparam>
<!-- TODO 临时解决不规范的javadoc生成报错,后面要规范化后把这行去掉 -->
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- GPG -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<!--Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<fork>true</fork>
<verbose>true</verbose>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!--Release -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>3.0.1</version>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<developers>
<developer>
<name>Jun Xia</name>
<email>3979434@qq.com</email>
<roles>
<role>Developer</role>
</roles>
<timezone>+8</timezone>
</developer>
</developers>
</project>

View File

@ -0,0 +1 @@
mvn clean install deploy -P release

View File

@ -0,0 +1 @@
mvn clean install deploy -P release

View File

@ -29,7 +29,7 @@ import android.net.Uri;
class CIMCacheManager {
public static final String KEY_ACCOUNT = "KEY_ACCOUNT";
public static final String KEY_UID = "KEY_UID";
public static final String KEY_DEVICE_ID = "KEY_DEVICE_ID";
@ -43,8 +43,21 @@ class CIMCacheManager {
public static final String KEY_CIM_CONNECTION_STATE = "KEY_CIM_CONNECTION_STATE";
public static final String KEY_NTC_SWITCH = "KEY_NTC_SWITCH";
public static final String KEY_NTC_CHANNEL_NAME = "KEY_NTC_CHANNEL_NAME";
public static final String KEY_NTC_CHANNEL_MESSAGE = "KEY_NTC_CHANNEL_MESSAGE";
public static final String KEY_NTC_CHANNEL_ICON = "KEY_NTC_ICON";
public static final String CONTENT_URI = "content://%s.cim.provider";
static final String COLUMN_KEY = "key";
static final String COLUMN_VALUE = "value";
public static void remove(Context context, String key) {
ContentResolver resolver = context.getContentResolver();
@ -55,8 +68,8 @@ class CIMCacheManager {
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues();
values.put("value", value);
values.put("key", key);
values.put(COLUMN_KEY, key);
values.put(COLUMN_VALUE, value);
resolver.insert(Uri.parse(String.format(CONTENT_URI, context.getPackageName())), values);
}
@ -100,5 +113,4 @@ class CIMCacheManager {
String value = getString(context, key);
return value == null ? 0 : Integer.parseInt(value);
}
}

View File

@ -33,7 +33,7 @@ public class CIMCacheProvider extends ContentProvider {
@Override
public int delete(Uri arg0, String key, String[] arg2) {
getContext().getSharedPreferences(MODEL_KEY, Context.MODE_PRIVATE).edit().remove(key).apply();
getContext().getSharedPreferences(MODEL_KEY, Context.MODE_PRIVATE).edit().remove(key).commit();
return 0;
}
@ -44,9 +44,9 @@ public class CIMCacheProvider extends ContentProvider {
@Override
public Uri insert(Uri arg0, ContentValues values) {
String key = values.getAsString("key");
String value = values.getAsString("value");
getContext().getSharedPreferences(MODEL_KEY, Context.MODE_PRIVATE).edit().putString(key, value).apply();
String key = values.getAsString(CIMCacheManager.COLUMN_KEY);
String value = values.getAsString(CIMCacheManager.COLUMN_VALUE);
getContext().getSharedPreferences(MODEL_KEY, Context.MODE_PRIVATE).edit().putString(key, value).commit();
return null;
}
@ -57,7 +57,7 @@ public class CIMCacheProvider extends ContentProvider {
@Override
public Cursor query(Uri arg0, String[] arg1, String key, String[] arg3, String arg4) {
MatrixCursor cursor = new MatrixCursor(new String[]{"value"});
MatrixCursor cursor = new MatrixCursor(new String[]{CIMCacheManager.COLUMN_VALUE});
String value = getContext().getSharedPreferences(MODEL_KEY, Context.MODE_PRIVATE).getString(arg1[0], null);
cursor.addRow(new Object[]{value});
return cursor;

View File

@ -24,47 +24,45 @@ package com.farsunset.cim.sdk.android;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import com.farsunset.cim.sdk.android.logger.CIMLogger;
import com.farsunset.cim.sdk.android.coder.ClientMessageDecoder;
import com.farsunset.cim.sdk.android.coder.ClientMessageEncoder;
import com.farsunset.cim.sdk.android.constant.BundleKey;
import com.farsunset.cim.sdk.android.constant.CIMConstant;
import com.farsunset.cim.sdk.android.constant.IntentAction;
import com.farsunset.cim.sdk.android.logger.CIMLogger;
import com.farsunset.cim.sdk.android.model.*;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/*
* 连接服务端管理cim核心处理类管理连接以及消息处理
*/
class CIMConnectorManager {
private static CIMConnectorManager manager;
class CIMConnectManager {
private static final int READ_BUFFER_SIZE = 2048;
private static final int WRITE_BUFFER_SIZE = 1024;
private static final int CONNECT_TIME_OUT = 10 * 1000;
private static final int CONNECT_TIME_OUT = 5 * 1000;
/*
服务端在连接写空闲120秒的时候发送心跳请求给客户端所以客户端在空闲150秒后都没有收到任何数据则关闭链接并重新创建
*/
private static final int CONNECT_ALIVE_TIME_OUT = 150 * 1000;
private static final int CONNECT_ALIVE_TIME_OUT = 120 * 1000;
private static final CIMLogger LOGGER = CIMLogger.getLogger();
private static final HandlerThread IDLE_HANDLER_THREAD = new HandlerThread("READ-IDLE", Process.THREAD_PRIORITY_BACKGROUND);
private volatile SocketChannel socketChannel;
private SocketChannel socketChannel;
private final Context context;
@ -77,37 +75,27 @@ class CIMConnectorManager {
private final ClientMessageEncoder messageEncoder = new ClientMessageEncoder();
private final ClientMessageDecoder messageDecoder = new ClientMessageDecoder();
static {
IDLE_HANDLER_THREAD.start();
}
private final Random random = new Random();
private CIMConnectorManager(Context context) {
private final AtomicBoolean connecting = new AtomicBoolean(false);
public CIMConnectManager(Context context) {
this.context = context;
}
public synchronized static CIMConnectorManager getManager(Context context) {
if (manager == null) {
manager = new CIMConnectorManager(context);
}
return manager;
}
public void connect(final String host, final int port) {
if (!CIMPushManager.isNetworkConnected(context)) {
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_CONNECT_FAILED);
intent.setAction(IntentAction.ACTION_CONNECT_FAILED);
context.sendBroadcast(intent);
return;
}
if (isConnected()) {
if (isConnected() || connecting.get()) {
return;
}
@ -123,6 +111,8 @@ class CIMConnectorManager {
try {
connecting.set(true);
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(true);
socketChannel.socket().setTcpNoDelay(true);
@ -134,6 +124,8 @@ class CIMConnectorManager {
handleConnectedEvent();
connecting.set(false);
/*
*开始读取来自服务端的消息先读取3个字节的消息头
*/
@ -146,10 +138,12 @@ class CIMConnectorManager {
*/
close();
} catch (ConnectException | SocketTimeoutException ignore) {
handleConnectAbortedEvent();
} catch (IOException ignore) {
} catch (ConnectException | SocketTimeoutException | UnknownHostException exception) {
handleConnectFailedEvent(exception);
} catch (IOException exception) {
handleDisconnectedEvent();
}finally {
connecting.set(false);
}
});
}
@ -160,19 +154,14 @@ class CIMConnectorManager {
return;
}
try {
socketChannel.close();
} catch (IOException ignore) {
} finally {
this.onSessionClosed();
}
this.closeForce();
}
public boolean isConnected() {
return socketChannel != null && socketChannel.isConnected();
}
public void sendHeartbeat() {
public void pong() {
send(Pong.getInstance());
}
@ -205,13 +194,22 @@ class CIMConnectorManager {
}
private void closeForce(){
try {
socketChannel.close();
} catch (IOException ignore) {
} finally {
this.onSessionClosed();
}
}
private void onSessionCreated() {
LOGGER.sessionCreated(socketChannel);
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_CONNECT_FINISHED);
intent.setAction(IntentAction.ACTION_CONNECT_FINISHED);
context.sendBroadcast(intent);
}
@ -224,7 +222,7 @@ class CIMConnectorManager {
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_CONNECTION_CLOSED);
intent.setAction(IntentAction.ACTION_CONNECTION_CLOSED);
context.sendBroadcast(intent);
}
@ -243,7 +241,7 @@ class CIMConnectorManager {
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_MESSAGE_RECEIVED);
intent.setAction(IntentAction.ACTION_MESSAGE_RECEIVED);
intent.putExtra(Message.class.getName(), (Message) obj);
context.sendBroadcast(intent);
@ -252,7 +250,7 @@ class CIMConnectorManager {
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_REPLY_RECEIVED);
intent.setAction(IntentAction.ACTION_REPLY_RECEIVED);
intent.putExtra(ReplyBody.class.getName(), (ReplyBody) obj);
context.sendBroadcast(intent);
}
@ -266,33 +264,45 @@ class CIMConnectorManager {
if (message instanceof SentBody) {
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_SEND_FINISHED);
intent.setAction(IntentAction.ACTION_SEND_FINISHED);
intent.putExtra(SentBody.class.getName(), (SentBody) message);
context.sendBroadcast(intent);
}
}
private final Handler idleHandler = new Handler(IDLE_HANDLER_THREAD.getLooper()) {
private final Handler idleHandler = new Handler() {
@Override
public void handleMessage(android.os.Message m) {
onSessionIdle();
workerExecutor.execute(CIMConnectManager.this::onSessionIdle);
}
};
private void handleDisconnectedEvent() {
close();
closeForce();
}
private void handleConnectAbortedEvent() {
private void handleConnectFailedEvent(Exception exception) {
long interval = CIMConstant.RECONNECT_INTERVAL_TIME - (5 * 1000 - new Random().nextInt(15 * 1000));
long retryAfter;
LOGGER.connectFailure(interval);
if (exception instanceof UnknownHostException){
/*
* 通常是网络由WIFI切换为移动网出现这个异常
*/
retryAfter = 3000L;
}else {
/*
* 随机3-10秒后重连
*/
retryAfter = 3000L + random.nextInt(7001) ;
}
LOGGER.connectFailure(retryAfter);
Intent intent = new Intent();
intent.setPackage(context.getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_CONNECT_FAILED);
intent.putExtra("interval", interval);
intent.setAction(IntentAction.ACTION_CONNECT_FAILED);
intent.putExtra(BundleKey.KEY_RECONNECT_AFTER, retryAfter);
context.sendBroadcast(intent);
}

View File

@ -25,8 +25,10 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.os.Build;
import com.farsunset.cim.sdk.android.constant.BundleKey;
import com.farsunset.cim.sdk.android.constant.CIMConstant;
import com.farsunset.cim.sdk.android.constant.IntentAction;
import com.farsunset.cim.sdk.android.constant.ServiceAction;
import com.farsunset.cim.sdk.android.model.Message;
import com.farsunset.cim.sdk.android.model.ReplyBody;
import com.farsunset.cim.sdk.android.model.SentBody;
@ -38,7 +40,6 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
protected Context context;
@SuppressWarnings("deprecation")
@Override
public void onReceive(Context context, Intent intent) {
@ -50,6 +51,7 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
* 操作事件广播用于提高service存活率
*/
if (Intent.ACTION_USER_PRESENT.equals(action)
|| Intent.ACTION_BOOT_COMPLETED.equals(action)
|| Intent.ACTION_POWER_CONNECTED.equals(action)
|| Intent.ACTION_POWER_DISCONNECTED.equals(action)) {
startPushService();
@ -58,7 +60,7 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
/*
* 设备网络状态变化事件
*/
if (CIMConstant.IntentAction.ACTION_NETWORK_CHANGED.equals(action)
if (IntentAction.ACTION_NETWORK_CHANGED.equals(action)
|| ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
onDevicesNetworkChanged();
@ -67,36 +69,36 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
/*
* cim断开服务器事件
*/
if (CIMConstant.IntentAction.ACTION_CONNECTION_CLOSED.equals(action)) {
if (IntentAction.ACTION_CONNECTION_CLOSED.equals(action)) {
onInnerConnectionClosed();
}
/*
* cim连接服务器失败事件
*/
if (CIMConstant.IntentAction.ACTION_CONNECT_FAILED.equals(action)) {
long interval = intent.getLongExtra("interval", CIMConstant.RECONNECT_INTERVAL_TIME);
if (IntentAction.ACTION_CONNECT_FAILED.equals(action)) {
long interval = intent.getLongExtra(BundleKey.KEY_RECONNECT_AFTER, CIMConstant.RECONNECT_INTERVAL_TIME);
onInnerConnectFailed(interval);
}
/*
* cim连接服务器成功事件
*/
if (CIMConstant.IntentAction.ACTION_CONNECT_FINISHED.equals(action)) {
if (IntentAction.ACTION_CONNECT_FINISHED.equals(action)) {
onInnerConnectFinished();
}
/*
* 收到推送消息事件
*/
if (CIMConstant.IntentAction.ACTION_MESSAGE_RECEIVED.equals(action)) {
if (IntentAction.ACTION_MESSAGE_RECEIVED.equals(action)) {
onInnerMessageReceived((Message) intent.getSerializableExtra(Message.class.getName()), intent);
}
/*
* 获取收到replyBody成功事件
*/
if (CIMConstant.IntentAction.ACTION_REPLY_RECEIVED.equals(action)) {
if (IntentAction.ACTION_REPLY_RECEIVED.equals(action)) {
onReplyReceived((ReplyBody) intent.getSerializableExtra(ReplyBody.class.getName()));
}
@ -104,49 +106,36 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
/*
* 获取sendBody发送成功事件
*/
if (CIMConstant.IntentAction.ACTION_SEND_FINISHED.equals(action)) {
if (IntentAction.ACTION_SEND_FINISHED.equals(action)) {
onSentSucceed((SentBody) intent.getSerializableExtra(SentBody.class.getName()));
}
/*
* 重新连接如果断开的话
*/
if (CIMConstant.IntentAction.ACTION_CONNECTION_RECOVERY.equals(action)) {
if (IntentAction.ACTION_CONNECTION_RECOVERY.equals(action)) {
connect(0);
}
}
private void startPushService() {
Intent intent = new Intent(context, CIMPushService.class);
intent.setAction(CIMPushManager.ACTION_ACTIVATE_PUSH_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
intent.setAction(ServiceAction.ACTION_ACTIVATE_PUSH_SERVICE);
CIMPushManager.startService(context,intent);
}
private void onInnerConnectionClosed() {
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_CIM_CONNECTION_STATE, false);
if (CIMPushManager.isNetworkConnected(context)) {
connect(0);
}
connect(0L);
onConnectionClosed();
}
private void onInnerConnectFailed(long interval) {
if (CIMPushManager.isNetworkConnected(context)) {
onConnectFailed();
onConnectFailed();
connect(interval);
connect(interval);
}
}
private void onInnerConnectFinished() {
@ -157,18 +146,13 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
}
private void onDevicesNetworkChanged() {
if (CIMPushManager.isNetworkConnected(context)) {
connect(0);
}
onNetworkChanged();
}
private void connect(long delay) {
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.putExtra(CIMPushService.KEY_DELAYED_TIME, delay);
serviceIntent.setAction(CIMPushManager.ACTION_CREATE_CIM_CONNECTION);
serviceIntent.putExtra(BundleKey.KEY_DELAYED_TIME, delay);
serviceIntent.setAction(ServiceAction.ACTION_CREATE_CONNECTION);
CIMPushManager.startService(context, serviceIntent);
}
@ -181,13 +165,11 @@ public abstract class CIMEventBroadcastReceiver extends BroadcastReceiver {
}
private boolean isForceOfflineMessage(String action) {
return CIMConstant.MessageAction.ACTION_999.equals(action);
return CIMConstant.ACTION_999.equals(action);
}
/**
* 接收消息实现方法
*
* @param message
* @param intent
*/

View File

@ -33,35 +33,30 @@ public interface CIMEventListener {
/**
* 当收到服务端推送过来的消息时调用
*
* @param message
*/
void onMessageReceived(Message message);
/**
* 当调用CIMPushManager.sendRequest()向服务端发送请求获得相应时调用
*
* 当调用CIMPushManager.sendRequest()向服务端发送请求获得服务端响应时调用
* @param body
*/
void onReplyReceived(ReplyBody body);
/**
* 当调用CIMPushManager.sendRequest()向服务端发送请求成功时
*
* @param body
*/
void onSendFinished(SentBody body);
/**
* 当手机网络发生变化时调用
*
* @param info
*/
void onNetworkChanged(NetworkInfo info);
/**
* 当连接服务器成功时回调
*
* @param hasAutoBind true 已经自动绑定账号到服务器了不需要再手动调用bindAccount
*/
void onConnectFinished(boolean hasAutoBind);
@ -82,4 +77,5 @@ public interface CIMEventListener {
* @return 排序 值越大优先级越高
*/
int getEventDispatchOrder();
}

View File

@ -22,22 +22,21 @@
package com.farsunset.cim.sdk.android;
import android.net.NetworkInfo;
import android.util.Log;
import com.farsunset.cim.sdk.android.model.Message;
import com.farsunset.cim.sdk.android.model.ReplyBody;
import com.farsunset.cim.sdk.android.model.SentBody;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* CIM 消息监听器管理
*/
public class CIMListenerManager {
private static final ArrayList<CIMEventListener> cimListeners = new ArrayList<CIMEventListener>();
private static final List<CIMEventListener> cimListeners = new LinkedList<>();
private static final ReceiveComparator comparator = new ReceiveComparator();
private CIMListenerManager() {
@ -48,70 +47,57 @@ public class CIMListenerManager {
if (!cimListeners.contains(listener)) {
cimListeners.add(listener);
Collections.sort(cimListeners, comparator);
Collections.sort(cimListeners,comparator);
}
}
public static void removeMessageListener(CIMEventListener listener) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
CIMEventListener target = iterable.next();
if (listener.getClass() == target.getClass()) {
iterable.remove();
}
}
cimListeners.remove(listener);
}
public static void notifyOnNetworkChanged(NetworkInfo info) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onNetworkChanged(info);
for (CIMEventListener cimListener : getListeners()) {
cimListener.onNetworkChanged(info);
}
}
public static void notifyOnConnectFinished(boolean hasAutoBind) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onConnectFinished(hasAutoBind);
for (CIMEventListener cimListener : getListeners()) {
cimListener.onConnectFinished(hasAutoBind);
}
}
public static void notifyOnMessageReceived(Message message) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onMessageReceived(message);
for (CIMEventListener cimListener : getListeners()) {
cimListener.onMessageReceived(message);
}
}
public static void notifyOnConnectionClosed() {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onConnectionClosed();
for (CIMEventListener cimListener : getListeners()) {
cimListener.onConnectionClosed();
}
}
public static void notifyOnConnectFailed() {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onConnectFailed();
for (CIMEventListener cimListener : getListeners()) {
cimListener.onConnectFailed();
}
}
public static void notifyOnReplyReceived(ReplyBody body) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onReplyReceived(body);
for (CIMEventListener cimListener : getListeners()) {
cimListener.onReplyReceived(body);
}
}
public static void notifyOnSendFinished(SentBody body) {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
iterable.next().onSendFinished(body);
for (CIMEventListener cimListener : getListeners()) {
cimListener.onSendFinished(body);
}
}
@ -119,11 +105,8 @@ public class CIMListenerManager {
cimListeners.clear();
}
public static void logListenersName() {
Iterator<CIMEventListener> iterable = cimListeners.iterator();
while (iterable.hasNext()) {
Log.i(CIMEventListener.class.getSimpleName(), "#######" + iterable.next().getClass().getName() + "#######");
}
public static List<CIMEventListener> getListeners() {
return new LinkedList<>(cimListeners);
}
/**
@ -133,7 +116,6 @@ public class CIMListenerManager {
@Override
public int compare(CIMEventListener arg1, CIMEventListener arg2) {
int order1 = arg1.getEventDispatchOrder();
int order2 = arg2.getEventDispatchOrder();
return Integer.compare(order2, order1);

View File

@ -28,39 +28,29 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.LocaleList;
import android.text.TextUtils;
import com.farsunset.cim.sdk.android.constant.BundleKey;
import com.farsunset.cim.sdk.android.constant.IntentAction;
import com.farsunset.cim.sdk.android.constant.RequestKey;
import com.farsunset.cim.sdk.android.constant.ServiceAction;
import com.farsunset.cim.sdk.android.logger.CIMLogger;
import com.farsunset.cim.sdk.android.constant.CIMConstant;
import com.farsunset.cim.sdk.android.model.SentBody;
import java.util.Locale;
import java.util.UUID;
/**
* CIM 功能接口
* CIM客户端sdk功能接口
*/
public class CIMPushManager {
protected static final String ACTION_CREATE_CIM_CONNECTION = "ACTION_CREATE_CIM_CONNECTION";
protected static final String ACTION_DESTROY_CIM_SERVICE = "ACTION_DESTROY_CIM_SERVICE";
protected static final String ACTION_ACTIVATE_PUSH_SERVICE = "ACTION_ACTIVATE_PUSH_SERVICE";
protected static final String ACTION_SEND_REQUEST_BODY = "ACTION_SEND_REQUEST_BODY";
protected static final String ACTION_CLOSE_CIM_CONNECTION = "ACTION_CLOSE_CIM_CONNECTION";
protected static final String ACTION_SET_LOGGER_EATABLE = "ACTION_SET_LOGGER_EATABLE";
protected static final String ACTION_SHOW_PERSIST_NOTIFICATION = "ACTION_SHOW_PERSIST_NOTIFICATION";
protected static final String ACTION_HIDE_PERSIST_NOTIFICATION = "ACTION_HIDE_PERSIST_NOTIFICATION";
protected static final String ACTION_CIM_CONNECTION_PONG = "ACTION_CIM_CONNECTION_PONG";
/**
* 初始化,连接服务端在程序启动页或者 在Application里调用
* @param context
* @param host cim服务端IP或者域名
* @param port cim服务端端口
*/
public static void connect(Context context, String host, int port) {
@ -69,100 +59,160 @@ public class CIMPushManager {
return;
}
CIMCacheManager.putString(context, CIMCacheManager.KEY_CIM_SERVER_HOST, host);
CIMCacheManager.putInt(context, CIMCacheManager.KEY_CIM_SERVER_PORT, port);
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_CIM_DESTROYED, false);
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_MANUAL_STOP, false);
CIMCacheManager.remove(context, CIMCacheManager.KEY_ACCOUNT);
CIMCacheManager.remove(context, CIMCacheManager.KEY_UID);
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ACTION_CREATE_CIM_CONNECTION);
serviceIntent.setAction(ServiceAction.ACTION_CREATE_CONNECTION);
startService(context, serviceIntent);
}
/**
* 设置SDK日志打印开关
* @param context
* @param enable
*/
public static void setLoggerEnable(Context context, boolean enable) {
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.putExtra(CIMPushService.KEY_LOGGER_ENABLE, enable);
serviceIntent.setAction(ACTION_SET_LOGGER_EATABLE);
serviceIntent.putExtra(BundleKey.KEY_LOGGER_ENABLE, enable);
serviceIntent.setAction(ServiceAction.ACTION_SET_LOGGER_EATABLE);
startService(context, serviceIntent);
}
/**
* 开启常驻通知栏
* @param context
* @param icon 通知图标
* @param channel 通知channel
* @param message 显示内容
*/
public static void startForeground(Context context,int icon, String channel , String message) {
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.putExtra(CIMPushService.KEY_NOTIFICATION_MESSAGE, message);
serviceIntent.putExtra(CIMPushService.KEY_NOTIFICATION_CHANNEL, channel);
serviceIntent.putExtra(CIMPushService.KEY_NOTIFICATION_ICON, icon);
serviceIntent.setAction(ACTION_SHOW_PERSIST_NOTIFICATION);
startService(context, serviceIntent);
}
public static void cancelForeground(Context context) {
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ACTION_HIDE_PERSIST_NOTIFICATION);
serviceIntent.putExtra(BundleKey.KEY_NOTIFICATION_MESSAGE, message);
serviceIntent.putExtra(BundleKey.KEY_NOTIFICATION_CHANNEL, channel);
serviceIntent.putExtra(BundleKey.KEY_NOTIFICATION_ICON, icon);
serviceIntent.setAction(ServiceAction.ACTION_SHOW_PERSIST_NOTIFICATION);
startService(context, serviceIntent);
}
/**
* 设置一个账号登录到服务端
* 关闭常驻通知栏
* @param context
*/
public static void bindAccount(Context context, String account) {
public static void cancelForeground(Context context) {
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ServiceAction.ACTION_HIDE_PERSIST_NOTIFICATION);
startService(context, serviceIntent);
}
if (isDestroyed(context) || account == null || account.trim().length() == 0) {
/**
* bind账户
* 通知服务端 长连接和uid进行关联
* @param context
* @param uid 用户标识
*/
public static void bind(Context context, long uid) {
bind(context,String.valueOf(uid));
}
/**
* bind账户
* 通知服务端 长连接和uid进行关联
* @param context
* @param uid 用户标识
*/
public static void bind(Context context, String uid) {
if (isDestroyed(context)) {
return;
}
sendBindRequest(context, account);
sendBindRequest(context, uid);
}
/**
* 通知服务端给当前长连接设置tag
* @param context
* @param tag 标识
*/
public static void setTag(Context context, String tag) {
SentBody sent = new SentBody();
sent.setKey(RequestKey.CLIENT_SET_TAG);
sent.put("tag", tag);
sendRequest(context, sent);
}
/**
* 通知服务端清除tag
* @param context
*/
public static void removeTag(Context context) {
SentBody sent = new SentBody();
sent.setKey(RequestKey.CLIENT_REMOVE_TAG);
sendRequest(context, sent);
}
/**
* 长连接发送一次心跳响应
* @param context
*/
public static void pong(Context context) {
if (isDestroyed(context) || isStopped(context)) {
return;
}
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ACTION_CIM_CONNECTION_PONG);
serviceIntent.setAction(ServiceAction.ACTION_CREATE_CONNECTION);
startService(context, serviceIntent);
}
private static void sendBindRequest(Context context, String account) {
private static void sendBindRequest(Context context, String uid) {
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_MANUAL_STOP, false);
CIMCacheManager.putString(context, CIMCacheManager.KEY_ACCOUNT, account);
CIMCacheManager.putString(context, CIMCacheManager.KEY_UID, uid);
SentBody sent = new SentBody();
sent.setKey(CIMConstant.RequestKey.CLIENT_BIND);
sent.put("account", account);
sent.put("deviceId", getDeviceId(context));
sent.setKey(RequestKey.CLIENT_BIND);
sent.put("uid", String.valueOf(uid));
sent.put("channel", "android");
sent.put("device", Build.MODEL);
sent.put("deviceId", getDeviceId(context));
sent.put("deviceName", Build.MODEL);
sent.put("appVersion", getVersionName(context));
sent.put("osVersion", Build.VERSION.RELEASE);
sent.put("packageName", context.getPackageName());
sent.put("language", getLanguage());
sent.setTimestamp(System.currentTimeMillis());
sendRequest(context, sent);
}
protected static boolean autoBindAccount(Context context) {
String account = CIMCacheManager.getString(context, CIMCacheManager.KEY_ACCOUNT);
if (account == null || account.trim().length() == 0 || isDestroyed(context)) {
String uid = CIMCacheManager.getString(context, CIMCacheManager.KEY_UID);
if (uid == null || isDestroyed(context)) {
return false;
}
sendBindRequest(context, account);
sendBindRequest(context, uid);
return true;
}
/**
* 发送一个CIM请求
* 向服务端发送一次自定义业务请求
* @param context
* @param body 请求体
*/
public static void sendRequest(Context context, SentBody body) {
@ -171,8 +221,8 @@ public class CIMPushManager {
}
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.putExtra(CIMPushService.KEY_SEND_BODY, body);
serviceIntent.setAction(ACTION_SEND_REQUEST_BODY);
serviceIntent.putExtra(BundleKey.KEY_SEND_BODY, body);
serviceIntent.setAction(ServiceAction.ACTION_SEND_REQUEST_BODY);
startService(context, serviceIntent);
}
@ -189,7 +239,7 @@ public class CIMPushManager {
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_MANUAL_STOP, true);
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ACTION_CLOSE_CIM_CONNECTION);
serviceIntent.setAction(ServiceAction.ACTION_CREATE_CONNECTION);
startService(context, serviceIntent);
}
@ -200,10 +250,10 @@ public class CIMPushManager {
public static void destroy(Context context) {
CIMCacheManager.putBoolean(context, CIMCacheManager.KEY_CIM_DESTROYED, true);
CIMCacheManager.putString(context, CIMCacheManager.KEY_ACCOUNT, null);
CIMCacheManager.remove(context, CIMCacheManager.KEY_UID);
Intent serviceIntent = new Intent(context, CIMPushService.class);
serviceIntent.setAction(ACTION_DESTROY_CIM_SERVICE);
serviceIntent.setAction(ServiceAction.ACTION_CREATE_CONNECTION);
startService(context, serviceIntent);
}
@ -220,33 +270,59 @@ public class CIMPushManager {
autoBindAccount(context);
}
/**
* 获取sdk是否已经销毁的
* @param context
* @return
*/
public static boolean isDestroyed(Context context) {
return CIMCacheManager.getBoolean(context, CIMCacheManager.KEY_CIM_DESTROYED);
}
/**
* 判断是否暂停接收消息
* @param context
* @return
*/
public static boolean isStopped(Context context) {
return CIMCacheManager.getBoolean(context, CIMCacheManager.KEY_MANUAL_STOP);
}
/**
* 判断于服务端连接是否正常
* @param context
*/
public static boolean isConnected(Context context) {
return CIMCacheManager.getBoolean(context, CIMCacheManager.KEY_CIM_CONNECTION_STATE);
}
/**
* 判断客户端网络连接是否正常
* @param context
*/
public static boolean isNetworkConnected(Context context) {
NetworkInfo networkInfo = getNetworkInfo(context);
return networkInfo != null && networkInfo.isConnected();
}
/**
* 获取服务端网络信息
* @param context
*/
public static NetworkInfo getNetworkInfo(Context context) {
return ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
}
public static void startService(Context context, Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
protected static void startService(Context context, Intent intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent);
return;
}
try {
context.startForegroundService(intent);
}catch (Exception ignore){
context.sendBroadcast(new Intent(IntentAction.ACTION_CONNECTION_RECOVERY));
}
}
@ -256,8 +332,7 @@ public class CIMPushManager {
try {
PackageInfo mPackageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return mPackageInfo.versionName;
} catch (NameNotFoundException ignore) {
}
} catch (NameNotFoundException ignore) {}
return null;
}
@ -276,4 +351,10 @@ public class CIMPushManager {
return deviceId;
}
private static String getLanguage(){
Locale locale = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? LocaleList.getDefault().get(0) : Locale.getDefault();
return locale.getLanguage() + "-" + locale.getCountry();
}
}

View File

@ -26,18 +26,22 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ServiceInfo;
import android.net.ConnectivityManager;
import android.net.Network;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.util.Log;
import com.farsunset.cim.sdk.android.constant.CIMConstant;
import com.farsunset.cim.sdk.android.constant.BundleKey;
import com.farsunset.cim.sdk.android.constant.IntentAction;
import com.farsunset.cim.sdk.android.constant.ServiceAction;
import com.farsunset.cim.sdk.android.logger.CIMLogger;
import com.farsunset.cim.sdk.android.model.Pong;
import com.farsunset.cim.sdk.android.model.SentBody;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
/**
* 与服务端连接服务
@ -46,63 +50,44 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class CIMPushService extends Service {
static final String KEY_SEND_BODY = "KEY_SEND_BODY";
static final String KEY_DELAYED_TIME = "KEY_DELAYED_TIME";
static final String KEY_LOGGER_ENABLE = "KEY_LOGGER_ENABLE";
static final String KEY_NOTIFICATION_MESSAGE = "KEY_NOTIFICATION_MESSAGE";
static final String KEY_NOTIFICATION_CHANNEL = "KEY_NOTIFICATION_CHANNEL";
static final String KEY_NOTIFICATION_ICON = "KEY_NOTIFICATION_ICON";
private static final String TRANSIENT_NTC_CHANNEL_ID = "CIM_PUSH_TRANSIENT_NTC_ID";
private static final String PERSIST_NTC_CHANNEL_ID = "CIM_PUSH_PERSIST_NTC_ID";
private static final String TRANSIENT_NTC_CHANNEL_ID = "PUSH_TRANSIENT_NTC_ID";
private static final String PERSIST_NTC_CHANNEL_ID = "PUSH_PERSIST_NTC_ID";
private static final int NOTIFICATION_ID = Integer.MAX_VALUE;
private static final int PERSIST_NOTIFICATION_ID = Integer.MIN_VALUE;
private CIMConnectManager connectManager;
private CIMConnectorManager connectorManager;
private KeepAliveBroadcastReceiver keepAliveReceiver;
private ConnectivityManager connectivityManager;
private NotificationManager notificationManager;
private final AtomicBoolean persistHolder = new AtomicBoolean(false);
@Override
public void onCreate() {
connectorManager = CIMConnectorManager.getManager(this.getApplicationContext());
connectManager = new CIMConnectManager(this);
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
keepAliveReceiver = new KeepAliveBroadcastReceiver();
registerReceiver(keepAliveReceiver, keepAliveReceiver.getIntentFilter());
}
keepAliveReceiver = new KeepAliveBroadcastReceiver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
this.registerReceiver(keepAliveReceiver, keepAliveReceiver.getIntentFilter());
connectivityManager = getSystemService(ConnectivityManager.class);
connectivityManager = getSystemService(ConnectivityManager.class);
connectivityManager.registerDefaultNetworkCallback(networkCallback);
connectivityManager.registerDefaultNetworkCallback(networkCallback);
}
}
private final ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
Intent intent = new Intent();
intent.setPackage(getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_NETWORK_CHANGED);
sendBroadcast(intent);
sendBroadcast(new Intent(IntentAction.ACTION_NETWORK_CHANGED).setPackage(getPackageName()));
handleKeepAlive();
}
@Override
public void onUnavailable() {
Intent intent = new Intent();
intent.setPackage(getPackageName());
intent.setAction(CIMConstant.IntentAction.ACTION_NETWORK_CHANGED);
sendBroadcast(intent);
public void onLost(Network network) {
sendBroadcast(new Intent(IntentAction.ACTION_NETWORK_CHANGED).setPackage(getPackageName()));
}
};
private final Handler connectHandler = new Handler() {
@ -115,67 +100,82 @@ public class CIMPushService extends Service {
private final Handler notificationHandler = new Handler() {
@Override
public void handleMessage(android.os.Message message) {
if (persistHolder.get()){
return;
if (!CIMCacheManager.getBoolean(CIMPushService.this,CIMCacheManager.KEY_NTC_SWITCH)){
stopForeground(true);
}
stopForeground(true);
}
};
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return super.registerReceiver(keepAliveReceiver, keepAliveReceiver.getIntentFilter(),Context.RECEIVER_EXPORTED);
}else {
return super.registerReceiver(keepAliveReceiver, keepAliveReceiver.getIntentFilter());
}
}
private void startForegroundNotification(int id, Notification notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(id,notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING);
}else {
startForeground(id,notification);
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent == null ? CIMPushManager.ACTION_ACTIVATE_PUSH_SERVICE : intent.getAction();
Intent newIntent = intent == null ? new Intent(ServiceAction.ACTION_ACTIVATE_PUSH_SERVICE) : intent;
if (!persistHolder.get()) {
createNotification();
String action = newIntent.getAction();
createTransientNotification();
if (ServiceAction.ACTION_CREATE_CONNECTION.equals(action)) {
this.prepareConnect(newIntent.getLongExtra(BundleKey.KEY_DELAYED_TIME, 0));
}
if (CIMPushManager.ACTION_CREATE_CIM_CONNECTION.equals(action)) {
this.prepareConnect(intent.getLongExtra(KEY_DELAYED_TIME, 0));
if (ServiceAction.ACTION_SEND_REQUEST_BODY.equals(action)) {
connectManager.send((SentBody) newIntent.getSerializableExtra(BundleKey.KEY_SEND_BODY));
}
if (CIMPushManager.ACTION_SEND_REQUEST_BODY.equals(action)) {
connectorManager.send((SentBody) intent.getSerializableExtra(KEY_SEND_BODY));
if (ServiceAction.ACTION_CLOSE_CONNECTION.equals(action)) {
connectManager.close();
}
if (CIMPushManager.ACTION_CLOSE_CIM_CONNECTION.equals(action)) {
connectorManager.close();
}
if (CIMPushManager.ACTION_ACTIVATE_PUSH_SERVICE.equals(action)) {
if (ServiceAction.ACTION_ACTIVATE_PUSH_SERVICE.equals(action)) {
handleKeepAlive();
}
if (CIMPushManager.ACTION_DESTROY_CIM_SERVICE.equals(action)) {
connectorManager.close();
if (ServiceAction.ACTION_DESTROY_SERVICE.equals(action)) {
connectManager.close();
this.stopSelf();
}
if (CIMPushManager.ACTION_CIM_CONNECTION_PONG.equals(action)) {
connectorManager.send(Pong.getInstance());
if (ServiceAction.ACTION_CONNECTION_PONG.equals(action)) {
connectManager.send(Pong.getInstance());
}
if (CIMPushManager.ACTION_SET_LOGGER_EATABLE.equals(action)) {
boolean enable = intent.getBooleanExtra(KEY_LOGGER_ENABLE, true);
if (ServiceAction.ACTION_SET_LOGGER_EATABLE.equals(action)) {
boolean enable = newIntent.getBooleanExtra(BundleKey.KEY_LOGGER_ENABLE, true);
CIMLogger.getLogger().debugMode(enable);
}
if (CIMPushManager.ACTION_SHOW_PERSIST_NOTIFICATION.equals(action)) {
createPersistNotification(intent.getStringExtra(KEY_NOTIFICATION_CHANNEL),
intent.getStringExtra(KEY_NOTIFICATION_MESSAGE),
intent.getIntExtra(KEY_NOTIFICATION_ICON,0));
persistHolder.set(true);
if (ServiceAction.ACTION_HIDE_PERSIST_NOTIFICATION.equals(action)) {
this.stopForeground(true);
CIMCacheManager.putBoolean(this, CIMCacheManager.KEY_NTC_SWITCH,false);
}
if (CIMPushManager.ACTION_HIDE_PERSIST_NOTIFICATION.equals(action)) {
stopForeground(true);
persistHolder.set(false);
if (ServiceAction.ACTION_SHOW_PERSIST_NOTIFICATION.equals(action)) {
createPersistNotification(newIntent.getStringExtra(BundleKey.KEY_NOTIFICATION_CHANNEL),
newIntent.getStringExtra(BundleKey.KEY_NOTIFICATION_MESSAGE),
newIntent.getIntExtra(BundleKey.KEY_NOTIFICATION_ICON,0));
return super.onStartCommand(intent,flags,startId);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationHandler.sendEmptyMessageDelayed(0, 200);
}
notificationHandler.sendEmptyMessageDelayed(0, 200);
return super.onStartCommand(intent,flags,startId);
}
@ -187,6 +187,8 @@ public class CIMPushService extends Service {
return;
}
connectHandler.removeMessages(0);
connectHandler.sendEmptyMessageDelayed(0, delayMillis);
}
@ -199,12 +201,12 @@ public class CIMPushService extends Service {
String host = CIMCacheManager.getString(this, CIMCacheManager.KEY_CIM_SERVER_HOST);
int port = CIMCacheManager.getInt(this, CIMCacheManager.KEY_CIM_SERVER_PORT);
if (host == null || host.trim().length() == 0 || port <= 0) {
if (host == null || host.trim().isEmpty() || port <= 0) {
Log.e(this.getClass().getSimpleName(), "Invalid hostname or port. host:" + host + " port:" + port);
return;
}
connectorManager.connect(host, port);
connectManager.connect(host, port);
}
@ -212,8 +214,8 @@ public class CIMPushService extends Service {
CIMLogger.getLogger().connectState(true, CIMPushManager.isStopped(this), CIMPushManager.isDestroyed(this));
if (connectorManager.isConnected()) {
connectorManager.sendHeartbeat();
if (connectManager.isConnected()) {
connectManager.pong();
return;
}
@ -228,32 +230,31 @@ public class CIMPushService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
release();
}
private void release() {
connectHandler.removeMessages(0);
stopForeground(true);
persistHolder.set(false);
unregisterReceiver(keepAliveReceiver);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
unregisterReceiver(keepAliveReceiver);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.unregisterNetworkCallback(networkCallback);
}
connectivityManager.unregisterNetworkCallback(networkCallback);
}
private void createNotification() {
private void createTransientNotification() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(PERSIST_NTC_CHANNEL_ID) != null) {
int icon = CIMCacheManager.getInt(this, CIMCacheManager.KEY_NTC_CHANNEL_ICON);
String title = CIMCacheManager.getString(this, CIMCacheManager.KEY_NTC_CHANNEL_NAME);
String message = CIMCacheManager.getString(this, CIMCacheManager.KEY_NTC_CHANNEL_MESSAGE);
Notification notification = makeNotification(PERSIST_NTC_CHANNEL_ID,icon,title,message);
startForegroundNotification(NOTIFICATION_ID, notification);
return;
}
if (notificationManager.getNotificationChannel(TRANSIENT_NTC_CHANNEL_ID) == null) {
NotificationChannel channel = new NotificationChannel(TRANSIENT_NTC_CHANNEL_ID, getClass().getSimpleName(), NotificationManager.IMPORTANCE_LOW);
channel.enableLights(false);
@ -262,17 +263,21 @@ public class CIMPushService extends Service {
notificationManager.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this,TRANSIENT_NTC_CHANNEL_ID)
.setContentTitle(CIMPushService.class.getSimpleName())
.build();
Notification notification = makeNotification(TRANSIENT_NTC_CHANNEL_ID,0, CIMPushService.class.getSimpleName(),null);
startForegroundNotification(NOTIFICATION_ID, notification);
startForeground(NOTIFICATION_ID, notification);
}
private void createPersistNotification(String channelName ,String message,int icon) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(PERSIST_NTC_CHANNEL_ID) == null) {
CIMCacheManager.putString(this, CIMCacheManager.KEY_NTC_CHANNEL_NAME,channelName);
CIMCacheManager.putString(this, CIMCacheManager.KEY_NTC_CHANNEL_MESSAGE,message);
CIMCacheManager.putInt(this, CIMCacheManager.KEY_NTC_CHANNEL_ICON,icon);
CIMCacheManager.putBoolean(this, CIMCacheManager.KEY_NTC_SWITCH,true);
if (notificationManager.getNotificationChannel(PERSIST_NTC_CHANNEL_ID) == null) {
NotificationChannel channel = new NotificationChannel(PERSIST_NTC_CHANNEL_ID,channelName, NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.setShowBadge(false);
@ -281,26 +286,36 @@ public class CIMPushService extends Service {
notificationManager.createNotificationChannel(channel);
}
Notification notification = makeNotification(PERSIST_NTC_CHANNEL_ID,icon,channelName,message);
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setPackage(getPackageName());
startForegroundNotification(NOTIFICATION_ID,notification);
}
private Notification makeNotification(String channel,int icon,String title,String message){
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
builder = new Notification.Builder(this,PERSIST_NTC_CHANNEL_ID);
}else {
builder = new Notification.Builder(this);
}
builder = new Notification.Builder(this, channel);
builder.setAutoCancel(false)
.setOngoing(false)
.setSmallIcon(icon)
.setWhen(System.currentTimeMillis())
.setContentIntent(PendingIntent.getActivity(this, 0, intent, 0))
.setContentTitle(channelName)
.setContentText(message);
.setOngoing(false)
.setWhen(System.currentTimeMillis())
.setContentIntent(getPendingIntent())
.setContentTitle(title)
.setContentText(message);
startForeground(PERSIST_NOTIFICATION_ID, builder.build());
if (icon > 0){
builder.setSmallIcon(icon);
}
return builder.build();
}
private PendingIntent getPendingIntent(){
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT);
intent.setPackage(getPackageName());
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
}
private class KeepAliveBroadcastReceiver extends BroadcastReceiver {
@ -315,6 +330,8 @@ public class CIMPushService extends Service {
intentFilter.addAction(Intent.ACTION_POWER_CONNECTED);
intentFilter.addAction(Intent.ACTION_POWER_DISCONNECTED);
intentFilter.addAction(Intent.ACTION_USER_PRESENT);
intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
intentFilter.addAction(IntentAction.ACTION_CONNECTION_RECOVERY);
return intentFilter;
}

View File

@ -22,9 +22,9 @@
package com.farsunset.cim.sdk.android.coder;
import com.farsunset.cim.sdk.android.constant.CIMConstant;
import com.farsunset.cim.sdk.android.model.Ping;
import com.farsunset.cim.sdk.android.constant.ProtobufType;
import com.farsunset.cim.sdk.android.model.Message;
import com.farsunset.cim.sdk.android.model.Ping;
import com.farsunset.cim.sdk.android.model.ReplyBody;
import com.farsunset.cim.sdk.android.model.proto.MessageProto;
import com.farsunset.cim.sdk.android.model.proto.ReplyBodyProto;
@ -72,12 +72,12 @@ public class ClientMessageDecoder {
/*
消息读取完成后通过type来解析成对应的消息体
*/
if (CIMConstant.ProtobufType.S_H_RQ == type) {
if (ProtobufType.PING == type) {
return Ping.getInstance();
}
if (CIMConstant.ProtobufType.REPLY_BODY == type) {
ReplyBodyProto.Model bodyProto = ReplyBodyProto.Model.parseFrom(bodyBuffer.array());
if (ProtobufType.REPLY_BODY == type) {
ReplyBodyProto.ReplyModel bodyProto = ReplyBodyProto.ReplyModel.parseFrom(bodyBuffer.array());
ReplyBody body = new ReplyBody();
body.setKey(bodyProto.getKey());
body.setTimestamp(bodyProto.getTimestamp());
@ -87,7 +87,7 @@ public class ClientMessageDecoder {
return body;
}
MessageProto.Model bodyProto = MessageProto.Model.parseFrom(bodyBuffer.array());
MessageProto.MessageModel bodyProto = MessageProto.MessageModel.parseFrom(bodyBuffer.array());
Message message = new Message();
message.setId(bodyProto.getId());
message.setAction(bodyProto.getAction());

Some files were not shown because too many files have changed in this diff Show More