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

20212408 2023-2024-2《移动平台开发与实践》实验四实验报告

一、实验内容

1.1 知识回顾

网络编程技术

  • TCP/IP协议
    TCP/UDP用于端到端的链接、IP寻址和路由

    Socket(套接字)是计算机网络中实现通信的一种机制,可以实现不同计算机之间的进程间通信或同一计算机内部进程间的通信。Socket通常用于实现网络编程,使得不同计算机之间可以通过网络进行数据传输和通信。
    Socket通常包括两种类型:TCP(传输控制协议)Socket和UDP(用户数据报协议)Socket。TCP Socket提供了可靠的、面向连接的数据传输服务,确保数据按照顺序到达并且不丢失;而UDP Socket则提供了不可靠的、无连接的数据传输服务,适用于实时性要求较高的场景。

本地编程技术

  • 文件操作
    读写权限……
  • SharedPreferences
    存储辅助类,用来保存应用的常用配置

1.2 实验目的

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

二、实验过程

2.1 服务端的实现

(1) 创建一个新的Android项目,并在项目中添加必要的权限,如INTERNET权限。
在这里插入图片描述
(2) 编写服务端代码,创建一个ServerSocket监听指定端口。
在这里插入图片描述

handler = Handler(Looper.getMainLooper()):创建一个 Handler 对象,用于在主线程中处理消息。
messageList = findViewById(R.id.messageList):查找布局中 id 为 messageList 的
TextView 控件。 server = Server(2408, handler, messageList):创建 Server
实例,指定端口号、Handler 对象和 TextView 对象。
GlobalScope.launch(Dispatchers.IO):使用 Kotlin 协程启动一个新的后台线程。
server.start():在后台线程中启动服务器。

在这里插入图片描述

构造函数: 接收端口号、Handler 对象和TextView 对象,并将其保存到相应的属性中。 start 方法:
serverSocket = ServerSocket(port):创建一个 ServerSocket 对象,并绑定到指定端口上。
println(“Server started on port $port”):在控制台打印服务器启动消息。

while(true):无限循环,等待客户端连接。
val clientSocket =serverSocket.accept():接受客户端连接,返回一个 Socket 对象。 println(“Clientconnected from${clientSocket.inetAddress.hostAddress}”):在控制台打印客户端连接消息。
valclientHandler = ClientHandler(clientSocket, handler, messageList):创建一个ClientHandler 实例,用于处理客户端通信。
clients.add(clientHandler):将新的客户端处理器添加到列表中。
clientHandler.start():启动客户端处理器线程,开始处理客户端消息。

在这里插入图片描述

构造函数: 接收客户端的 Socket 对象、Handler 对象和 TextView 对象,并将其保存到相应的属性中。 run 方法:
reader 和 writer:分别用于从客户端读取数据和向客户端写入数据的 BufferedReader 和 PrintWriter对象。
while (true):无限循环,等待读取客户端发送的消息。
val message = reader.readLine() ?:break:从输入流中读取一行消息,如果为 null 则退出循环。 println(“Received message from client: $message”):在控制台打印接收到的消息。
Log.d(“Server”, “Received message from client: $ message”):在 Logcat 中记录接收到的消息。
handler.post {messageList.append(“$message\n”) }:通过 Handler 将接收到的消息追加TextView中,以更新 UI。
writer.println(“Server received: $message”):向客户端发送回复消息。

在 MainActivity 中创建服务器实例,并在后台线程中启动该服务器。
服务器通过 ServerSocket 在指定端口上监听客户端连接。
每当有新的客户端连接时,服务器创建一个 ClientHandler 实例来处理该客户端的通信。
ClientHandler 在单独的线程中运行,负责接收客户端消息并将其显示在 TextView 中,并向客户端发送回复消息。
这个应用程序的核心是服务器端的设计,它使用多线程处理客户端连接和通信,同时利用了 Handler 在主线程中更新 UI。

核心代码:

class Server(private val port: Int, private val handler: Handler, private val messageList: TextView) {

    private val clients = CopyOnWriteArrayList<ClientHandler>()
    private lateinit var serverSocket: ServerSocket

