相关文章推荐
不敢表白的斑马  ·  Android ...·  1 年前    · 
谈吐大方的铁链  ·  docker oracle io ...·  1 年前    · 
大鼻子的书包  ·  Java8中 stream,filter ...·  1 年前    · 
每天一个Android实例之TCP客户端(Kotlin)

每天一个Android实例之TCP客户端(Kotlin)

  1. 概述
  • 同蓝牙开发一样,在物联网开发过程中,wifi控制是非常常见的,一方面,现在常见的ESP8266价格便宜(最便宜的已经到了几块钱,价格明显低于蓝牙模块),另一方面,由于WiFi可以连接路由器,所以通过使用WiFi模块,可以很方便的实现远程控制与监控,本文主要探讨基于TCP协议的客户端实现
  • 同蓝牙相比,WiFi模块虽然功耗高一点,但正因为这样,才有很高的传输速率,所以只要服务端和客户端编码格式一致,基本不会出现中文乱码问题,但蓝牙传输速率过快时,经常会出现中文乱码
  • 不管是什么样的无线传输,都不建议使用中文,毕竟有出现乱码的可能性

2. 实现效果

3. 仍然存在的问题

    • 没有实现短时间的心跳包监听在线状态,这里提供一下实现思路,这需要服务器和客户端约定好,比如客户端定时发送数据给服务端,如果服务端连续几次都没有收到数据,那么就默认客户端断联,反之亦然。socket本身自带的isclose()和isconnected()方法,是无法监听socket是否一直连接的,它们监听的是socket在内存中的状态,所以只要socket建立,并且没有手动关闭,那么程序其实默认socket一直处于连接状态。

4. 项目相关代码

    • 下面附项目相关代码及GitHub链接

5. 项目目录

6. 项目代码(kt)

    • MainActivity
package com.flywinter.tcpclient
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.text.method.ScrollingMovementMethod
import android.util.Log
import kotlinx.android.synthetic.main.activity_main.*
import java.io.*
import java.lang.Exception
import java.net.*
import kotlin.concurrent.thread
 * @author Zhang Xingkun
 * @note 基于socket的TCP客户端实例,注意,如果服务端断开连接,客户端需要等两个小时才能
 * 知道,可以自己实现一个心跳包机制
