├── LICENSE
├── README.md
├── bootpush-android-demo
└── app
│ └── libs
│ └── bootpush-api-1.0-SNAPSHOT.jar
├── bootpush-api
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com
│ └── github
│ └── bootsrc
│ └── bootpush
│ └── api
│ ├── constant
│ └── ChannelAttrKey.java
│ ├── enums
│ ├── MessageType.java
│ └── RegisterState.java
│ ├── model
│ ├── StandardBody.java
│ ├── StandardHeader.java
│ └── StandardMessage.java
│ └── util
│ ├── IdUtil.java
│ └── KryoUtil.java
├── bootpush-java-client
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── github
│ │ └── bootsrc
│ │ └── bootpush
│ │ └── client
│ │ ├── ClientApp.java
│ │ ├── biz
│ │ └── Client.java
│ │ ├── config
│ │ ├── AppContextUtil.java
│ │ └── PushClientConfig.java
│ │ ├── constant
│ │ └── GsonSingleton.java
│ │ └── handler
│ │ ├── DecoderHandler.java
│ │ ├── EncoderHandler.java
│ │ ├── HeartbeatClientHandler.java
│ │ ├── PushClientHandler.java
│ │ └── RegisterClientHandler.java
│ └── resources
│ └── application.properties
├── bootpush-server
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── com
│ │ └── github
│ │ └── bootsrc
│ │ └── bootpush
│ │ └── server
│ │ ├── ServerApp.java
│ │ ├── biz
│ │ └── PushUtil.java
│ │ ├── boot
│ │ ├── AppContextHolder.java
│ │ ├── GsonSingleton.java
│ │ └── PushServer.java
│ │ ├── channel
│ │ └── ChannelMap.java
│ │ ├── config
│ │ └── BootpushServerConfig.java
│ │ ├── controller
│ │ ├── ApiController.java
│ │ └── HomeController.java
│ │ └── handler
│ │ ├── DecoderHandler.java
│ │ ├── EncoderHandler.java
│ │ ├── HeartbeatServerHandler.java
│ │ ├── MainServerHandler.java
│ │ └── RegisterServerHandler.java
│ └── resources
│ └── application.properties
├── doc
├── android-client-1.jpeg
├── android-client-2.jpeg
├── apk.png
└── bootpush.apk
└── pom.xml
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bootpush
2 |
3 | ## 技术栈
4 |
5 | 1.Spring Boot 5 2.Netty4 3.序列化到字节的框架kryo 5.2.0
6 |
7 | ## Demo
8 |
9 | Intelij IDEA导入项目后 启动bootpush-server的ServerApp 再启动bootpush-client的ClientApp 查看bootpush-server的client日志
10 |
11 | 测试从server推送消息到client 浏览器访问
12 |
13 | ## Demo1,推送消息到java-cient
14 |
15 | http://localhost:9101/api/push?regId=reg-id-001&msg=This-is-pushed-msg
16 |
17 | ## Demo2,推送消息到android-cient
18 |
19 | 安装doc/bootpush.apk到自己的android手机上
20 | 
21 |
22 | Host填写自己电脑的ip就行(bootpush-server的ip, 确保电脑和手机连接的是同一个WIFI) 类似于下图
23 | 
24 | 然后在App上点击按钮"连接"
25 |
26 | 在电脑浏览器上访问如下url
27 | http://localhost:9101/api/push?regId=reg-id-android-001&msg=This-is-pushed-msg
28 |
29 | 查看Android手机上是否弹出推送的消息。
30 | 
--------------------------------------------------------------------------------
/bootpush-android-demo/app/libs/bootpush-api-1.0-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bootsrc/bootpush/be63a8406809b5af0efde80d2b5173f0c740ed6a/bootpush-android-demo/app/libs/bootpush-api-1.0-SNAPSHOT.jar
--------------------------------------------------------------------------------
/bootpush-api/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | bootpush
7 | com.github.bootsrc
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | bootpush-api
13 |
14 |
15 | 8
16 | UTF-8
17 | UTF-8
18 | ${java.version}
19 | ${java.version}
20 |
21 |
22 |
23 |
24 | io.netty
25 | netty-all
26 | 4.1.70.Final
27 |
28 |
29 | org.slf4j
30 | slf4j-api
31 | 1.7.32
32 | provided
33 |
34 |
35 | com.esotericsoftware
36 | kryo
37 | 5.2.0
38 |
39 |
40 |
41 |
42 |
43 |
44 | org.apache.maven.plugins
45 | maven-compiler-plugin
46 | 3.8.1
47 |
48 | ${java.version}
49 | ${java.version}
50 | ${maven.compiler.encoding}
51 | true
52 |
53 |
54 |
55 | org.apache.maven.plugins
56 | maven-source-plugin
57 | 3.2.1
58 |
59 |
60 | attach-sources
61 |
62 | jar
63 |
64 |
65 |
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-javadoc-plugin
70 | 3.2.0
71 |
72 | ../../javadoc
73 | bootpush-api
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/constant/ChannelAttrKey.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.constant;
2 |
3 | import io.netty.util.AttributeKey;
4 |
5 | public class ChannelAttrKey {
6 | public static final AttributeKey REGISTRATION_ID = AttributeKey.valueOf("REGISTRATION_ID");
7 | }
8 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/enums/MessageType.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.enums;
2 |
3 | public enum MessageType {
4 | // 利用构造函数传参
5 | LOGIN_REQUEST((byte) 0x1, "LOGIN_REQUEST"),
6 | LOGIN_RESPONSE((byte) 0x2, "LOGIN_RESPONSE"),
7 |
8 | REGISTER_REQUEST((byte) 0x3, "REGISTER_REQUEST"),
9 | REGISTER_RESPONSE((byte) 0x4, "REGISTER_RESPONSE"),
10 |
11 | HEARTBEAT_REQUEST((byte) 0x5, "HEARTBEAT_REQUEST"),
12 | HEARTBEAT_RESPONSE((byte) 0x6, "HEARTBEAT_RESPONSE"),
13 |
14 | PUSH((byte) 0x7, "PUSH"),
15 | PUSH_CONFIRM((byte) 0x8, "PUSH_CONFIRM");
16 |
17 | // 定义私有变量
18 |
19 | private final int value;
20 | private final String text;
21 |
22 | // 构造函数,枚举类型只能为私有
23 |
24 | private MessageType(int value, String text) {
25 | this.value = value;
26 | this.text = text;
27 | }
28 |
29 | public int getValue() {
30 | return value;
31 | }
32 |
33 | public String getText() {
34 | return text;
35 | }
36 |
37 | @Override
38 | public String toString() {
39 | return "MessageType{" +
40 | "value=" + value +
41 | ", text='" + text + '\'' +
42 | '}';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/enums/RegisterState.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.enums;
2 |
3 | public enum RegisterState {
4 | SUCCESS("000", "成功");
5 |
6 | private final String code;
7 | private final String text;
8 |
9 | private RegisterState(String code, String text) {
10 | this.code = code;
11 | this.text = text;
12 | }
13 |
14 | public String getCode() {
15 | return code;
16 | }
17 |
18 | public String getText() {
19 | return text;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/model/StandardBody.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.model;
2 |
3 | import java.io.Serializable;
4 |
5 | public class StandardBody implements Serializable {
6 | private static final long serialVersionUID = -6400481841100013448L;
7 |
8 | /**
9 | * contentType:text/json
10 | */
11 | private String contentType;
12 | private String content;
13 | private Object extras;
14 |
15 | public String getContentType() {
16 | return contentType;
17 | }
18 |
19 | public void setContentType(String contentType) {
20 | this.contentType = contentType;
21 | }
22 |
23 | public String getContent() {
24 | return content;
25 | }
26 |
27 | public void setContent(String content) {
28 | this.content = content;
29 | }
30 |
31 | public Object getExtras() {
32 | return extras;
33 | }
34 |
35 | public void setExtras(Object extras) {
36 | this.extras = extras;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/model/StandardHeader.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.model;
2 |
3 | import java.io.Serializable;
4 |
5 | public class StandardHeader implements Serializable {
6 | private static final long serialVersionUID = 395281078633878131L;
7 | /**
8 | * 消息类型
9 | */
10 | private int type;
11 | /**
12 | * 长链接会话id
13 | */
14 | private String sessionId;
15 | /**
16 | * 消息的优先级
17 | */
18 | private int priority;
19 | /**
20 | * 客户端设备的注册id(每个设备的registrationId是唯一的), 简写为regId
21 | * 并且这个regId是客户端通过用户名登陆后,后台返回给客户端的
22 | * 同一个用户使用多个设备,使用不同的regId
23 | */
24 | private String regId;
25 | /**
26 | * 客户端token(跟registrationId对应的客户端token)
27 | */
28 | private String clientToken;
29 | /**
30 | * 推送/IM应用系统的应用id,每个应用项目的appId是唯一的
31 | */
32 | private String appId;
33 | private String resultCode;
34 | private String resultText;
35 |
36 | public int getType() {
37 | return type;
38 | }
39 |
40 | public void setType(int type) {
41 | this.type = type;
42 | }
43 |
44 | public String getSessionId() {
45 | return sessionId;
46 | }
47 |
48 | public void setSessionId(String sessionId) {
49 | this.sessionId = sessionId;
50 | }
51 |
52 | public int getPriority() {
53 | return priority;
54 | }
55 |
56 | public void setPriority(int priority) {
57 | this.priority = priority;
58 | }
59 |
60 | public String getRegId() {
61 | return regId;
62 | }
63 |
64 | public void setRegId(String regId) {
65 | this.regId = regId;
66 | }
67 |
68 | public String getClientToken() {
69 | return clientToken;
70 | }
71 |
72 | public void setClientToken(String clientToken) {
73 | this.clientToken = clientToken;
74 | }
75 |
76 | public String getAppId() {
77 | return appId;
78 | }
79 |
80 | public void setAppId(String appId) {
81 | this.appId = appId;
82 | }
83 |
84 | public String getResultCode() {
85 | return resultCode;
86 | }
87 |
88 | public void setResultCode(String resultCode) {
89 | this.resultCode = resultCode;
90 | }
91 |
92 | public String getResultText() {
93 | return resultText;
94 | }
95 |
96 | public void setResultText(String resultText) {
97 | this.resultText = resultText;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/model/StandardMessage.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.model;
2 |
3 | import java.io.Serializable;
4 |
5 | public class StandardMessage implements Serializable {
6 | private static final long serialVersionUID = -8882418738633533519L;
7 | private StandardHeader header;
8 | private StandardBody body;
9 |
10 | public StandardHeader getHeader() {
11 | return header;
12 | }
13 |
14 | public void setHeader(StandardHeader header) {
15 | this.header = header;
16 | }
17 |
18 | public StandardBody getBody() {
19 | return body;
20 | }
21 |
22 | public void setBody(StandardBody body) {
23 | this.body = body;
24 | }
25 |
26 | @Override
27 | public String toString() {
28 | return "StandardMessage{" +
29 | "header=" + header +
30 | ", body=" + body +
31 | '}';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/util/IdUtil.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.util;
2 |
3 | import java.util.UUID;
4 |
5 | public class IdUtil {
6 | public static String newUUID() {
7 | return UUID.randomUUID().toString().replaceAll("-", "");
8 | }
9 |
10 | public static void main(String[] args) {
11 | String str = newUUID();
12 | System.out.println(str);
13 | System.out.println("str.length=" + str.length());
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/bootpush-api/src/main/java/com/github/bootsrc/bootpush/api/util/KryoUtil.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.api.util;
2 |
3 | import com.esotericsoftware.kryo.Kryo;
4 | import com.esotericsoftware.kryo.io.Input;
5 | import com.esotericsoftware.kryo.io.Output;
6 | import org.objenesis.strategy.StdInstantiatorStrategy;
7 |
8 | import java.io.ByteArrayInputStream;
9 | import java.io.ByteArrayOutputStream;
10 | import java.io.UnsupportedEncodingException;
11 | import java.util.Base64;
12 |
13 | public class KryoUtil {
14 |
15 | private static final String DEFAULT_ENCODING = "UTF-8";
16 |
17 | //每个线程Kryo的实例副本
18 | private static final ThreadLocal kryoLocal = new ThreadLocal() {
19 | @Override
20 | protected Kryo initialValue() {
21 | Kryo kryo = new Kryo();
22 |
23 | //支持对象循环引用(否则会栈溢出),其默认值为true
24 | kryo.setReferences(true);
25 |
26 | //默认值就是false
27 | kryo.setRegistrationRequired(false);
28 |
29 | //Fix the NPE bug when deserializing Collections.
30 | kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
31 |
32 | return kryo;
33 | }
34 | };
35 |
36 | /**
37 | * 获得当前线程的Kryo实例副本
38 | */
39 | public static Kryo getInstance() {
40 | return kryoLocal.get();
41 | }
42 |
43 | /**
44 | * 记得在每个线程被销毁前调用下这个remove()方法,以避免内存泄露
45 | */
46 | public static void remove() {
47 | kryoLocal.remove();
48 | }
49 |
50 | //-----------------------------------------------
51 | // 序列化/反序列化对象,及类型信息
52 | // 序列化的结果里,包含类型的信息
53 | // 反序列化时不再需要提供类型
54 | //-----------------------------------------------
55 |
56 | /**
57 | * 将对象【及类型】序列化为字节数组
58 | *
59 | * @param obj 任意对象
60 | * @param 对象的类型
61 | * @return 序列化后的字节数组
62 | */
63 | public static byte[] writeToByteArray(T obj) {
64 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
65 | Output output = new Output(byteArrayOutputStream);
66 |
67 | Kryo kryo = getInstance();
68 | kryo.writeClassAndObject(output, obj);
69 | output.flush();
70 |
71 | return byteArrayOutputStream.toByteArray();
72 | }
73 |
74 | /**
75 | * 将对象【及类型】序列化为 String
76 | * 利用了 Base64 编码
77 | *
78 | * @param obj 任意对象
79 | * @param 对象的类型
80 | * @return 序列化后的字符串
81 | */
82 | public static String writeToString(T obj) {
83 | try {
84 | return new String(Base64.getEncoder().encode(writeToByteArray(obj)), DEFAULT_ENCODING);
85 | } catch (UnsupportedEncodingException e) {
86 | throw new IllegalStateException(e);
87 | }
88 | }
89 |
90 | /**
91 | * 将字节数组反序列化为原对象
92 | *
93 | * @param byteArray writeToByteArray 方法序列化后的字节数组
94 | * @param 原对象的类型
95 | * @return 原对象
96 | */
97 | @SuppressWarnings("unchecked")
98 | public static T readFromByteArray(byte[] byteArray) {
99 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
100 | Input input = new Input(byteArrayInputStream);
101 |
102 | Kryo kryo = getInstance();
103 | return (T) kryo.readClassAndObject(input);
104 | }
105 |
106 | /**
107 | * 将 String 反序列化为原对象
108 | * 利用了 Base64 编码
109 | *
110 | * @param str writeToString 方法序列化后的字符串
111 | * @param 原对象的类型
112 | * @return 原对象
113 | */
114 | public static T readFromString(String str) {
115 | try {
116 | return readFromByteArray(Base64.getDecoder().decode(str.getBytes(DEFAULT_ENCODING)));
117 | } catch (UnsupportedEncodingException e) {
118 | throw new IllegalStateException(e);
119 | }
120 | }
121 |
122 |
123 | //-----------------------------------------------
124 | // 只序列化/反序列化对象
125 | // 序列化的结果里,不包含类型的信息
126 | //-----------------------------------------------
127 |
128 | /**
129 | * 将对象序列化为字节数组
130 | * TODO 这里应该增加一个参数 Class T
131 | * @param obj 任意对象
132 | * @param 对象的类型
133 | * @return 序列化后的字节数组
134 | */
135 | public static byte[] writeObjectToByteArray(T obj) {
136 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
137 | Output output = new Output(byteArrayOutputStream);
138 |
139 | Kryo kryo = getInstance();
140 | kryo.writeObject(output, obj);
141 | output.flush();
142 |
143 | return byteArrayOutputStream.toByteArray();
144 | }
145 |
146 | /**
147 | * 将对象序列化为 String
148 | * 利用了 Base64 编码
149 | *
150 | * @param obj 任意对象
151 | * @param 对象的类型
152 | * @return 序列化后的字符串
153 | */
154 | public static String writeObjectToString(T obj) {
155 | try {
156 | return new String(Base64.getEncoder().encode(writeObjectToByteArray(obj)), DEFAULT_ENCODING);
157 | } catch (UnsupportedEncodingException e) {
158 | throw new IllegalStateException(e);
159 | }
160 | }
161 |
162 | /**
163 | * 将字节数组反序列化为原对象
164 | *
165 | * @param byteArray writeToByteArray 方法序列化后的字节数组
166 | * @param clazz 原对象的 Class
167 | * @param 原对象的类型
168 | * @return 原对象
169 | */
170 | @SuppressWarnings("unchecked")
171 | public static T readObjectFromByteArray(byte[] byteArray, Class clazz) {
172 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
173 | Input input = new Input(byteArrayInputStream);
174 |
175 | Kryo kryo = getInstance();
176 | return kryo.readObject(input, clazz);
177 | }
178 |
179 | /**
180 | * 将 String 反序列化为原对象
181 | * 利用了 Base64 编码
182 | *
183 | * @param str writeToString 方法序列化后的字符串
184 | * @param clazz 原对象的 Class
185 | * @param 原对象的类型
186 | * @return 原对象
187 | */
188 | public static T readObjectFromString(String str, Class clazz) {
189 | try {
190 | return readObjectFromByteArray(Base64.getDecoder().decode(str.getBytes(DEFAULT_ENCODING)), clazz);
191 | } catch (UnsupportedEncodingException e) {
192 | throw new IllegalStateException(e);
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/bootpush-java-client/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | bootpush
7 | com.github.bootsrc
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | bootpush-java-client
13 |
14 |
15 | 8
16 | 8
17 |
18 |
19 |
20 | com.github.bootsrc
21 | bootpush-api
22 | 1.0-SNAPSHOT
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-test
31 | test
32 |
33 |
34 | com.google.code.gson
35 | gson
36 | 2.8.6
37 |
38 |
39 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/ClientApp.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client;
2 |
3 | import com.github.bootsrc.bootpush.client.biz.Client;
4 | import org.springframework.boot.CommandLineRunner;
5 | import org.springframework.boot.SpringApplication;
6 | import org.springframework.boot.autoconfigure.SpringBootApplication;
7 |
8 | @SpringBootApplication
9 | public class ClientApp implements CommandLineRunner {
10 | public static void main(String[] args) {
11 | SpringApplication.run(ClientApp.class, args);
12 | }
13 |
14 | @Override
15 | public void run(String... args) throws Exception {
16 | Client.start();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/biz/Client.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.biz;
2 |
3 | import com.github.bootsrc.bootpush.client.handler.*;
4 | import io.netty.bootstrap.Bootstrap;
5 | import io.netty.channel.ChannelFuture;
6 | import io.netty.channel.ChannelInitializer;
7 | import io.netty.channel.ChannelOption;
8 | import io.netty.channel.EventLoopGroup;
9 | import io.netty.channel.nio.NioEventLoopGroup;
10 | import io.netty.channel.socket.SocketChannel;
11 | import io.netty.channel.socket.nio.NioSocketChannel;
12 | import io.netty.handler.timeout.ReadTimeoutHandler;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | public class Client {
17 | private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);
18 |
19 | public static void start() {
20 | doStart();
21 | }
22 |
23 | private static void doStart() {
24 | Bootstrap bootstrap = new Bootstrap();
25 | EventLoopGroup workgroup = new NioEventLoopGroup();
26 | bootstrap.group(workgroup).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true).handler(new ChannelInitializer() {
27 | @Override
28 | protected void initChannel(SocketChannel socketChannel) throws Exception {
29 | socketChannel.pipeline()
30 | // out
31 | .addLast(new EncoderHandler())
32 | // in
33 | .addLast(new ReadTimeoutHandler(40))
34 | .addLast(new DecoderHandler())
35 | .addLast(new RegisterClientHandler())
36 | .addLast(new HeartbeatClientHandler())
37 | .addLast(new PushClientHandler());
38 | }
39 | });
40 |
41 | try {
42 | ChannelFuture future = bootstrap.connect("127.0.0.1", 9111);
43 | LOGGER.info("Client connected to 127.0.0.1, 9111");
44 | ChannelFuture future1 = future.sync();
45 | } catch (InterruptedException e) {
46 | e.printStackTrace();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/config/AppContextUtil.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.config;
2 |
3 | import org.springframework.beans.BeansException;
4 | import org.springframework.context.ApplicationContext;
5 | import org.springframework.context.ApplicationContextAware;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class AppContextUtil implements ApplicationContextAware {
10 | private static ApplicationContext appContext;
11 |
12 | @Override
13 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
14 | appContext = applicationContext;
15 | }
16 |
17 | public static ApplicationContext getAppContext() {
18 | return appContext;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/config/PushClientConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.config;
2 |
3 |
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class PushClientConfig {
8 | private String regId = "reg-id-001";
9 |
10 | public String getRegId() {
11 | return regId;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/constant/GsonSingleton.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.constant;
2 |
3 | import com.google.gson.Gson;
4 |
5 | public class GsonSingleton {
6 | private final static Gson gson = new Gson();
7 |
8 | public static Gson getGson() {
9 | return gson;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/handler/DecoderHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
4 | import com.github.bootsrc.bootpush.api.util.KryoUtil;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.channel.ChannelHandler;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.handler.codec.ByteToMessageDecoder;
9 |
10 | import java.util.List;
11 |
12 | public class DecoderHandler extends ByteToMessageDecoder {
13 | @Override
14 | protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception {
15 | int length = byteBuf.readInt();
16 | byte[] bytes = new byte[length];
17 | byteBuf.readBytes(bytes);
18 | StandardMessage message = KryoUtil.readObjectFromByteArray(bytes, StandardMessage.class);
19 | list.add(message);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/handler/EncoderHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
4 | import com.github.bootsrc.bootpush.api.util.KryoUtil;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.channel.ChannelHandler;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.handler.codec.MessageToByteEncoder;
9 | import org.slf4j.Logger;
10 | import org.slf4j.LoggerFactory;
11 |
12 | public class EncoderHandler extends MessageToByteEncoder {
13 | private static final Logger LOGGER = LoggerFactory.getLogger(EncoderHandler.class);
14 |
15 | @Override
16 | protected void encode(ChannelHandlerContext channelHandlerContext, StandardMessage standardMessage, ByteBuf byteBuf) throws Exception {
17 | byte[] bytes = KryoUtil.writeObjectToByteArray(standardMessage);
18 | byteBuf.writeInt(bytes.length);
19 | byteBuf.writeBytes(bytes);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/handler/HeartbeatClientHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.enums.RegisterState;
5 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
6 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
7 | import com.github.bootsrc.bootpush.client.config.AppContextUtil;
8 | import com.github.bootsrc.bootpush.client.config.PushClientConfig;
9 | import com.github.bootsrc.bootpush.client.constant.GsonSingleton;
10 | import io.netty.channel.ChannelHandlerContext;
11 | import io.netty.channel.ChannelInboundHandlerAdapter;
12 | import io.netty.handler.timeout.IdleStateEvent;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | import java.util.concurrent.ScheduledFuture;
17 | import java.util.concurrent.TimeUnit;
18 |
19 | /**
20 | * 此ChannelInboundHandler工作在tcp client
21 | */
22 | public class HeartbeatClientHandler extends ChannelInboundHandlerAdapter {
23 | private static final Logger LOGGER = LoggerFactory.getLogger(HeartbeatClientHandler.class);
24 |
25 | private volatile ScheduledFuture> scheduledFuture;
26 |
27 | public HeartbeatClientHandler() {
28 |
29 | }
30 |
31 | @Override
32 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
33 | if (msg instanceof StandardMessage) {
34 | StandardMessage message = (StandardMessage) msg;
35 | StandardHeader header = message.getHeader();
36 | // 握手成功, 主动发送心跳信息
37 | if (header != null) {
38 |
39 | if (header.getType() == MessageType.REGISTER_RESPONSE.getValue()
40 | && RegisterState.SUCCESS.getCode().equals(header.getResultCode())) {
41 | String regId = header.getRegId();
42 | LOGGER.info("===Start to send HEARTBEAT to Server>>>");
43 | scheduledFuture = ctx.executor().scheduleAtFixedRate(new HeartbeatTask(ctx)
44 | , 0, 5000, TimeUnit.MILLISECONDS);
45 | } else if (header.getType() == MessageType.HEARTBEAT_RESPONSE.getValue()) {
46 | LOGGER.info("<== READ HEARTBEAT RESPONSE from Server, msg={}", GsonSingleton.getGson().toJson(message));
47 | }
48 | }
49 | }
50 |
51 | ctx.fireChannelRead(msg);
52 | }
53 |
54 | @Override
55 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
56 | throws Exception {
57 | if (scheduledFuture != null) {
58 | scheduledFuture.cancel(true);
59 | scheduledFuture = null;
60 | }
61 | LOGGER.error(cause.getMessage(), cause);
62 | }
63 |
64 | @Override
65 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
66 | LOGGER.info("HeartbeatServerHandler#userEventTriggered invoked.");
67 | if (evt instanceof IdleStateEvent) {
68 | IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
69 | if (IdleStateEvent.READER_IDLE_STATE_EVENT.state() == idleStateEvent.state()) {
70 | // ctx.close();
71 | // 如果收不到客户端的心跳消息,则说明客户端已经失联了,断开跟客户端的channel连接
72 | ctx.channel().close();
73 | LOGGER.info("Missing connection with Server, close the channel!");
74 | }
75 | } else {
76 | LOGGER.info("NOT IdleStateEvent");
77 | }
78 | super.userEventTriggered(ctx, evt);
79 | }
80 |
81 | private static class HeartbeatTask implements Runnable {
82 | private final ChannelHandlerContext ctx;
83 |
84 | public HeartbeatTask(ChannelHandlerContext ctx) {
85 | this.ctx = ctx;
86 | }
87 |
88 | @Override
89 | public void run() {
90 | StandardMessage heartbeat = buildHeartbeat();
91 | ctx.channel().writeAndFlush(heartbeat);
92 | }
93 |
94 | private StandardMessage buildHeartbeat() {
95 | String regId = AppContextUtil.getAppContext().getBean(PushClientConfig.class).getRegId();
96 |
97 | StandardMessage message = new StandardMessage();
98 | StandardHeader header = new StandardHeader();
99 | header.setRegId(regId);
100 | header.setType(MessageType.HEARTBEAT_REQUEST.getValue());
101 | header.setSessionId(null);
102 | header.setPriority(1);
103 | header.setAppId(null);
104 | header.setClientToken(null);
105 | message.setHeader(header);
106 | return message;
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/handler/PushClientHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
5 | import com.github.bootsrc.bootpush.client.constant.GsonSingleton;
6 | import io.netty.channel.ChannelHandlerContext;
7 | import io.netty.channel.ChannelInboundHandlerAdapter;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class PushClientHandler extends ChannelInboundHandlerAdapter {
12 | private static final Logger LOGGER = LoggerFactory.getLogger(PushClientHandler.class);
13 |
14 | @Override
15 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
16 | if (msg instanceof StandardMessage) {
17 | StandardMessage message = (StandardMessage) msg;
18 | if (null != message.getHeader() && MessageType.PUSH.getValue() == message.getHeader().getType()) {
19 | LOGGER.info("<== RECEIVED PUSHED MESSAGE from Server, msg data={}"
20 | , GsonSingleton.getGson().toJson(message));
21 | }
22 | }
23 | ctx.fireChannelRead(msg);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/java/com/github/bootsrc/bootpush/client/handler/RegisterClientHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.client.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.enums.RegisterState;
5 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
6 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
7 | import com.github.bootsrc.bootpush.client.config.AppContextUtil;
8 | import com.github.bootsrc.bootpush.client.config.PushClientConfig;
9 | import io.netty.channel.ChannelHandlerContext;
10 | import io.netty.channel.ChannelInboundHandlerAdapter;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | /**
15 | * 此ChannelInboundHandler工作在tcp client
16 | */
17 | public class RegisterClientHandler extends ChannelInboundHandlerAdapter {
18 | private static final Logger LOGGER = LoggerFactory.getLogger(RegisterClientHandler.class);
19 |
20 | public RegisterClientHandler() {
21 |
22 | }
23 |
24 |
25 | @Override
26 | public void channelActive(ChannelHandlerContext ctx) throws Exception {
27 | LOGGER.info("RegisterClientHandler#channelActive");
28 | StandardMessage message = buildRegisterRequest();
29 | ctx.channel().writeAndFlush(message);
30 | LOGGER.info("==> WRITE msg to server, msg={}", message);
31 | }
32 |
33 | @Override
34 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
35 | if (msg instanceof StandardMessage) {
36 | StandardMessage message = (StandardMessage) msg;
37 | StandardHeader receivedHeader = message.getHeader();
38 | if (receivedHeader != null && receivedHeader.getType() == MessageType.REGISTER_RESPONSE.getValue()) {
39 | if (RegisterState.SUCCESS.getCode().equals(receivedHeader.getResultCode())) {
40 | LOGGER.info("<== READ REGISTER_RESP message. msg={}", message);
41 | ctx.fireChannelRead(msg);
42 | } else {
43 | LOGGER.info("---client:register_FAILED---DisconnectIt!!!--");
44 | ctx.close();
45 | }
46 | } else {
47 | ctx.fireChannelRead(msg);
48 | }
49 | } else {
50 | ctx.fireChannelRead(msg);
51 | }
52 | }
53 |
54 | private StandardMessage buildRegisterRequest() {
55 | // TODO regId是客户端登陆后从服务器端获取到的
56 | String regId = AppContextUtil.getAppContext().getBean(PushClientConfig.class).getRegId();
57 | StandardMessage message = new StandardMessage();
58 | StandardHeader header = new StandardHeader();
59 | // 用regId去push server注册,以建立长连接,方便后面进行tcp通信
60 | header.setRegId(regId);
61 | header.setType(MessageType.REGISTER_REQUEST.getValue());
62 | header.setSessionId(null);
63 | header.setPriority(9);
64 | header.setAppId(null);
65 | header.setClientToken(null);
66 | message.setHeader(header);
67 | return message;
68 | }
69 |
70 | @Override
71 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
72 | throws Exception {
73 | ctx.fireExceptionCaught(cause);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/bootpush-java-client/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=9121
2 |
3 | spring.application.name=bootpush-java-client
4 |
--------------------------------------------------------------------------------
/bootpush-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | bootpush
7 | com.github.bootsrc
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | bootpush-server
13 |
14 |
15 | 8
16 | UTF-8
17 | UTF-8
18 | ${java.version}
19 | ${java.version}
20 |
21 |
22 |
23 |
24 | com.github.bootsrc
25 | bootpush-api
26 | 1.0-SNAPSHOT
27 |
28 |
29 | io.netty
30 | netty-all
31 | 4.1.70.Final
32 |
33 |
34 | org.slf4j
35 | slf4j-api
36 | 1.7.32
37 | provided
38 |
39 |
40 | com.google.code.gson
41 | gson
42 | 2.8.6
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-starter-web
47 |
48 |
49 | org.springframework.boot
50 | spring-boot-starter-test
51 | test
52 |
53 |
54 | mysql
55 | mysql-connector-java
56 | 8.0.16
57 |
58 |
59 | org.apache.commons
60 | commons-lang3
61 | 3.8
62 |
63 |
64 |
65 |
66 |
67 |
68 | org.apache.maven.plugins
69 | maven-compiler-plugin
70 | 3.8.1
71 |
72 | ${java.version}
73 | ${java.version}
74 | ${maven.compiler.encoding}
75 | true
76 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/ServerApp.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server;
2 |
3 | import com.github.bootsrc.bootpush.server.boot.AppContextHolder;
4 | import com.github.bootsrc.bootpush.server.boot.PushServer;
5 | import com.github.bootsrc.bootpush.server.config.BootpushServerConfig;
6 | import org.springframework.boot.CommandLineRunner;
7 | import org.springframework.boot.SpringApplication;
8 | import org.springframework.boot.autoconfigure.SpringBootApplication;
9 |
10 | @SpringBootApplication
11 | public class ServerApp implements CommandLineRunner {
12 | public static void main(String[] args) {
13 | SpringApplication.run(ServerApp.class, args);
14 | }
15 |
16 | @Override
17 | public void run(String... args) throws Exception {
18 | BootpushServerConfig config = AppContextHolder.getAppContext().getBean(BootpushServerConfig.class);
19 | PushServer pushServer = new PushServer(config);
20 | pushServer.start();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/biz/PushUtil.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.biz;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.model.StandardBody;
5 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
6 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
7 | import com.github.bootsrc.bootpush.server.boot.GsonSingleton;
8 | import com.github.bootsrc.bootpush.server.channel.ChannelMap;
9 | import io.netty.channel.Channel;
10 | import io.netty.channel.ChannelFuture;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | public class PushUtil {
15 | private static final Logger LOGGER = LoggerFactory.getLogger(PushUtil.class);
16 |
17 | public static void push(String regId, String text) {
18 | StandardMessage msg = new StandardMessage();
19 | StandardHeader header = new StandardHeader();
20 | StandardBody body = new StandardBody();
21 | header.setRegId(regId);
22 | header.setType(MessageType.PUSH.getValue());
23 | msg.setHeader(header);
24 |
25 | body.setContentType("text");
26 | body.setContent(text);
27 | msg.setBody(body);
28 |
29 |
30 | // 从缓存中根据regId获取到对应的Channel
31 | Channel channel = ChannelMap.get(regId);
32 | if (null != channel) {
33 | ChannelFuture channelFuture = channel.writeAndFlush(msg);
34 | LOGGER.info("==> PUSH MESSAGE msg={}", GsonSingleton.getGson().toJson(msg));
35 | } else {
36 | LOGGER.warn("push text_msg failed, causing is 'channel is null' with regId={}, msg={}"
37 | , regId, GsonSingleton.getGson().toJson(msg));
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/boot/AppContextHolder.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.boot;
2 |
3 | import org.springframework.beans.BeansException;
4 | import org.springframework.context.ApplicationContext;
5 | import org.springframework.context.ApplicationContextAware;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class AppContextHolder implements ApplicationContextAware {
10 | private static ApplicationContext appContext;
11 |
12 | @Override
13 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
14 | appContext = applicationContext;
15 | }
16 |
17 | public static ApplicationContext getAppContext() {
18 | return appContext;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/boot/GsonSingleton.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.boot;
2 |
3 | import com.google.gson.Gson;
4 |
5 | public class GsonSingleton {
6 | private final static Gson gson = new Gson();
7 |
8 | public static Gson getGson() {
9 | return gson;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/boot/PushServer.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.boot;
2 |
3 | import com.github.bootsrc.bootpush.server.handler.DecoderHandler;
4 | import com.github.bootsrc.bootpush.server.handler.EncoderHandler;
5 | import com.github.bootsrc.bootpush.server.config.BootpushServerConfig;
6 | import com.github.bootsrc.bootpush.server.handler.HeartbeatServerHandler;
7 | import com.github.bootsrc.bootpush.server.handler.RegisterServerHandler;
8 | import io.netty.bootstrap.ServerBootstrap;
9 | import io.netty.channel.ChannelInitializer;
10 | import io.netty.channel.ChannelPipeline;
11 | import io.netty.channel.EventLoopGroup;
12 | import io.netty.channel.nio.NioEventLoopGroup;
13 | import io.netty.channel.socket.SocketChannel;
14 | import io.netty.channel.socket.nio.NioServerSocketChannel;
15 | import io.netty.handler.timeout.ReadTimeoutHandler;
16 | import org.slf4j.Logger;
17 | import org.slf4j.LoggerFactory;
18 |
19 | public class PushServer {
20 | private static final Logger LOGGER= LoggerFactory.getLogger(PushServer.class);
21 |
22 | private BootpushServerConfig config;
23 | private ServerBootstrap bootstrap;
24 | private EventLoopGroup group;
25 |
26 |
27 | public PushServer(BootpushServerConfig config) {
28 | this.config = config;
29 | }
30 |
31 | public void start() {
32 | LOGGER.info("bootpush-server is starting...");
33 |
34 | bootstrap = new ServerBootstrap();
35 | group = new NioEventLoopGroup(config.getIoThreads());
36 | bootstrap.group(group);
37 | bootstrap.channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer() {
38 | @Override
39 | protected void initChannel(SocketChannel socketChannel) throws Exception {
40 | ChannelPipeline pipeline = socketChannel.pipeline();
41 | // out
42 | pipeline.addLast(new EncoderHandler());
43 |
44 | // in
45 | pipeline.addLast(new ReadTimeoutHandler(40));
46 | pipeline.addLast(new DecoderHandler());
47 | pipeline.addLast(new HeartbeatServerHandler());
48 | pipeline.addLast(new RegisterServerHandler());
49 | // TODO add PushHandler
50 |
51 | }
52 | });
53 | // TODO bootstrap option
54 | bootstrap.bind(config.getTcpPort());
55 | LOGGER.info("bootpush-server started>>>tcp-port={}", config.getTcpPort());
56 | }
57 |
58 | public void stop() {
59 |
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/channel/ChannelMap.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.channel;
2 |
3 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
4 | import io.netty.channel.Channel;
5 |
6 | import java.util.Map;
7 | import java.util.concurrent.ConcurrentHashMap;
8 | import java.util.concurrent.ConcurrentMap;
9 |
10 | public class ChannelMap {
11 | private static final ConcurrentMap map = new ConcurrentHashMap();
12 |
13 | /**
14 | * @param registrationId 是{@link StandardHeader#getRegId()}
15 | * @param channel channel
16 | */
17 | public static void put(String registrationId, Channel channel) {
18 | map.put(registrationId, channel);
19 | }
20 |
21 | /**
22 | * 是FHeader.getAlias()
23 | *
24 | * @param registrationId clientId
25 | * @return Channel
26 | */
27 | public static Channel get(String registrationId) {
28 | return map.get(registrationId);
29 | }
30 |
31 | public static void remove(Channel channel) {
32 | for (Map.Entry entry : map.entrySet()) {
33 | if (channel.compareTo(entry.getValue()) == 0) {
34 | map.remove(entry.getKey());
35 | }
36 | }
37 | }
38 |
39 | public static Map getMap() {
40 | return map;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/config/BootpushServerConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.config;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class BootpushServerConfig {
8 | @Value("${bootpush.server.ioThreads:3}")
9 | private int ioThreads;
10 |
11 | @Value("${bootpush.server.tcpPort:9111}")
12 | private int tcpPort;
13 |
14 | public int getIoThreads() {
15 | return ioThreads;
16 | }
17 |
18 | public int getTcpPort() {
19 | return tcpPort;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/controller/ApiController.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.controller;
2 |
3 | import com.github.bootsrc.bootpush.server.biz.PushUtil;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 | import org.springframework.web.bind.annotation.RestController;
6 |
7 | @RequestMapping("/api")
8 | @RestController
9 | public class ApiController {
10 |
11 | @RequestMapping("/push")
12 | public String push(String regId, String msg) {
13 | PushUtil.push(regId, msg);
14 | return "push success";
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/controller/HomeController.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.controller;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.RequestMapping;
6 | import org.springframework.web.bind.annotation.ResponseBody;
7 |
8 | @Controller
9 | @RequestMapping("")
10 | public class HomeController {
11 |
12 | @GetMapping("")
13 | @ResponseBody
14 | public String index() {
15 | return "bootpush-server works";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/handler/DecoderHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
4 | import com.github.bootsrc.bootpush.api.util.KryoUtil;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.channel.ChannelHandler;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.handler.codec.ByteToMessageDecoder;
9 |
10 | import java.util.List;
11 |
12 | public class DecoderHandler extends ByteToMessageDecoder {
13 | @Override
14 | protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception {
15 | int length = byteBuf.readInt();
16 | byte[] bytes = new byte[length];
17 | byteBuf.readBytes(bytes);
18 | StandardMessage message = KryoUtil.readObjectFromByteArray(bytes, StandardMessage.class);
19 | list.add(message);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/handler/EncoderHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
4 | import com.github.bootsrc.bootpush.api.util.KryoUtil;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.channel.ChannelHandlerContext;
7 | import io.netty.handler.codec.MessageToByteEncoder;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class EncoderHandler extends MessageToByteEncoder {
12 | private static final Logger LOGGER = LoggerFactory.getLogger(EncoderHandler.class);
13 |
14 | @Override
15 | protected void encode(ChannelHandlerContext channelHandlerContext, StandardMessage standardMessage, ByteBuf byteBuf) throws Exception {
16 | byte[] bytes = KryoUtil.writeObjectToByteArray(standardMessage);
17 | byteBuf.writeInt(bytes.length);
18 | byteBuf.writeBytes(bytes);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/handler/HeartbeatServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.model.StandardBody;
5 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
6 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
7 | import com.github.bootsrc.bootpush.server.boot.GsonSingleton;
8 | import io.netty.channel.ChannelHandlerContext;
9 | import io.netty.channel.ChannelInboundHandlerAdapter;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | /**
14 | * 跟ReadTimeoutHandler配合起来检测客户端发过来的心跳消息
15 | */
16 | public class HeartbeatServerHandler extends ChannelInboundHandlerAdapter {
17 | private static final Logger LOGGER = LoggerFactory.getLogger(HeartbeatServerHandler.class);
18 |
19 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
20 | if (msg instanceof StandardMessage) {
21 | StandardMessage message = (StandardMessage) msg;
22 | if (null != message.getHeader() && MessageType.HEARTBEAT_REQUEST.getValue() == message.getHeader().getType()) {
23 | LOGGER.info("<== READ heartbeat from client, heartbeat={}", GsonSingleton.getGson().toJson(message));
24 | StandardMessage outMsg = buildHeartbeat(message);
25 | ctx.channel().writeAndFlush(outMsg);
26 | LOGGER.info("==> WRITE heartbeat response to client, heartbeat={}", GsonSingleton.getGson().toJson(outMsg));
27 | }
28 | }
29 | ctx.fireChannelRead(msg);
30 | }
31 |
32 | private StandardMessage buildHeartbeat(StandardMessage reqMsg) {
33 | StandardMessage msg = new StandardMessage();
34 | StandardHeader reqHeader = reqMsg.getHeader();
35 | StandardHeader header = new StandardHeader();
36 | header.setType(MessageType.HEARTBEAT_RESPONSE.getValue());
37 | header.setRegId(reqHeader.getRegId());
38 | StandardBody body = new StandardBody();
39 | msg.setHeader(header);
40 | msg.setBody(body);
41 | return msg;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/handler/MainServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.enums.MessageType;
4 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
5 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
6 | import io.netty.channel.ChannelHandlerContext;
7 | import io.netty.channel.ChannelInboundHandlerAdapter;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class MainServerHandler extends ChannelInboundHandlerAdapter {
12 | private static final Logger LOGGER = LoggerFactory.getLogger(MainServerHandler.class);
13 |
14 | @Override
15 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
16 | if (msg instanceof StandardMessage) {
17 | StandardMessage message = (StandardMessage) msg;
18 | StandardHeader header = message.getHeader();
19 | if (header != null
20 | && header.getType() == MessageType.REGISTER_REQUEST.getValue()) {
21 | // TODO 针对各种message type的分别处理
22 |
23 | } else {
24 | ctx.fireChannelRead(msg);
25 | }
26 | } else {
27 | ctx.fireChannelRead(msg);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/java/com/github/bootsrc/bootpush/server/handler/RegisterServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.github.bootsrc.bootpush.server.handler;
2 |
3 | import com.github.bootsrc.bootpush.api.constant.ChannelAttrKey;
4 | import com.github.bootsrc.bootpush.api.enums.MessageType;
5 | import com.github.bootsrc.bootpush.api.enums.RegisterState;
6 | import com.github.bootsrc.bootpush.api.model.StandardBody;
7 | import com.github.bootsrc.bootpush.api.model.StandardHeader;
8 | import com.github.bootsrc.bootpush.api.model.StandardMessage;
9 | import com.github.bootsrc.bootpush.server.boot.GsonSingleton;
10 | import com.github.bootsrc.bootpush.server.channel.ChannelMap;
11 | import io.netty.channel.ChannelHandlerContext;
12 | import io.netty.channel.ChannelInboundHandlerAdapter;
13 | import org.slf4j.Logger;
14 | import org.slf4j.LoggerFactory;
15 |
16 | /**
17 | *
18 | */
19 | public class RegisterServerHandler extends ChannelInboundHandlerAdapter {
20 | private static final Logger LOGGER = LoggerFactory.getLogger(RegisterServerHandler.class);
21 |
22 | @Override
23 | public void channelInactive(ChannelHandlerContext ctx) throws Exception {
24 | ChannelMap.remove(ctx.channel());
25 | ctx.fireChannelInactive();
26 | }
27 |
28 | @Override
29 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
30 | if (msg instanceof StandardMessage) {
31 | StandardMessage message = (StandardMessage) msg;
32 | StandardHeader header = message.getHeader();
33 | if (header != null && header.getType() == MessageType.REGISTER_REQUEST.getValue()) {
34 | StandardMessage targetMessage = buildRegisterResponse(header, ctx);
35 | ctx.writeAndFlush(targetMessage);
36 | LOGGER.info("==> WRITE msg to client, msg={}", GsonSingleton.getGson().toJson(targetMessage));
37 | } else {
38 | ctx.fireChannelRead(msg);
39 | }
40 | } else {
41 | ctx.fireChannelRead(msg);
42 | }
43 | }
44 |
45 | @Override
46 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
47 | // ctx.fireExceptionCaught(cause);
48 | LOGGER.error(cause.getMessage(), cause);
49 | }
50 |
51 | private StandardMessage buildRegisterResponse(StandardHeader header, ChannelHandlerContext ctx) {
52 | StandardMessage message = new StandardMessage();
53 | header.setType(MessageType.REGISTER_RESPONSE.getValue());
54 | // TODO checkByClientToken
55 | // boolean passed = PassportUtil.checkByClientToken(header.getAppId(), header.getClientToken());
56 | boolean passed = true;
57 | if (passed) {
58 | String regId = header.getRegId();
59 | ctx.channel().attr(ChannelAttrKey.REGISTRATION_ID).set(regId);
60 | LOGGER.info("receive regId from client regId={}", regId);
61 | LOGGER.info("one_channel_is_registered_in_map");
62 | LOGGER.info("channelMap.size={}", ChannelMap.getMap().size());
63 | ChannelMap.put(regId, ctx.channel());
64 | header.setResultCode(RegisterState.SUCCESS.getCode());
65 | header.setResultText(RegisterState.SUCCESS.getText());
66 | } else {
67 | header.setResultCode("001");
68 | header.setResultText("客户端设备注册失败");
69 | LOGGER.info("---Server:deviceRegisterFailed.");
70 | }
71 | message.setHeader(header);
72 | StandardBody body = new StandardBody();
73 | body.setContentType("text");
74 | body.setContent("This is a Description");
75 | body.setExtras(null);
76 | message.setBody(body);
77 | return message;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/bootpush-server/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | server.port=9101
2 |
3 | spring.application.name=bootpush-server
4 |
5 | bootpush.server.ioThreads=3
6 | bootpush.server.tcpPort=9111
7 |
--------------------------------------------------------------------------------
/doc/android-client-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bootsrc/bootpush/be63a8406809b5af0efde80d2b5173f0c740ed6a/doc/android-client-1.jpeg
--------------------------------------------------------------------------------
/doc/android-client-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bootsrc/bootpush/be63a8406809b5af0efde80d2b5173f0c740ed6a/doc/android-client-2.jpeg
--------------------------------------------------------------------------------
/doc/apk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bootsrc/bootpush/be63a8406809b5af0efde80d2b5173f0c740ed6a/doc/apk.png
--------------------------------------------------------------------------------
/doc/bootpush.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bootsrc/bootpush/be63a8406809b5af0efde80d2b5173f0c740ed6a/doc/bootpush.apk
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.github.bootsrc
8 | bootpush
9 | pom
10 | 1.0-SNAPSHOT
11 |
12 |
13 | org.springframework.boot
14 | spring-boot-starter-parent
15 | 2.5.6
16 |
17 |
18 |
19 |
20 | bootpush-api
21 | bootpush-server
22 | bootpush-java-client
23 |
24 |
25 |
26 | 8
27 | 8
28 |
29 |
30 |
--------------------------------------------------------------------------------