20212409 2023-2024-2 《移动平台开发与实践》第4次作业

一、实验目的

实验目标

掌握Android平台上Socket编程的基本概念。
学习使用Kotlin语言实现Android Socket服务端和客户端。
理解TCP/UDP协议在Socket通信中的应用。

实验环境

Android Studio
Kotlin编程语言
模拟器或真实Android设备

本周学习内容

  本周我主要学习了webView控件及其用法,学习了如何使用HTTP访问网络,例如使用HttpURLConnection、OkHttp,同时学习了JSON数据格式等基础知识,并使用JSONObject或GSON解析JSON格式数据。我也了解了Retrofit网络库的简介以及基本用法。
在课堂上,我动手实践了在Android平台上进行Socket编程,完成客户端与服务端成功连接,实现消息传输,只是课堂上我使用的是JAVA语言进行编程。我也做了关于使用ViewVideo类实现视频播放的实践,也对其语法与基础知识有了基本了解。

实验知识点

1.Android平台上Socket编程的基本概念

  Socket是一个用于描述IP地址和端口的通信连接句柄。通过这个双向的通信连接,网络上的两个程序可以实现数据的交换。

  在Android中进行Socket编程时,一般需要使用Socket和ServerSocket类来创建客户端和服务器端,从而实现双向通信。一个基本的Socket编程步骤包括:
  创建一个Socket对象,指定服务器的IP地址和端口号;
  获取Socket的输入流和输出流,用于读取和发送数据;
  通过输入流和输出流进行数据的读取和发送;
  最后关闭Socket连接。

  在这个过程中,Server端会监听某个端口是否有连接请求,而Client端则会向Server端发出连接请求。一旦Server端接受请求,一个连接就建立起来了。然后,通过Socket的输入流和输出流,双方就可以进行数据的读取和发送了。

  总的来说,Socket编程为Android平台上的应用程序提供了在网络上进行数据传输的能力,是实现网络通信的重要工具。

2.TCP/UDP协议在Socket通信中的应用
  Socket主要有两种类型,流式套接字(TCP)和数据报套接字(UDP)。
  TCP(传输控制协议)是一种面向连接的协议,它提供可靠的数据传输服务。在Socket通信中,当使用TCP协议时,双方会首先建立连接,然后通过这个连接进行数据的发送和接收。TCP确保数据无差错、不丢失、不重复,并且按序到达。此外,TCP还提供流量控制和拥塞控制,以防止网络过载。因此,对于需要高可靠性的应用,如文件传输、网页浏览等,TCP是首选的协议。

  相比之下,UDP(用户数据报协议)是一种面向无连接的协议,它不提供可靠的数据传输服务。在Socket通信中,使用UDP时,数据报直接从一端发送到另一端,无需建立连接。UDP不保证数据的可靠性、顺序性或是否丢失。因此,UDP适用于对实时性要求较高,但对数据可靠性要求不高的应用,如视频流、实时游戏等。

  在Socket编程中,开发者可以根据应用的需求选择合适的协议。如果需要确保数据的完整性和顺序性,那么应该选择TCP;如果更关心实时性和传输效率,那么UDP可能更合适。无论选择哪种协议,Socket编程都提供了相应的API和工具,使得开发者能够方便地在网络上进行数据的传输和交换。

  综上所述,TCP和UDP协议在Socket通信中各有其特点和适用场景。选择哪种协议取决于应用的具体需求。

二、实验步骤

先看看效果图:

 

1.设计布局
activity_select_type.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".ui.SelectTypeActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/purple_500"
        app:title="选择类型"
        app:titleTextColor="@color/white" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <Button
            android:id="@+id/btn_server"
            android:layout_width="240dp"
            android:layout_height="120dp"
            android:layout_marginBottom="20dp"
            android:text="服务端"
            android:textSize="18sp" />

        <Button
            android:id="@+id/btn_client"
            android:layout_width="240dp"
            android:layout_height="120dp"
            android:layout_marginTop="20dp"
            android:text="客户端"
            android:textSize="18sp" />
    </LinearLayout>
</LinearLayout>