    fun start() {
        serverSocket = ServerSocket(port)
        println("Server started on port $port")
        while (true) {
            val clientSocket = serverSocket.accept()
            println("Client connected from ${clientSocket.inetAddress.hostAddress}")
            val clientHandler = ClientHandler(clientSocket, handler, messageList)
            clients.add(clientHandler)
            clientHandler.start()
        }
    }
    private inner class ClientHandler(private val socket: Socket,
                                      private val handler: Handler,
                                      private val messageList: TextView) : Thread() {
        private val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
        private val writer = PrintWriter(socket.getOutputStream(), true)
        override fun run() {
            try {
                while (true) {
                    val message = reader.readLine() ?: break
                    println("Received message from client: $message")
                    // 在 logcat 中打印客户端发送的信息内容
                    Log.d("Server", "Received message from client: $message")

                    // 在界面上显示接收到的消息
                    handler.post {
                        messageList.append("$message\n")
                    }
                    // 服务器端回复客户端消息
                    writer.println("Server received: $message")
                }
            } catch (e: IOException) {
                e.printStackTrace()
            } finally {
                try {
                    socket.close()
                } catch (e: IOException) {
                    println("Error occurred while closing client socket")
                    e.printStackTrace()
                }
            }
        }
    }

2.2 客户端实现

(1) 编写客户端代码,创建一个Socket对象连接服务端。
在这里插入图片描述

MainActivity 类 成员变量: clientThread: Thread:用于在后台运行连接到服务器的客户端线程。
handler: Handler:用于在主线程中处理消息,通常用于更新 UI。 inputText:EditText、sendButton: Button、disconnectButton: Button、textView:TextView:分别对应布局中的输入文本框、发送按钮、断开连接按钮和文本显示框。
client:Client:用于与服务器通信的客户端实例。 isRunning: Boolean:用于控制客户端监听循环是否运行。
onCreate方法: 设置布局为 activity_main.xml。 初始化 handler 和 UI 控件。 调用 connectToServer方法连接到服务器。 设置发送按钮的点击监听器,当按钮点击时发送输入框中的消息。设置断开连接按钮的点击监听器,当按钮点击时断开与服务器的连接。
connectToServer 方法: 创建一个新的线程,用于连接到服务器。在新线程中创建 Client 实例,并调用 startListening 方法开始监听从服务器接收的消息。

在这里插入图片描述

Client 类 构造函数: 接收服务器地址和端口号,并在初始化阶段连接到服务器。
connectToServer 方法: 创建一个新的Socket 实例,并连接到指定的服务器地址和端口号。 获取输出流和输入流,用于向服务器发送数据和接收数据。
sendData 方法:将数据转换为字节数组,并通过输出流发送给服务器。
receiveData 方法: 读取从服务器接收到的数据,并返回一个字符串。
closeConnection 方法: 关闭与服务器的连接。

在 MainActivity 中创建客户端实例,并在后台线程中连接到服务器。
可以通过发送按钮发送消息给服务器,消息将通过 sendData 方法发送。
客户端会持续监听服务器发送的消息,并将其显示在文本视图中。
断开连接按钮会关闭与服务器的连接,停止监听循环。

核心代码:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        handler = Handler(Looper.getMainLooper())

        inputText = findViewById(R.id.inputText)
        sendButton = findViewById(R.id.sendButton)
        disconnectButton = findViewById(R.id.disconnectButton)
        textView = findViewById(R.id.textView)

        connectToServer()

        sendButton.setOnClickListener {
            val message = inputText.text.toString()
            if (message.isNotEmpty()) {
                sendMessage(message)
            }
        }

