个人博客:haichenyi.com。感谢关注
Android中常用的网络编程方式:Http和Socket,Http通讯方式,我们在前面已经加到框架里面了,用的是现在主流的RxJava+Retrofit+OkHttp的方式,用起来也很方便。今天,我们来说说Socket网络编程。前面已经有一篇讲过IO和NIO的区别,还有一篇讲过Netty了,今天这篇的目的就是把Netty加到我们这个框架里面。
添加依赖
implementation 'io.netty:netty-all:5.0.0.Alpha2'
添加依赖,老生常谈的问题了,我就不多说了。
TCP
package com.haichenyi.myproject.model.socket
import android.util.Log
import com.haichenyi.myproject.model.bean.SocketTcpBean
import io.netty.bootstrap.Bootstrap
import io.netty.channel.Channel
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.LineBasedFrameDecoder
import io.netty.handler.codec.string.StringDecoder
import io.netty.handler.codec.string.StringEncoder
import io.netty.util.CharsetUtil
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
/**
* Author: 海晨忆
* Date: 2018/2/26
* Desc:
*/
class SocketTcp {
companion object {
private val socket = SocketTcp()
fun getInstance(): SocketTcp = socket
}
private var port: Int = 0
private var host: String = ""
private var channel: Channel? = null
private var group: EventLoopGroup? = null
fun setPort(port: Int): SocketTcp {
this.port = port
return this
}
fun setHost(host: String): SocketTcp {
this.host = host
return this
}
fun connect() {
if (null != channel) return
if (null == group) {
if (!EventBus.getDefault().isRegistered(this))
EventBus.getDefault().register(this)
group = NioEventLoopGroup()
}
val bootstrap = Bootstrap()
try {
bootstrap.group(group)
.channel(NioSocketChannel::class.java)
.option(ChannelOption.TCP_NODELAY, true)
.handler(object : ChannelInitializer<SocketChannel>() {
override fun initChannel(ch: SocketChannel?) {
//以换行符为结束标记
val pipeline = ch!!.pipeline()
pipeline.addLast("encoder", StringEncoder(CharsetUtil.UTF_8))
pipeline.addLast(LineBasedFrameDecoder(Integer.MAX_VALUE))
pipeline.addLast(StringDecoder())
pipeline.addLast(HeartTcp())
//以"#_"作为分隔符
/*val pipeline = ch!!.pipeline()
pipeline.addLast("encoder", StringEncoder(CharsetUtil.UTF_8))
val s = "#_"
val byteBuf = Unpooled.copiedBuffer(s.toByteArray())
pipeline.addLast(DelimiterBasedFrameDecoder(Integer.MAX_VALUE, byteBuf))
pipeline.addLast(StringDecoder())*/
}
})
//发起异步连接操作
val channelFuture = bootstrap.connect(host, port).sync()
channel = channelFuture.channel()
//等待服务端监听端口关闭
channel!!.closeFuture().sync()
} catch (e: Exception) {
e.printStackTrace()
} finally {
disConnect()
}
}
//连接成功后,通过Channel提供的接口进行IO操作
fun sendMessage(msg: String) {
try {
if (channel != null && channel!!.isOpen) {
channel!!.writeAndFlush(msg).sync()
Log.d("wz", "send succeed " + msg)
} else {
throw Exception("channel is null | closed")
}
} catch (e: Exception) {
reConnect()
e.printStackTrace()
}
}
/**
* 断开tcp连接.
*/
private fun disConnect() {
if (null != group) {
group!!.shutdownGracefully()
}
// EventBus.getDefault().unregister(this)
group = null
channel = null
Log.v("WZ", "disConnect")
}
@Subscribe
fun handle(socketTcpBean: SocketTcpBean) {
sendMessage(socketTcpBean.msg)
}
/**
* 重连.
*/
private fun reConnect() {
Thread(Runnable { this.connect() }).start()
}
}
package com.haichenyi.myproject.model.socket
import android.os.SystemClock
import android.util.Log
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import java.util.concurrent.TimeUnit
class HeartTcp : SimpleChannelInboundHandler<Any>() {
private var ctx: ChannelHandlerContext? = null
private var isConnect = false
@Throws(Exception::class)
override fun channelActive(ctx: ChannelHandlerContext) {
super.channelActive(ctx)
Log.v("WZ", "连接正常channelActive")
isConnect = true
if (this.ctx == null) {
synchronized(HeartTcp::class.java) {
if (this.ctx == null) {
this.ctx = ctx
myAppHeart()
}
}
}
}
private fun myAppHeart() {
Thread {
while (ctx != null && isConnect) {
val data = "123"
val bytes = data.toByteArray()
if (isConnect) {
ctx!!.writeAndFlush(Unpooled.buffer(bytes.size).writeBytes(bytes))
SystemClock.sleep(3000)
}
}
}.start()
}
@Throws(Exception::class)
override fun channelInactive(ctx: ChannelHandlerContext) {
val loop = ctx.channel().eventLoop()
loop.schedule({ SocketTcp.getInstance().connect() }, 5, TimeUnit.SECONDS)
super.channelInactive(ctx)
Log.v("WZ", "重新连接socket服务器")
isConnect = false
}
@Throws(Exception::class)
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
super.userEventTriggered(ctx, evt)
Log.v("WZ", "发送数据包")
}
@Throws(Exception::class)
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
super.exceptionCaught(ctx, cause)
Log.v("WZ", "连接出现异常")
this.ctx = null
}
@Throws(Exception::class)
override fun messageReceived(ctx: ChannelHandlerContext, msg: Any) {
Log.v("WZ", "连接正常messageReceived")
/*val msg1 = msg
val bytes = ByteArray(msg1.readableBytes())
msg1.readBytes(bytes)
val s = String(bytes*//*, "UTF-8"*//*)*/
val s = msg.toString()
Log.v("WZ", "接收到的消息:" + s)
}
}
我这里就是把我前面那个讲Netty创建Socket连接的代码拷贝过来转成了kotlin代码,这里我就不做过多的解释了。
一般,我们都是后台新开一个服务去启动socket,所以,我们还要创建一个Service,去处理socket。我这里就是非绑定启动这个服务。在oncreate方法里面去连接服务器即可
package com.haichenyi.myproject.model.socket
import android.app.Service
import android.content.Intent
import android.os.IBinder
/**
* Author: 海晨忆
* Date: 2018/2/26
* Desc:
*/
class SocketService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Thread(Runnable { SocketTcp.getInstance().setPort(8080).setHost("192.168.0.235").connect() }).start()
}
}
之所以要新建一个服务,就是可能你在连接socket的时候,可能要做一些其他信息的初始化,而这些信息初始化也是只有在连接socket服务器之前才需要的,写在这里方便一起管理,要是写在activity里面不便于管理。上面都是扯淡,最主要的是我们在最初学Android的时候,教你们的老师应该讲过,什么时候需要用到服务,就是不需要页面,用户并不关心它长什么样子,需要长连接的任务,都放在服务里面。正好,socket连接正好满足这些条件。
细心的同学可能注意了,我在Tcp类里面有一个EventBus,我这里是把发TCP消息给拿出来了,便于管理。我这边还新建了一个SocketUtils类,就是处理Socket请求的方法,我们发消息之类的方法,都是调用SocketUtils类里面的方法,不用去改Tcp类。这样做就只有一个目的,就是便于统一管理,代码如下
package com.haichenyi.myproject.model.socket
import com.haichenyi.myproject.model.bean.SocketTcpBean
import org.greenrobot.eventbus.EventBus
/**
* Author: 海晨忆
* Date: 2018/2/26
* Desc:
*/
object SocketUtils {
fun sendTcpMsg(msg: String) {
sendTcpMsg(SocketTcpBean(msg))
}
private fun sendTcpMsg(socketTcpBean: SocketTcpBean) {
EventBus.getDefault().post(socketTcpBean)
}
}
至于这个SocketTcpBean类,就是一个普通的java bean类,里面就只有一个参数msg,你需要发的消息
UDP
我先给出来代码
package com.haichenyi.myproject.model.socket
import android.util.Log
import com.haichenyi.myproject.model.bean.SocketUdpBean
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.Unpooled
import io.netty.channel.Channel
import io.netty.channel.ChannelOption
import io.netty.channel.EventLoopGroup
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.DatagramPacket
import io.netty.channel.socket.nio.NioDatagramChannel
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.net.InetSocketAddress
import java.nio.charset.Charset
/**
* Author: 海晨忆
* Date: 2018/2/26
* Desc:
*/
class SocketUdp {
companion object {
private val socket = SocketUdp()
fun getInstance(): SocketUdp = socket
}
private var port: Int = 0
private var channel: Channel? = null
private var group: EventLoopGroup? = null
fun setPort(port: Int): SocketUdp {
this.port = port
return this
}
fun connect() {
if (null != channel) return
if (null == group) {
if (!EventBus.getDefault().isRegistered(this))
EventBus.getDefault().register(this)
group = NioEventLoopGroup()
}
val bootstrap = Bootstrap()
bootstrap.group(group)
.channel(NioDatagramChannel::class.java)
.option(ChannelOption.SO_BROADCAST, true)
.handler(UdpHandler())
try {
channel = bootstrap.bind(port).sync().channel()
channel!!.closeFuture().sync()
} catch (e: InterruptedException) {
e.printStackTrace()
} finally {
disConnect()
}
}
/**
* 断开tcp连接.
*/
private fun disConnect() {
if (null != group) {
group!!.shutdownGracefully()
}
// EventBus.getDefault().unregister(this)
group = null
channel = null
Log.v("WZ", "disConnect")
}
//连接成功后,通过Channel提供的接口进行IO操作
private fun sendMessage(host: String, port: Int, data: ByteArray) {
val packet = DatagramPacket(Unpooled.copiedBuffer(data),
InetSocketAddress(host, port))
channel?.let {
try {
it.writeAndFlush(packet).sync()
Log.d("wz", "send succeed " + String(data, Charset.forName("UTF-8")))
} catch (e: Exception) {
reConnect()
e.printStackTrace()
}
}
}
@Subscribe
fun handle(socketUdpBean: SocketUdpBean) {
sendMessage(socketUdpBean.host,socketUdpBean.port,socketUdpBean.data)
}
/**
* 重连.
*/
private fun reConnect() {
Thread(Runnable { this.connect() }).start()
}
}
package com.haichenyi.myproject.model.socket
import android.util.Log
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.socket.DatagramPacket
import java.nio.charset.Charset
/**
* Author: 海晨忆
* Date: 2018/2/26
* Desc:
*/
class UdpHandler : SimpleChannelInboundHandler<DatagramPacket>() {
override fun messageReceived(ctx: ChannelHandlerContext?, msg: DatagramPacket?) {
val byteBuf = msg!!.content()
val bytes = ByteArray(byteBuf.readableBytes())
byteBuf.readBytes(bytes)
val s = String(bytes, Charset.forName("UTF-8"))
Log.v("WZ", "UDP收到的消息是-->$s")
}
}
这里的代码跟TCP类似,我想说的就只有三点:
- 首先你客户端开启UDP的时候,需要bind一个端口号,这个端口号是服务器给你发消息的时候,需要的端口号。
- 它的管道里面的handle类型DatagramPacket
- 再有就是给目标主机发消息,UDP需要传目标地址,端口号,还有消息。这里的目标地址就是服务器的地址,端口号就是服务器跟你通信的端口号,消息就是你需要发送给服务器的信息
其实,这里的区别就是TCP与UDP的区别,TCP和UDP什么区别呢?
类型 | TCP | UDP |
---|---|---|
是否连接 | 面向连接 | 面向非连接 |
传输效率 | 慢 | 快 |
数据是否有序 | 有序 | 无序 |
安全性 | 安全 | 不安全 |
模式 | 流模式 | 报文模式 |
系统资源 | 多 | 少 |
区别的总结:最典型的对比就是打电话(TCP)和发短信(UDP)的区别
- 是否连接方面,TCP是1对1,UDP可以1对N
- 传输效率:TCP必须等到一个消息全部传完了才能传下一个,UDP可以一直发
- 数据是否有序:TCP是按顺序一条一条的发,当然是有序的。UDP则是无序的
- 安全性方面:TCP有3次握手机制,采用的全双工的可靠信道,保证了数据的安全,UDP则没有3次握手机制,采用的是不可靠信道
- 模式方面:TCP是面向字节流,实际上TCP把数据看成了一连串无结构的字节流,UDP则是面向报文的
- 系统资源方面:TCP首部开销20个字节,UDP首部开销8个字节