activity_base_socket.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    android:background="@color/bg_color"
    tools:context=".ui.BaseSocketActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="50dp"
        android:orientation="vertical">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@color/purple_500"
            app:navigationIcon="@drawable/ic_back_black"
            app:navigationIconTint="@color/white"
            app:subtitleTextColor="@color/white"
            app:title="通用页面"
            app:titleTextColor="@color/white">

            <TextView
                android:id="@+id/tv_func"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end"
                android:padding="16dp"
                android:text="功能按钮"
                android:textColor="@color/white"
                android:textSize="14sp" />
        </com.google.android.material.appbar.MaterialToolbar>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_msg"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </LinearLayout>

    <include
        android:id="@+id/lay_bottom_sheet_edit"
        layout="@layout/bottom_sheet_edit" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

item_rv_msg.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="8dp"
    android:paddingBottom="8dp"
    android:orientation="vertical">

    <RelativeLayout
        android:id="@+id/lay_other"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/iv_other"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:layout_marginStart="16dp"
            android:src="@drawable/icon_server"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:shapeAppearanceOverlay="@style/circleImageStyle" />

        <TextView
            android:id="@+id/tv_other_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:background="@drawable/shape_left_msg_bg"
            android:text="123"
            android:textColor="@color/black"
            android:layout_toEndOf="@id/iv_other" />
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/lay_myself"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_myself_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:layout_toStartOf="@id/iv_myself"
            android:background="@drawable/shape_right_msg_bg"
            android:text="123"
            android:textColor="@color/white"
            app:layout_constraintEnd_toStartOf="@+id/iv_myself"
            app:layout_constraintTop_toTopOf="@+id/iv_myself" />

        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/iv_myself"
            android:layout_width="60dp"
            android:layout_alignParentEnd="true"
            android:layout_marginEnd="16dp"
            android:layout_height="60dp"
            android:src="@drawable/icon_client"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:shapeAppearanceOverlay="@style/circleImageStyle" />
    </RelativeLayout>
</LinearLayout>

2.后端逻辑实现

在com.llw.socket包下新建一个server包,我们服务端的代码就写在这个server包下。新建一个ServerCallback接口,代码如下:

interface ServerCallback {
    //接收客户端的消息
    fun receiveClientMsg(success: Boolean, msg: String)
    //其他消息
    fun otherMsg(msg: String)
}
BaseSocketActivity
package com.llw.socket.ui

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.llw.socket.R
import com.llw.socket.SocketApp
import com.llw.socket.adapter.EmojiAdapter
import com.llw.socket.adapter.MsgAdapter
import com.llw.socket.bean.Message
import com.llw.socket.client.ClientCallback
import com.llw.socket.client.SocketClient
import com.llw.socket.databinding.ActivityBaseSocketBinding
import com.llw.socket.server.ServerCallback
import com.llw.socket.server.SocketServer

open class BaseSocketActivity : BaseActivity(), ServerCallback, ClientCallback, EmojiCallback {

    lateinit var binding: ActivityBaseSocketBinding

    private val TAG = BaseSocketActivity::class.java.simpleName

    lateinit var etMsg: EditText
    lateinit var btnSendMsg: Button
    lateinit var ivMore: ImageView

    //Socket服务是否打开
    var openSocket = false

    //Socket服务是否连接
    var connectSocket = false

    //消息列表
    private val messages = ArrayList<Message>()

    //消息适配器
    private lateinit var msgAdapter: MsgAdapter

    //是否显示表情
    private var isShowEmoji = false