        disconnectButton.setOnClickListener {
            disconnect()
        }
    }

    private fun connectToServer() {
        clientThread = Thread {
            client = Client("10.0.2.15", 2408)
            startListening()
        }
        clientThread.start()
    }

    private fun sendMessage(message: String) {
        Thread {
            client.sendData(message)
        }.start()
    }
    class Client(private val serverAddress: String, private val serverPort: Int) {

    private lateinit var socket: Socket
    private lateinit var outputStream: OutputStream
    private lateinit var reader: BufferedReader

    init {
        connectToServer()
    }

    private fun connectToServer() {
        socket = Socket()
        socket.connect(InetSocketAddress(serverAddress, serverPort), 10000)
        outputStream = socket.getOutputStream()
        reader = BufferedReader(InputStreamReader(socket.getInputStream()))
        println("Connected to server at $serverAddress:$serverPort")
    }

    fun sendData(data: String) {
        outputStream.write(data.toByteArray())
        outputStream.flush()
        println("Data sent to server: $data")
    }

    fun receiveData(): String {
        val buffer = CharArray(1024)
        val bytesRead = reader.read(buffer)
        return String(buffer, 0, bytesRead)
    }

    fun closeConnection() {
        try {
            socket.close()
        } catch (e: IOException) {
            println("Error occurred while closing connection")
            e.printStackTrace()
        }
    }

2.3 3. 运行和测试

(1) 启动服务端,在后台线程中调用acceptClients()方法监听客户端连接。

(2) 启动客户端,连接服务端,并使用sendData()方法发送数据。

初次尝试,实验结果和老师的要求并不一样,只能够实现客户端和服务器端的连接和消息发送,但是这是所发送的消息并不是用户可以自己输入的,而是在代码运行前直接写入到client.data中的,虽然能够实现连通和消息互传,但是并不能自定义消息内容,于是对此进行进一步的升级。

Server_test _1

在消息发送这个地方由于之前学习网编的时候,也有一些类似的地方可以借鉴的思想,于是就有了不同的想法,首先就是在建立连接时将消息打包发送过去,类似与只进行一次通信,将消息完整的输入并传输后,连接就会断开,开始的实验情况也确实是这样的,在发送一次信息后就没办法继续发送了,只能重启后服务端和客户端才能实现消息的再次交流。
还有一种想法就是在建立连接后生成用于信息交流的子进程,这个子进程是在主进程中循环产生的,就是对话结束结束子进程后又可以迅速的再次生成子进程,完成多次交流,这样就能够实现多次对话。

Server_test_2


但是这里仍是存在问题这个视频中可以看出来,发送20212408之后需要将客户端断开才能在服务端显示所发送的信息,这个地方是我自己的一个理解误区,就是在误认为是我并没有实现发送信息后将子进程断开,其实是因为机制是将回车作为发送消息结束的标志,因此需要在消息完成后加上一个回车,这样就能顺利实现连续的消息发送。

Server_test _3

三、问题与解决

问题一 Android studio的虚拟机的ip地址不知道如何查看,IP地址连不上

开始进行实验时,我的思路是使用两台虚拟机,即服务端独占一台虚拟机,客户端独占一台虚拟机,实现两台虚拟机的连通,但是我发现Android虚拟机的ip无法直接获取,于是上网查询,得到解决的方法,就是需要进入SDK下的platform-tools目录下使用adb的指令来查询。这样就可以获取ip地址。

问题二 解决android中java.lang.RuntimeException: Unable to start activity ComponentInfo问题

在运行代码时在出现闪退问题,logcat提示出java.lang.RuntimeException: Unable to start activity ComponentInfo问题,这个地方我在网上查找了很多的博客尝试去解决,但是我发现很多博客只是针对他们自己的问题,并不是一个普适的解决方法,但是虽然没有找到可以直接解决问题的途径,但是从博客中可以找到解决此类方法的思路,解决思路
主要过程就是仔细查看logcat,虽然里面会有很多乱七八糟的看不清楚,但是一般这种报错都会有cause by,通过仔细查看问题原因,就可以对症下药,解决问题。

问题三 Caused by: android.os.NetworkOnMainThreadException

出现另外的报错,提示网络配置问题,在主线程中的网络异常,这个问题我在网上查找解决方法,主要有两种解决的操作,一种是使用子进程,将请求网络资源的代码使用Thread去操作。在Runnable中做HTTP请求,不用阻塞UI线程。另外一种则是比较暴力的破除,使用StrictMode进行修改。
使用StrictMode修改需要用到如下代码:

if (android.os.Build.VERSION.SDK_INT > 9) {
    StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
    StrictMode.setThreadPolicy(policy);
}

但是这种方法比较危险,并不建议。
第二种方法就比较安全。在 Thread 中创建了一个 HTTP 请求,并在 Runnable 中处理响应。

四、心得体会

通过这次实验,我深入了解了在Android平台上进行Socket编程的基本概念和实现方法。在实验中,我学会了如何在Android Studio中使用Kotlin语言编写Socket服务端和客户端的代码,并成功实现了基本的TCP通信功能。在实验过程中,我遇到了一些问题,比如在服务端创建ServerSocket时需要获取INTERNET权限,而在客户端连接服务端时需要在AndroidManifest.xml中声明该权限。另外,我还学习了如何在后台线程中监听客户端连接,并实现了客户端发送数据的功能。
通过这次实验,我对TCP/UDP协议在Socket通信中的应用有了更深入的理解。我意识到TCP协议提供了可靠的数据传输,适用于需要保证数据完整性的场景,而UDP协议则提供了更高的传输速度,适用于实时性要求较高的场景。在实际应用中,我们需要根据具体的需求选择合适的协议。
总的来说,这次实验让我收获颇丰,不仅加深了对Android开发的理解,还提升了我的编程能力和解决问题的能力。我相信在未来的学习和工作中,这些经验将会对我有很大的帮助。

五、参考资料

安卓虚拟机网络配置
https://blog.csdn.net/weixin_44524687/article/details/123897937
查看安卓虚拟机的ip
https://blog.51cto.com/u_16213438/7129790
报错的解决思路
https://blog.csdn.net/m0_57399102/article/details/125732546
解决主线程网络异常
https://blog.csdn.net/qq_36698956/article/details/87958615

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值