class MainActivity : AppCompatActivity() {
 //初始化常量
 companion object {
 private const val TCP_CLIENT_GET_MSG_BUNDLE = "tcpClientGetMsg"
 private const val TCP_CLIENT_GET_MSG = 4555
 private const val TCP_CLIENT_CONNECT_STATUS_BUNDLE = "TCPConnectStatus"
 private const val TCP_CLIENT_CONNECT_STATUS_MSG = 1245
 private var tcpClient = Socket()
 private val encodingFormat = "GBK"
 private var tcpClientConnectStatus = false
 private var tcpClientTargetServerIP = String()
 private var tcpClientTargetServerPort = 8080
 private var tcpClientOutputStream: OutputStream? = null
 private var tcpClientInputStreamReader: InputStreamReader? = null
 private val tcpClientReceiveBuffer = StringBuffer()
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 //设置接收信息框上下滚动,如果设置了多个,只会有一个起作用
 txt_tcp_client_receive.movementMethod = ScrollingMovementMethod.getInstance()
 //清除客户端接收
 btn_tcp_client_receive_clear.setOnClickListener {
 tcpClientReceiveBuffer.delete(0, tcpClientReceiveBuffer.length)
 txt_tcp_client_receive.text = tcpClientReceiveBuffer
 //连接服务端或者断开连接
 switch_tcp_client_status.setOnClickListener {
 tcpClientTargetServerIP = edit_tcp_client_target_ip.text.toString()
 tcpClientTargetServerPort = edit_tcp_client_target_port.text.toString().toInt()
 if (switch_tcp_client_status.isChecked) {
 switch_tcp_client_status.isChecked = true
 thread {
 funTCPClientConnect()
            } else {
 switch_tcp_client_status.isChecked = false
 tcpClientConnectStatus = false
 tcpClient.close()
 txt_tcp_client_local_ip.text = funGetLocalAddress()
 //客户端发送消息
 btn_tcp_client_send.setOnClickListener {
 var text = edit_tcp_client_send.text.toString()
 if (check_tcp_client_add_newline.isChecked) {
 text += "\r\n"
 if (check_tcp_client_add_renew.isChecked) {
 txt_tcp_client_receive.text = tcpClientReceiveBuffer.append("客户端发送的消息:$text")
 if (tcpClientConnectStatus) {
 thread {
 funTCPClientSend(text)
 //客户端连接
 //需要子线程
 private fun funTCPClientConnect() {
 if (tcpClientTargetServerIP.isEmpty()) {
 Log.e("目标服务端IP不能为空,否则无法连接", "")
 return
 try {
 //一定要注意,每次连接必须是一个新的Socket对象,否则如果在其他地方关闭了socket对象,那么就无法
 //继续连接了,因为默认对象已经关闭了
 tcpClient = Socket()
 tcpClient.connect(
 InetSocketAddress(tcpClientTargetServerIP, tcpClientTargetServerPort),
 //发送心跳包
 tcpClient.keepAlive = true
 //注意这里,不同的电脑PC端可能用到编码方式不同,通常会使用GBK格式而不是UTF-8格式
 val printWriter =
 PrintWriter(OutputStreamWriter(tcpClient.getOutputStream(), encodingFormat), true)
 // 将缓冲区的数据强制输出,用于清空缓冲区,若直接调用close()方法,则可能会丢失缓冲区的数据。所以通俗来讲它起到的是刷新的作用。
 //printWriter.flush();
 // 用于关闭数据流
 ///printWriter.close();
 printWriter.write("客户端连接成功")
 printWriter.flush()
 tcpClientConnectStatus = true
 Log.e("连接服务端成功", "TCPClient")
 Log.e("开启客户端接收", "TCPClient")
 funTCPClientReceive()
        } catch (e: Exception) {
 when (e) {
 is SocketTimeoutException -> {
 //一般TCP连接第一次都会失败,不知道是编程的问题还是这个协议本身的问题
 //所以自己设置下重连,如果第二次依旧重连失败,那就表明连接确实是有问题了,
 //就需要手动重连了
 Log.e("连接超时,重新连接", "dd");
 e.printStackTrace()
 is NoRouteToHostException -> {
 Log.e("该地址不存在,请检查", "DD");
 e.printStackTrace()
 is ConnectException -> {
 Log.e("连接异常或被拒绝,请检查", "DD");
 e.printStackTrace()
 else -> {
 e.printStackTrace()
 Log.e("连接结束", e.toString())
 val message = Message()
 val bundle = Bundle()
 bundle.putBoolean(TCP_CLIENT_CONNECT_STATUS_BUNDLE, false)
 message.what = TCP_CLIENT_CONNECT_STATUS_MSG
 message.data = bundle
 handler.sendMessage(message)
 tcpClientConnectStatus = false
 tcpClient.close()
 //客户端发送
 //需要子线程
 private fun funTCPClientSend(msg: String) {
 if (msg.isNotEmpty() && tcpClientConnectStatus) {
 //这里要注意,只要曾经连接过,isConnected便一直返回true,无论现在是否正在连接
 if (tcpClient.isConnected) {
 try {
 tcpClientOutputStream = tcpClient.getOutputStream()
 val printWriter =
 PrintWriter(
 OutputStreamWriter(
 tcpClientOutputStream,
 encodingFormat
                            ), true
 printWriter.write(msg)
 printWriter.flush()
 Log.e("信息发送成功", msg)
                } catch (e: IOException) {
 Log.e("信息发送失败", msg)
 e.printStackTrace()
 tcpClientInputStreamReader?.close()
 tcpClientOutputStream?.close()
 tcpClient.close()
 //客户端接收的消息
 //添加子线程
 private fun funTCPClientReceive() {
 Log.e("开启客户端接收成功", "TCPClient")
 while (tcpClientConnectStatus) {
 if (tcpClient.isConnected) {
 tcpClientInputStreamReader = InputStreamReader(tcpClient.getInputStream(), "GBK")
 val bufferedReader = BufferedReader(tcpClientInputStreamReader)
 val readLine = bufferedReader.readLine()
 val message = Message()
 val bundle = Bundle()
 bundle.putString(TCP_CLIENT_GET_MSG_BUNDLE, readLine)
 message.what = TCP_CLIENT_GET_MSG
 message.data = bundle
 handler.sendMessage(message)
 Log.e("客户端收到的消息", readLine)
            } else {
 Log.e("开启客户端接收失败", "TCPClient")
 tcpClientInputStreamReader?.close()
 tcpClientOutputStream?.close()
 tcpClient.close()
 break
 //这是官方推荐的方法
 private val handler = object : Handler(Looper.getMainLooper()) {
 override fun handleMessage(msg: Message) {
 when (msg.what) {
 TCP_CLIENT_GET_MSG -> {
 val string = msg.data.getString(TCP_CLIENT_GET_MSG_BUNDLE)
 tcpClientReceiveBuffer.append(string)
 txt_tcp_client_receive.text = tcpClientReceiveBuffer.toString()
 TCP_CLIENT_CONNECT_STATUS_MSG -> {
 val boolean = msg.data.getBoolean(TCP_CLIENT_CONNECT_STATUS_BUNDLE)
 if (!boolean) {
 switch_tcp_client_status.isChecked = false
 //获取设备局域网IP,没开wifi的情况下获取的会是内网ip
 private fun funGetLocalAddress(): String {
 val wifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
 val wifiInfo: WifiInfo = wifiManager.connectionInfo
 val ipAddress = wifiInfo.ipAddress
 val localIP =
            (ipAddress and 0xff).toString() + "." + (ipAddress shr 8 and 0xff) + "." + (ipAddress shr 16 and 0xff) + "." + (ipAddress shr 24 and 0xff)
 Log.e("localIP", localIP)
 return localIP
}

7. 项目代码(xml)

    • AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.flywinter.tcpclient">
<!--    添加网络权限-->
 <uses-permission android:name="android.permission.INTERNET"/>
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
 <application
 android:allowBackup="true"
 android:icon="@mipmap/ic_launcher"
 android:label="@string/app_name"
 android:roundIcon="@mipmap/ic_launcher_round"
 android:supportsRtl="true"
 android:theme="@style/AppTheme">
 <activity android:name=".MainActivity">
 <intent-filter>
 <action android:name="android.intent.action.MAIN" />
 <category android:name="android.intent.category.LAUNCHER" />
 </intent-filter>
 </activity>
 </application>
</manifest>
    • activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 tools:context=".MainActivity">
 <TextView
 android:id="@+id/txt_tcp_client_receive"
 android:layout_width="0dp"
 android:layout_height="0dp"
 android:text="Hello World!"
 android:scrollbars="vertical"
 app:layout_constraintBottom_toTopOf="@+id/guideline7"
 app:layout_constraintEnd_toStartOf="@+id/guideline4"
 app:layout_constraintStart_toStartOf="@+id/guideline3"
 app:layout_constraintTop_toTopOf="@+id/guideline5" />
 <Button
 android:id="@+id/btn_tcp_client_send"
 android:layout_width="0dp"
 android:layout_height="0dp"
 android:text="Send"
 app:layout_constraintBottom_toTopOf="@+id/guideline9"
 app:layout_constraintEnd_toStartOf="@+id/guideline14"
 app:layout_constraintStart_toStartOf="@+id/guideline3"
 app:layout_constraintTop_toTopOf="@+id/guideline6" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_begin="16dp" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline2"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_end="16dp" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline3"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 app:layout_constraintGuide_begin="16dp" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline4"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 app:layout_constraintGuide_end="16dp" />
 <EditText
 android:id="@+id/edit_tcp_client_target_ip"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:ems="10"
 android:inputType="number"
 android:text="192.168.31.78"
 app:layout_constraintBottom_toTopOf="@+id/guideline10"
 app:layout_constraintEnd_toStartOf="@+id/guideline12"
 app:layout_constraintStart_toStartOf="@+id/guideline14"
 app:layout_constraintTop_toTopOf="@+id/guideline11" />
 <EditText
 android:id="@+id/edit_tcp_client_target_port"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:ems="10"
 android:inputType="number"
 android:text="8080"
 app:layout_constraintBottom_toTopOf="@+id/guideline15"
 app:layout_constraintEnd_toStartOf="@+id/guideline12"
 app:layout_constraintStart_toStartOf="@+id/edit_tcp_client_target_ip"
 app:layout_constraintTop_toTopOf="@+id/guideline10" />
 <TextView
 android:id="@+id/textView"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="IP"
 app:layout_constraintBottom_toTopOf="@+id/guideline10"
 app:layout_constraintEnd_toStartOf="@+id/guideline14"
 app:layout_constraintStart_toStartOf="@+id/guideline3"
 app:layout_constraintTop_toTopOf="@+id/guideline11" />
 <TextView
 android:id="@+id/textView2"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Port"
 app:layout_constraintBottom_toTopOf="@+id/guideline15"
 app:layout_constraintEnd_toStartOf="@+id/guideline14"
 app:layout_constraintStart_toStartOf="@+id/guideline3"
 app:layout_constraintTop_toTopOf="@+id/guideline10" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline5"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.13" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline6"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.8" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline7"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.57" />
 <EditText
 android:id="@+id/edit_tcp_client_send"
 android:layout_width="0dp"
 android:layout_height="wrap_content"
 android:ems="10"
 android:inputType="textPersonName"
 android:text="Leaves"
 app:layout_constraintBottom_toTopOf="@+id/guideline9"
 app:layout_constraintEnd_toStartOf="@+id/guideline12"
 app:layout_constraintStart_toStartOf="@+id/guideline14"
 app:layout_constraintTop_toTopOf="@+id/guideline6" />
 <Switch
 android:id="@+id/switch_tcp_client_status"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="连接状态"
 app:layout_constraintBottom_toTopOf="@+id/guideline8"
 app:layout_constraintEnd_toStartOf="@+id/guideline12"
 app:layout_constraintStart_toStartOf="@+id/guideline14"
 app:layout_constraintTop_toTopOf="@+id/guideline" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline8"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.11" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline9"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.86" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline10"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.7" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline11"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.63" />
 <Button
 android:id="@+id/btn_tcp_client_receive_clear"
 android:layout_width="0dp"
 android:layout_height="0dp"
 android:text="clear"
 app:layout_constraintBottom_toTopOf="@+id/guideline11"
 app:layout_constraintEnd_toStartOf="@+id/guideline4"
 app:layout_constraintStart_toStartOf="@+id/guideline12"
 app:layout_constraintTop_toTopOf="@+id/guideline7" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline12"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 app:layout_constraintGuide_percent="0.75" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline13"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_begin="684dp" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline14"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 app:layout_constraintGuide_percent="0.25" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline15"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 app:layout_constraintGuide_percent="0.76" />
 <CheckBox
 android:id="@+id/check_tcp_client_add_newline"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="加入换行符"
 app:layout_constraintEnd_toStartOf="@+id/guideline16"
 app:layout_constraintStart_toStartOf="@+id/guideline14"
 app:layout_constraintTop_toTopOf="@+id/guideline9" />
 <androidx.constraintlayout.widget.Guideline
 android:id="@+id/guideline16"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 app:layout_constraintGuide_percent="0.5" />
 <CheckBox
 android:id="@+id/check_tcp_client_add_renew"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="加入回显"
 app:layout_constraintEnd_toStartOf="@+id/guideline12"
 app:layout_constraintStart_toStartOf="@+id/guideline16"
 app:layout_constraintTop_toTopOf="@+id/guideline9" />
 <TextView
 android:id="@+id/textView4"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="本机IP"
 app:layout_constraintBottom_toTopOf="@+id/guideline11"
 app:layout_constraintEnd_toStartOf="@+id/guideline14"
 app:layout_constraintStart_toStartOf="@+id/guideline3"
 app:layout_constraintTop_toTopOf="@+id/guideline7" />
 <TextView
 android:id="@+id/txt_tcp_client_local_ip"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"