    private var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityBaseSocketBinding.inflate(layoutInflater)
        setContentView(binding.root)
        initView()
    }

    /**
     * 初始化视图
     */
    private fun initView() {
        etMsg = binding.layBottomSheetEdit.etMsg
        btnSendMsg = binding.layBottomSheetEdit.btnSendMsg
        ivMore = binding.layBottomSheetEdit.ivMore
        //初始化BottomSheet
        initBottomSheet()

        //输入监听
/*        etMsg.addTextChangedListener {
            if (it?.toString()?.isNotEmpty() == true) {
                btnSendMsg.visibility = View.VISIBLE
                ivMore.visibility = View.GONE
            } else {
                btnSendMsg.visibility = View.GONE
                ivMore.visibility = View.VISIBLE
            }
        }*/
        //初始化列表
        msgAdapter = MsgAdapter(messages)
        binding.rvMsg.apply {
            layoutManager = LinearLayoutManager(this@BaseSocketActivity)
            adapter = msgAdapter
        }
    }

    /**
     * 设置服务端页面标题
     */
    fun setServerTitle(startService: View.OnClickListener) =
        setTitle("服务端", "IP:${getIp()}", "开启服务", startService)

    /**
     * 设置客户端页面标题
     */
    fun setClientTitle(connectService: View.OnClickListener) =
        setTitle(mTitle = "客户端", funcTitle = "连接服务", click = connectService)

    /**
     * 设置标题
     */
    private fun setTitle(
        mTitle: String, mSubtitle: String = "", funcTitle: String,
        click: View.OnClickListener
    ) {
        binding.toolbar.apply {
            title = mTitle
            subtitle = mSubtitle
            setNavigationOnClickListener { onBackPressed() }
        }
        binding.tvFunc.text = funcTitle
        binding.tvFunc.setOnClickListener(click)
    }

    /**
     * 开启服务
     */
    fun startServer() {
        openSocket = true
        SocketServer.startServer(this)
        showMsg("开启服务")
        binding.tvFunc.text = "关闭服务"
    }

    /**
     * 停止服务
     */
    fun stopServer() {
        openSocket = false
        SocketServer.stopServer()
        showMsg("关闭服务")
        binding.tvFunc.text = "开启服务"
    }

    /**
     * 连接服务
     */
    fun connectServer(ipAddress: String) {
        connectSocket = true
        SocketClient.connectServer(ipAddress, this)
        showMsg("连接服务")
        binding.tvFunc.text = "关闭连接"
    }

    /**
     * 关闭连接
     */
    fun closeConnect() {
        connectSocket = false
        SocketClient.closeConnect()
        showMsg("关闭连接")
        binding.tvFunc.text = "连接服务"
    }

    /**
     * 发送到客户端
     */
    fun sendToClient(msg: String) {
        SocketServer.sendToClient(msg)
        etMsg.setText("")
        updateList(true, msg)
    }

    /**
     * 发送到服务端
     */
    fun sendToServer(msg: String) {
        SocketClient.sendToServer(msg)
        etMsg.setText("")
        updateList(true, msg)
    }

    /**
     * 初始化BottomSheet
     */
    private fun initBottomSheet() {
        //Emoji布局
        bottomSheetBehavior =
            BottomSheetBehavior.from(binding.layBottomSheetEdit.bottomSheet).apply {
                state = BottomSheetBehavior.STATE_HIDDEN
                isHideable = false
                isDraggable = false
            }
        //表情列表适配器
        binding.layBottomSheetEdit.rvEmoji.apply {
            layoutManager = GridLayoutManager(context, 6)
            adapter = EmojiAdapter(SocketApp.instance().emojiList).apply {
                setOnItemClickListener(object : EmojiAdapter.OnClickListener {
                    override fun onItemClick(position: Int) {
                        val charSequence = SocketApp.instance().emojiList[position]
                        checkedEmoji(charSequence)
                    }
                })
            }
        }
        //显示emoji
        binding.layBottomSheetEdit.ivEmoji.setOnClickListener {
            bottomSheetBehavior!!.state =
                if (isShowEmoji) BottomSheetBehavior.STATE_COLLAPSED else BottomSheetBehavior.STATE_EXPANDED
        }
        //BottomSheet显示隐藏的相关处理
        bottomSheetBehavior!!.addBottomSheetCallback(object :
            BottomSheetBehavior.BottomSheetCallback() {
            override fun onStateChanged(bottomSheet: View, newState: Int) {
                isShowEmoji = when (newState) {
                    BottomSheetBehavior.STATE_EXPANDED -> {//显示
                        binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                            ContextCompat.getDrawable(
                                this@BaseSocketActivity,
                                R.drawable.ic_emoji_checked
                            )
                        )
                        true
                    }
                    BottomSheetBehavior.STATE_COLLAPSED -> {//隐藏
                        binding.layBottomSheetEdit.ivEmoji.setImageDrawable(
                            ContextCompat.getDrawable(this@BaseSocketActivity, R.drawable.ic_emoji)
                        )
                        false
                    }
                    else -> false
                }
            }

            override fun onSlide(bottomSheet: View, slideOffset: Float) {}
        })
    }

    /**
     * 更新列表
     */
    private fun updateList(isMyself: Boolean, msg: String) {
        messages.add(Message(isMyself, msg))
        runOnUiThread {
            (if (messages.size == 0) 0 else messages.size - 1).apply {
                msgAdapter.notifyItemChanged(this)
                binding.rvMsg.smoothScrollToPosition(this)
            }
        }
    }

    /**
     * 接收客户端消息
     */
    override fun receiveClientMsg(ipAddress: String, msg: String) = updateList(false, msg)

    /**
     * 接收服务端消息
     */
    override fun receiveServerMsg(ipAddress: String, msg: String) = updateList(false, msg)

    /**
     * 其他消息
     */
    override fun otherMsg(msg: String) {
        Log.d(TAG, "otherMsg: $msg")
    }

    /**
     * 选择表情
     */
    override fun checkedEmoji(charSequence: CharSequence) {
        etMsg.apply {
            setText(text.toString() + charSequence)
            setSelection(text.toString().length)//光标置于最后
        }
    }

}

