├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── freddy │ └── kulaims │ └── ExampleInstrumentedTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── freddy │ │ └── kulaims │ │ ├── IMSFactory.java │ │ ├── IMSKit.java │ │ ├── bean │ │ └── IMSMsg.java │ │ ├── config │ │ ├── CommunicationProtocol.java │ │ ├── IMSConfig.java │ │ ├── IMSConnectStatus.java │ │ ├── IMSOptions.java │ │ ├── ImplementationMode.java │ │ └── TransportProtocol.java │ │ ├── interf │ │ └── IMSInterface.java │ │ ├── listener │ │ ├── IMSConnectStatusListener.java │ │ ├── IMSMsgReceivedListener.java │ │ └── IMSMsgSentStatusListener.java │ │ ├── mina │ │ ├── tcp │ │ │ └── MinaTCPIMS.java │ │ └── websocket │ │ │ └── MinaWebSocketIMS.java │ │ ├── net │ │ ├── NetworkManager.java │ │ └── NetworkType.java │ │ ├── netty │ │ ├── tcp │ │ │ ├── NettyTCPChannelInitializerHandler.java │ │ │ ├── NettyTCPIMS.java │ │ │ ├── NettyTCPReadHandler.java │ │ │ └── NettyTCPReconnectTask.java │ │ └── websocket │ │ │ ├── NettyWebSocketChannelInitializerHandler.java │ │ │ ├── NettyWebSocketIMS.java │ │ │ ├── NettyWebSocketReadHandler.java │ │ │ └── NettyWebSocketReconnectTask.java │ │ ├── nio │ │ ├── tcp │ │ │ └── NioTCPIMS.java │ │ └── websocket │ │ │ └── NioWebSocketIMS.java │ │ └── utils │ │ ├── ExecutorServiceFactory.java │ │ └── UUID.java └── proto │ └── msg.proto └── test └── java └── com └── freddy └── kulaims └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | -------------------------------------------------------------------------------- /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 | # License 2 | 3 | 4 | Copyright 2020, chenshichao 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.google.protobuf' 3 | 4 | android { 5 | compileSdkVersion 29 6 | buildToolsVersion "29.0.3" 7 | 8 | defaultConfig { 9 | minSdkVersion 23 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "0.1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles 'consumer-rules.pro' 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | sourceSets { 26 | main { 27 | java { 28 | srcDir 'src/main/java' 29 | } 30 | 31 | proto { 32 | srcDir 'src/main/proto' 33 | } 34 | } 35 | } 36 | } 37 | 38 | protobuf { 39 | //配置protoc编译器 40 | protoc { 41 | artifact = 'com.google.protobuf:protoc:3.8.0' 42 | } 43 | //这里配置生成目录,编译后会在build的目录下生成对应的java文件 44 | generateProtoTasks { 45 | all().each { task -> 46 | task.builtins { 47 | remove java 48 | } 49 | task.builtins { 50 | java {} 51 | } 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | implementation fileTree(dir: 'libs', include: ['*.jar']) 58 | 59 | implementation 'androidx.appcompat:appcompat:1.1.0' 60 | testImplementation 'junit:junit:4.12' 61 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 63 | 64 | implementation 'com.google.protobuf:protobuf-java:3.8.0' 65 | implementation group: 'io.netty', name: 'netty-all', version: '4.1.42.Final' 66 | } 67 | -------------------------------------------------------------------------------- /consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreddyChen/ims_kula/1480c9517b501fbc694fe225fdfc26f71001741a/consumer-rules.pro -------------------------------------------------------------------------------- /proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /src/androidTest/java/com/freddy/kulaims/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.freddy.kulaims.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/IMSFactory.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims; 2 | 3 | import com.freddy.kulaims.config.CommunicationProtocol; 4 | import com.freddy.kulaims.config.ImplementationMode; 5 | import com.freddy.kulaims.interf.IMSInterface; 6 | import com.freddy.kulaims.mina.tcp.MinaTCPIMS; 7 | import com.freddy.kulaims.mina.websocket.MinaWebSocketIMS; 8 | import com.freddy.kulaims.netty.tcp.NettyTCPIMS; 9 | import com.freddy.kulaims.netty.websocket.NettyWebSocketIMS; 10 | import com.freddy.kulaims.nio.tcp.NioTCPIMS; 11 | import com.freddy.kulaims.nio.websocket.NioWebSocketIMS; 12 | 13 | public class IMSFactory { 14 | 15 | public static IMSInterface getIMS(ImplementationMode implementationMode, CommunicationProtocol communicationProtocol) { 16 | switch (implementationMode) { 17 | case Nio: { 18 | switch (communicationProtocol) { 19 | case TCP: 20 | return NioTCPIMS.getInstance(); 21 | 22 | case WebSocket: 23 | return NioWebSocketIMS.getInstance(); 24 | 25 | default: 26 | break; 27 | } 28 | break; 29 | } 30 | 31 | case Mina: { 32 | switch (communicationProtocol) { 33 | case TCP: 34 | return MinaTCPIMS.getInstance(); 35 | 36 | case WebSocket: 37 | return MinaWebSocketIMS.getInstance(); 38 | 39 | default: 40 | break; 41 | } 42 | break; 43 | } 44 | 45 | case Netty: 46 | default: 47 | switch (communicationProtocol) { 48 | case TCP: 49 | return NettyTCPIMS.getInstance(); 50 | 51 | case WebSocket: 52 | return NettyWebSocketIMS.getInstance(); 53 | 54 | default: 55 | break; 56 | } 57 | break; 58 | } 59 | 60 | return null; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/IMSKit.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.freddy.kulaims.config.CommunicationProtocol; 7 | import com.freddy.kulaims.config.IMSOptions; 8 | import com.freddy.kulaims.config.ImplementationMode; 9 | import com.freddy.kulaims.config.TransportProtocol; 10 | import com.freddy.kulaims.interf.IMSInterface; 11 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 12 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 13 | 14 | /** 15 | * IMS核心类 16 | */ 17 | public class IMSKit { 18 | 19 | private static final String TAG = "FreddyChen";// todo 不知道为什么tag为IMSKit的时候,Logcat无法打印日志 20 | private IMSInterface ims; 21 | 22 | private IMSKit() { 23 | } 24 | 25 | public static IMSKit getInstance() { 26 | return SingletonHolder.INSTANCE; 27 | } 28 | 29 | private static final class SingletonHolder { 30 | private static final IMSKit INSTANCE = new IMSKit(); 31 | } 32 | 33 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 34 | Log.d(TAG, "IMSKit初始化开始"); 35 | if (context == null) { 36 | Log.w(TAG, "IMSKit初始化失败:Context 为 null"); 37 | return false; 38 | } 39 | 40 | if (options == null) { 41 | Log.w(TAG, "IMSKit初始化失败:IMSOptions 为 null"); 42 | return false; 43 | } 44 | 45 | ImplementationMode implementationMode = options.getImplementationMode(); 46 | if (implementationMode == null) { 47 | Log.w(TAG, "IMSKit初始化失败:ImplementationMode 为 null"); 48 | return false; 49 | } 50 | 51 | CommunicationProtocol communicationProtocol = options.getCommunicationProtocol(); 52 | if (communicationProtocol == null) { 53 | Log.w(TAG, "IMSKit初始化失败:CommunicationProtocol 为 null"); 54 | return false; 55 | } 56 | 57 | TransportProtocol transportProtocol = options.getTransportProtocol(); 58 | if (transportProtocol == null) { 59 | Log.w(TAG, "IMSKit初始化失败:TransportProtocol 为 null"); 60 | return false; 61 | } 62 | 63 | ims = IMSFactory.getIMS(implementationMode, communicationProtocol); 64 | if (ims == null) { 65 | Log.w(TAG, "IMSKit初始化失败:ims 为 null"); 66 | return false; 67 | } 68 | 69 | boolean initialized = ims.init(context, options, connectStatusListener, msgReceivedListener); 70 | if (!initialized) { 71 | Log.w(TAG, "IMSKit初始化失败:请查看 " + ims.getClass().getSimpleName() + " 相关的日志"); 72 | return false; 73 | } 74 | 75 | Log.d(TAG, "IMSKit初始化完成\nims = " + ims.getClass().getSimpleName() + "\noptions = " + options); 76 | return true; 77 | } 78 | 79 | public void connect() { 80 | if (ims == null) { 81 | Log.d(TAG, "IMSKit启动失败"); 82 | return; 83 | } 84 | 85 | ims.connect(); 86 | } 87 | 88 | public void disconnect() { 89 | if(ims == null) return; 90 | ims.release(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/bean/IMSMsg.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.bean; 2 | 3 | import com.freddy.kulaims.utils.UUID; 4 | 5 | /** 6 | * @author FreddyChen 7 | * @name IMS消息 8 | * @date 2020/05/21 16:38 9 | * @email chenshichao@outlook.com 10 | * @github https://github.com/FreddyChen 11 | * @desc 通用的消息格式定义,可转换成json或protobuf传输 12 | */ 13 | public class IMSMsg { 14 | private String msgId;// 消息唯一标识 15 | private int msgType; // 消息类型 16 | private String sender;// 发送者标识 17 | private String receiver;// 接收者标识 18 | private long timestamp;// 消息发送时间,单位:毫秒 19 | private int report;// 消息发送状态报告 20 | private String content;// 消息内容 21 | private int contentType;// 消息内容类型 22 | private String data; // 扩展字段,以key/value形式存储的json字符串 23 | 24 | public IMSMsg(Builder builder) { 25 | if(builder == null) { 26 | return; 27 | } 28 | 29 | this.msgId = builder.msgId; 30 | this.msgType = builder.msgType; 31 | this.sender = builder.sender; 32 | this.receiver = builder.receiver; 33 | this.timestamp = builder.timestamp; 34 | this.report = builder.report; 35 | this.content = builder.content; 36 | this.contentType = builder.contentType; 37 | this.data = builder.data; 38 | } 39 | 40 | public String getMsgId() { 41 | return msgId; 42 | } 43 | 44 | public int getMsgType() { 45 | return msgType; 46 | } 47 | 48 | public String getSender() { 49 | return sender; 50 | } 51 | 52 | public String getReceiver() { 53 | return receiver; 54 | } 55 | 56 | public long getTimestamp() { 57 | return timestamp; 58 | } 59 | 60 | public int getReport() { 61 | return report; 62 | } 63 | 64 | public String getContent() { 65 | return content; 66 | } 67 | 68 | public int getContentType() { 69 | return contentType; 70 | } 71 | 72 | public String getData() { 73 | return data; 74 | } 75 | 76 | public static class Builder { 77 | private String msgId;// 消息唯一标识 78 | private int msgType; // 消息类型 79 | private String sender;// 发送者标识 80 | private String receiver;// 接收者标识 81 | private long timestamp;// 消息发送时间,单位:毫秒 82 | private int report;// 消息发送状态报告 83 | private String content;// 消息内容 84 | private int contentType;// 消息内容类型 85 | private String data; // 扩展字段,以key/value形式存储的json字符串 86 | 87 | public Builder() { 88 | this.msgId = UUID.generateShortUuid(); 89 | } 90 | 91 | public Builder setMsgType(int msgType) { 92 | this.msgType = msgType; 93 | return this; 94 | } 95 | 96 | public Builder setSender(String sender) { 97 | this.sender = sender; 98 | return this; 99 | } 100 | 101 | public Builder setReceiver(String receiver) { 102 | this.receiver = receiver; 103 | return this; 104 | } 105 | 106 | public Builder setTimestamp(long timestamp) { 107 | this.timestamp = timestamp; 108 | return this; 109 | } 110 | 111 | public Builder setReport(int report) { 112 | this.report = report; 113 | return this; 114 | } 115 | 116 | public Builder setContent(String content) { 117 | this.content = content; 118 | return this; 119 | } 120 | 121 | public Builder setContentType(int contentType) { 122 | this.contentType = contentType; 123 | return this; 124 | } 125 | 126 | public Builder setData(String data) { 127 | this.data = data; 128 | return this; 129 | } 130 | 131 | public IMSMsg build() { 132 | return new IMSMsg(this); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/CommunicationProtocol.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | /** 4 | * @author FreddyChen 5 | * @name 通讯协议 6 | * @date 2020/05/21 16:32 7 | * @email chenshichao@outlook.com 8 | * @github https://github.com/FreddyChen 9 | */ 10 | public enum CommunicationProtocol { 11 | TCP, 12 | WebSocket 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/IMSConfig.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | /** 4 | * IMS配置 5 | */ 6 | public class IMSConfig { 7 | 8 | public static final int CONNECT_TIMEOUT = 10 * 1000;// 连接超时时间,单位:毫秒 9 | public static final int RECONNECT_INTERVAL = 8 * 1000;// 重连间隔时间,单位:毫秒 10 | public static final int RECONNECT_COUNT = 3;// 单个地址一个周期最大重连次数 11 | public static final int FOREGROUND_HEARTBEAT_INTERVAL = 8 * 1000;// 应用在前台时心跳间隔时间,单位:毫秒 12 | public static final int BACKGROUND_HEARTBEAT_INTERVAL = 30 * 1000;// 应用在后台时心跳间隔时间,单位:毫秒 13 | public static final boolean AUTO_RESEND = true;// 是否自动重发消息 14 | public static final int RESEND_INTERVAL = 3 * 1000;// 自动重发间隔时间,单位:毫秒 15 | public static final int RESEND_COUNT = 5;// 消息最大重发次数 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/IMSConnectStatus.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | public enum IMSConnectStatus { 4 | Unconnected(0, "未连接"), 5 | Connecting(1, "连接中"), 6 | Connected(2, "连接成功"), 7 | ConnectFailed(-100, "连接失败"), 8 | ConnectFailed_IMSClosed(-101, "连接失败:IMS已关闭"), 9 | ConnectFailed_ServerListEmpty(-102, "连接失败:服务器地址列表为空"), 10 | ConnectFailed_ServerEmpty(-103, "连接失败:服务器地址为空"), 11 | ConnectFailed_ServerIllegitimate(-104, "连接失败:服务器地址不合法"), 12 | ConnectFailed_NetworkUnavailable(-105, "连接失败:网络不可用"); 13 | 14 | private int errCode; 15 | private String errMsg; 16 | IMSConnectStatus(int errCode, String errMsg) { 17 | this.errCode = errCode; 18 | this.errMsg = errMsg; 19 | } 20 | 21 | public int getErrCode() { 22 | return errCode; 23 | } 24 | 25 | public String getErrMsg() { 26 | return errMsg; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/IMSOptions.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * IMS初始化配置项 7 | */ 8 | public class IMSOptions { 9 | 10 | private ImplementationMode implementationMode;// 实现方式 11 | private CommunicationProtocol communicationProtocol;// 通信协议 12 | private TransportProtocol transportProtocol;// 传输协议 13 | private int connectTimeout;// 连接超时时间,单位:毫秒 14 | private int reconnectInterval;// 重连间隔时间,单位:毫秒 15 | private int reconnectCount;// 单个地址一个周期最大重连次数 16 | private int foregroundHeartbeatInterval;// 应用在前台时心跳间隔时间,单位:毫秒 17 | private int backgroundHeartbeatInterval;// 应用在后台时心跳间隔时间,单位:毫秒 18 | private boolean autoResend;// 是否自动重发消息 19 | private int resendInterval;// 自动重发间隔时间,单位:毫秒 20 | private int resendCount;// 消息最大重发次数 21 | private List serverList;// 服务器地址列表 22 | 23 | private IMSOptions() { 24 | } 25 | 26 | private IMSOptions(Builder builder) { 27 | if (builder == null) return; 28 | this.implementationMode = builder.implementationMode; 29 | this.communicationProtocol = builder.communicationProtocol; 30 | this.transportProtocol = builder.transportProtocol; 31 | this.connectTimeout = builder.connectTimeout; 32 | this.reconnectInterval = builder.reconnectInterval; 33 | this.reconnectCount = builder.reconnectCount; 34 | this.foregroundHeartbeatInterval = builder.foregroundHeartbeatInterval; 35 | this.backgroundHeartbeatInterval = builder.backgroundHeartbeatInterval; 36 | this.autoResend = builder.autoResend; 37 | this.resendInterval = builder.resendInterval; 38 | this.resendCount = builder.resendCount; 39 | this.serverList = builder.serverList; 40 | } 41 | 42 | public ImplementationMode getImplementationMode() { 43 | return implementationMode; 44 | } 45 | 46 | public CommunicationProtocol getCommunicationProtocol() { 47 | return communicationProtocol; 48 | } 49 | 50 | public TransportProtocol getTransportProtocol() { 51 | return transportProtocol; 52 | } 53 | 54 | public int getConnectTimeout() { 55 | return connectTimeout; 56 | } 57 | 58 | public int getReconnectInterval() { 59 | return reconnectInterval; 60 | } 61 | 62 | public int getReconnectCount() { 63 | return reconnectCount; 64 | } 65 | 66 | public int getForegroundHeartbeatInterval() { 67 | return foregroundHeartbeatInterval; 68 | } 69 | 70 | public int getBackgroundHeartbeatInterval() { 71 | return backgroundHeartbeatInterval; 72 | } 73 | 74 | public boolean isAutoResend() { 75 | return autoResend; 76 | } 77 | 78 | public int getResendInterval() { 79 | return resendInterval; 80 | } 81 | 82 | public int getResendCount() { 83 | return resendCount; 84 | } 85 | 86 | public List getServerList() { 87 | return serverList; 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return "IMSOptions\n{" + 93 | "\n\timplementationMode=" + implementationMode + 94 | "\n\tcommunicationProtocol=" + communicationProtocol + 95 | "\n\ttransportProtocol=" + transportProtocol + 96 | "\n\tconnectTimeout=" + connectTimeout + 97 | "\n\treconnectInterval=" + reconnectInterval + 98 | "\n\treconnectCount=" + reconnectCount + 99 | "\n\tforegroundHeartbeatInterval=" + foregroundHeartbeatInterval + 100 | "\n\tbackgroundHeartbeatInterval=" + backgroundHeartbeatInterval + 101 | "\n\tautoResend=" + autoResend + 102 | "\n\tresendInterval=" + resendInterval + 103 | "\n\tresendCount=" + resendCount + 104 | "\n\tserverList=" + serverList + 105 | "\n}"; 106 | } 107 | 108 | public static class Builder { 109 | private ImplementationMode implementationMode;// 实现方式 110 | private CommunicationProtocol communicationProtocol;// 通信协议 111 | private TransportProtocol transportProtocol;// 传输协议 112 | private int connectTimeout;// 连接超时时间,单位:毫秒 113 | private int reconnectInterval;// 重连间隔时间,单位:毫秒 114 | private int reconnectCount;// 单个地址一个周期最大重连次数 115 | private int foregroundHeartbeatInterval;// 应用在前台时心跳间隔时间,单位:毫秒 116 | private int backgroundHeartbeatInterval;// 应用在后台时心跳间隔时间,单位:毫秒 117 | private boolean autoResend;// 是否自动重发消息 118 | private int resendInterval;// 自动重发间隔时间,单位:毫秒 119 | private int resendCount;// 消息最大重发次数 120 | private List serverList;// 服务器地址列表 121 | 122 | public Builder() { 123 | this.implementationMode = ImplementationMode.Netty; 124 | this.connectTimeout = IMSConfig.CONNECT_TIMEOUT; 125 | this.reconnectInterval = IMSConfig.RECONNECT_INTERVAL; 126 | this.reconnectCount = IMSConfig.RECONNECT_COUNT; 127 | this.foregroundHeartbeatInterval = IMSConfig.FOREGROUND_HEARTBEAT_INTERVAL; 128 | this.backgroundHeartbeatInterval = IMSConfig.BACKGROUND_HEARTBEAT_INTERVAL; 129 | this.autoResend = IMSConfig.AUTO_RESEND; 130 | this.resendInterval = IMSConfig.RESEND_INTERVAL; 131 | this.resendCount = IMSConfig.RESEND_COUNT; 132 | } 133 | 134 | public Builder setImplementationMode(ImplementationMode implementationMode) { 135 | this.implementationMode = implementationMode; 136 | return this; 137 | } 138 | 139 | public Builder setCommunicationProtocol(CommunicationProtocol communicationProtocol) { 140 | this.communicationProtocol = communicationProtocol; 141 | return this; 142 | } 143 | 144 | public Builder setTransportProtocol(TransportProtocol transportProtocol) { 145 | this.transportProtocol = transportProtocol; 146 | return this; 147 | } 148 | 149 | public Builder setConnectTimeout(int connectTimeout) { 150 | this.connectTimeout = connectTimeout; 151 | return this; 152 | } 153 | 154 | public Builder setReconnectInterval(int reconnectInterval) { 155 | this.reconnectInterval = reconnectInterval; 156 | return this; 157 | } 158 | 159 | public Builder setReconnectCount(int reconnectCount) { 160 | this.reconnectCount = reconnectCount; 161 | return this; 162 | } 163 | 164 | public Builder setForegroundHeartbeatInterval(int foregroundHeartbeatInterval) { 165 | this.foregroundHeartbeatInterval = foregroundHeartbeatInterval; 166 | return this; 167 | } 168 | 169 | public Builder setBackgroundHeartbeatInterval(int backgroundHeartbeatInterval) { 170 | this.backgroundHeartbeatInterval = backgroundHeartbeatInterval; 171 | return this; 172 | } 173 | 174 | public Builder setAutoResend(boolean autoResend) { 175 | this.autoResend = autoResend; 176 | return this; 177 | } 178 | 179 | public Builder setResendInterval(int resendInterval) { 180 | this.resendInterval = resendInterval; 181 | return this; 182 | } 183 | 184 | public Builder setResendCount(int resendCount) { 185 | this.resendCount = resendCount; 186 | return this; 187 | } 188 | 189 | public Builder setServerList(List serverList) { 190 | this.serverList = serverList; 191 | return this; 192 | } 193 | 194 | public IMSOptions build() { 195 | return new IMSOptions(this); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/ImplementationMode.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | public enum ImplementationMode { 4 | Netty, 5 | Nio, 6 | Mina 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/config/TransportProtocol.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.config; 2 | 3 | /** 4 | * @author FreddyChen 5 | * @name 传输协议 6 | * @date 2020/05/21 16:32 7 | * @email chenshichao@outlook.com 8 | * @github https://github.com/FreddyChen 9 | */ 10 | public enum TransportProtocol { 11 | Protobuf, 12 | Json 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/interf/IMSInterface.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.interf; 2 | 3 | import android.content.Context; 4 | 5 | import com.freddy.kulaims.bean.IMSMsg; 6 | import com.freddy.kulaims.config.IMSOptions; 7 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 8 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 9 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 10 | 11 | /** 12 | * @author FreddyChen 13 | * @name IMS抽象接口 14 | * @date 2020/05/21 16:32 15 | * @email chenshichao@outlook.com 16 | * @github https://github.com/FreddyChen 17 | * @desc 不同的客户端协议实现此接口即可,例: 18 | * {@link com.freddy.kulaims.netty.tcp.NettyTCPIMS} 19 | * {@link com.freddy.kulaims.netty.websocket.NettyWebSocketIMS} 20 | * {@link com.freddy.kulaims.nio.tcp.NioTCPIMS} 21 | * {@link com.freddy.kulaims.nio.websocket.NioWebSocketIMS} 22 | * {@link com.freddy.kulaims.mina.tcp.MinaTCPIMS} 23 | * {@link com.freddy.kulaims.mina.websocket.MinaWebSocketIMS} 24 | */ 25 | public interface IMSInterface { 26 | 27 | /** 28 | * 初始化 29 | * 30 | * @param context 31 | * @param options IMS初始化配置 32 | * @param connectStatusListener IMS连接状态监听 33 | * @param msgReceivedListener IMS消息接收监听 34 | */ 35 | boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener); 36 | 37 | /** 38 | * 连接 39 | */ 40 | void connect(); 41 | 42 | /** 43 | * 重连 44 | * 45 | * @param isFirstConnect 是否首次连接 46 | */ 47 | void reconnect(boolean isFirstConnect); 48 | 49 | /** 50 | * 发送消息 51 | * 52 | * @param msg 53 | */ 54 | void sendMsg(IMSMsg msg); 55 | 56 | /** 57 | * 发送消息 58 | * 重载 59 | * 60 | * @param msg 61 | * @param listener 消息发送状态监听器 62 | */ 63 | void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener); 64 | 65 | /** 66 | * 发送消息 67 | * 重载 68 | * 69 | * @param msg 70 | * @param isJoinResendManager 是否加入消息重发管理器 71 | */ 72 | void sendMsg(IMSMsg msg, boolean isJoinResendManager); 73 | 74 | /** 75 | * 发送消息 76 | * 重载 77 | * 78 | * @param msg 79 | * @param listener 消息发送状态监听器 80 | * @param isJoinResendManager 是否加入消息重发管理器 81 | */ 82 | void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager); 83 | 84 | /** 85 | * 释放资源 86 | */ 87 | void release(); 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/listener/IMSConnectStatusListener.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.listener; 2 | 3 | /** 4 | * @author FreddyChen 5 | * @name IMS连接状态监听器 6 | * @date 2020/05/21 16:32 7 | * @email chenshichao@outlook.com 8 | * @github https://github.com/FreddyChen 9 | */ 10 | public interface IMSConnectStatusListener { 11 | void onUnconnected(); 12 | void onConnecting(); 13 | void onConnected(); 14 | void onConnectFailed(int errCode, String errMsg); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/listener/IMSMsgReceivedListener.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.listener; 2 | 3 | import com.freddy.kulaims.bean.IMSMsg; 4 | 5 | /** 6 | * @author FreddyChen 7 | * @name IMS消息接收监听器 8 | * @date 2020/05/21 16:32 9 | * @email chenshichao@outlook.com 10 | * @github https://github.com/FreddyChen 11 | */ 12 | public interface IMSMsgReceivedListener { 13 | void onMsgReceived(IMSMsg msg); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/listener/IMSMsgSentStatusListener.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.listener; 2 | 3 | import com.freddy.kulaims.bean.IMSMsg; 4 | 5 | /** 6 | * @author FreddyChen 7 | * @name IMS消息发送状态监听器 8 | * @date 2020/05/21 16:32 9 | * @email chenshichao@outlook.com 10 | * @github https://github.com/FreddyChen 11 | */ 12 | public interface IMSMsgSentStatusListener { 13 | 14 | /** 15 | * 消息发送成功 16 | */ 17 | void onSendSucceed(IMSMsg msg); 18 | 19 | /** 20 | * 消息发送失败 21 | */ 22 | void onSendFailed(IMSMsg msg, String errMsg); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/mina/tcp/MinaTCPIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.mina.tcp; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | 6 | import com.freddy.kulaims.bean.IMSMsg; 7 | import com.freddy.kulaims.config.IMSOptions; 8 | import com.freddy.kulaims.interf.IMSInterface; 9 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 10 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 11 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 12 | 13 | import io.netty.channel.Channel; 14 | 15 | public class MinaTCPIMS implements IMSInterface { 16 | 17 | private MinaTCPIMS() { 18 | } 19 | 20 | public static MinaTCPIMS getInstance() { 21 | return SingletonHolder.INSTANCE; 22 | } 23 | 24 | private static final class SingletonHolder { 25 | @SuppressLint("StaticFieldLeak") 26 | private static final MinaTCPIMS INSTANCE = new MinaTCPIMS(); 27 | } 28 | 29 | @Override 30 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 31 | return false; 32 | } 33 | 34 | @Override 35 | public void connect() { 36 | 37 | } 38 | 39 | @Override 40 | public void reconnect(boolean isFirstConnect) { 41 | 42 | } 43 | 44 | @Override 45 | public void sendMsg(IMSMsg msg) { 46 | 47 | } 48 | 49 | @Override 50 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 51 | 52 | } 53 | 54 | @Override 55 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 56 | 57 | } 58 | 59 | @Override 60 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 61 | } 62 | 63 | @Override 64 | public void release() { 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/mina/websocket/MinaWebSocketIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.mina.websocket; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | 6 | import com.freddy.kulaims.bean.IMSMsg; 7 | import com.freddy.kulaims.config.IMSOptions; 8 | import com.freddy.kulaims.interf.IMSInterface; 9 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 10 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 11 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 12 | 13 | public class MinaWebSocketIMS implements IMSInterface { 14 | 15 | private MinaWebSocketIMS() { 16 | } 17 | 18 | public static MinaWebSocketIMS getInstance() { 19 | return SingletonHolder.INSTANCE; 20 | } 21 | 22 | private static final class SingletonHolder { 23 | @SuppressLint("StaticFieldLeak") 24 | private static final MinaWebSocketIMS INSTANCE = new MinaWebSocketIMS(); 25 | } 26 | 27 | @Override 28 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 29 | return false; 30 | } 31 | 32 | @Override 33 | public void connect() { 34 | 35 | } 36 | 37 | @Override 38 | public void reconnect(boolean isFirstConnect) { 39 | 40 | } 41 | 42 | @Override 43 | public void sendMsg(IMSMsg msg) { 44 | 45 | } 46 | 47 | @Override 48 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 49 | 50 | } 51 | 52 | @Override 53 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 54 | 55 | } 56 | 57 | @Override 58 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 59 | 60 | } 61 | 62 | @Override 63 | public void release() { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/net/NetworkManager.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.net; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.Network; 6 | import android.net.NetworkCapabilities; 7 | import android.net.NetworkRequest; 8 | import android.util.Log; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class NetworkManager extends ConnectivityManager.NetworkCallback { 16 | 17 | private static final String TAG = NetworkManager.class.getSimpleName(); 18 | private List mObservers = new ArrayList<>(); 19 | private NetworkType networkType; 20 | 21 | private NetworkManager() { 22 | } 23 | 24 | public static NetworkManager getInstance() { 25 | return SingletonHolder.INSTANCE; 26 | } 27 | 28 | private static final class SingletonHolder { 29 | private static final NetworkManager INSTANCE = new NetworkManager(); 30 | } 31 | 32 | @Override 33 | public void onAvailable(@NonNull Network network) { 34 | Log.d(TAG, "onAvailable() network = " + network); 35 | notifyObservers(true); 36 | } 37 | 38 | @Override 39 | public void onLost(@NonNull Network network) { 40 | Log.d(TAG, "onLost() network = " + network); 41 | notifyObservers(false); 42 | } 43 | 44 | @Override 45 | public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { 46 | if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { 47 | if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { 48 | updateNetworkType(NetworkType.Wifi); 49 | } else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { 50 | updateNetworkType(NetworkType.Cellular); 51 | } else { 52 | updateNetworkType(NetworkType.Other); 53 | } 54 | } 55 | } 56 | 57 | private void updateNetworkType(NetworkType type) { 58 | if (type == networkType) { 59 | return; 60 | } 61 | 62 | Log.d(TAG, "updateNetworkType() type = " + type); 63 | this.networkType = type; 64 | } 65 | 66 | public void registerObserver(Context context, INetworkStateChangedObserver observer) { 67 | if (context == null) { 68 | return; 69 | } 70 | 71 | if (observer == null) { 72 | return; 73 | } 74 | 75 | if (mObservers == null) { 76 | return; 77 | } 78 | 79 | if (mObservers.contains(observer)) { 80 | return; 81 | } 82 | 83 | NetworkRequest request = new NetworkRequest.Builder().build(); 84 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 85 | cm.registerNetworkCallback(request, this); 86 | 87 | mObservers.add(observer); 88 | } 89 | 90 | public void unregisterObserver(Context context, INetworkStateChangedObserver observer) { 91 | if (context == null) { 92 | return; 93 | } 94 | 95 | if (observer == null) { 96 | return; 97 | } 98 | 99 | if (mObservers == null) { 100 | return; 101 | } 102 | 103 | if (!mObservers.contains(observer)) { 104 | return; 105 | } 106 | 107 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 108 | cm.unregisterNetworkCallback(this); 109 | 110 | mObservers.remove(observer); 111 | } 112 | 113 | public void notifyObservers(boolean available) { 114 | if (mObservers == null || mObservers.isEmpty()) { 115 | return; 116 | } 117 | 118 | for (INetworkStateChangedObserver observer : mObservers) { 119 | if (available) { 120 | observer.onNetworkAvailable(); 121 | } else { 122 | observer.onNetworkUnavailable(); 123 | } 124 | } 125 | } 126 | 127 | public interface INetworkStateChangedObserver { 128 | void onNetworkAvailable(); 129 | 130 | void onNetworkUnavailable(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/net/NetworkType.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.net; 2 | 3 | public enum NetworkType { 4 | Wifi, 5 | Cellular, 6 | Other 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/tcp/NettyTCPChannelInitializerHandler.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.tcp; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | 6 | public class NettyTCPChannelInitializerHandler extends ChannelInitializer { 7 | 8 | private NettyTCPIMS ims; 9 | 10 | NettyTCPChannelInitializerHandler(NettyTCPIMS ims) { 11 | this.ims = ims; 12 | } 13 | 14 | @Override 15 | protected void initChannel(Channel ch) throws Exception { 16 | ch.pipeline().addLast(NettyTCPReadHandler.class.getSimpleName(), new NettyTCPReadHandler(ims)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/tcp/NettyTCPIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.tcp; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | import com.freddy.kulaims.bean.IMSMsg; 8 | import com.freddy.kulaims.config.IMSConnectStatus; 9 | import com.freddy.kulaims.config.IMSOptions; 10 | import com.freddy.kulaims.interf.IMSInterface; 11 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 12 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 13 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 14 | import com.freddy.kulaims.net.NetworkManager; 15 | import com.freddy.kulaims.utils.ExecutorServiceFactory; 16 | 17 | import io.netty.bootstrap.Bootstrap; 18 | import io.netty.channel.Channel; 19 | import io.netty.channel.ChannelOption; 20 | import io.netty.channel.ChannelPipeline; 21 | import io.netty.channel.nio.NioEventLoopGroup; 22 | import io.netty.channel.socket.nio.NioSocketChannel; 23 | 24 | /** 25 | * @author FreddyChen 26 | * @name Netty TCP IM Service 27 | * @date 2020/05/21 16:33 28 | * @email chenshichao@outlook.com 29 | * @github https://github.com/FreddyChen 30 | * @desc 基于Netty实现的TCP协议客户端 31 | */ 32 | public class NettyTCPIMS implements IMSInterface, NetworkManager.INetworkStateChangedObserver { 33 | 34 | private static final String TAG = NettyTCPIMS.class.getSimpleName(); 35 | private Context mContext; 36 | // ims配置项 37 | private IMSOptions mIMSOptions; 38 | // ims连接状态监听器 39 | private IMSConnectStatusListener mIMSConnectStatusListener; 40 | // ims消息接收监听器 41 | private IMSMsgReceivedListener mIMSMsgReceivedListener; 42 | // ims是否已关闭 43 | private volatile boolean isClosed = true; 44 | // 是否正在进行重连 45 | private volatile boolean isReconnecting = false; 46 | // 是否已初始化成功 47 | private boolean initialized = false; 48 | private Bootstrap bootstrap; 49 | private Channel channel; 50 | // ims连接状态 51 | private volatile IMSConnectStatus imsConnectStatus; 52 | // 线程池组 53 | private ExecutorServiceFactory executors; 54 | // 网络是否可用标识 55 | private boolean isNetworkAvailable; 56 | 57 | private NettyTCPIMS() { 58 | } 59 | 60 | public static NettyTCPIMS getInstance() { 61 | return SingletonHolder.INSTANCE; 62 | } 63 | 64 | private static final class SingletonHolder { 65 | @SuppressLint("StaticFieldLeak") 66 | private static final NettyTCPIMS INSTANCE = new NettyTCPIMS(); 67 | } 68 | 69 | /** 70 | * 网络可用回调 71 | */ 72 | @Override 73 | public void onNetworkAvailable() { 74 | this.isNetworkAvailable = true; 75 | Log.d(TAG, "网络可用,启动ims"); 76 | this.isClosed = false; 77 | // 网络连接时,自动重连ims 78 | this.reconnect(true); 79 | } 80 | 81 | /** 82 | * 网络不可用回调 83 | */ 84 | @Override 85 | public void onNetworkUnavailable() { 86 | this.isNetworkAvailable = false; 87 | Log.d(TAG, "网络不可用,关闭ims"); 88 | this.isClosed = true; 89 | this.isReconnecting = false; 90 | // 网络断开时,销毁重连线程组(停止重连任务) 91 | executors.destroyBossLoopGroup(); 92 | // 回调ims连接状态 93 | callbackIMSConnectStatus(IMSConnectStatus.ConnectFailed_NetworkUnavailable); 94 | // 关闭channel 95 | closeChannel(); 96 | // 关闭bootstrap 97 | closeBootstrap(); 98 | } 99 | 100 | /** 101 | * 初始化 102 | * @param context 103 | * @param options IMS初始化配置 104 | * @param connectStatusListener IMS连接状态监听 105 | * @param msgReceivedListener IMS消息接收监听 106 | * @return 107 | */ 108 | @Override 109 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 110 | if (context == null) { 111 | Log.d(TAG, "初始化失败:Context is null."); 112 | initialized = false; 113 | return false; 114 | } 115 | 116 | if (options == null) { 117 | Log.d(TAG, "初始化失败:IMSOptions is null."); 118 | initialized = false; 119 | return false; 120 | } 121 | this.mContext = context; 122 | this.mIMSOptions = options; 123 | this.mIMSConnectStatusListener = connectStatusListener; 124 | this.mIMSMsgReceivedListener = msgReceivedListener; 125 | executors = new ExecutorServiceFactory(); 126 | // 初始化重连线程池 127 | executors.initBossLoopGroup(); 128 | // 注册网络连接状态监听 129 | NetworkManager.getInstance().registerObserver(context, this); 130 | // 标识ims初始化成功 131 | initialized = true; 132 | // 标识ims已打开 133 | isClosed = false; 134 | callbackIMSConnectStatus(IMSConnectStatus.Unconnected); 135 | return true; 136 | } 137 | 138 | /** 139 | * 连接 140 | */ 141 | @Override 142 | public void connect() { 143 | if(!initialized) { 144 | Log.w(TAG, "IMS初始化失败,initialized is false"); 145 | return; 146 | } 147 | this.reconnect(true); 148 | } 149 | 150 | /** 151 | * 重连 152 | * @param isFirstConnect 是否首次连接 153 | */ 154 | @Override 155 | public void reconnect(boolean isFirstConnect) { 156 | if (!isFirstConnect) { 157 | // 非首次连接,代表之前已经进行过重连,延时一段时间再去重连 158 | try { 159 | Log.w(TAG, String.format("非首次连接,延时%1$dms再次尝试重连", mIMSOptions.getReconnectInterval())); 160 | Thread.sleep(mIMSOptions.getReconnectInterval()); 161 | } catch (InterruptedException e) { 162 | e.printStackTrace(); 163 | } 164 | } 165 | 166 | if (!isClosed && !isReconnecting) { 167 | synchronized (this) { 168 | if (!isClosed && !isReconnecting) { 169 | if(imsConnectStatus == IMSConnectStatus.Connected) { 170 | Log.w(TAG, "已连接,无需重连"); 171 | return; 172 | } 173 | // 标识正在进行重连 174 | setReconnecting(true); 175 | // 关闭channel 176 | closeChannel(); 177 | // 开启重连任务 178 | executors.execBossTask(new NettyTCPReconnectTask(this)); 179 | } 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * 发送消息 186 | * @param msg 187 | */ 188 | @Override 189 | public void sendMsg(IMSMsg msg) { 190 | this.sendMsg(msg, null, true); 191 | } 192 | 193 | /** 194 | * 发送消息 195 | * 重载 196 | * @param msg 197 | * @param listener 消息发送状态监听器 198 | */ 199 | @Override 200 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 201 | this.sendMsg(msg, listener, true); 202 | } 203 | 204 | /** 205 | * 发送消息 206 | * 重载 207 | * @param msg 208 | * @param isJoinResendManager 是否加入消息重发管理器 209 | */ 210 | @Override 211 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 212 | this.sendMsg(msg, null, isJoinResendManager); 213 | } 214 | 215 | /** 216 | * 发送消息 217 | * 重载 218 | * @param msg 219 | * @param listener 消息发送状态监听器 220 | * @param isJoinResendManager 是否加入消息重发管理器 221 | */ 222 | @Override 223 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 224 | if(!initialized) { 225 | Log.w(TAG, "IMS初始化失败,请查看日志"); 226 | return; 227 | } 228 | } 229 | 230 | /** 231 | * 释放资源 232 | */ 233 | @Override 234 | public void release() { 235 | // 关闭channel 236 | closeChannel(); 237 | // 关闭bootstrap 238 | closeBootstrap(); 239 | // 标识未进行初始化 240 | initialized = false; 241 | // 释放线程池组 242 | if(executors != null) { 243 | executors.destroy(); 244 | executors = null; 245 | } 246 | // 取消注册网络连接状态监听 247 | NetworkManager.getInstance().unregisterObserver(mContext, this); 248 | } 249 | 250 | /** 251 | * 初始化bootstrap 252 | */ 253 | void initBootstrap() { 254 | closeBootstrap();// 初始化前先关闭 255 | NioEventLoopGroup loopGroup = new NioEventLoopGroup(4); 256 | bootstrap = new Bootstrap(); 257 | bootstrap.group(loopGroup).channel(NioSocketChannel.class) 258 | // 设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文 259 | .option(ChannelOption.SO_KEEPALIVE, true) 260 | // 设置禁用nagle算法,如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false 261 | .option(ChannelOption.TCP_NODELAY, true) 262 | // 设置TCP发送缓冲区大小(字节数) 263 | .option(ChannelOption.SO_SNDBUF, 32 * 1024) 264 | // 设置TCP接收缓冲区大小(字节数) 265 | .option(ChannelOption.SO_RCVBUF, 32 * 1024) 266 | // 设置连接超时时长,单位:毫秒 267 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, mIMSOptions.getConnectTimeout()) 268 | // 设置初始化ChannelHandler 269 | .handler(new NettyTCPChannelInitializerHandler(this)); 270 | } 271 | 272 | /** 273 | * 关闭channel 274 | */ 275 | private void closeChannel() { 276 | try { 277 | if (channel != null) { 278 | removeHandler(NettyTCPReadHandler.class.getSimpleName()); 279 | try { 280 | channel.close(); 281 | } catch (Exception e) { 282 | e.printStackTrace(); 283 | } 284 | try { 285 | channel.eventLoop().shutdownGracefully(); 286 | } catch (Exception e) { 287 | e.printStackTrace(); 288 | } 289 | } 290 | }finally { 291 | channel = null; 292 | } 293 | } 294 | 295 | private void removeHandler(String name) { 296 | try { 297 | ChannelPipeline pipeline = channel.pipeline(); 298 | if(pipeline.get(name) != null) { 299 | pipeline.remove(name); 300 | } 301 | }catch (Exception e) { 302 | e.printStackTrace(); 303 | Log.e(TAG, "移除handler失败:" + name); 304 | } 305 | } 306 | 307 | /** 308 | * 关闭bootstrap 309 | */ 310 | void closeBootstrap() { 311 | try { 312 | if (bootstrap != null) { 313 | bootstrap.config().group().shutdownGracefully(); 314 | } 315 | } catch (Exception e) { 316 | e.printStackTrace(); 317 | } finally { 318 | bootstrap = null; 319 | } 320 | } 321 | 322 | /** 323 | * 回调ims连接状态 324 | * 325 | * @param imsConnectStatus 326 | */ 327 | void callbackIMSConnectStatus(IMSConnectStatus imsConnectStatus) { 328 | Log.d(TAG, "回调ims连接状态:" + imsConnectStatus); 329 | if(this.imsConnectStatus == imsConnectStatus) { 330 | Log.w(TAG, "连接状态与上一次相同,无需执行任何操作"); 331 | return; 332 | } 333 | 334 | this.imsConnectStatus = imsConnectStatus; 335 | switch (imsConnectStatus) { 336 | case Unconnected: 337 | Log.w(TAG, "IMS未连接"); 338 | if (mIMSConnectStatusListener != null) { 339 | mIMSConnectStatusListener.onUnconnected(); 340 | } 341 | break; 342 | 343 | case Connecting: 344 | Log.d(TAG, "IMS连接中"); 345 | if (mIMSConnectStatusListener != null) { 346 | mIMSConnectStatusListener.onConnecting(); 347 | } 348 | break; 349 | 350 | case Connected: 351 | Log.d(TAG, "IMS连接成功"); 352 | if (mIMSConnectStatusListener != null) { 353 | mIMSConnectStatusListener.onConnected(); 354 | } 355 | break; 356 | 357 | case ConnectFailed: 358 | case ConnectFailed_IMSClosed: 359 | case ConnectFailed_ServerListEmpty: 360 | case ConnectFailed_ServerEmpty: 361 | case ConnectFailed_ServerIllegitimate: 362 | case ConnectFailed_NetworkUnavailable: 363 | int errCode = imsConnectStatus.getErrCode(); 364 | String errMsg = imsConnectStatus.getErrMsg(); 365 | Log.w(TAG, "errCode = " + errCode + "\terrMsg = " + errMsg); 366 | if (mIMSConnectStatusListener != null) { 367 | mIMSConnectStatusListener.onConnectFailed(errCode, errMsg); 368 | } 369 | break; 370 | } 371 | } 372 | 373 | ExecutorServiceFactory getExecutors() { 374 | return executors; 375 | } 376 | 377 | /** 378 | * 网络是否可用 379 | * 380 | * @return 381 | */ 382 | boolean isNetworkAvailable() { 383 | return isNetworkAvailable; 384 | } 385 | 386 | /** 387 | * ims是否关闭 388 | * 389 | * @return 390 | */ 391 | boolean isClosed() { 392 | return isClosed; 393 | } 394 | 395 | IMSOptions getIMSOptions() { 396 | return mIMSOptions; 397 | } 398 | 399 | Bootstrap getBootstrap() { 400 | return bootstrap; 401 | } 402 | 403 | void setChannel(Channel channel) { 404 | this.channel = channel; 405 | } 406 | 407 | /** 408 | * 标识是否正在进行重连 409 | * @param isReconnecting 410 | */ 411 | void setReconnecting(boolean isReconnecting) { 412 | this.isReconnecting = isReconnecting; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/tcp/NettyTCPReadHandler.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.tcp; 2 | 3 | import android.util.Log; 4 | 5 | import com.freddy.kulaims.config.IMSConnectStatus; 6 | 7 | import io.netty.channel.Channel; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | 11 | public class NettyTCPReadHandler extends ChannelInboundHandlerAdapter { 12 | 13 | private static final String TAG = NettyTCPReadHandler.class.getSimpleName(); 14 | private NettyTCPIMS ims; 15 | 16 | NettyTCPReadHandler(NettyTCPIMS ims) { 17 | this.ims = ims; 18 | } 19 | 20 | @Override 21 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 22 | Log.d(TAG, "channelActive() ctx = " + ctx); 23 | } 24 | 25 | /** 26 | * ims连接断开回调 27 | * @param ctx 28 | * @throws Exception 29 | */ 30 | @Override 31 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 32 | Log.w(TAG, "channelInactive() ctx = " + ctx); 33 | closeChannelAndReconnect(ctx); 34 | } 35 | 36 | /** 37 | * ims异常回调 38 | * @param ctx 39 | * @param cause 40 | * @throws Exception 41 | */ 42 | @Override 43 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 44 | Log.e(TAG, "exceptionCaught() ctx = " + ctx + "\tcause = " + cause); 45 | closeChannelAndReconnect(ctx); 46 | } 47 | 48 | /** 49 | * 收到消息回调 50 | * @param ctx 51 | * @param msg 52 | * @throws Exception 53 | */ 54 | @Override 55 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 56 | Log.d(TAG, "channelRead() ctx = " + ctx + "\tmsg = " + msg); 57 | } 58 | 59 | /** 60 | * 关闭channel并重连 61 | * @param ctx 62 | */ 63 | private void closeChannelAndReconnect(ChannelHandlerContext ctx) { 64 | Log.d(TAG, "准备关闭channel并重连"); 65 | Channel channel = ctx.channel(); 66 | if(channel != null) { 67 | channel.close(); 68 | } 69 | // 回调连接状态 70 | ims.callbackIMSConnectStatus(IMSConnectStatus.ConnectFailed); 71 | // 触发重连 72 | ims.reconnect(false); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/tcp/NettyTCPReconnectTask.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.tcp; 2 | 3 | import android.util.Log; 4 | 5 | import com.freddy.kulaims.config.IMSConnectStatus; 6 | import com.freddy.kulaims.config.IMSOptions; 7 | 8 | import java.util.List; 9 | 10 | import io.netty.channel.Channel; 11 | import io.netty.util.internal.StringUtil; 12 | 13 | public class NettyTCPReconnectTask implements Runnable { 14 | 15 | private static final String TAG = NettyTCPReconnectTask.class.getSimpleName(); 16 | private NettyTCPIMS ims; 17 | private IMSOptions mIMSOptions; 18 | 19 | NettyTCPReconnectTask(NettyTCPIMS ims) { 20 | this.ims = ims; 21 | this.mIMSOptions = ims.getIMSOptions(); 22 | } 23 | 24 | @Override 25 | public void run() { 26 | try { 27 | // 重连时,释放工作线程组,也就是停止心跳 28 | ims.getExecutors().destroyWorkLoopGroup(); 29 | 30 | // ims未关闭并且网络可用的情况下,才去连接 31 | while (!ims.isClosed() && ims.isNetworkAvailable()) { 32 | IMSConnectStatus status; 33 | if ((status = connect()) == IMSConnectStatus.Connected) { 34 | ims.callbackIMSConnectStatus(status); 35 | break;// 连接成功,跳出循环 36 | } 37 | 38 | if (status == IMSConnectStatus.ConnectFailed 39 | || status == IMSConnectStatus.ConnectFailed_IMSClosed 40 | || status == IMSConnectStatus.ConnectFailed_ServerListEmpty 41 | || status == IMSConnectStatus.ConnectFailed_ServerEmpty 42 | || status == IMSConnectStatus.ConnectFailed_ServerIllegitimate 43 | || status == IMSConnectStatus.ConnectFailed_NetworkUnavailable) { 44 | ims.callbackIMSConnectStatus(status); 45 | 46 | if(ims.isClosed() || !ims.isNetworkAvailable()) { 47 | return; 48 | } 49 | // 一个服务器地址列表都连接失败后,说明网络情况可能很差,延时指定时间(重连间隔时间*2)再去进行下一个服务器地址的连接 50 | Log.w(TAG, String.format("一个周期连接失败,等待%1$dms后再次尝试重连", mIMSOptions.getReconnectInterval() * 2)); 51 | try { 52 | Thread.sleep(mIMSOptions.getReconnectInterval() * 2); 53 | } catch (InterruptedException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | } 58 | } finally { 59 | // 标识重连任务停止 60 | ims.setReconnecting(false); 61 | } 62 | } 63 | 64 | /** 65 | * 连接服务器 66 | * @return 67 | */ 68 | private IMSConnectStatus connect() { 69 | if (ims.isClosed()) return IMSConnectStatus.ConnectFailed_IMSClosed; 70 | if(!ims.isNetworkAvailable()) return IMSConnectStatus.ConnectFailed_NetworkUnavailable; 71 | List serverList = mIMSOptions.getServerList(); 72 | if (serverList == null || serverList.isEmpty()) { 73 | return IMSConnectStatus.ConnectFailed_ServerListEmpty; 74 | } 75 | 76 | ims.initBootstrap(); 77 | for (int i = 0; i < serverList.size(); i++) { 78 | String server = serverList.get(i); 79 | if (StringUtil.isNullOrEmpty(server)) { 80 | return IMSConnectStatus.ConnectFailed_ServerEmpty; 81 | } 82 | 83 | String[] params = null; 84 | try { 85 | params = server.split(" "); 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | } 89 | if (params == null || params.length < 2) { 90 | return IMSConnectStatus.ConnectFailed_ServerIllegitimate; 91 | } 92 | 93 | if(i == 0) { 94 | ims.callbackIMSConnectStatus(IMSConnectStatus.Connecting); 95 | } 96 | 97 | // +1是因为首次连接也认为是重连,所以如果重连次数设置为3,则最大连接次数为3+1次 98 | for (int j = 0; j < mIMSOptions.getReconnectCount() + 1; j++) { 99 | if (ims.isClosed()) { 100 | return IMSConnectStatus.ConnectFailed_IMSClosed; 101 | } 102 | if (!ims.isNetworkAvailable()) { 103 | return IMSConnectStatus.ConnectFailed_NetworkUnavailable; 104 | } 105 | 106 | Log.d(TAG, String.format("正在进行【%1$s】的第%2$d次连接", server, j + 1)); 107 | try { 108 | String host = params[0]; 109 | int port = Integer.parseInt(params[1]); 110 | Channel channel = toServer(host, port); 111 | if (channel != null && channel.isOpen() && channel.isActive() && channel.isRegistered() && channel.isWritable()) { 112 | ims.setChannel(channel); 113 | return IMSConnectStatus.Connected; 114 | } else { 115 | if (j == mIMSOptions.getReconnectCount()) { 116 | // 如果当前已达到最大重连次数,并且是最后一个服务器地址,则回调连接失败 117 | if(i == serverList.size() - 1) { 118 | Log.w(TAG, String.format("【%1$s】连接失败", server)); 119 | return IMSConnectStatus.ConnectFailed; 120 | } 121 | // 否则,无需回调连接失败,等待一段时间再去进行下一个服务器地址连接即可 122 | // 也就是说,当服务器地址列表里的地址都连接失败,才认为是连接失败 123 | else { 124 | // 一个服务器地址连接失败后,延时指定时间再去进行下一个服务器地址的连接 125 | Log.w(TAG, String.format("【%1$s】连接失败,正在等待进行下一个服务器地址的重连,当前重连延时时长:%2$dms", server, mIMSOptions.getReconnectInterval())); 126 | Log.w(TAG, "========================================================================================="); 127 | Thread.sleep(mIMSOptions.getReconnectInterval()); 128 | } 129 | } else { 130 | // 连接失败,则线程休眠(重连间隔时长 / 2 * n) ms 131 | int delayTime = mIMSOptions.getReconnectInterval() + mIMSOptions.getReconnectInterval() / 2 * j; 132 | Log.w(TAG, String.format("【%1$s】连接失败,正在等待重连,当前重连延时时长:%2$dms", server, delayTime)); 133 | Thread.sleep(delayTime); 134 | } 135 | } 136 | } catch (InterruptedException e) { 137 | break;// 线程被中断,则强制关闭 138 | } 139 | } 140 | } 141 | 142 | return IMSConnectStatus.ConnectFailed; 143 | } 144 | 145 | /** 146 | * 真正连接服务器的地方 147 | * @param host 148 | * @param port 149 | * @return 150 | */ 151 | private Channel toServer(String host, int port) { 152 | Channel channel; 153 | try { 154 | channel = ims.getBootstrap().connect(host, port).sync().channel(); 155 | } catch (Exception e) { 156 | e.printStackTrace(); 157 | channel = null; 158 | } 159 | 160 | return channel; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/websocket/NettyWebSocketChannelInitializerHandler.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.websocket; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | 6 | public class NettyWebSocketChannelInitializerHandler extends ChannelInitializer { 7 | 8 | private NettyWebSocketIMS ims; 9 | 10 | NettyWebSocketChannelInitializerHandler(NettyWebSocketIMS ims) { 11 | this.ims = ims; 12 | } 13 | 14 | @Override 15 | protected void initChannel(Channel ch) throws Exception { 16 | ch.pipeline().addLast(new NettyWebSocketReadHandler(ims)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/websocket/NettyWebSocketIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.websocket; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.freddy.kulaims.bean.IMSMsg; 7 | import com.freddy.kulaims.config.IMSConnectStatus; 8 | import com.freddy.kulaims.config.IMSOptions; 9 | import com.freddy.kulaims.interf.IMSInterface; 10 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 11 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 12 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 13 | import com.freddy.kulaims.net.NetworkManager; 14 | import com.freddy.kulaims.netty.tcp.NettyTCPChannelInitializerHandler; 15 | import com.freddy.kulaims.netty.tcp.NettyTCPIMS; 16 | import com.freddy.kulaims.netty.tcp.NettyTCPReconnectTask; 17 | import com.freddy.kulaims.utils.ExecutorServiceFactory; 18 | 19 | import io.netty.bootstrap.Bootstrap; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.ChannelOption; 22 | import io.netty.channel.ChannelPipeline; 23 | import io.netty.channel.nio.NioEventLoopGroup; 24 | import io.netty.channel.socket.nio.NioSocketChannel; 25 | 26 | /** 27 | * @author FreddyChen 28 | * @name Netty WebSocket IM Service 29 | * @date 2020/05/21 16:33 30 | * @email chenshichao@outlook.com 31 | * @github https://github.com/FreddyChen 32 | * @desc 基于Netty实现的WebSocket协议客户端 33 | */ 34 | public class NettyWebSocketIMS implements IMSInterface, NetworkManager.INetworkStateChangedObserver { 35 | 36 | private static final String TAG = NettyWebSocketIMS.class.getSimpleName(); 37 | private Context mContext; 38 | // ims配置项 39 | private IMSOptions mIMSOptions; 40 | // ims连接状态监听器 41 | private IMSConnectStatusListener mIMSConnectStatusListener; 42 | // ims消息接收监听器 43 | private IMSMsgReceivedListener mIMSMsgReceivedListener; 44 | // ims是否已关闭 45 | private volatile boolean isClosed = true; 46 | // 是否正在进行重连 47 | private volatile boolean isReconnecting = false; 48 | // 是否已初始化成功 49 | private boolean initialized = false; 50 | private Bootstrap bootstrap; 51 | private Channel channel; 52 | // ims连接状态 53 | private volatile IMSConnectStatus imsConnectStatus; 54 | // 线程池组 55 | private ExecutorServiceFactory executors; 56 | // 网络是否可用标识 57 | private boolean isNetworkAvailable; 58 | // 是否执行过连接,如果未执行过,在onAvailable()的时候,无需进行重连 59 | private boolean isExecConnect = false; 60 | 61 | private NettyWebSocketIMS() { } 62 | 63 | public static NettyWebSocketIMS getInstance() { 64 | return SingletonHolder.INSTANCE; 65 | } 66 | 67 | private static final class SingletonHolder { 68 | private static final NettyWebSocketIMS INSTANCE = new NettyWebSocketIMS(); 69 | } 70 | 71 | /** 72 | * 网络可用回调 73 | */ 74 | @Override 75 | public void onNetworkAvailable() { 76 | this.isNetworkAvailable = true; 77 | if(!isExecConnect) { 78 | return; 79 | } 80 | Log.d(TAG, "网络可用,启动ims"); 81 | this.isClosed = false; 82 | // 网络连接时,自动重连ims 83 | this.reconnect(false); 84 | } 85 | 86 | /** 87 | * 网络不可用回调 88 | */ 89 | @Override 90 | public void onNetworkUnavailable() { 91 | this.isNetworkAvailable = false; 92 | if(!isExecConnect) { 93 | return; 94 | } 95 | Log.d(TAG, "网络不可用,关闭ims"); 96 | this.isClosed = true; 97 | this.isReconnecting = false; 98 | // 网络断开时,销毁重连线程组(停止重连任务) 99 | executors.destroyBossLoopGroup(); 100 | // 回调ims连接状态 101 | callbackIMSConnectStatus(IMSConnectStatus.ConnectFailed_NetworkUnavailable); 102 | // 关闭channel 103 | closeChannel(); 104 | // 关闭bootstrap 105 | closeBootstrap(); 106 | } 107 | 108 | /** 109 | * 初始化 110 | * @param context 111 | * @param options IMS初始化配置 112 | * @param connectStatusListener IMS连接状态监听 113 | * @param msgReceivedListener IMS消息接收监听 114 | * @return 115 | */ 116 | @Override 117 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 118 | if (context == null) { 119 | Log.d(TAG, "初始化失败:Context is null."); 120 | initialized = false; 121 | return false; 122 | } 123 | 124 | if (options == null) { 125 | Log.d(TAG, "初始化失败:IMSOptions is null."); 126 | initialized = false; 127 | return false; 128 | } 129 | this.mContext = context; 130 | this.mIMSOptions = options; 131 | this.mIMSConnectStatusListener = connectStatusListener; 132 | this.mIMSMsgReceivedListener = msgReceivedListener; 133 | executors = new ExecutorServiceFactory(); 134 | // 初始化重连线程池 135 | executors.initBossLoopGroup(); 136 | // 注册网络连接状态监听 137 | NetworkManager.getInstance().registerObserver(context, this); 138 | // 标识ims初始化成功 139 | initialized = true; 140 | // 标识ims已打开 141 | isClosed = false; 142 | callbackIMSConnectStatus(IMSConnectStatus.Unconnected); 143 | return true; 144 | } 145 | 146 | /** 147 | * 连接 148 | */ 149 | @Override 150 | public void connect() { 151 | if(!initialized) { 152 | Log.w(TAG, "IMS初始化失败,请查看日志"); 153 | return; 154 | } 155 | isExecConnect = true;// 标识已执行过连接 156 | this.reconnect(true); 157 | } 158 | 159 | /** 160 | * 重连 161 | * @param isFirstConnect 是否首次连接 162 | */ 163 | @Override 164 | public void reconnect(boolean isFirstConnect) { 165 | if (!isFirstConnect) { 166 | // 非首次连接,代表之前已经进行过重连,延时一段时间再去重连 167 | try { 168 | Log.w(TAG, String.format("非首次连接,延时%1$dms再次尝试重连", mIMSOptions.getReconnectInterval())); 169 | Thread.sleep(mIMSOptions.getReconnectInterval()); 170 | } catch (InterruptedException e) { 171 | e.printStackTrace(); 172 | } 173 | } 174 | 175 | if (!isClosed && !isReconnecting) { 176 | synchronized (this) { 177 | if (!isClosed && !isReconnecting) { 178 | // 标识正在进行重连 179 | setReconnecting(true); 180 | // 关闭channel 181 | closeChannel(); 182 | // 开启重连任务 183 | executors.execBossTask(new NettyWebSocketReconnectTask(this)); 184 | } 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * 发送消息 191 | * @param msg 192 | */ 193 | @Override 194 | public void sendMsg(IMSMsg msg) { 195 | this.sendMsg(msg, null, true); 196 | } 197 | 198 | /** 199 | * 发送消息 200 | * 重载 201 | * @param msg 202 | * @param listener 消息发送状态监听器 203 | */ 204 | @Override 205 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 206 | this.sendMsg(msg, listener, true); 207 | } 208 | 209 | /** 210 | * 发送消息 211 | * 重载 212 | * @param msg 213 | * @param isJoinResendManager 是否加入消息重发管理器 214 | */ 215 | @Override 216 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 217 | this.sendMsg(msg, null, isJoinResendManager); 218 | } 219 | 220 | /** 221 | * 发送消息 222 | * 重载 223 | * @param msg 224 | * @param listener 消息发送状态监听器 225 | * @param isJoinResendManager 是否加入消息重发管理器 226 | */ 227 | @Override 228 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 229 | if(!initialized) { 230 | Log.w(TAG, "IMS初始化失败,请查看日志"); 231 | return; 232 | } 233 | } 234 | 235 | /** 236 | * 释放资源 237 | */ 238 | @Override 239 | public void release() { 240 | // 关闭channel 241 | closeChannel(); 242 | // 关闭bootstrap 243 | closeBootstrap(); 244 | // 标识未进行初始化 245 | initialized = false; 246 | // 释放线程池组 247 | if(executors != null) { 248 | executors.destroy(); 249 | executors = null; 250 | } 251 | // 取消注册网络连接状态监听 252 | NetworkManager.getInstance().unregisterObserver(mContext, this); 253 | } 254 | 255 | /** 256 | * 初始化bootstrap 257 | */ 258 | void initBootstrap() { 259 | closeBootstrap();// 初始化前先关闭 260 | NioEventLoopGroup loopGroup = new NioEventLoopGroup(4); 261 | bootstrap = new Bootstrap(); 262 | bootstrap.group(loopGroup).channel(NioSocketChannel.class) 263 | // 设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文 264 | .option(ChannelOption.SO_KEEPALIVE, true) 265 | // 设置禁用nagle算法,如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false 266 | .option(ChannelOption.TCP_NODELAY, true) 267 | // 设置TCP发送缓冲区大小(字节数) 268 | .option(ChannelOption.SO_SNDBUF, 32 * 1024) 269 | // 设置TCP接收缓冲区大小(字节数) 270 | .option(ChannelOption.SO_RCVBUF, 32 * 1024) 271 | // 设置连接超时时长,单位:毫秒 272 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, mIMSOptions.getConnectTimeout()) 273 | // 设置初始化ChannelHandler 274 | .handler(new NettyWebSocketChannelInitializerHandler(this)); 275 | } 276 | 277 | /** 278 | * 关闭channel 279 | */ 280 | private void closeChannel() { 281 | try { 282 | if (channel != null) { 283 | // 关闭channel时,需要先移除对应handler 284 | removeHandler(NettyWebSocketReadHandler.class.getSimpleName()); 285 | try { 286 | channel.close(); 287 | } catch (Exception e) { 288 | e.printStackTrace(); 289 | } 290 | try { 291 | channel.eventLoop().shutdownGracefully(); 292 | } catch (Exception e) { 293 | e.printStackTrace(); 294 | } 295 | } 296 | }finally { 297 | channel = null; 298 | } 299 | } 300 | 301 | /** 302 | * 移除handler 303 | * @param name 304 | */ 305 | private void removeHandler(String name) { 306 | try { 307 | ChannelPipeline pipeline = channel.pipeline(); 308 | if(pipeline.get(name) != null) { 309 | pipeline.remove(name); 310 | } 311 | }catch (Exception e) { 312 | e.printStackTrace(); 313 | Log.e(TAG, "移除handler失败:" + name); 314 | } 315 | } 316 | 317 | /** 318 | * 关闭bootstrap 319 | */ 320 | void closeBootstrap() { 321 | try { 322 | if (bootstrap != null) { 323 | bootstrap.config().group().shutdownGracefully(); 324 | } 325 | } catch (Exception e) { 326 | e.printStackTrace(); 327 | } finally { 328 | bootstrap = null; 329 | } 330 | } 331 | 332 | /** 333 | * 回调ims连接状态 334 | * 335 | * @param imsConnectStatus 336 | */ 337 | void callbackIMSConnectStatus(IMSConnectStatus imsConnectStatus) { 338 | Log.d(TAG, "回调ims连接状态:" + imsConnectStatus); 339 | if(this.imsConnectStatus == imsConnectStatus) { 340 | Log.w(TAG, "连接状态与上一次相同,无需执行任何操作"); 341 | return; 342 | } 343 | 344 | this.imsConnectStatus = imsConnectStatus; 345 | switch (imsConnectStatus) { 346 | case Unconnected: 347 | Log.w(TAG, "IMS未连接"); 348 | if (mIMSConnectStatusListener != null) { 349 | mIMSConnectStatusListener.onUnconnected(); 350 | } 351 | break; 352 | 353 | case Connecting: 354 | Log.d(TAG, "IMS连接中"); 355 | if (mIMSConnectStatusListener != null) { 356 | mIMSConnectStatusListener.onConnecting(); 357 | } 358 | break; 359 | 360 | case Connected: 361 | Log.d(TAG, "IMS连接成功"); 362 | if (mIMSConnectStatusListener != null) { 363 | mIMSConnectStatusListener.onConnected(); 364 | } 365 | break; 366 | 367 | case ConnectFailed: 368 | case ConnectFailed_IMSClosed: 369 | case ConnectFailed_ServerListEmpty: 370 | case ConnectFailed_ServerEmpty: 371 | case ConnectFailed_ServerIllegitimate: 372 | case ConnectFailed_NetworkUnavailable: 373 | int errCode = imsConnectStatus.getErrCode(); 374 | String errMsg = imsConnectStatus.getErrMsg(); 375 | Log.w(TAG, "errCode = " + errCode + "\terrMsg = " + errMsg); 376 | if (mIMSConnectStatusListener != null) { 377 | mIMSConnectStatusListener.onConnectFailed(errCode, errMsg); 378 | } 379 | break; 380 | } 381 | } 382 | 383 | ExecutorServiceFactory getExecutors() { 384 | return executors; 385 | } 386 | 387 | /** 388 | * 网络是否可用 389 | * 390 | * @return 391 | */ 392 | boolean isNetworkAvailable() { 393 | return isNetworkAvailable; 394 | } 395 | 396 | /** 397 | * ims是否关闭 398 | * 399 | * @return 400 | */ 401 | boolean isClosed() { 402 | return isClosed; 403 | } 404 | 405 | IMSOptions getIMSOptions() { 406 | return mIMSOptions; 407 | } 408 | 409 | Bootstrap getBootstrap() { 410 | return bootstrap; 411 | } 412 | 413 | void setChannel(Channel channel) { 414 | this.channel = channel; 415 | } 416 | 417 | /** 418 | * 标识是否正在进行重连 419 | * @param isReconnecting 420 | */ 421 | void setReconnecting(boolean isReconnecting) { 422 | this.isReconnecting = isReconnecting; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/websocket/NettyWebSocketReadHandler.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.websocket; 2 | 3 | import android.util.Log; 4 | 5 | import com.freddy.kulaims.config.IMSConnectStatus; 6 | 7 | import io.netty.channel.Channel; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelInboundHandlerAdapter; 10 | 11 | public class NettyWebSocketReadHandler extends ChannelInboundHandlerAdapter { 12 | 13 | private static final String TAG = NettyWebSocketReadHandler.class.getSimpleName(); 14 | private NettyWebSocketIMS ims; 15 | 16 | NettyWebSocketReadHandler(NettyWebSocketIMS ims) { 17 | this.ims = ims; 18 | } 19 | 20 | @Override 21 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 22 | super.channelActive(ctx); 23 | Log.d(TAG, "channelActive() ctx = " + ctx); 24 | } 25 | 26 | /** 27 | * ims连接断开回调 28 | * @param ctx 29 | * @throws Exception 30 | */ 31 | @Override 32 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 33 | super.channelInactive(ctx); 34 | Log.w(TAG, "channelInactive() ctx = " + ctx); 35 | closeChannelAndReconnect(ctx); 36 | } 37 | 38 | /** 39 | * ims异常回调 40 | * @param ctx 41 | * @param cause 42 | * @throws Exception 43 | */ 44 | @Override 45 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 46 | super.exceptionCaught(ctx, cause); 47 | Log.e(TAG, "exceptionCaught() ctx = " + ctx + "\tcause = " + cause); 48 | closeChannelAndReconnect(ctx); 49 | } 50 | 51 | /** 52 | * 收到消息回调 53 | * @param ctx 54 | * @param msg 55 | * @throws Exception 56 | */ 57 | @Override 58 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 59 | super.channelRead(ctx, msg); 60 | Log.d(TAG, "channelRead() ctx = " + ctx + "\tmsg = " + msg); 61 | } 62 | 63 | /** 64 | * 关闭channel并重连 65 | * @param ctx 66 | */ 67 | private void closeChannelAndReconnect(ChannelHandlerContext ctx) { 68 | Log.d(TAG, "准备关闭channel并重连"); 69 | Channel channel = ctx.channel(); 70 | if(channel != null) { 71 | channel.close(); 72 | } 73 | // 回调连接状态 74 | ims.callbackIMSConnectStatus(IMSConnectStatus.ConnectFailed); 75 | // 触发重连 76 | ims.reconnect(false); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/netty/websocket/NettyWebSocketReconnectTask.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.netty.websocket; 2 | 3 | import android.util.Log; 4 | 5 | import com.freddy.kulaims.config.IMSConnectStatus; 6 | import com.freddy.kulaims.config.IMSOptions; 7 | 8 | import java.net.URI; 9 | import java.util.List; 10 | 11 | import io.netty.channel.Channel; 12 | import io.netty.util.internal.StringUtil; 13 | 14 | public class NettyWebSocketReconnectTask implements Runnable { 15 | 16 | private static final String TAG = NettyWebSocketReconnectTask.class.getSimpleName(); 17 | private NettyWebSocketIMS ims; 18 | private IMSOptions mIMSOptions; 19 | 20 | NettyWebSocketReconnectTask(NettyWebSocketIMS ims) { 21 | this.ims = ims; 22 | this.mIMSOptions = ims.getIMSOptions(); 23 | } 24 | 25 | @Override 26 | public void run() { 27 | try { 28 | // 重连时,释放工作线程组,也就是停止心跳 29 | ims.getExecutors().destroyWorkLoopGroup(); 30 | 31 | // ims未关闭并且网络可用的情况下,才去连接 32 | while (!ims.isClosed() && ims.isNetworkAvailable()) { 33 | IMSConnectStatus status; 34 | if ((status = connect()) == IMSConnectStatus.Connected) { 35 | ims.callbackIMSConnectStatus(status); 36 | break;// 连接成功,跳出循环 37 | } 38 | 39 | if (status == IMSConnectStatus.ConnectFailed 40 | || status == IMSConnectStatus.ConnectFailed_IMSClosed 41 | || status == IMSConnectStatus.ConnectFailed_ServerListEmpty 42 | || status == IMSConnectStatus.ConnectFailed_ServerEmpty 43 | || status == IMSConnectStatus.ConnectFailed_ServerIllegitimate 44 | || status == IMSConnectStatus.ConnectFailed_NetworkUnavailable) { 45 | ims.callbackIMSConnectStatus(status); 46 | 47 | if(ims.isClosed() || !ims.isNetworkAvailable()) { 48 | return; 49 | } 50 | // 一个服务器地址列表都连接失败后,说明网络情况可能很差,延时指定时间(重连间隔时间*2)再去进行下一个服务器地址的连接 51 | Log.w(TAG, String.format("一个周期连接失败,等待%1$dms后再次尝试重连", mIMSOptions.getReconnectInterval() * 2)); 52 | try { 53 | Thread.sleep(mIMSOptions.getReconnectInterval() * 2); 54 | } catch (InterruptedException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | } 59 | } finally { 60 | // 标识重连任务停止 61 | ims.setReconnecting(false); 62 | } 63 | } 64 | 65 | /** 66 | * 连接服务器 67 | * @return 68 | */ 69 | private IMSConnectStatus connect() { 70 | if (ims.isClosed()) return IMSConnectStatus.ConnectFailed_IMSClosed; 71 | if(!ims.isNetworkAvailable()) return IMSConnectStatus.ConnectFailed_NetworkUnavailable; 72 | List serverList = mIMSOptions.getServerList(); 73 | if (serverList == null || serverList.isEmpty()) { 74 | return IMSConnectStatus.ConnectFailed_ServerListEmpty; 75 | } 76 | 77 | ims.initBootstrap(); 78 | for (int i = 0; i < serverList.size(); i++) { 79 | String server = serverList.get(i); 80 | if (StringUtil.isNullOrEmpty(server)) { 81 | return IMSConnectStatus.ConnectFailed_ServerEmpty; 82 | } 83 | 84 | URI uri; 85 | try { 86 | uri = URI.create(server); 87 | }catch (IllegalArgumentException e) { 88 | e.printStackTrace(); 89 | if(i == serverList.size() - 1) { 90 | Log.w(TAG, String.format("【%1$s】连接失败,地址不合法", server)); 91 | return IMSConnectStatus.ConnectFailed_ServerIllegitimate; 92 | }else { 93 | Log.w(TAG, String.format("【%1$s】连接失败,地址不合法,正在等待重连,当前重连延时时长:%2$d", server, mIMSOptions.getReconnectInterval())); 94 | Log.w(TAG, "========================================================================================="); 95 | try { 96 | Thread.sleep(mIMSOptions.getReconnectInterval()); 97 | } catch (InterruptedException ex) { 98 | ex.printStackTrace(); 99 | } 100 | continue; 101 | } 102 | } 103 | 104 | if(!"ws".equals(uri.getScheme())) { 105 | if(i == serverList.size() - 1) { 106 | Log.w(TAG, String.format("【%1$s】连接失败,地址不合法", server)); 107 | return IMSConnectStatus.ConnectFailed_ServerIllegitimate; 108 | }else { 109 | Log.w(TAG, String.format("【%1$s】连接失败,地址不合法,正在等待重连,当前重连延时时长:%2$d", server, mIMSOptions.getReconnectInterval())); 110 | Log.w(TAG, "========================================================================================="); 111 | try { 112 | Thread.sleep(mIMSOptions.getReconnectInterval()); 113 | } catch (InterruptedException ex) { 114 | ex.printStackTrace(); 115 | } 116 | continue; 117 | } 118 | } 119 | 120 | if(i == 0) { 121 | ims.callbackIMSConnectStatus(IMSConnectStatus.Connecting); 122 | } 123 | 124 | // +1是因为首次连接也认为是重连,所以如果重连次数设置为3,则最大连接次数为3+1次 125 | for (int j = 0; j < mIMSOptions.getReconnectCount() + 1; j++) { 126 | if (ims.isClosed()) { 127 | return IMSConnectStatus.ConnectFailed_IMSClosed; 128 | } 129 | if (!ims.isNetworkAvailable()) { 130 | return IMSConnectStatus.ConnectFailed_NetworkUnavailable; 131 | } 132 | 133 | Log.d(TAG, String.format("正在进行【%1$s】的第%2$d次连接", server, j + 1)); 134 | try { 135 | String host = uri.getHost(); 136 | int port = uri.getPort(); 137 | Channel channel = toServer(host, port); 138 | if (channel != null && channel.isOpen() && channel.isActive() && channel.isRegistered() && channel.isWritable()) { 139 | ims.setChannel(channel); 140 | return IMSConnectStatus.Connected; 141 | } else { 142 | if (j == mIMSOptions.getReconnectCount()) { 143 | // 如果当前已达到最大重连次数,并且是最后一个服务器地址,则回调连接失败 144 | if(i == serverList.size() - 1) { 145 | Log.w(TAG, String.format("【%1$s】连接失败", server)); 146 | return IMSConnectStatus.ConnectFailed; 147 | } 148 | // 否则,无需回调连接失败,等待一段时间再去进行下一个服务器地址连接即可 149 | // 也就是说,当服务器地址列表里的地址都连接失败,才认为是连接失败 150 | else { 151 | // 一个服务器地址连接失败后,延时指定时间再去进行下一个服务器地址的连接 152 | Log.w(TAG, String.format("【%1$s】连接失败,正在等待进行下一个服务器地址的重连,当前重连延时时长:%2$dms", server, mIMSOptions.getReconnectInterval())); 153 | Log.w(TAG, "========================================================================================="); 154 | Thread.sleep(mIMSOptions.getReconnectInterval()); 155 | } 156 | } else { 157 | // 连接失败,则线程休眠(重连间隔时长 / 2 * n) ms 158 | int delayTime = mIMSOptions.getReconnectInterval() + mIMSOptions.getReconnectInterval() / 2 * j; 159 | Log.w(TAG, String.format("【%1$s】连接失败,正在等待重连,当前重连延时时长:%2$dms", server, delayTime)); 160 | Thread.sleep(delayTime); 161 | } 162 | } 163 | } catch (InterruptedException e) { 164 | break;// 线程被中断,则强制关闭 165 | } 166 | } 167 | } 168 | 169 | return IMSConnectStatus.ConnectFailed; 170 | } 171 | 172 | /** 173 | * 真正连接服务器的地方 174 | * @param host 175 | * @param port 176 | * @return 177 | */ 178 | private Channel toServer(String host, int port) { 179 | Channel channel; 180 | try { 181 | channel = ims.getBootstrap().connect(host, port).sync().channel(); 182 | } catch (Exception e) { 183 | e.printStackTrace(); 184 | channel = null; 185 | } 186 | 187 | return channel; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/nio/tcp/NioTCPIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.nio.tcp; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | 6 | import com.freddy.kulaims.bean.IMSMsg; 7 | import com.freddy.kulaims.config.IMSOptions; 8 | import com.freddy.kulaims.interf.IMSInterface; 9 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 10 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 11 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 12 | 13 | public class NioTCPIMS implements IMSInterface { 14 | 15 | private NioTCPIMS() { 16 | } 17 | 18 | public static NioTCPIMS getInstance() { 19 | return SingletonHolder.INSTANCE; 20 | } 21 | 22 | private static final class SingletonHolder { 23 | @SuppressLint("StaticFieldLeak") 24 | private static final NioTCPIMS INSTANCE = new NioTCPIMS(); 25 | } 26 | 27 | @Override 28 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 29 | return false; 30 | } 31 | 32 | @Override 33 | public void connect() { 34 | 35 | } 36 | 37 | @Override 38 | public void reconnect(boolean isFirstConnect) { 39 | 40 | } 41 | 42 | @Override 43 | public void sendMsg(IMSMsg msg) { 44 | 45 | } 46 | 47 | @Override 48 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 49 | 50 | } 51 | 52 | @Override 53 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 54 | 55 | } 56 | 57 | @Override 58 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 59 | 60 | } 61 | 62 | @Override 63 | public void release() { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/nio/websocket/NioWebSocketIMS.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.nio.websocket; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | 6 | import com.freddy.kulaims.bean.IMSMsg; 7 | import com.freddy.kulaims.config.IMSOptions; 8 | import com.freddy.kulaims.interf.IMSInterface; 9 | import com.freddy.kulaims.listener.IMSConnectStatusListener; 10 | import com.freddy.kulaims.listener.IMSMsgReceivedListener; 11 | import com.freddy.kulaims.listener.IMSMsgSentStatusListener; 12 | 13 | public class NioWebSocketIMS implements IMSInterface { 14 | 15 | private NioWebSocketIMS() { 16 | } 17 | 18 | public static NioWebSocketIMS getInstance() { 19 | return SingletonHolder.INSTANCE; 20 | } 21 | 22 | private static final class SingletonHolder { 23 | @SuppressLint("StaticFieldLeak") 24 | private static final NioWebSocketIMS INSTANCE = new NioWebSocketIMS(); 25 | } 26 | 27 | @Override 28 | public boolean init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener) { 29 | return false; 30 | } 31 | 32 | @Override 33 | public void connect() { 34 | 35 | } 36 | 37 | @Override 38 | public void reconnect(boolean isFirstConnect) { 39 | 40 | } 41 | 42 | @Override 43 | public void sendMsg(IMSMsg msg) { 44 | 45 | } 46 | 47 | @Override 48 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener) { 49 | 50 | } 51 | 52 | @Override 53 | public void sendMsg(IMSMsg msg, boolean isJoinResendManager) { 54 | 55 | } 56 | 57 | @Override 58 | public void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager) { 59 | 60 | } 61 | 62 | @Override 63 | public void release() { 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/utils/ExecutorServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.utils; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | /** 7 | *

