├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── safframework │ │ └── netty4android │ │ ├── client │ │ ├── NettyTcpClient.kt │ │ ├── constant │ │ │ └── ConnectState.kt │ │ ├── handler │ │ │ └── NettyClientHandler.kt │ │ └── listener │ │ │ ├── MessageStateListener.kt │ │ │ └── NettyClientListener.kt │ │ └── ui │ │ ├── ConfigClientActivity.kt │ │ ├── MainActivity.kt │ │ ├── adapter │ │ └── MessageAdapter.kt │ │ └── domain │ │ └── MessageBean.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_config_client.xml │ ├── activity_main.xml │ └── item_message.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── server ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── safframework │ │ └── netty4android │ │ ├── server │ │ ├── ChannelActiveHandler.kt │ │ ├── CustomerServerHandler.kt │ │ ├── NettyServer.kt │ │ ├── NettyServerInitializer.kt │ │ ├── NettyServerListener.kt │ │ ├── PipelineAdd.kt │ │ └── SocketChooseHandler.kt │ │ └── ui │ │ ├── ConfigServerActivity.kt │ │ ├── MainActivity.kt │ │ ├── adapter │ │ ├── CustomSpinnerAdapter.kt │ │ └── MessageAdapter.kt │ │ └── domain │ │ ├── ClientChanel.kt │ │ └── MessageBean.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_config_server.xml │ ├── activity_main.xml │ └── item_message.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netty4Android 2 | 3 | demo 包括 Netty 的服务端和客户端,支持 TCP、WebSocket 协议。 4 | 5 | 详见: https://www.jianshu.com/p/9d99d6239e6a 6 | 7 | 该库只是一个 demo,具体的框架可以看我的实现另一个库:https://github.com/fengzhizi715/AndroidServer 8 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 28 7 | defaultConfig { 8 | applicationId "com.safframework.netty4android.client" 9 | minSdkVersion 15 10 | targetSdkVersion 28 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(dir: 'libs', include: ['*.jar']) 25 | implementation 'com.android.support:appcompat-v7:28.0.0' 26 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 27 | testImplementation 'junit:junit:4.12' 28 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 29 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 30 | 31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 32 | implementation 'io.netty:netty-all:4.1.38.Final' 33 | implementation 'com.android.support:recyclerview-v7:28.0.0' 34 | } 35 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/client/NettyTcpClient.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.client 2 | 3 | import android.os.SystemClock 4 | import android.util.Log 5 | import com.safframework.netty4android.client.constant.ConnectState 6 | import com.safframework.netty4android.client.handler.NettyClientHandler 7 | import com.safframework.netty4android.client.listener.MessageStateListener 8 | import com.safframework.netty4android.client.listener.NettyClientListener 9 | import io.netty.bootstrap.Bootstrap 10 | import io.netty.channel.* 11 | import io.netty.channel.nio.NioEventLoopGroup 12 | import io.netty.channel.socket.SocketChannel 13 | import io.netty.channel.socket.nio.NioSocketChannel 14 | import io.netty.handler.codec.LineBasedFrameDecoder 15 | import io.netty.handler.codec.string.StringDecoder 16 | import io.netty.handler.codec.string.StringEncoder 17 | import io.netty.handler.timeout.IdleStateHandler 18 | import io.netty.util.CharsetUtil 19 | import java.util.concurrent.TimeUnit 20 | 21 | /** 22 | * 23 | * @FileName: 24 | * com.safframework.netty4android.client.NettyTcpClient 25 | * @author: Tony Shen 26 | * @date: 2019-08-05 20:52 27 | * @version: V1.0 <描述当前版本功能> 28 | */ 29 | class NettyTcpClient private constructor(val host: String, val tcp_port: Int, val index: Int) { 30 | 31 | private lateinit var group: EventLoopGroup 32 | 33 | private lateinit var listener: NettyClientListener 34 | 35 | private var channel: Channel? = null 36 | 37 | /** 38 | * 获取TCP连接状态 39 | * 40 | * @return 获取TCP连接状态 41 | */ 42 | var connectStatus = false 43 | 44 | /** 45 | * 最大重连次数 46 | */ 47 | var maxConnectTimes = Integer.MAX_VALUE 48 | private set 49 | 50 | private var reconnectNum = maxConnectTimes 51 | 52 | private var isNeedReconnect = true 53 | 54 | var isConnecting = false 55 | private set 56 | 57 | var reconnectIntervalTime: Long = 5000 58 | private set 59 | 60 | /** 61 | * 心跳间隔时间 62 | */ 63 | var heartBeatInterval: Long = 5 64 | private set//单位秒 65 | 66 | /** 67 | * 是否发送心跳 68 | */ 69 | var isSendheartBeat = false 70 | private set 71 | 72 | /** 73 | * 心跳数据,可以是String类型,也可以是byte[]. 74 | */ 75 | private var heartBeatData: Any? = null 76 | 77 | fun connect() { 78 | if (isConnecting) { 79 | return 80 | } 81 | 82 | val clientThread = object : Thread("Netty-Client") { 83 | override fun run() { 84 | super.run() 85 | isNeedReconnect = true 86 | reconnectNum = maxConnectTimes 87 | connectServer() 88 | } 89 | } 90 | clientThread.start() 91 | } 92 | 93 | 94 | private fun connectServer() { 95 | 96 | synchronized(this@NettyTcpClient) { 97 | 98 | var channelFuture: ChannelFuture?=null 99 | 100 | if (!connectStatus) { 101 | isConnecting = true 102 | group = NioEventLoopGroup() 103 | val bootstrap = Bootstrap().group(group) 104 | .option(ChannelOption.TCP_NODELAY, true)//屏蔽Nagle算法试图 105 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) 106 | .channel(NioSocketChannel::class.java as Class?) 107 | .handler(object : ChannelInitializer() { 108 | 109 | @Throws(Exception::class) 110 | public override fun initChannel(ch: SocketChannel) { 111 | 112 | if (isSendheartBeat) { 113 | ch.pipeline().addLast("ping", IdleStateHandler(0, heartBeatInterval, 0, TimeUnit.SECONDS)) //5s未发送数据,回调userEventTriggered 114 | } 115 | 116 | ch.pipeline().addLast(StringEncoder(CharsetUtil.UTF_8)) 117 | ch.pipeline().addLast(StringDecoder(CharsetUtil.UTF_8)) 118 | ch.pipeline().addLast(LineBasedFrameDecoder(1024))//黏包处理,需要客户端、服务端配合 119 | ch.pipeline().addLast(NettyClientHandler(listener, index, isSendheartBeat, heartBeatData)) 120 | } 121 | }) 122 | 123 | try { 124 | channelFuture = bootstrap.connect(host, tcp_port).addListener { 125 | if (it.isSuccess) { 126 | Log.d(TAG, "连接成功") 127 | reconnectNum = maxConnectTimes 128 | connectStatus = true 129 | channel = channelFuture?.channel() 130 | } else { 131 | Log.d(TAG, "连接失败") 132 | connectStatus = false 133 | } 134 | isConnecting = false 135 | }.sync() 136 | 137 | // Wait until the connection is closed. 138 | channelFuture.channel().closeFuture().sync() 139 | Log.d(TAG, " 断开连接") 140 | } catch (e: Exception) { 141 | e.printStackTrace() 142 | } finally { 143 | connectStatus = false 144 | listener.onClientStatusConnectChanged(ConnectState.STATUS_CONNECT_CLOSED, index) 145 | 146 | if (channelFuture != null) { 147 | if (channelFuture.channel() != null && channelFuture.channel().isOpen) { 148 | channelFuture.channel().close() 149 | } 150 | } 151 | group.shutdownGracefully() 152 | reconnect() 153 | } 154 | } 155 | } 156 | } 157 | 158 | 159 | fun disconnect() { 160 | Log.d(TAG, "disconnect") 161 | isNeedReconnect = false 162 | group.shutdownGracefully() 163 | } 164 | 165 | fun reconnect() { 166 | Log.d(TAG, "reconnect") 167 | if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { 168 | reconnectNum-- 169 | SystemClock.sleep(reconnectIntervalTime) 170 | if (isNeedReconnect && reconnectNum > 0 && !connectStatus) { 171 | Log.e(TAG, "重新连接") 172 | connectServer() 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * 异步发送 179 | * 180 | * @param data 要发送的数据 181 | * @param listener 发送结果回调 182 | * @return 方法执行结果 183 | */ 184 | fun sendMsgToServer(data: String, listener: MessageStateListener) = channel?.run { 185 | 186 | val flag = this != null && connectStatus 187 | 188 | if (flag) { 189 | 190 | this.writeAndFlush(data + System.getProperty("line.separator")).addListener { channelFuture -> listener.isSendSuccss(channelFuture.isSuccess) } 191 | } 192 | 193 | flag 194 | 195 | } ?: false 196 | 197 | /** 198 | * 同步发送 199 | * 200 | * @param data 要发送的数据 201 | * @return 方法执行结果 202 | */ 203 | fun sendMsgToServer(data: String) = channel?.run { 204 | 205 | val flag = this != null && connectStatus 206 | 207 | if (flag) { 208 | 209 | val channelFuture = this.writeAndFlush(data + System.getProperty("line.separator")).awaitUninterruptibly() 210 | return channelFuture.isSuccess 211 | } 212 | 213 | false 214 | 215 | }?:false 216 | 217 | fun setListener(listener: NettyClientListener) { 218 | this.listener = listener 219 | } 220 | 221 | /** 222 | * Builder 模式创建NettyTcpClient 223 | */ 224 | class Builder { 225 | 226 | /** 227 | * 最大重连次数 228 | */ 229 | private var MAX_CONNECT_TIMES = Integer.MAX_VALUE 230 | 231 | /** 232 | * 重连间隔 233 | */ 234 | private var reconnectIntervalTime: Long = 5000 235 | /** 236 | * 服务器地址 237 | */ 238 | private var host: String? = null 239 | /** 240 | * 服务器端口 241 | */ 242 | private var tcp_port: Int = 0 243 | /** 244 | * 客户端标识,(因为可能存在多个连接) 245 | */ 246 | private var mIndex: Int = 0 247 | 248 | /** 249 | * 是否发送心跳 250 | */ 251 | private var isSendheartBeat: Boolean = false 252 | /** 253 | * 心跳时间间隔 254 | */ 255 | private var heartBeatInterval: Long = 5 256 | 257 | /** 258 | * 心跳数据,可以是String类型,也可以是byte[]. 259 | */ 260 | private var heartBeatData: Any? = null 261 | 262 | 263 | fun setMaxReconnectTimes(reConnectTimes: Int): Builder { 264 | this.MAX_CONNECT_TIMES = reConnectTimes 265 | return this 266 | } 267 | 268 | 269 | fun setReconnectIntervalTime(reconnectIntervalTime: Long): Builder { 270 | this.reconnectIntervalTime = reconnectIntervalTime 271 | return this 272 | } 273 | 274 | 275 | fun setHost(host: String): Builder { 276 | this.host = host 277 | return this 278 | } 279 | 280 | fun setTcpPort(tcp_port: Int): Builder { 281 | this.tcp_port = tcp_port 282 | return this 283 | } 284 | 285 | fun setIndex(mIndex: Int): Builder { 286 | this.mIndex = mIndex 287 | return this 288 | } 289 | 290 | fun setHeartBeatInterval(intervalTime: Long): Builder { 291 | this.heartBeatInterval = intervalTime 292 | return this 293 | } 294 | 295 | fun setSendheartBeat(isSendheartBeat: Boolean): Builder { 296 | this.isSendheartBeat = isSendheartBeat 297 | return this 298 | } 299 | 300 | fun setHeartBeatData(heartBeatData: Any): Builder { 301 | this.heartBeatData = heartBeatData 302 | return this 303 | } 304 | 305 | fun build(): NettyTcpClient { 306 | val nettyTcpClient = NettyTcpClient(host!!, tcp_port, mIndex) 307 | nettyTcpClient.maxConnectTimes = this.MAX_CONNECT_TIMES 308 | nettyTcpClient.reconnectIntervalTime = this.reconnectIntervalTime 309 | nettyTcpClient.heartBeatInterval = this.heartBeatInterval 310 | nettyTcpClient.isSendheartBeat = this.isSendheartBeat 311 | nettyTcpClient.heartBeatData = this.heartBeatData 312 | return nettyTcpClient 313 | } 314 | } 315 | 316 | companion object { 317 | private val TAG = "NettyTcpClient" 318 | private val CONNECT_TIMEOUT_MILLIS = 5000 319 | } 320 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/client/constant/ConnectState.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.client.constant 2 | 3 | /** 4 | * 5 | * @FileName: 6 | * com.safframework.netty4android.client.constant.ConnectState 7 | * @author: Tony Shen 8 | * @date: 2019-08-05 21:01 9 | * @version: V1.0 <描述当前版本功能> 10 | */ 11 | class ConnectState { 12 | 13 | companion object { 14 | 15 | @JvmField 16 | val STATUS_CONNECT_ERROR = -1 17 | 18 | @JvmField 19 | val STATUS_CONNECT_CLOSED = 0 20 | 21 | @JvmField 22 | val STATUS_CONNECT_SUCCESS = 1 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/client/handler/NettyClientHandler.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.client.handler 2 | 3 | import android.util.Log 4 | import com.safframework.netty4android.client.constant.ConnectState 5 | import com.safframework.netty4android.client.listener.NettyClientListener 6 | import io.netty.buffer.Unpooled 7 | import io.netty.channel.ChannelHandlerContext 8 | import io.netty.channel.SimpleChannelInboundHandler 9 | import io.netty.handler.timeout.IdleState 10 | import io.netty.handler.timeout.IdleStateEvent 11 | 12 | /** 13 | * 14 | * @FileName: 15 | * com.safframework.netty4android.client.handler.NettyClientHandler 16 | * @author: Tony Shen 17 | * @date: 2019-08-05 21:00 18 | * @version: V1.0 <描述当前版本功能> 19 | */ 20 | class NettyClientHandler(private val listener: NettyClientListener, private val index: Int, private val isSendheartBeat: Boolean, private val heartBeatData: Any?) : SimpleChannelInboundHandler() { 21 | 22 | /** 23 | * 24 | * 设定IdleStateHandler心跳检测每x秒进行一次读检测, 25 | * 如果x秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法 26 | * 27 | * @param ctx ChannelHandlerContext 28 | * @param evt IdleStateEvent 29 | */ 30 | override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) { 31 | 32 | if (evt is IdleStateEvent) { 33 | if (evt.state() == IdleState.WRITER_IDLE) { //发送心跳 34 | 35 | if (isSendheartBeat) { 36 | if (heartBeatData == null) { 37 | 38 | ctx.channel().writeAndFlush("Heartbeat" + System.getProperty("line.separator")!!) 39 | } else { 40 | 41 | if (heartBeatData is String) { 42 | Log.d(TAG, "userEventTriggered: String") 43 | ctx.channel().writeAndFlush(heartBeatData + System.getProperty("line.separator")!!) 44 | } else if (heartBeatData is ByteArray) { 45 | Log.d(TAG, "userEventTriggered: byte") 46 | val buf = Unpooled.copiedBuffer((heartBeatData as ByteArray?)!!) 47 | ctx.channel().writeAndFlush(buf) 48 | } else { 49 | 50 | Log.d(TAG, "userEventTriggered: heartBeatData type error") 51 | } 52 | } 53 | } else { 54 | Log.d(TAG, "不发送心跳") 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 62 | * 客户端上线 63 | * 64 | * @param ctx ChannelHandlerContext 65 | */ 66 | override fun channelActive(ctx: ChannelHandlerContext) { 67 | 68 | Log.d(TAG, "channelActive") 69 | listener.onClientStatusConnectChanged(ConnectState.STATUS_CONNECT_SUCCESS, index) 70 | } 71 | 72 | /** 73 | * 74 | * 客户端下线 75 | * 76 | * @param ctx ChannelHandlerContext 77 | */ 78 | override fun channelInactive(ctx: ChannelHandlerContext) { 79 | 80 | Log.d(TAG, "channelInactive") 81 | } 82 | 83 | /** 84 | * 客户端收到消息 85 | * 86 | * @param channelHandlerContext ChannelHandlerContext 87 | * @param msg 消息 88 | */ 89 | override fun channelRead0(channelHandlerContext: ChannelHandlerContext, msg: String) { 90 | 91 | Log.d(TAG, "channelRead0:") 92 | listener.onMessageResponseClient(msg, index) 93 | } 94 | 95 | /** 96 | * @param ctx ChannelHandlerContext 97 | * @param cause 异常 98 | */ 99 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 100 | 101 | Log.e(TAG, "exceptionCaught") 102 | listener.onClientStatusConnectChanged(ConnectState.STATUS_CONNECT_ERROR, index) 103 | cause.printStackTrace() 104 | ctx.close() 105 | } 106 | 107 | companion object { 108 | 109 | private val TAG = "NettyClientHandler" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/client/listener/MessageStateListener.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.client.listener 2 | 3 | /** 4 | * 5 | * @FileName: 6 | * com.safframework.netty4android.client.listener.MessageStateListener 7 | * @author: Tony Shen 8 | * @date: 2019-08-05 20:59 9 | * @version: V1.0 <描述当前版本功能> 10 | */ 11 | interface MessageStateListener { 12 | 13 | fun isSendSuccss(isSuccess: Boolean) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/client/listener/NettyClientListener.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.client.listener 2 | 3 | /** 4 | * 5 | * @FileName: 6 | * com.safframework.netty4android.client.listener.NettyClientListener 7 | * @author: Tony Shen 8 | * @date: 2019-08-05 20:57 9 | * @version: V1.0 <描述当前版本功能> 10 | */ 11 | interface NettyClientListener { 12 | 13 | /** 14 | * 当接收到系统消息 15 | * @param msg 消息 16 | * @param index tcp 客户端的标识,因为一个应用程序可能有很多个长链接 17 | */ 18 | fun onMessageResponseClient(msg: T, index: Int) 19 | 20 | /** 21 | * 当服务状态发生变化时触发 22 | * @param statusCode 状态变化 23 | * @param index tcp 客户端的标识,因为一个应用程序可能有很多个长链接 24 | */ 25 | fun onClientStatusConnectChanged(statusCode: Int, index: Int) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/ui/ConfigClientActivity.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.ui 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.text.Editable 8 | import android.widget.Toast 9 | import com.safframework.netty4android.R 10 | import kotlinx.android.synthetic.main.activity_config_client.* 11 | 12 | /** 13 | * 14 | * @FileName: 15 | * com.safframework.netty4android.ui.ConfigClientActivity 16 | * @author: Tony Shen 17 | * @date: 2019-08-10 12:01 18 | * @version: V1.0 <描述当前版本功能> 19 | */ 20 | class ConfigClientActivity : AppCompatActivity(){ 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContentView(R.layout.activity_config_client) 25 | 26 | if (intent.extras!=null) { 27 | 28 | val ip = intent.extras.getString("ip") 29 | ip_edit.text = Editable.Factory.getInstance().newEditable(ip) 30 | ip_edit.setSelection(ip.length) 31 | 32 | val port = intent.extras.getInt("port") 33 | port_edit.text = Editable.Factory.getInstance().newEditable(port.toString()) 34 | port_edit.setSelection(port_edit.text.toString().length) 35 | } 36 | 37 | update.setOnClickListener { 38 | 39 | if (ip_edit.text.isNotBlank() && port_edit.text.isNotBlank()) { 40 | 41 | val intent = Intent(this@ConfigClientActivity, MainActivity::class.java) 42 | intent.putExtra("port", port_edit.text.toString()) 43 | intent.putExtra("ip", ip_edit.text.toString()) 44 | setResult(Activity.RESULT_OK, intent) 45 | finish() 46 | } else { 47 | 48 | Toast.makeText(this@ConfigClientActivity, "请输入IP、端口号", Toast.LENGTH_LONG).show() 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.ui 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.support.v7.app.AppCompatActivity 6 | import android.support.v7.widget.LinearLayoutManager 7 | import android.text.TextUtils 8 | import android.util.Log 9 | import android.view.View 10 | import android.widget.Toast 11 | import android.widget.Toast.LENGTH_SHORT 12 | import com.safframework.netty4android.R 13 | import com.safframework.netty4android.client.NettyTcpClient 14 | import com.safframework.netty4android.client.constant.ConnectState 15 | import com.safframework.netty4android.client.listener.MessageStateListener 16 | import com.safframework.netty4android.client.listener.NettyClientListener 17 | import com.safframework.netty4android.ui.adapter.MessageAdapter 18 | import com.safframework.netty4android.ui.domain.MessageBean 19 | import kotlinx.android.synthetic.main.activity_main.* 20 | 21 | /** 22 | * 23 | * @FileName: 24 | * com.safframework.netty4android.ui.MainActivity 25 | * @author: Tony Shen 26 | * @date: 2019-08-10 10:31 27 | * @version: V1.0 <描述当前版本功能> 28 | */ 29 | class MainActivity : AppCompatActivity(), View.OnClickListener, NettyClientListener { 30 | 31 | private val mSendMessageAdapter = MessageAdapter() 32 | private val mReceMessageAdapter = MessageAdapter() 33 | private lateinit var mNettyTcpClient: NettyTcpClient 34 | 35 | private var ip:String = "10.184.16.77" 36 | private var port:Int = 8888 37 | 38 | private val REQUEST_CODE_CONFIG:Int = 1000 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | super.onCreate(savedInstanceState) 42 | setContentView(R.layout.activity_main) 43 | findViews() 44 | initView() 45 | 46 | mNettyTcpClient = NettyTcpClient.Builder() 47 | .setHost(ip) //设置服务端地址 48 | .setTcpPort(port) //设置服务端端口号 49 | .setMaxReconnectTimes(5) //设置最大重连次数 50 | .setReconnectIntervalTime(5) //设置重连间隔时间。单位:秒 51 | .setSendheartBeat(false) //设置发送心跳 52 | .setHeartBeatInterval(5) //设置心跳间隔时间。单位:秒 53 | .setHeartBeatData("I'm is HeartBeatData") //设置心跳数据,可以是String类型,也可以是byte[],以后设置的为准 54 | .setIndex(0) //设置客户端标识.(因为可能存在多个tcp连接) 55 | .build() 56 | 57 | mNettyTcpClient.setListener(this@MainActivity) //设置TCP监听 58 | } 59 | 60 | private fun findViews() { 61 | 62 | configClient.setOnClickListener(this) 63 | connect.setOnClickListener(this) 64 | send_btn.setOnClickListener(this) 65 | clear.setOnClickListener(this) 66 | } 67 | 68 | private fun initView() { 69 | 70 | send_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) 71 | send_list.adapter = mSendMessageAdapter 72 | 73 | rece_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) 74 | rece_list.adapter = mReceMessageAdapter 75 | } 76 | 77 | override fun onClick(v: View) { 78 | when (v.id) { 79 | 80 | R.id.configClient -> configClient() 81 | 82 | R.id.connect -> connect() 83 | 84 | R.id.send_btn -> if (!mNettyTcpClient.connectStatus) { 85 | 86 | Toast.makeText(applicationContext, "未连接,请先连接", LENGTH_SHORT).show() 87 | } else { 88 | val msg = send_et.text.toString() 89 | if (TextUtils.isEmpty(msg.trim { it <= ' ' })) { 90 | return 91 | } 92 | 93 | mNettyTcpClient.sendMsgToServer(msg, object : MessageStateListener { 94 | override fun isSendSuccss(isSuccess: Boolean) { 95 | if (isSuccess) { 96 | Log.d(TAG, "Write auth successful") 97 | msgSend(msg) 98 | } else { 99 | Log.d(TAG, "Write auth error") 100 | } 101 | } 102 | }) 103 | send_et.setText("") 104 | } 105 | 106 | R.id.clear -> { 107 | mReceMessageAdapter.dataList.clear() 108 | mSendMessageAdapter.dataList.clear() 109 | mReceMessageAdapter.notifyDataSetChanged() 110 | mSendMessageAdapter.notifyDataSetChanged() 111 | } 112 | } 113 | } 114 | 115 | private fun configClient() { 116 | 117 | val intent = Intent(this@MainActivity,ConfigClientActivity::class.java) 118 | intent.putExtra("ip",ip) 119 | intent.putExtra("port",port) 120 | startActivityForResult(intent,REQUEST_CODE_CONFIG) 121 | } 122 | 123 | private fun connect() { 124 | Log.d(TAG, "connect") 125 | if (!mNettyTcpClient.connectStatus) { 126 | mNettyTcpClient.connect()//连接服务器 127 | } else { 128 | mNettyTcpClient.disconnect() 129 | } 130 | } 131 | 132 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 133 | super.onActivityResult(requestCode, resultCode, data) 134 | 135 | if (requestCode == REQUEST_CODE_CONFIG && data!=null) { 136 | 137 | port = data.getStringExtra("port").toInt() 138 | ip = data.getStringExtra("ip") 139 | 140 | Log.i(TAG," ip=$ip, port=$port") 141 | 142 | mNettyTcpClient = NettyTcpClient.Builder() 143 | .setHost(ip) //设置服务端地址 144 | .setTcpPort(port) //设置服务端端口号 145 | .setMaxReconnectTimes(5) //设置最大重连次数 146 | .setReconnectIntervalTime(5) //设置重连间隔时间。单位:秒 147 | .setSendheartBeat(false) //设置发送心跳 148 | .setHeartBeatInterval(5) //设置心跳间隔时间。单位:秒 149 | .setHeartBeatData("I'm is HeartBeatData") //设置心跳数据,可以是String类型,也可以是byte[],以后设置的为准 150 | .setIndex(0) //设置客户端标识.(因为可能存在多个tcp连接) 151 | .build() 152 | 153 | mNettyTcpClient.setListener(this@MainActivity) //设置TCP监听 154 | } 155 | } 156 | 157 | override fun onMessageResponseClient(msg: String, index: Int) { 158 | Log.d(TAG, "onMessageResponse:$msg") 159 | msgReceive("$index:$msg") 160 | } 161 | 162 | override fun onClientStatusConnectChanged(statusCode: Int, index: Int) { 163 | runOnUiThread { 164 | if (statusCode == ConnectState.STATUS_CONNECT_SUCCESS) { 165 | Log.d(TAG, "STATUS_CONNECT_SUCCESS:") 166 | connect.text = "DisConnect:$index" 167 | } else { 168 | Log.d(TAG, "onServiceStatusConnectChanged:$statusCode") 169 | connect.text = "Connect:$index" 170 | } 171 | } 172 | } 173 | 174 | private fun msgSend(message: String) { 175 | val messageBean = MessageBean(System.currentTimeMillis(), message) 176 | mSendMessageAdapter.dataList.add(0, messageBean) 177 | runOnUiThread { mSendMessageAdapter.notifyDataSetChanged() } 178 | 179 | } 180 | 181 | private fun msgReceive(message: String) { 182 | val messageBean = MessageBean(System.currentTimeMillis(), message) 183 | mReceMessageAdapter.dataList.add(0, messageBean) 184 | runOnUiThread { mReceMessageAdapter.notifyDataSetChanged() } 185 | } 186 | 187 | fun disconnect(view: View) { 188 | mNettyTcpClient.disconnect() 189 | } 190 | 191 | companion object { 192 | 193 | private val TAG = "MainActivity" 194 | } 195 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/ui/adapter/MessageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.ui.adapter 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.support.v7.widget.RecyclerView 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.TextView 11 | import android.widget.Toast 12 | import com.safframework.netty4android.R 13 | import com.safframework.netty4android.ui.domain.MessageBean 14 | 15 | 16 | /** 17 | * 18 | * @FileName: 19 | * com.safframework.netty4android.ui.adapter.MessageAdapter 20 | * @author: Tony Shen 21 | * @date: 2019-08-05 23:18 22 | * @version: V1.0 <描述当前版本功能> 23 | */ 24 | class MessageAdapter : RecyclerView.Adapter() { 25 | 26 | val dataList: MutableList = mutableListOf() 27 | 28 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { 29 | return ItemHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false)) 30 | } 31 | 32 | override fun onBindViewHolder(holder: ItemHolder, position: Int) { 33 | val bean = dataList[position] 34 | 35 | holder.mTime.text = bean.mTime 36 | holder.mMsg.text = bean.mMsg 37 | 38 | holder.itemView.setOnLongClickListener(object : View.OnLongClickListener { 39 | override fun onLongClick(v: View): Boolean { 40 | val cmb = v.getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 41 | val msgBean = dataList[holder.adapterPosition] 42 | val msg = msgBean.mTime + " " + msgBean.mMsg 43 | cmb.setPrimaryClip(ClipData.newPlainText(null, msg)) 44 | Toast.makeText(v.getContext(), "已复制到剪贴板", Toast.LENGTH_LONG).show() 45 | return true 46 | } 47 | }) 48 | } 49 | 50 | override fun getItemCount(): Int { 51 | return dataList.size 52 | } 53 | 54 | inner class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 55 | var mTime: TextView 56 | var mMsg: TextView 57 | 58 | init { 59 | mTime = itemView.findViewById(R.id.time) 60 | mMsg = itemView.findViewById(R.id.message) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/safframework/netty4android/ui/domain/MessageBean.kt: -------------------------------------------------------------------------------- 1 | package com.safframework.netty4android.ui.domain 2 | 3 | import java.text.SimpleDateFormat 4 | 5 | /** 6 | * 7 | * @FileName: 8 | * com.safframework.netty4android.ui.domain.MessageBean 9 | * @author: Tony Shen 10 | * @date: 2019-08-05 23:18 11 | * @version: V1.0 <描述当前版本功能> 12 | */ 13 | class MessageBean(time: Long, var mMsg: String) { 14 | var mTime: String 15 | 16 | init { 17 | mTime = SimpleDateFormat("HH:mm:ss").format(time) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /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/layout/activity_config_client.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 21 | 22 |