SocketApp
package com.llw.socket

import android.annotation.SuppressLint
import android.app.Application
import android.util.Log
import androidx.annotation.Nullable
import androidx.emoji2.bundled.BundledEmojiCompatConfig
import androidx.emoji2.text.EmojiCompat
import androidx.emoji2.text.EmojiCompat.InitCallback
import java.io.BufferedReader
import java.io.InputStreamReader
import kotlin.properties.Delegates

class SocketApp : Application() {

    private val TAG = SocketApp::class.java.simpleName

    val emojiList = arrayListOf<CharSequence>()

    companion object {
        private var instance: SocketApp by Delegates.notNull()
        fun instance() = instance
    }

    @SuppressLint("RestrictedApi")
    override fun onCreate() {
        super.onCreate()
        instance = this

        initEmoji2()
    }

    /**
     * 初始化Emoji2
     */
    private fun initEmoji2() = EmojiCompat.init(BundledEmojiCompatConfig(this).apply {
        setReplaceAll(true)
        registerInitCallback(object : InitCallback() {
            override fun onInitialized() {
                //初始化成功回调
                Log.d(TAG, "onInitialized")
                //加载表情列表
                loadEmoji()
            }

            override fun onFailed(@Nullable throwable: Throwable?) {
                //初始化失败回调
                Log.e(TAG, throwable.toString())
            }
        })
    })

    /**
     * 加载表情列表
     */
    private fun loadEmoji() {
        val inputStream = assets.open("emoji.txt")
        BufferedReader(InputStreamReader(inputStream)).use {
            var line: String
            while (true) {
                line = it.readLine() ?: break
                emojiList.add(line)
            }
        }
    }
}

下面就是主要的服务端代码了,SocketClient.java

package com.llw.socket.client

import android.os.Handler
import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

/**
 * Socket客户端
 */
object SocketClient {

    private val TAG = SocketClient::class.java.simpleName

    private var socket: Socket? = null

    private var outputStream: OutputStream? = null

    private var inputStreamReader: InputStreamReader? = null

    private lateinit var mCallback: ClientCallback

    private const val SOCKET_PORT = 9527

    // 客户端线程池
    private var clientThreadPool: ExecutorService? = null

    //心跳发送间隔
    private const val HEART_SPACETIME = 3 * 1000

    private val mHandler: Handler = Handler()

