├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── netty-all-4.1.24.Final.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── nettydemo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── nettydemo │ │ │ ├── MainActivity.kt │ │ │ ├── UIScheduler.java │ │ │ ├── tcp │ │ │ ├── DemoDecoder.java │ │ │ ├── DemoEncoder.java │ │ │ ├── DemoMessage.java │ │ │ ├── ProtocolUtils.java │ │ │ ├── Unpack.java │ │ │ ├── client │ │ │ │ ├── ConnectionClient.java │ │ │ │ └── IConnectionListener.java │ │ │ └── server │ │ │ │ ├── ConnectionServer.java │ │ │ │ └── IClientListener.java │ │ │ └── ui │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── example │ └── nettydemo │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NettyDemo 2 | 3 | ## TCP Client 4 | 5 | #### 初始化 6 | ``` 7 | ConnectionClient.getConnectionClient().init() 8 | ConnectionClient.getConnectionClient().connect() 9 | ``` 10 | #### 发送消息 11 | 12 | ``` 13 | ConnectionClient.getConnectionClient().writeMessage() 14 | ``` 15 | 16 | #### 断开连接 17 | ``` 18 | ConnectionClient.getConnectionClient().disConnect() 19 | ``` 20 | 21 | ## TCP Server 22 | 23 | #### 启动服务 24 | 25 | ``` 26 | ConnectionServer.getEngin().init() 27 | ``` 28 | 29 | #### 向某个客户端发送消息 30 | 31 | ``` 32 | ConnectionServer.getEngin().writeMessage() 33 | ``` 34 | 35 | #### 关闭服务 36 | 37 | ``` 38 | ConnectionServer.getEngin().disConnect() 39 | ``` 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'com.example.nettydemo' 8 | compileSdk 33 9 | 10 | defaultConfig { 11 | applicationId "com.example.nettydemo" 12 | minSdk 24 13 | targetSdk 33 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | kotlinOptions { 34 | jvmTarget = '1.8' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion '1.3.2' 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation 'androidx.core:core-ktx:1.8.0' 52 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') 53 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' 54 | implementation 'androidx.activity:activity-compose:1.5.1' 55 | implementation platform('androidx.compose:compose-bom:2022.10.00') 56 | implementation 'androidx.compose.ui:ui' 57 | implementation 'androidx.compose.ui:ui-graphics' 58 | implementation 'androidx.compose.ui:ui-tooling-preview' 59 | implementation 'androidx.compose.material3:material3' 60 | implementation files('libs/netty-all-4.1.24.Final.jar') 61 | testImplementation 'junit:junit:4.13.2' 62 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 64 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') 65 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4' 66 | debugImplementation 'androidx.compose.ui:ui-tooling' 67 | debugImplementation 'androidx.compose.ui:ui-test-manifest' 68 | } -------------------------------------------------------------------------------- /app/libs/netty-all-4.1.24.Final.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/libs/netty-all-4.1.24.Final.jar -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/nettydemo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.example.nettydemo", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import com.example.nettydemo.ui.theme.NettyDemoTheme 14 | 15 | class MainActivity : ComponentActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContent { 19 | NettyDemoTheme { 20 | // A surface container using the 'background' color from the theme 21 | Surface( 22 | modifier = Modifier.fillMaxSize(), 23 | color = MaterialTheme.colorScheme.background 24 | ) { 25 | Greeting("Android") 26 | } 27 | } 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | fun Greeting(name: String, modifier: Modifier = Modifier) { 34 | Text( 35 | text = "Hello $name!", 36 | modifier = modifier 37 | ) 38 | } 39 | 40 | @Preview(showBackground = true) 41 | @Composable 42 | fun GreetingPreview() { 43 | NettyDemoTheme { 44 | Greeting("Android") 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/UIScheduler.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | public class UIScheduler { 7 | 8 | private UIScheduler() {} 9 | 10 | private static final UIScheduler sUIScheduler = new UIScheduler(); 11 | 12 | private final Handler handler = new Handler(Looper.getMainLooper()); 13 | 14 | public static UIScheduler getUIScheduler() { 15 | return sUIScheduler; 16 | } 17 | 18 | public void postRunnable(Runnable runnable) { 19 | handler.post(runnable); 20 | } 21 | 22 | 23 | public void postRunnableDelayed(Runnable runnable, long delay) { 24 | handler.postDelayed(runnable, delay); 25 | } 26 | 27 | public void removeRunnable(Runnable runnable) { 28 | handler.removeCallbacks(runnable); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/DemoDecoder.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp; 2 | 3 | import java.util.List; 4 | 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.handler.codec.ByteToMessageDecoder; 8 | 9 | 10 | /** 11 | * @author banshan 12 | * Describe: 解码 13 | */ 14 | public class DemoDecoder extends ByteToMessageDecoder { 15 | 16 | private volatile static byte[] LENGTH_BYTES = new byte[4]; 17 | private static final int MAX_PACK_SIZE = 1024 * 1024 * 64; 18 | 19 | private volatile static int packetSize = 0; 20 | /** 21 | * 在工作线程中回调 22 | */ 23 | @Override 24 | protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) { 25 | //记录读取的位置 26 | byteBuf.markReaderIndex(); 27 | 28 | //判断缓冲区是否可读 29 | if (!byteBuf.isReadable()) { 30 | byteBuf.resetReaderIndex(); 31 | return; 32 | } 33 | 34 | //如果可读消息不足四个字节,那么重置指针位置,返回 35 | if (byteBuf.readableBytes() < LENGTH_BYTES.length) { 36 | byteBuf.resetReaderIndex(); 37 | return; 38 | } 39 | byteBuf.readBytes(LENGTH_BYTES); 40 | Unpack unpack = new Unpack(LENGTH_BYTES); 41 | packetSize = unpack.popInt(); 42 | if (packetSize < 0 || packetSize > MAX_PACK_SIZE) { 43 | throw new RuntimeException("Invalid packet, size = " + packetSize); 44 | } 45 | 46 | //如果可读字节数不足,那么重置指针位置,返回 47 | if (byteBuf.readableBytes() < packetSize - LENGTH_BYTES.length) { 48 | byteBuf.resetReaderIndex(); 49 | return; 50 | } 51 | ByteBuf readBytes = byteBuf.readBytes(packetSize); 52 | 53 | DemoMessage message = new DemoMessage(); 54 | message.setMessageSize(packetSize); 55 | message.setBody(ProtocolUtils.getString(readBytes, packetSize)); 56 | 57 | packetSize = 0; 58 | list.add(message); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/DemoEncoder.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.MessageToByteEncoder; 6 | 7 | /** 8 | * @author banshan 9 | * Describe: 编码 10 | */ 11 | public class DemoEncoder extends MessageToByteEncoder { 12 | 13 | @Override 14 | protected void encode(ChannelHandlerContext channelHandlerContext, DemoMessage message, ByteBuf byteBuf) { 15 | ProtocolUtils.writeDemoMessage(byteBuf, message); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/DemoMessage.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp; 2 | 3 | /** 4 | * Created by banshan 5 | * Describe: 6 | * 协议:IntString 7 | * Int : 包体大小(四个字节) 8 | * String(JSONString) 9 | */ 10 | public class DemoMessage { 11 | 12 | private int messageSize; 13 | 14 | private String body; 15 | 16 | public int getMessageSize() { 17 | return messageSize; 18 | } 19 | 20 | public void setMessageSize(int messageSize) { 21 | this.messageSize = messageSize; 22 | } 23 | 24 | public String getBody() { 25 | return body; 26 | } 27 | 28 | public void setBody(String body) { 29 | this.body = body; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/ProtocolUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | import io.netty.buffer.ByteBuf; 6 | 7 | /** 8 | * Created by banshan 9 | */ 10 | public class ProtocolUtils { 11 | 12 | public static void writeDemoMessage(ByteBuf byteBuf, DemoMessage message) { 13 | try { 14 | //每个字符串之前需要加一个int,用于指定这个字符串的长度!!! 15 | byteBuf.writeIntLE(message.getMessageSize()); 16 | byteBuf.writeBytes(message.getBody().getBytes(StandardCharsets.UTF_8)); 17 | } catch (Exception e) { 18 | e.printStackTrace(); 19 | } 20 | } 21 | 22 | public static String getString(ByteBuf byteBuf, int length) { 23 | byte[] keyBytes = new byte[length]; 24 | byteBuf.readBytes(keyBytes); 25 | try { 26 | return new String(keyBytes, StandardCharsets.UTF_8); 27 | } catch (Exception e) { 28 | e.printStackTrace(); 29 | } 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/Unpack.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.nio.BufferUnderflowException; 5 | import java.nio.ByteBuffer; 6 | import java.nio.ByteOrder; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | /** 10 | * Created by banshan 11 | */ 12 | public class Unpack { 13 | 14 | protected ByteBuffer mBuffer; 15 | 16 | 17 | public Unpack(byte[] bytes, int offset, int length) { 18 | mBuffer = ByteBuffer.wrap(bytes, offset, length); 19 | mBuffer.order(ByteOrder.LITTLE_ENDIAN); 20 | } 21 | 22 | public Unpack(byte[] bytes) { 23 | this(bytes, 0, bytes.length); 24 | } 25 | 26 | public int popInt() { 27 | try { 28 | return mBuffer.getInt(); 29 | } catch (BufferUnderflowException bEx) { 30 | bEx.printStackTrace(); 31 | } 32 | return 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/client/ConnectionClient.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp.client; 2 | 3 | import android.util.Log; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.example.nettydemo.UIScheduler; 8 | import com.example.nettydemo.tcp.DemoDecoder; 9 | import com.example.nettydemo.tcp.DemoEncoder; 10 | import com.example.nettydemo.tcp.DemoMessage; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import io.netty.bootstrap.Bootstrap; 15 | import io.netty.channel.Channel; 16 | import io.netty.channel.ChannelFuture; 17 | import io.netty.channel.ChannelHandlerContext; 18 | import io.netty.channel.ChannelInitializer; 19 | import io.netty.channel.ChannelOption; 20 | import io.netty.channel.ChannelPipeline; 21 | import io.netty.channel.EventLoopGroup; 22 | import io.netty.channel.SimpleChannelInboundHandler; 23 | import io.netty.channel.nio.NioEventLoopGroup; 24 | import io.netty.channel.socket.SocketChannel; 25 | import io.netty.channel.socket.nio.NioSocketChannel; 26 | import io.netty.handler.timeout.IdleStateEvent; 27 | import io.netty.handler.timeout.IdleStateHandler; 28 | import io.netty.util.concurrent.Future; 29 | import io.netty.util.concurrent.FutureListener; 30 | import io.netty.util.concurrent.GenericFutureListener; 31 | 32 | /** 33 | * Created by banshan 34 | */ 35 | public class ConnectionClient { 36 | 37 | private static final String TAG = "netty"; 38 | 39 | private static final long TIME_DELAY_CONNECT = 3000; 40 | 41 | private static final int RECONNECT_TIMEOUT = 3; 42 | /** 43 | * 是否已连接 44 | */ 45 | private volatile boolean isConnected = false; 46 | /** 47 | * 正在连接 48 | */ 49 | private volatile boolean connecting = false; 50 | /** 51 | * 主动断开状态, 52 | */ 53 | private volatile boolean disConnected = false; 54 | /** 55 | * 是否连接失败 56 | */ 57 | private volatile boolean connectFailed = false; 58 | /** 59 | * 负责管理EventLoop 60 | * 集成自 ExecutorService 可以理解为线程池 61 | */ 62 | private EventLoopGroup mGroup; 63 | /** 64 | * 引导类 65 | * 配置线程池,IP地址,端口号,Channel,业务Handler 66 | */ 67 | private Bootstrap mBootstrap; 68 | private Channel mChannel; 69 | 70 | private ChannelHandlerContext mChannelHandlerContext; 71 | 72 | /** 73 | * 解码 74 | * 读取数据,处理拆包,粘包,压缩 75 | */ 76 | private DemoDecoder mDemoDecoder; 77 | /** 78 | * 编码 79 | */ 80 | private DemoEncoder mDemoEncoder; 81 | 82 | private CustomHandler mCustomHandler; 83 | 84 | private IdleStateHandler mIdleStateHandler; 85 | 86 | IConnectionListener connectionListener; 87 | 88 | private ConnectionClient() { 89 | 90 | } 91 | 92 | private static final ConnectionClient engin = new ConnectionClient(); 93 | 94 | private int reconnectIndex = 0; 95 | private final Runnable mReconnectTask = () -> { 96 | connect(); 97 | }; 98 | 99 | /** 100 | * 获取引擎 101 | */ 102 | public static ConnectionClient getConnectionClient() { 103 | return engin; 104 | } 105 | 106 | 107 | /** 108 | * 初始化 109 | */ 110 | public void init(@NonNull String ip, int port, IConnectionListener listener) { 111 | reconnectIndex = 0; 112 | this.connectionListener = listener; 113 | this.disConnected = false; 114 | try { 115 | if (isConnected) { 116 | notifyConnect(); 117 | return; 118 | } 119 | if (mGroup != null) { 120 | mGroup.shutdownGracefully(); 121 | } 122 | disConnect(false); 123 | //构建线程池 124 | mGroup = new NioEventLoopGroup(); 125 | //构建引导程序 126 | mBootstrap = new Bootstrap(); 127 | //设置EventGroup 128 | mBootstrap.group(mGroup); 129 | //设置Channel 130 | mBootstrap.channel(NioSocketChannel.class); 131 | //设置的好处是禁用Nagle算法。表示不延迟立即发送 132 | //这个算法试图减少TCP包的数量和结构性开销,将多个较小的包组合较大的包进行发送。 133 | //这个算法收TCP延迟确认影响,会导致相继两次向链接发送请求包。 134 | mBootstrap.option(ChannelOption.TCP_NODELAY, false); 135 | mBootstrap.option(ChannelOption.SO_KEEPALIVE, true); 136 | mBootstrap.remoteAddress(ip, port); 137 | mBootstrap.handler(new CustomChannelInitializer()); 138 | } catch (Exception e) { 139 | e.printStackTrace(); 140 | } 141 | } 142 | 143 | 144 | /** 145 | * 连接 146 | */ 147 | public void connect() { 148 | try { 149 | if (connectionListener != null) { 150 | connectionListener.onStartConnect(); 151 | } 152 | if (mBootstrap == null || isConnected || connecting) { 153 | return; 154 | } 155 | connectFailed = false; 156 | connecting = true; 157 | final ChannelFuture channelFuture = mBootstrap.connect(); 158 | channelFuture.addListener(new FutureListener() { 159 | @Override 160 | public void operationComplete(Future future) { 161 | connecting = false; 162 | final boolean isSuccess = future.isSuccess(); 163 | if (isSuccess) { 164 | mChannel = channelFuture.channel(); 165 | disConnected = false; 166 | isConnected = true; 167 | connectFailed = false; 168 | } else { 169 | isConnected = false; 170 | connectFailed = true; 171 | reconnectIndex ++; 172 | if (reconnectIndex > RECONNECT_TIMEOUT) { 173 | notifyConnectFailure(); 174 | return; 175 | } 176 | UIScheduler.getUIScheduler().removeRunnable(mReconnectTask); 177 | UIScheduler.getUIScheduler().postRunnableDelayed(mReconnectTask, TIME_DELAY_CONNECT); 178 | } 179 | } 180 | }); 181 | } catch (Exception e) { 182 | connecting = false; 183 | e.printStackTrace(); 184 | } 185 | } 186 | 187 | /** 188 | * 写数据 189 | * 190 | * @param object Object 191 | */ 192 | public void writeMessage(final Object object) { 193 | if (mChannel != null && mChannel.isOpen() && mChannel.isActive()) { 194 | mChannel.writeAndFlush(object).addListener(new GenericFutureListener>() { 195 | @Override 196 | public void operationComplete(Future future) { 197 | DemoMessage message = (DemoMessage) object; 198 | writeLog("writeMessage success " + message.getBody()); 199 | } 200 | }); 201 | } 202 | } 203 | 204 | /** 205 | * 断开连接 206 | * 207 | * @param onPurpose true 主动 208 | */ 209 | public void disConnect(boolean onPurpose) { 210 | disConnected = onPurpose; 211 | isConnected = false; 212 | connecting = false; 213 | try { 214 | if (mGroup != null) { 215 | mGroup.shutdownGracefully(); 216 | } 217 | } catch (Exception e) { 218 | e.printStackTrace(); 219 | } 220 | try { 221 | if (mChannel != null) { 222 | mChannel.close(); 223 | } 224 | } catch (Exception e) { 225 | e.printStackTrace(); 226 | } 227 | try { 228 | if (mChannelHandlerContext != null) { 229 | mChannelHandlerContext.close(); 230 | } 231 | } catch (Exception e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | 236 | private void removeHandler() { 237 | try { 238 | mChannel.pipeline().remove(mDemoDecoder); 239 | mChannel.pipeline().remove(mCustomHandler); 240 | mChannel.pipeline().remove(mDemoEncoder); 241 | mChannel.pipeline().remove(mIdleStateHandler); 242 | } catch (Exception e) { 243 | e.printStackTrace(); 244 | } 245 | } 246 | 247 | /** 248 | * 是否连接状态 249 | * 250 | * @return true 连接 251 | */ 252 | public boolean isConnected() { 253 | return isConnected && mChannel.isActive(); 254 | } 255 | 256 | public class CustomChannelInitializer extends ChannelInitializer { 257 | 258 | @Override 259 | protected void initChannel(SocketChannel socketChannel) { 260 | ChannelPipeline pipeline = socketChannel.pipeline(); 261 | mDemoDecoder = new DemoDecoder(); 262 | mDemoEncoder = new DemoEncoder(); 263 | mCustomHandler = new CustomHandler(); 264 | mIdleStateHandler = new IdleStateHandler(0, 90, 0, TimeUnit.SECONDS); 265 | pipeline.addLast(mIdleStateHandler); 266 | pipeline.addLast(mDemoDecoder); 267 | pipeline.addLast(mDemoEncoder); 268 | pipeline.addLast(mCustomHandler); 269 | } 270 | 271 | @Override 272 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 273 | super.handlerAdded(ctx); 274 | } 275 | 276 | @Override 277 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 278 | super.exceptionCaught(ctx, cause); 279 | } 280 | } 281 | 282 | public class CustomHandler extends SimpleChannelInboundHandler { 283 | 284 | @Override 285 | protected void channelRead0(ChannelHandlerContext channelHandlerContext, final DemoMessage message) { 286 | //接收来自服务端的消息 287 | notifyReceiveMessage(message); 288 | } 289 | 290 | /** 291 | * 连接成功 292 | */ 293 | @Override 294 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 295 | super.channelActive(ctx); 296 | //链接成功 297 | isConnected = true; 298 | notifyConnect(); 299 | mChannelHandlerContext = ctx; 300 | } 301 | 302 | /** 303 | * 链接失败 304 | */ 305 | @Override 306 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 307 | super.channelInactive(ctx); 308 | //链接失败 309 | inActive(ctx); 310 | isConnected = false; 311 | connecting = false; 312 | notifyDisConnect(); 313 | if (!disConnected) { 314 | notifyReInit(); 315 | } 316 | } 317 | 318 | /** 319 | * 异常 320 | */ 321 | @Override 322 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 323 | super.exceptionCaught(ctx, cause); 324 | //异常处理 325 | inActive(ctx); 326 | if (cause != null) { 327 | writeLog(cause.getMessage()); 328 | cause.printStackTrace(); 329 | } 330 | isConnected = false; 331 | connecting = false; 332 | notifyDisConnect(); 333 | notifyReInit(); 334 | } 335 | 336 | @Override 337 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 338 | super.handlerAdded(ctx); 339 | } 340 | 341 | @Override 342 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 343 | super.userEventTriggered(ctx, evt); 344 | if (evt instanceof IdleStateEvent) { 345 | IdleStateEvent e = (IdleStateEvent) evt; 346 | switch (e.state()) { 347 | case WRITER_IDLE: 348 | // TODO: 2024/6/12 心跳包 349 | break; 350 | default: 351 | break; 352 | } 353 | } 354 | } 355 | 356 | @Override 357 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 358 | super.channelWritabilityChanged(ctx); 359 | } 360 | } 361 | 362 | private void inActive(ChannelHandlerContext ctx) { 363 | 364 | removeHandler(); 365 | try { 366 | Channel channel = ctx.channel(); 367 | channel.close(); 368 | ctx.close(); 369 | } catch (Exception e) { 370 | e.printStackTrace(); 371 | } 372 | } 373 | 374 | private void notifyConnectFailure() { 375 | runOnUi(() -> { 376 | if (connectionListener != null) { 377 | connectionListener.onConnectFailure(); 378 | } 379 | }); 380 | } 381 | 382 | /** 383 | * 通过建立TCP连接 384 | */ 385 | private void notifyConnect() { 386 | runOnUi(() -> { 387 | reconnectIndex = 0; 388 | if (connectionListener != null) { 389 | connectionListener.onConnect(); 390 | } 391 | }); 392 | } 393 | 394 | /** 395 | * 通知TCP连接断开 396 | */ 397 | private void notifyDisConnect() { 398 | runOnUi(() -> { 399 | reconnectIndex = 0; 400 | if (connectionListener != null) { 401 | connectionListener.onDisConnect(disConnected); 402 | } 403 | }); 404 | } 405 | 406 | /** 407 | * 通过主进程接收到一条消息 408 | * 409 | * @param message 消息 410 | */ 411 | private void notifyReceiveMessage(DemoMessage message) { 412 | runOnUi(() -> { 413 | if (connectionListener != null) { 414 | connectionListener.onReceiveMessage(message); 415 | } 416 | }); 417 | } 418 | 419 | /** 420 | * 通知重新连接 421 | */ 422 | private void notifyReInit() { 423 | runOnUiDelay(() -> { 424 | if (connectionListener != null) { 425 | connectionListener.onReInit(); 426 | } 427 | }, TIME_DELAY_CONNECT); 428 | 429 | } 430 | 431 | private void runOnUi(Runnable runnable) { 432 | UIScheduler.getUIScheduler().postRunnable(runnable); 433 | } 434 | 435 | private void runOnUiDelay(Runnable runnable, long delay) { 436 | UIScheduler.getUIScheduler().postRunnableDelayed(runnable, delay); 437 | } 438 | 439 | private void writeLog(String info) { 440 | Log.d(TAG, info); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/client/IConnectionListener.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp.client; 2 | 3 | import com.example.nettydemo.tcp.DemoMessage; 4 | 5 | /** 6 | * Created by banshan 7 | */ 8 | public interface IConnectionListener { 9 | 10 | /** 11 | * 开始连接 12 | */ 13 | void onStartConnect(); 14 | 15 | /** 16 | * 已连接 17 | */ 18 | void onConnect(); 19 | 20 | /** 21 | * 断开连接 22 | */ 23 | void onDisConnect(boolean onPurpose); 24 | 25 | /** 26 | * 连接失败 27 | */ 28 | void onConnectFailure(); 29 | 30 | /** 31 | * 初始化,重连接 32 | */ 33 | void onReInit(); 34 | 35 | void onReceiveMessage(DemoMessage message); 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/server/ConnectionServer.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp.server; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import com.example.nettydemo.tcp.DemoDecoder; 7 | import com.example.nettydemo.tcp.DemoEncoder; 8 | import com.example.nettydemo.tcp.DemoMessage; 9 | 10 | import io.netty.bootstrap.ServerBootstrap; 11 | import io.netty.channel.Channel; 12 | import io.netty.channel.ChannelFuture; 13 | import io.netty.channel.ChannelHandlerContext; 14 | import io.netty.channel.ChannelInitializer; 15 | import io.netty.channel.ChannelOption; 16 | import io.netty.channel.ChannelPipeline; 17 | import io.netty.channel.EventLoopGroup; 18 | import io.netty.channel.SimpleChannelInboundHandler; 19 | import io.netty.channel.nio.NioEventLoopGroup; 20 | import io.netty.channel.socket.SocketChannel; 21 | import io.netty.channel.socket.nio.NioServerSocketChannel; 22 | import io.netty.util.concurrent.Future; 23 | import io.netty.util.concurrent.GenericFutureListener; 24 | 25 | /** 26 | * Created by banshan 27 | */ 28 | public class ConnectionServer { 29 | 30 | private static final String TAG = "netty"; 31 | /** 32 | * 服务是否已经启动 33 | */ 34 | private volatile boolean isServerStart = false; 35 | 36 | /** 37 | * 引导类 38 | * 配置线程池,IP地址,端口号,Channel,业务Handler 39 | */ 40 | private ServerBootstrap mBootstrap; 41 | 42 | /** 43 | * Context 44 | */ 45 | private Context context; 46 | 47 | EventLoopGroup bossGroup; 48 | 49 | EventLoopGroup workerGroup; 50 | /** 51 | * 解码 52 | * 读取数据,处理拆包,粘包,压缩 53 | */ 54 | private DemoDecoder mDemoDecoder; 55 | /** 56 | * 编码 57 | */ 58 | private DemoEncoder mDemoEncoder; 59 | 60 | private SimpleChannelHandler mHandler; 61 | 62 | private IClientListener clientListener; 63 | 64 | private int inetPort; 65 | 66 | private ChannelFuture channelFuture; 67 | 68 | private ConnectionServer() { 69 | 70 | } 71 | 72 | private static ConnectionServer engin = new ConnectionServer(); 73 | 74 | /** 75 | * 获取引擎 76 | * 77 | * @return ConnectionEngin 78 | */ 79 | public static ConnectionServer getEngin() { 80 | return engin; 81 | } 82 | 83 | 84 | /** 85 | * 初始化 86 | * 87 | * @param context 上下文 88 | */ 89 | 90 | public void init(Context context, IClientListener listener, int inetPort) { 91 | this.inetPort = inetPort; 92 | this.context = context; 93 | this.clientListener = listener; 94 | try { 95 | if (isServerStart) { 96 | return; 97 | } 98 | if (bossGroup != null) { 99 | bossGroup.shutdownGracefully(); 100 | } 101 | if (workerGroup != null) { 102 | workerGroup.shutdownGracefully(); 103 | } 104 | disConnect(); 105 | bossGroup = new NioEventLoopGroup(); 106 | workerGroup = new NioEventLoopGroup(); 107 | //构建引导程序 108 | mBootstrap = new ServerBootstrap(); 109 | //设置EventGroup 110 | mBootstrap.group(bossGroup, workerGroup); 111 | //设置Channel 112 | mBootstrap.channel(NioServerSocketChannel.class); 113 | mBootstrap.option(ChannelOption.SO_BACKLOG, 128); 114 | //设置的好处是禁用Nagle算法。表示不延迟立即发送 115 | //这个算法试图减少TCP包的数量和结构性开销,将多个较小的包组合较大的包进行发送。 116 | //这个算法收TCP延迟确认影响,会导致相继两次向链接发送请求包。 117 | mBootstrap.option(ChannelOption.TCP_NODELAY, false); 118 | mBootstrap.option(ChannelOption.SO_KEEPALIVE, true); 119 | mBootstrap.childHandler(new CustomChannelInitializer()); 120 | channelFuture = mBootstrap.bind(inetPort).sync(); 121 | if (channelFuture.isSuccess()) { 122 | isServerStart = true; 123 | } 124 | } catch (Exception e) { 125 | e.printStackTrace(); 126 | } 127 | } 128 | 129 | 130 | public void checkServer() { 131 | if (isServerStart && !isServerOpen()) { 132 | disConnect(); 133 | init(context, clientListener, inetPort); 134 | } 135 | } 136 | 137 | private boolean isServerOpen() { 138 | if (channelFuture != null && channelFuture.channel() != null && channelFuture.channel().isWritable()) { 139 | return true; 140 | } 141 | return false; 142 | } 143 | 144 | /** 145 | * 写数据 146 | * 147 | * @param object Object 148 | */ 149 | public void writeMessage(final Object object, Channel channel) { 150 | if (channel != null && channel.isOpen() && channel.isActive()) { 151 | channel.writeAndFlush(object).addListener(new GenericFutureListener>() { 152 | @Override 153 | public void operationComplete(Future future) { 154 | writeLog("writeMessage success"); 155 | } 156 | }); 157 | } 158 | } 159 | 160 | /** 161 | * 关闭服务 162 | */ 163 | public void disConnect() { 164 | try { 165 | if (bossGroup != null) { 166 | bossGroup.shutdownGracefully(); 167 | } 168 | if (workerGroup != null) { 169 | workerGroup.shutdownGracefully(); 170 | } 171 | } catch (Exception e) { 172 | e.printStackTrace(); 173 | } 174 | isServerStart = false; 175 | } 176 | 177 | private void removeHandler(Channel channel) { 178 | try { 179 | channel.pipeline().remove(mDemoDecoder); 180 | channel.pipeline().remove(mHandler); 181 | channel.pipeline().remove(mDemoEncoder); 182 | } catch (Exception e) { 183 | e.printStackTrace(); 184 | } 185 | } 186 | 187 | /** 188 | * 是否连接状态 189 | * 190 | * @return true 连接 191 | */ 192 | public boolean isServerStart() { 193 | return isServerStart; 194 | } 195 | 196 | 197 | public class CustomChannelInitializer extends ChannelInitializer { 198 | 199 | @Override 200 | protected void initChannel(SocketChannel socketChannel) { 201 | ChannelPipeline pipeline = socketChannel.pipeline(); 202 | mDemoDecoder = new DemoDecoder(); 203 | mDemoEncoder = new DemoEncoder(); 204 | mHandler = new SimpleChannelHandler(); 205 | pipeline.addLast(mDemoDecoder); 206 | pipeline.addLast(mDemoEncoder); 207 | pipeline.addLast(mHandler); 208 | } 209 | 210 | @Override 211 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 212 | super.handlerAdded(ctx); 213 | } 214 | 215 | @Override 216 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 217 | super.exceptionCaught(ctx, cause); 218 | } 219 | } 220 | 221 | public class SimpleChannelHandler extends SimpleChannelInboundHandler { 222 | 223 | 224 | @Override 225 | protected void channelRead0(ChannelHandlerContext channelHandlerContext, final DemoMessage message) { 226 | //接收来自服务端的消息 227 | Channel channel = channelHandlerContext.channel(); 228 | notifyReceiveMessage(message, channel); 229 | } 230 | 231 | /** 232 | * 连接成功 233 | */ 234 | @Override 235 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 236 | super.channelActive(ctx); 237 | //链接成功 238 | notifyClientConnect(ctx.channel()); 239 | } 240 | 241 | /** 242 | * 链接失败 243 | */ 244 | @Override 245 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 246 | super.channelInactive(ctx); 247 | //链接失败 248 | inActive(ctx); 249 | notifyClientDisConnect(ctx.channel()); 250 | } 251 | 252 | /** 253 | * 异常 254 | */ 255 | @Override 256 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 257 | super.exceptionCaught(ctx, cause); 258 | //异常处理 259 | inActive(ctx); 260 | if (cause != null) { 261 | writeLog(cause.getMessage()); 262 | cause.printStackTrace(); 263 | } 264 | } 265 | 266 | @Override 267 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 268 | super.handlerAdded(ctx); 269 | } 270 | 271 | @Override 272 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 273 | super.userEventTriggered(ctx, evt); 274 | } 275 | 276 | @Override 277 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 278 | super.channelWritabilityChanged(ctx); 279 | } 280 | } 281 | 282 | private void inActive(ChannelHandlerContext ctx) { 283 | 284 | try { 285 | Channel channel = ctx.channel(); 286 | removeHandler(channel); 287 | channel.close(); 288 | ctx.close(); 289 | } catch (Exception e) { 290 | e.printStackTrace(); 291 | } 292 | } 293 | 294 | private void notifyClientConnect(Channel channel) { 295 | if (clientListener != null) { 296 | clientListener.onClientConnect(channel); 297 | } 298 | } 299 | 300 | 301 | private void notifyClientDisConnect(Channel channel) { 302 | if (clientListener != null) { 303 | clientListener.onClientDisConnect(channel); 304 | } 305 | } 306 | 307 | /** 308 | * 接收到一条消息 309 | * 310 | * @param message 消息 311 | */ 312 | private void notifyReceiveMessage(DemoMessage message, Channel channel) { 313 | if (clientListener != null) { 314 | clientListener.onClientMessage(message, channel); 315 | } 316 | } 317 | 318 | private void writeLog(String info) { 319 | Log.d(TAG, info); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/tcp/server/IClientListener.java: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.tcp.server; 2 | 3 | import com.example.nettydemo.tcp.DemoMessage; 4 | 5 | import io.netty.channel.Channel; 6 | 7 | /** 8 | * Created by banshan 9 | */ 10 | public interface IClientListener { 11 | 12 | /** 13 | * 客户端建立连接 14 | * 15 | */ 16 | void onClientConnect(Channel channel); 17 | 18 | /** 19 | * 客户端断开连接 20 | */ 21 | void onClientDisConnect(Channel channel); 22 | 23 | /** 24 | * 接收到客户端消息 25 | */ 26 | void onClientMessage(DemoMessage message, Channel channel); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.platform.LocalView 16 | import androidx.core.view.WindowCompat 17 | 18 | private val DarkColorScheme = darkColorScheme( 19 | primary = Purple80, 20 | secondary = PurpleGrey80, 21 | tertiary = Pink80 22 | ) 23 | 24 | private val LightColorScheme = lightColorScheme( 25 | primary = Purple40, 26 | secondary = PurpleGrey40, 27 | tertiary = Pink40 28 | 29 | /* Other default colors to override 30 | background = Color(0xFFFFFBFE), 31 | surface = Color(0xFFFFFBFE), 32 | onPrimary = Color.White, 33 | onSecondary = Color.White, 34 | onTertiary = Color.White, 35 | onBackground = Color(0xFF1C1B1F), 36 | onSurface = Color(0xFF1C1B1F), 37 | */ 38 | ) 39 | 40 | @Composable 41 | fun NettyDemoTheme( 42 | darkTheme: Boolean = isSystemInDarkTheme(), 43 | // Dynamic color is available on Android 12+ 44 | dynamicColor: Boolean = true, 45 | content: @Composable () -> Unit 46 | ) { 47 | val colorScheme = when { 48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 49 | val context = LocalContext.current 50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 51 | } 52 | 53 | darkTheme -> DarkColorScheme 54 | else -> LightColorScheme 55 | } 56 | val view = LocalView.current 57 | if (!view.isInEditMode) { 58 | SideEffect { 59 | val window = (view.context as Activity).window 60 | window.statusBarColor = colorScheme.primary.toArgb() 61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme 62 | } 63 | } 64 | 65 | MaterialTheme( 66 | colorScheme = colorScheme, 67 | typography = Typography, 68 | content = content 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/com/example/nettydemo/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.example.nettydemo.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BanshanAndroid/NettyDemo/6e16924e39c2e58e225d78a996433e0d2efe78eb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | NettyDemo 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |