20212324 2023-2024-2 《移动平台开发与实践》第4次作业
1.实验内容
(1)掌握在Android Studio上实现Socket编程的原理、过程。
- Socket 是一种通信机制,用于在网络上的两个节点之间进行通信。
- 在 Android 平台上,Socket 编程是一种常见的方式,用于实现网络通信,包括客户端和服务端之间的通信。
- TCP 是一种面向连接的、可靠的、基于字节流的协议,适用于对数据传输的完整性要求较高的场景。
- UDP 是一种无连接的、不可靠的、基于数据报的协议,适用于对数据传输速度要求较高、允许丢失部分数据的场景。
(2)掌握使用Kotlin语言实现Android Socket服务端和客户端。
- 使用 Kotlin 实现 Socket通信,包括创建 Socket 对象、建立连接、发送和接收数据等操作。
- 在 Kotlin 中使用 Socket 类来创建 TCP 套接字对象,Socket 类提供了连接到远程服务器、发送数据、接收数据等方法,是进行网络通信的基础类。
(3)进一步掌握Android Studio自带的模拟器使用和adb工具的使用。
2.实验过程
(1)代码编写过程
-
本次实验实现两台虚拟机互相连接,实时进行类似微信式通信聊天。这需要程序同时用到server类和client类相关函数完成收发。
-
设计UI界面,界面需要有启动自己的监听端口和发送给对应ip对应端口信息两部分,还需要完成数据的回显。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <EditText android:id="@+id/server_port" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="端口" /> <Button android:id="@+id/start_server_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="启动本机监听端口" /> <EditText android:id="@+id/client_address" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="对方ip" /> <EditText android:id="@+id/client_port" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="登入节点" /> <EditText android:id="@+id/message" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="说点什么" /> <Button android:id="@+id/send_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="发送!" /> <TextView android:id="@+id/status" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="bottom" /> </LinearLayout>
-
根据老师提供的Server端和Client端编写MainActivity,实现的核心思路是双方能够进行即时通信,实际上服务端和客户端使用的代码相同,如下:
package com.example.ssocket import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import android.widget.Button import android.widget.EditText import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import java.io.IOException interface MessageCallback { fun onMessageReceived(message: String) } class MainActivity : AppCompatActivity(), MessageCallback { private lateinit var serverPortEditText: EditText private lateinit var clientAddressEditText: EditText private lateinit var clientPortEditText: EditText private lateinit var messageEditText: EditText private lateinit var startServerButton: Button private lateinit var sendButton: Button private lateinit var statusTextView: TextView private var serverThread: Thread? = null private var isServerRunning = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initializeUI() setListeners() } private fun initializeUI() { serverPortEditText = findViewById(R.id.server_port) clientAddressEditText = findViewById(R.id.client_address) clientPortEditText = findViewById(R.id.client_port) messageEditText = findViewById(R.id.message) startServerButton = findViewById(R.id.start_server_button) sendButton = findViewById(R.id.send_button) statusTextView = findViewById(R.id.status) } private fun setListeners() { sendButton.setOnClickListener { sendClientMessage() } startServerButton.setOnClickListener { startServer() } } private fun startServer() { val portStr = serverPortEditText.text.toString().trim() val port = portStr.toIntOrNull() if (port != null && !isServerRunning) { isServerRunning = true serverThread = Thread { try { val server = Server(port, this) statusTextView.append("Server started on port $port\n") server.acceptClients() } catch (e: IOException) { runOnUiThread { statusTextView.append("Error starting server: ${e.message}\n") } } } serverThread?.start() } else { runOnUiThread { statusTextView.append("Please enter a valid port or stop the current server first.\n") } } } private fun sendClientMessage() { val address = clientAddressEditText.text.toString() val portStr = clientPortEditText.text.toString().trim() val port = portStr.toIntOrNull() val message = messageEditText.text.toString() if (address.isNotEmpty() && port != null && message.isNotEmpty()) { Thread { try { val client = Client(address, port) runOnUiThread { statusTextView.append("Message sent to server: $message\n") } val response = client.sendAndReceive(message) { Log.d("MySocket", "Received response: $it") runOnUiThread { statusTextView.append("Received response: $it\n") } } } catch (e: IOException) { Log.e("MySocket", "Error occurred while sending/receiving data", e) runOnUiThread { statusTextView.append("Error occurred: ${e.message}\n") } } }.start() } else { runOnUiThread { statusTextView.append("Please enter a valid server address, port, and message.\n") } } } override fun onDestroy() { super.onDestroy() isServerRunning = false serverThread?.interrupt() } override fun onMessageReceived(message: String) { runOnUiThread { statusTextView.append("Message received from client: $message\n") } } }
-
Client类和Server类为老师提供的代码,但为了完成server接收到数据后的回显,需要进一步修改server端代码
-
要实现在server端接收到消息后将消息传送给 MainActivity 并在 statusTextView 中显示,可以使用回调函数(callback)。这样,当server端接收到消息时,它就可以调用 MainActivity 中的回调函数,从而在 UI 中更新消息。
-
这样,就需要在MainActivity中定义回调函数:
interface MessageCallback { fun onMessageReceived(message: String) }
-
之后修改MainActivity和Server,完成相关逻辑(MainActivity代码如上)
-
server:
package com.example.ssocket import android.util.Log import java.io.IOException import java.net.ServerSocket import java.net.Socket class Server(private val port: Int, private val messageCallback: MessageCallback) { fun acceptClients() { Thread { try { val serverSocket = ServerSocket(port) Log.d("MySocket", "Server started on port $port") while (true) { val clientSocket = serverSocket.accept() Log.d("MySocket", "Client connected from ${clientSocket.inetAddress.hostAddress}") handleClient(clientSocket) } } catch (e: IOException) { Log.e("MySocket", "Error occurred while starting server", e) } }.start() } private fun handleClient(clientSocket: Socket) { try { val inputStream = clientSocket.getInputStream() val outputStream = clientSocket.getOutputStream() // 读取客户端发送的消息 val buffer = ByteArray(1024) val bytesRead = inputStream.read(buffer) val messageFromClient = String(buffer, 0, bytesRead) Log.d("MySocket", "Data received from client: $messageFromClient") // 调用 MainActivity 的回调函数,传递消息 messageCallback.onMessageReceived(messageFromClient) // 发送 "get it" 响应回客户端 outputStream.write("get it".toByteArray()) outputStream.flush() } catch (e: IOException) { Log.e("MySocket", "Error occurred while handling client", e) } finally { try { clientSocket.close() } catch (e: IOException) { Log.e("MySocket", "Error occurred while closing client socket", e) } } } }
- client:
package com.example.ssocket import android.util.Log import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStreamWriter import java.net.Socket class Client(private val serverAddress: String, private val serverPort: Int) { fun sendAndReceive(message: String, onReceive: (String) -> Unit): String { return try { val socket = Socket(serverAddress, serverPort) val out = OutputStreamWriter(socket.getOutputStream(), "UTF-8") val inBuffer = BufferedReader(InputStreamReader(socket.getInputStream(), "UTF-8")) out.write(message) out.flush() val response = inBuffer.readLine() onReceive(response) response } catch (e: Exception) { Log.e("MySocket", "Error occurred while sending/receiving data", e) "" } } }
-
-
在Manifest文件中添加权限
<uses-permission android:name="android.permission.INTERNET" />
(2)效果演示
-
本程序能够让两个开启端口监听的虚拟机(主机)在已知对方ip和端口的情况下像微信一样互相“聊天”。在聊天框中,用户能够知道自己已经发送的信息和接收到的信息,并可知道端口开启等其他信息
-
演示效果如下:
2024-05-06 21-04-45
3.学习中遇到的问题及解决
-
问题1:两台虚拟机使用的ip地址相同无法通信
-
问题1解决方案:使用adb查看虚拟机设备,并修改端口映射,从而以物理机(10.0.2.2)为中介实现10.0.2.2上端口和虚拟机端口的映射,通过这样的单向桥梁实现两台虚拟机的互联
-
问题2:在编写时一开始实现了接收到的信息在logcat上面的显示,但无法直接在程序聊天框中显示
-
问题2解决方案:chat了一下以搜索找到了解决方法(见代码编写过程),但程序无法正确运行。之后通过问chat和设置断点找错误的方式找到问题的原因是chat把前面创建对象的代码弄错了,成功纠错!
4.学习感悟和思考
-
理论与实践的结合:虽然之前学习过 Socket 编程的理论并编写过c语言代码,但亲自动手实践并在 Android Studio 上实现更高级的socket,让我对网络通信的原理和过程有了更加深刻的理解。理论是基础,而实践则是检验理论的最好方式。
-
问题解决能力的提升:在实验过程中,我遇到了关于虚拟机 IP 地址和端口映射的问题。通过查阅资料、调试代码、使用生成式AI和使用 adb 工具,我不仅解决了问题,还学会了如何使用这些工具来诊断和修复问题。
-
用户体验的关注:设计 UI 界面时,需要站在使用者角度,更加关注用户体验。一开始我的程序不是这样的,但我感觉那样十分反人类,于是推倒重建形成了这个版本