    /**
     * 连接服务
     */
    fun connectServer(ipAddress: String, callback: ClientCallback) {
        mCallback = callback
        Thread {
            try {
                socket = Socket(ipAddress, SOCKET_PORT)
                //开启心跳,每隔3秒钟发送一次心跳
                mHandler.post(mHeartRunnable)
                ClientThread(socket!!, mCallback).start()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }.start()
    }

    /**
     * 关闭连接
     */
    fun closeConnect() {
        inputStreamReader?.close()
        outputStream?.close()
        socket?.close()
        //关闭线程池
        clientThreadPool?.shutdownNow()
        clientThreadPool = null
    }

    /**
     * 发送数据至服务器
     * @param msg 要发送至服务器的字符串
     */
    fun sendToServer(msg: String) {
        if (clientThreadPool == null) {
            clientThreadPool = Executors.newSingleThreadExecutor()
        }
        clientThreadPool?.execute {
            if (socket == null) {
                mCallback.otherMsg("客户端还未连接")
                return@execute
            }
            if (socket!!.isClosed) {
                mCallback.otherMsg("Socket已关闭")
                return@execute
            }
            outputStream = socket?.getOutputStream()
            try {
                outputStream?.write(msg.toByteArray())
                outputStream?.flush()
            } catch (e: IOException) {
                e.printStackTrace()
                mCallback.otherMsg("向服务端发送消息: $msg 失败")
            }
        }
    }

    private val mHeartRunnable = Runnable { sendHeartbeat() }

    /**
     * 发送心跳消息
     */
    private fun sendHeartbeat() {
        if (clientThreadPool == null) {
            clientThreadPool = Executors.newSingleThreadExecutor()
        }
        val msg = "洞幺洞幺,呼叫洞拐,听到请回答,听到请回答,Over!"
        clientThreadPool?.execute {
            if (socket == null) {
                mCallback.otherMsg("客户端还未连接")
                return@execute
            }
            if (socket!!.isClosed) {
                mCallback.otherMsg("Socket已关闭")
                return@execute
            }
            outputStream = socket?.getOutputStream()
            try {
                outputStream?.write(msg.toByteArray())
                outputStream?.flush()
                //发送成功以后,重新建立一个心跳消息
                mHandler.postDelayed(mHeartRunnable, HEART_SPACETIME.toLong())
                Log.i(TAG, msg)
            } catch (e: IOException) {
                e.printStackTrace()
                mCallback.otherMsg("向服务端发送消息: $msg 失败")
            }
        }
    }

    class ClientThread(private val socket: Socket, private val callback: ClientCallback) :
        Thread() {
        override fun run() {
            val inputStream: InputStream?
            try {
                inputStream = socket.getInputStream()
                val buffer = ByteArray(1024)
                var len: Int
                var receiveStr = ""
                if (inputStream.available() == 0) {
                    Log.e(TAG, "inputStream.available() == 0")
                }
                while (inputStream.read(buffer).also { len = it } != -1) {
                    receiveStr += String(buffer, 0, len, Charsets.UTF_8)
                    if (len < 1024) {
                        socket.inetAddress.hostAddress?.let {
                            if (receiveStr == "洞拐收到,洞拐收到,Over!") {//收到来自服务端的心跳回复消息
                                Log.i(TAG, "洞拐收到,洞拐收到,Over!")
                                //准备回复
                            } else {
                                callback.receiveServerMsg(it, receiveStr)
                            }
                        }
                        receiveStr = ""
                    }
                }
            } catch (e: IOException) {
                e.printStackTrace()
                when (e) {
                    is SocketTimeoutException -> {
                        Log.e(TAG, "连接超时,正在重连")
                    }
                    is NoRouteToHostException -> {
                        Log.e(TAG, "该地址不存在,请检查")
                    }
                    is ConnectException -> {
                        Log.e(TAG, "连接异常或被拒绝,请检查")
                    }
                    is SocketException -> {
                        when (e.message) {
                            "Already connected" -> Log.e(TAG, "连接异常或被拒绝,请检查")
                            "Socket closed" -> Log.e(TAG, "连接已关闭")
                        }
                    }
                }
            }
        }
    }

}
代码从上往下看,首先是初始化一些变量,然后就是startServer()函数,在这里进行回调接口的初始化然后开一个子线程进行ServerSocket的构建,构建成功之后会监听连接,得到一个socket,这个socket就是客户端,这里将连接客户端的地址显示出来。然后再开启一个子线程去处理客户端发送过来的消息。这个地方服务端和客户端差不多,下面看ServerThread中的代码。Socket通讯,发送和接收对应的是输入流和输入流,通过socket.getInputStream()得到输入流,获取字节数据然后转成String,通过接口回调,最后重置变量。关闭服务就没好说的,代码一目了然。最后就是发送到客户端的sendToClient()函数。接收发送字符串,开启子线程,获取输出流,写入字节数据然后刷新,最后回调到页面。
SocketServer.java
package com.llw.socket.server

import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

/**
 * Socket服务端
 */
object SocketServer {

    private val TAG = SocketServer::class.java.simpleName

    private const val SOCKET_PORT = 9527

    private var socket: Socket? = null
    private var serverSocket: ServerSocket? = null

    private lateinit var mCallback: ServerCallback

    private lateinit var outputStream: OutputStream

    var result = true

    // 服务端线程池
    private var serverThreadPool: ExecutorService? = null