@ProjectName: NettyChat

8 | *

@ClassName: ExecutorServiceFactory.java

9 | *

@PackageName: com.freddy.kulaims

10 | * 11 | *

@Description: 线程池工厂,负责重连和心跳线程调度

12 | *
13 | *

@author: FreddyChen

14 | *

@date: 2019/04/05 05:12

15 | *

@email: chenshichao@outlook.com

16 | */ 17 | public class ExecutorServiceFactory { 18 | 19 | private ExecutorService bossPool;// 管理线程组,负责重连 20 | private ExecutorService workPool;// 工作线程组,负责心跳 21 | 22 | /** 23 | * 初始化boss线程池 24 | */ 25 | public synchronized void initBossLoopGroup() { 26 | destroyBossLoopGroup(); 27 | bossPool = Executors.newSingleThreadExecutor(); 28 | } 29 | 30 | /** 31 | * 初始化work线程池 32 | */ 33 | public synchronized void initWorkLoopGroup() { 34 | destroyWorkLoopGroup(); 35 | workPool = Executors.newSingleThreadExecutor(); 36 | } 37 | 38 | /** 39 | * 执行boss任务 40 | * 41 | * @param r 42 | */ 43 | public void execBossTask(Runnable r) { 44 | if (bossPool == null) { 45 | initBossLoopGroup(); 46 | } 47 | bossPool.execute(r); 48 | } 49 | 50 | /** 51 | * 执行work任务 52 | * 53 | * @param r 54 | */ 55 | public void execWorkTask(Runnable r) { 56 | if (workPool == null) { 57 | initWorkLoopGroup(); 58 | } 59 | workPool.execute(r); 60 | } 61 | 62 | /** 63 | * 释放boss线程池 64 | */ 65 | public synchronized void destroyBossLoopGroup() { 66 | if (bossPool != null) { 67 | try { 68 | bossPool.shutdownNow(); 69 | } catch (Throwable t) { 70 | t.printStackTrace(); 71 | } finally { 72 | bossPool = null; 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * 释放work线程池 79 | */ 80 | public synchronized void destroyWorkLoopGroup() { 81 | if (workPool != null) { 82 | try { 83 | workPool.shutdownNow(); 84 | } catch (Throwable t) { 85 | t.printStackTrace(); 86 | } finally { 87 | workPool = null; 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * 释放所有线程池 94 | */ 95 | public synchronized void destroy() { 96 | destroyBossLoopGroup(); 97 | destroyWorkLoopGroup(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/freddy/kulaims/utils/UUID.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims.utils; 2 | 3 | public class UUID { 4 | 5 | private static final String[] CHARS = { 6 | "a", 7 | "b", 8 | "c", 9 | "d", 10 | "e", 11 | "f", 12 | "g", 13 | "h", 14 | "i", 15 | "j", 16 | "k", 17 | "l", 18 | "m", 19 | "n", 20 | "o", 21 | "p", 22 | "q", 23 | "r", 24 | "s", 25 | "t", 26 | "u", 27 | "v", 28 | "w", 29 | "x", 30 | "y", 31 | "z", 32 | "0", 33 | "1", 34 | "2", 35 | "3", 36 | "4", 37 | "5", 38 | "6", 39 | "7", 40 | "8", 41 | "9", 42 | "A", 43 | "B", 44 | "C", 45 | "D", 46 | "E", 47 | "F", 48 | "G", 49 | "H", 50 | "I", 51 | "J", 52 | "K", 53 | "L", 54 | "M", 55 | "N", 56 | "O", 57 | "P", 58 | "Q", 59 | "R", 60 | "S", 61 | "T", 62 | "U", 63 | "V", 64 | "W", 65 | "X", 66 | "Y", 67 | "Z" 68 | }; 69 | 70 | /** 71 | * 生成短8位UUID 72 | * @return 73 | */ 74 | public static String generateShortUuid() { 75 | StringBuilder shortBuffer = new StringBuilder(); 76 | String uuid = java.util.UUID.randomUUID().toString().replace("-", ""); 77 | for (int i = 0; i < 8; i++) { 78 | String str = uuid.substring(i * 4, i * 4 + 4); 79 | int x = Integer.parseInt(str, 16); 80 | shortBuffer.append(CHARS[x % 0x3E]); 81 | } 82 | return shortBuffer.toString(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/proto/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3";// 指定protobuf版本 2 | option java_package = "com.freddy.kulaims.protobuf";// 指定包名 3 | option java_outer_classname = "MessageProtobuf";// 指定类名 4 | 5 | message Msg { 6 | Head head = 1;// 消息头 7 | Body body = 2;// 消息体 8 | } 9 | 10 | message Head { 11 | string msgId = 1;// 消息id 12 | int32 msgType = 2;// 消息类型 13 | string sender = 3;// 发送者 14 | string receiver = 4;// 接收者 15 | int64 timestamp = 5;// 发送时间戳,单位:毫秒 16 | int32 report = 6;// 消息发送状态报告 17 | } 18 | 19 | message Body { 20 | string content = 1;// 消息内容 21 | int32 contentType = 2;// 消息内容类型 22 | string data = 3;// 扩展字段,以key/value形式存储的json字符串 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/test/java/com/freddy/kulaims/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.freddy.kulaims; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } --------------------------------------------------------------------------------