【socket】- 长连接
简介
很多时候项目需要及时获取消息或者推送消息,比如聊天,消息通知等等。当然市场上已经有很多成熟的聊天,推送产品。但是这些都需要收费,而免费的有些时候并不能满足我们的需求。况且引入一个sdk也会增加应用包的大小。如果项目存在敏感的数据,那么使用第三方工具也会有一定安全问题。基于以上考虑,可以自己实现Socket长连接,实现即时通讯。
这篇文章会基于前两篇文章进行讲解,如果还没有查看的,建议先看看前两篇文章,多Socket有个大致的了解。
客户端
创建客户端Service(服务),并让它允许在独立的进程中。当然我得尽量保证服务进程不被系统干掉。我们可以提高进程的优先级,开启前台进程,进程间互相唤醒,不定时检测进程是否被杀死等等。具体内容我将在后面的文章讲解。我们看一下客户端Socket部分的实现。
override fun connect(ip: String, port: Int) {
lock.lock()
if (isConnected()){
disConnect(false)
}
connectState = SState.STATE_CONNECTING
this.ip = ip
this.port = port
Log.i(TAG,"connecting ip=$ip , port = $port")
try {
while (true){
try {
socket = Socket()
if (null == socket){
throw (Exception("connect failed,unknown error"))
}
val address = InetSocketAddress(ip,port)
socket!!.keepAlive = false
//inputStream read 超时时间
socket!!.soTimeout = 2 * 3 * 60 * 1000
socket!!.tcpNoDelay = true
socket!!.connect(address)
if (socket!!.isConnected){
dataInputStream = DataInputStream(socket!!.getInputStream())
dataOutputStream = DataOutputStream(socket!!.getOutputStream())
connectState = SState.STATE_CONNECTED
this.sCallback.onConnect()
break
}else{
throw (Exception("connect failed,unknown error"))
}
}catch (e:Exception){
cRetryPolicy?.retry(e)
Thread.sleep(5*1000)
Log.i(TAG,"connect IOException =${e.message} , and retry count = ${cRetryPolicy?.getCurrentRetryCount()}")
}
}
}catch (e:Exception){
e.printStackTrace()
Log.i(TAG,"connect IOException = ${e.message}")
connectState = SState.STATE_CONNECT_FAILED
sCallback.onConnectFailed(e)
}finally {
lock.unlock()
}
if (connectState == SState.STATE_CONNECTED){
receiveData()
}
}
Socket连接的过程就不讲了,不明白的可以查看前面的文章。在while循环里面,直到到达重试次数之前,一直连接服务器,直到连接成功。重试代码如下:
fun retry(ex: Exception){
mCurrentRetryCount++
if (!hasAttemptRemaining())
throw ex
}
获取Socket输入,输出流对象
当Socket连接成功后,拿到输出,输入流对象,我们就可以读取服务器端发送的数据和向服务器写数据。
-
接受数据
private fun receiveData(){ Log.i(TAG,"receiveData ing isConnect = ${isConnected()}") while (isConnected()){ try { val c = dataInputStream?.readChar() Log.i(TAG,"receiveData connected receiveData type = $c") val t = dataInputStream?.available() val data = ByteArray(t!!) dataInputStream?.readFully(data) Log.i(TAG,"receiveData connected receiveData type = ${String(data)}") }catch (e:SocketTimeoutException){ Log.e(TAG,"receiveData SocketTimeoutException = ${e.message}") break }catch (e: IOException){ Log.e(TAG,"receiveData IOException = ${e.message}") break } } }
while循环一直读取服务端发送的数据,直到连接断开。读数据的过程是阻塞的,如果没有读取到指定的长度的数据,会一直阻塞在哪里直到连接断开。很多人使用Socket读取数据的时候,发现读取的数据不完整(拆包)或者数据重复(粘包),这是因为发生了丢包。设置读取的数据太长,发现一直阻塞在哪里,太短,数据总是不完整等等。
-
丢包解决方案
- 消息定长。不足的用空格或者不影响功能的字符补齐。
- 在包尾增加特殊的字符作为包结束标志符。
- 将消息分为消息头和消息体,在消息头中定义消息总长度(或者消息体长度),比如消息头的第一个字段用int来表示消息的总长度。
- 更复杂的应用层协议
-
关闭流和Socket
override fun disConnect(reconnect:Boolean) {
Log.i(TAG,"disConnect")
if(null != socket){
try {
closeInputStream()
closeOutputStream()
socket?.shutdownInput()
socket?.shutdownOutput()
socket?.close()
socket = null
}catch (e:Exception){
e.printStackTrace()
}
}
}
Java 的 Socket 类提供了 shutdownOutput() 和shutdownInput() 另个方法, 用来分别只关闭 Socket 的输出流和输入流, 而不影响其对应的输入流和输出流。
- shutdownInput: 禁用此套接字的输入流,这样发送到套接字的输入流端的任何数据都将被确认然后被静默丢弃。任何想从该套接字的输入流读取数据的操作都将返回-1。
- shutdownOutput:关闭套接字的输出流,告诉服务器我已经发送完所以数据,这样, 服务端的 read() 方法便会返回-1, 继续往下执行。
服务端
服务端监听客户端的连接消息,在连接成功后,获取流对象,便可以向客户端发送消息。
fun startAccept(){
try {
Log.i(TAG,"start accept")
//调用accept()方法开始监听,等待客户端的连接
val socket = serverSocket.accept()
//向客户端传递的信息
val outputStream = socket.getOutputStream()
val printWriter = PrintWriter(outputStream)
printWriter.write("-----------socket connect success------------")
printWriter.flush()
//获取输入流,并读取客户端信息
val inputStream = socket.getInputStream()
//把字节流转换成字符流
val inputStreamReader = InputStreamReader(inputStream)
//为字符流增加缓冲区
val bufferedReader = BufferedReader(inputStreamReader)
var data :String? = bufferedReader.readLine()
while (data != null){
Log.i(TAG,"accept data = $data")
data = bufferedReader.readLine()
}
socket.shutdownInput()
printWriter.write("-----------socket disconnect------------")
printWriter.flush()
//关闭资源
printWriter.close()
outputStream.close()
inputStream.close()
inputStreamReader.close()
bufferedReader.close()
socket.close()
serverSocket.close()
}catch (e:Exception){
e.printStackTrace()
}finally {
serverSocket.close()
}
}
serverSocket.accept()阻塞,当有连接到达时返回,获取输入,输出流,向客户端写数据。然后关闭流和socket连接。
- 通过Socket实现设备互踢