    /**
     * 开启服务
     */
    fun startServer(callback: ServerCallback): Boolean {
        mCallback = callback
        Thread {
            try {
                serverSocket = ServerSocket(SOCKET_PORT)
                while (result) {
                    socket = serverSocket?.accept()
                    mCallback.otherMsg("${socket?.inetAddress} to connected")
                    ServerThread(socket!!, mCallback).start()
                }
            } catch (e: IOException) {
                e.printStackTrace()
                result = false
            }
        }.start()
        return result
    }

    /**
     * 关闭服务
     */
    fun stopServer() {
        socket?.apply {
            shutdownInput()
            shutdownOutput()
            close()
        }
        serverSocket?.close()

        //关闭线程池
        serverThreadPool?.shutdownNow()
        serverThreadPool = null
    }

    /**
     * 发送到客户端
     */
    fun sendToClient(msg: String) {
        if (serverThreadPool == null) {
            serverThreadPool = Executors.newCachedThreadPool()
        }
        serverThreadPool?.execute {
            if (socket == null) {
                mCallback.otherMsg("客户端还未连接")
                return@execute
            }
            if (socket!!.isClosed) {
                mCallback.otherMsg("Socket已关闭")
                return@execute
            }
            outputStream = socket!!.getOutputStream()
            try {

                outputStream.write(msg.toByteArray())
                outputStream.flush()
            } catch (e: IOException) {
                e.printStackTrace()
                mCallback.otherMsg("向客户端发送消息: $msg 失败")
            }
        }
    }

    /**
     * 回复心跳消息
     */
    fun replyHeartbeat() {
        if (serverThreadPool == null) {
            serverThreadPool = Executors.newCachedThreadPool()
        }
        val msg = "洞拐收到,洞拐收到,Over!"
        serverThreadPool?.execute {
            if (socket == null) {
                mCallback.otherMsg("客户端还未连接")
                return@execute
            }
            if (socket!!.isClosed) {
                mCallback.otherMsg("Socket已关闭")
                return@execute
            }
            outputStream = socket!!.getOutputStream()
            try {
                outputStream.write(msg.toByteArray())
                outputStream.flush()
            } catch (e: IOException) {
                e.printStackTrace()
                mCallback.otherMsg("向客户端发送消息: $msg 失败")
            }
        }
    }

    class ServerThread(private val socket: Socket, private val callback: ServerCallback) :
        Thread() {

        override fun run() {
            val inputStream: InputStream?
            try {
                inputStream = socket.getInputStream()
                val buffer = ByteArray(1024)
                var len: Int
                var receiveStr = ""
                if (inputStream.available() == 0) {
                    Log.e(TAG, "inputStream.available() == 0")
                }
                while (inputStream.read(buffer).also { len = it } != -1) {
                    receiveStr += String(buffer, 0, len, Charsets.UTF_8)
                    if (len < 1024) {
                        socket.inetAddress.hostAddress?.let {
                            if (receiveStr == "洞幺洞幺,呼叫洞拐,听到请回答,听到请回答,Over!") {//收到客户端发送的心跳消息
                                //准备回复
                                replyHeartbeat()
                            } else {
                                callback.receiveClientMsg(it, receiveStr)
                            }
                        }
                        receiveStr = ""
                    }
                }
            } catch (e: IOException) {
                e.printStackTrace()
                when (e) {
                    is SocketTimeoutException -> {
                        Log.e(TAG, "连接超时,正在重连")
                    }
                    is NoRouteToHostException -> {
                        Log.e(TAG, "该地址不存在,请检查")
                    }
                    is ConnectException -> {
                        Log.e(TAG, "连接异常或被拒绝,请检查")
                    }
                    is SocketException -> {
                        when (e.message) {
                            "Already connected" -> Log.e(TAG, "连接异常或被拒绝,请检查")
                            "Socket closed" -> Log.e(TAG, "连接已关闭")
                        }
                    }
                }
            }
        }
    }

}

客户端的代码和服务端其实很相似,这里我就简单说明一下,首先就是连接服务,需要输入服务端的ip地址,端口号则是写死的一个端口号,也可以动态去设置。其他的地方和服务端相似。

ClientPlusActivity.java
package com.llw.socket.ui

import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog
import com.llw.socket.R
import com.llw.socket.client.SocketClient
import com.llw.socket.databinding.DialogEditIpBinding

/**
 * 客户端Plus页面
 */
class ClientPlusActivity: BaseSocketActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //连接服务/关闭服务
        setClientTitle { if (connectSocket) closeConnect() else showEditDialog() }
        //发送消息给服务端
        btnSendMsg.setOnClickListener {
            val msg = etMsg.text.toString().trim()
            if (msg.isEmpty()) {
                showMsg("请输入要发送的信息");return@setOnClickListener
            }
            //检查是否能发送消息
            val isSend = if (connectSocket) connectSocket else false
            if (!isSend) {
                showMsg("当前未开启服务或连接服务");return@setOnClickListener
            }
            sendToServer(msg)
        }
    }

    private fun showEditDialog() {
        val dialogBinding =
            DialogEditIpBinding.inflate(LayoutInflater.from(this@ClientPlusActivity), null, false)
        AlertDialog.Builder(this@ClientPlusActivity).apply {
            setIcon(R.drawable.ic_connect)
            setTitle("连接Ip地址")
            setView(dialogBinding.root)
            setPositiveButton("确定") { dialog, _ ->
                val ip = dialogBinding.etIpAddress.text.toString()
                if (ip.isEmpty()) {
                    showMsg("请输入Ip地址");return@setPositiveButton
                }
                connectServer(ip)
                dialog.dismiss()
            }
            setNegativeButton("取消") { dialog, _ -> dialog.dismiss() }
        }.show()
    }
}
ServerPlusActivity
package com.llw.socket.ui

import android.os.Bundle

/**
 * 服务端Plus页面
 */
class ServerPlusActivity : BaseSocketActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //开启服务/停止服务
        setServerTitle { if (openSocket) stopServer() else startServer() }

        //发送消息给服务端
        btnSendMsg.setOnClickListener {
            val msg = etMsg.text.toString().trim()
            if (msg.isEmpty()) {
                showMsg("请输入要发送的信息");return@setOnClickListener
            }
            //检查是否能发送消息
            val isSend = if (openSocket) openSocket else false
            if (!isSend) {
                showMsg("当前未开启服务或连接服务");return@setOnClickListener
            }
            sendToClient(msg)
        }
    }
}
为了在聊天的时候把内容顶上去,在layout下新建一个bottom_sheet_edit.xml,里面的代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/bottom_sheet"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:background="@color/white"
    android:orientation="vertical"
    app:behavior_hideable="true"
    app:behavior_peekHeight="50dp"
    app:layout_behavior="@string/bottom_sheet_behavior">

    <!--底部显示的内容-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical"
        android:paddingStart="8dp"
        android:paddingEnd="8dp">

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_emoji"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:layout_marginEnd="8dp"
            android:src="@drawable/ic_emoji" />

        <androidx.appcompat.widget.AppCompatEditText
            android:id="@+id/et_msg"
            android:layout_width="0dp"
            android:layout_height="40dp"
            android:layout_weight="1"
            android:background="@drawable/shape_et_bg"
            android:gravity="center_vertical"
            android:hint="发送给客户端"
            android:padding="10dp"
            android:textSize="14sp" />

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_more"
            android:layout_width="36dp"
            android:layout_height="36dp"
            android:visibility="gone"
            android:layout_marginStart="8dp"
            android:src="@drawable/ic_more" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_send_msg"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_marginStart="8dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="发送"
            android:textSize="12sp" />
    </LinearLayout>

    <!--底部弹出的内容-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_emoji"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never" />

</LinearLayout>

表情包代码:EmojiAdapter

package com.llw.socket.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.emoji2.text.EmojiCompat
import androidx.recyclerview.widget.RecyclerView
import com.llw.socket.databinding.ItemEmojiBinding

/**
 * Emoji表情适配器
 */
class EmojiAdapter(private val emojis: ArrayList<CharSequence>) :
    RecyclerView.Adapter<EmojiAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(ItemEmojiBinding.inflate(LayoutInflater.from(parent.context), parent, false))


    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val emoji = emojis[position]
        holder.mView.tvEmoji.apply {
            text = EmojiCompat.get().process(emoji)
            setOnClickListener { clickListener?.onItemClick(position) }
        }
    }

    override fun getItemCount() = emojis.size

    class ViewHolder(itemView: ItemEmojiBinding) : RecyclerView.ViewHolder(itemView.root) {
        var mView: ItemEmojiBinding

        init {
            mView = itemView
        }
    }


    interface OnClickListener {
        fun onItemClick(position: Int)
    }

    private var clickListener: OnClickListener? = null

    fun setOnItemClickListener(listener: OnClickListener) {
        clickListener = listener
    }

}
MsgAdapter
package com.llw.socket.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.llw.socket.bean.Message
import com.llw.socket.databinding.ItemRvMsgBinding

/**
 * 消息适配器
 */
class MsgAdapter(private val messages: ArrayList<Message>) : RecyclerView.Adapter<MsgAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(ItemRvMsgBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val message = messages[position]
        if (message.isMyself) {
            holder.mView.tvMyselfMsg.text = message.msg
        } else {
            holder.mView.tvOtherMsg.text = message.msg
        }

        holder.mView.layOther.visibility = if (message.isMyself) View.GONE else View.VISIBLE
        holder.mView.layMyself.visibility = if (message.isMyself) View.VISIBLE else View.GONE
    }

    override fun getItemCount() = messages.size

    class ViewHolder(itemView: ItemRvMsgBinding) : RecyclerView.ViewHolder(itemView.root) {
        var mView: ItemRvMsgBinding

        init {
            mView = itemView
        }
    }
}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.llw.socket">

    <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:name=".SocketApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.SocketDemo"
        tools:targetApi="31">

        <activity android:name=".ui.ServerPlusActivity"
            android:exported="false" />
        <activity android:name=".ui.ClientPlusActivity"
            android:exported="false" />
        <activity
            android:name=".ui.BaseSocketActivity"
            android:exported="false"
            android:windowSoftInputMode="adjustResize"/>
        <activity
            android:name=".ui.SelectTypeActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

build.gradle

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.llw.socket"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.3"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }

    buildFeatures {
        viewBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.6.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    def emoji2_version = "1.2.0"
    implementation "androidx.emoji2:emoji2:$emoji2_version"
    implementation "androidx.emoji2:emoji2-views:$emoji2_version"
    implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
    implementation 'androidx.emoji2:emoji2-bundled:1.0.0-alpha03'
}

结果运行和调试

两个演示视频,如下:

20240507012005

20240507012653

三、学习中遇到的问题及解决

  • 问题1:Android Studio模拟器无法启动。
  • 问题1解决方案:发现是SDK位置被改变。修改系统环境变量,使虚拟机成功运行。

四、学习感悟、思考等

  这次移动平台Socket编程实验让我对Android平台的网络编程有了更深入的理解和实践。通过使用Kotlin语言编写Socket服务端和客户端,我不仅掌握了Socket编程的基本概念,还学会了如何在移动平台上实现网络通信。

  在实验开始之前,我对Socket编程的理解还停留在理论层面,对其在实际应用中的运作方式知之甚少。然而,随着实验的深入,我逐渐明白了Socket编程的重要性以及它在构建网络通信应用中的关键作用。通过创建Socket服务端和客户端,我学会了如何建立网络连接、发送和接收数据,以及如何处理网络异常和断开连接的情况。

  在编写Socket客户端代码的过程中,我也遇到了不少困难。例如,如何建立与其他主机的服务端的连接、如何发送和接收数据等问题都需要我仔细思考和解决。通过不断地尝试和调试,我逐渐找到了解决这些问题的方法,并成功实现了一个能够与服务端进行通信的客户端。

  此外,我对TCP/UDP协议在Socket通信中的应用也有了更深入的理解。通过对比TCP和UDP的特点和适用场景,我更加明白了选择合适协议的重要性。在实验中,我选择了TCP协议作为通信协议,因为它能够确保数据的可靠传输,并且提供了流量控制和拥塞控制机制,使得数据传输更加稳定和安全。

  通过这次实验,我不仅学会了如何使用Kotlin语言进行Socket编程,还提高了我的编程能力和解决问题的能力。在实验过程中,我不断遇到新的问题和挑战,但正是这些困难促使我不断思考和学习,最终取得了成功。这次移动平台Socket编程实验不仅让我掌握了Socket编程的基本概念和技术,还让我学会了如何在移动平台上实现网络通信。

五、参考资料

  • 12
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值