实验四 Android Socket编程技术
1. 实验目的
- 掌握Android平台上Socket编程的基本概念。
- 学习使用Kotlin语言实现Android Socket服务端和客户端。
- 理解TCP/UDP协议在Socket通信中的应用。
2.实验过程
2.1 Android socket涉及的知识点
socket编程可以通过多种的高级编程语言来实现,本次实验使用的是kotlin语言并且相对应的要加入一些UI控件来实现简单的消息通讯。如下是本次实验我使用到的知识点。
关于使用kotlin实现socket
- 创建和关闭 Socket:在 Kotlin 中,我们使用 Socket(InetAddress, port) 构造函数创建一个 Socket。close() 函数用于关闭一个 Socket。
val socket = Socket(InetAddress.getByName("192.168.1.1"), 5000)
socket.close()
- 输入输出流:Socket 通信通过输入输出流进行,对应的函数是
getInputStream()
和getOutputStream()
。Kotlin 支持 use 函数,可以自动关闭流。
socket.getOutputStream().use { it.write("Hello".toByteArray()) }
- 线程处理:所有的网络操作都需要在子线程中进行。Kotlin 的 Thread 类和 runOnUiThread 函数让线程处理变得简单。
Thread {
// socket operations
runOnUiThread {
// UI operations
}
}.start()
关于Android控件的使用
- 使用EditText让用户输入要发送的消息,用Button做为发送动作的触发,用TextView显示接收到的信息。
- Android规定网络操作不能在主线程进行,需要开启子线程进行网络请求。所以需要使用Handler或者其它的方式,把网络请求运行在子线程,然后回到主线程更新UI。
- 本次实验使用的是handler。Handler用于处理线程中的消息。当你在子线程中获取到数据后,可以通过Handler将数据发送到主线程,然后在主线程中更新UI。
receive_message.append("\n"+ response)。
2.2 实验过程
2.2.1代码部分
本次实验参考指导书给的Java代码和布局文件的模式进行了修改。
布局文件activity_main.xml
布局如下。服务端和客户端布局大致相同,不同的只是更换控件的id。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/startServer"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:text="Start Server" />
<Button
android:id="@+id/stopServer"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:text="Stop Server" />
</LinearLayout>
<TextView
android:id="@+id/receive_message"
android:layout_width="match_parent"
android:layout_height="365dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<EditText
android:id="@+id/edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="7.5"/>
<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="2.5"
android:text="send"/>
</LinearLayout>
</LinearLayout>
接下来是客户端的MainActivity.kt
代码。
class MainActivity : AppCompatActivity() {
private var mMainHandler: Handler? = null
private var socket: Socket? = null
private val mThreadPool = Executors.newCachedThreadPool()
private var isReader: InputStream? = null
private var isr: InputStreamReader? = null
private var br: BufferedReader? = null
private var response: String? = null
private var outputStream: OutputStream? = null
var threadRunning = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val receive_message:TextView = findViewById(R.id.receive_message)
receive_message.gravity = Gravity.LEFT
mMainHandler = Handler(Handler.Callback { msg ->
when (msg.what) {
0 -> receive_message.append("\n"+ response)
}
false
})
val btnConnect:Button = findViewById(R.id.connect)
btnConnect.setOnClickListener {
mThreadPool.execute {
try {
socket = Socket("10.0.2.2", 8080)
threadRunning = true
val readThread = ReadThread()
readThread.start()
println(socket!!.isConnected)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
val btnSend:Button = findViewById(R.id.send)
btnSend.setOnClickListener {
val mEdit:EditText = findViewById(R.id.edit)
val message = mEdit.text.toString()
receive_message.gravity = Gravity.RIGHT
receive_message.append("\nSent: $message")
mThreadPool.execute {
try {
outputStream = socket!!.getOutputStream()
outputStream!!.write((message + "\n").toByteArray(charset("utf-8")))
outputStream!!.flush()
} catch (e: IOException) {
e.printStackTrace()
}
}
mEdit.setText("")
}
val btnDisconnect:Button = findViewById(R.id.disconnect)
btnDisconnect.setOnClickListener {
threadRunning = false
try {
outputStream!!.close()
br!!.close()
socket!!.close()
println(socket!!.isConnected)
} catch (e: IOException) {
e.printStackTrace()
}
}
}
inner class ReadThread : Thread() {
override fun run() {
super.run()
while (threadRunning) {
try {
isReader = socket!!.getInputStream()
isr = InputStreamReader(isReader)
br = BufferedReader(isr)
response = br!!.readLine()
if (response != null) {
val msg = Message.obtain()
msg.what = 0
mMainHandler!!.sendMessage(msg)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
}
在本代码中,主要有以下几个重要部分。
- 初始化部分:在 MainActivity 类中,定义了一些用于网络通信的变量,用于记录 TCP 连接、用于接收和发送数据的对象、用于记录从服务器接收的回复等。
- 连接服务器部分:当点击 connect 按钮时,使用定义的线程池 mThreadPool 创建了一个新线程,尝试通过 TCP 连接服务器,服务器的 IP 地址和端口在 Socket 构造函数中给出。如果连接成功,就创建并启动一个 ReadThread 线程来读取服务器返回的消息。
- 当点击 send 按钮时,通过 outputStream 向服务器发送输入框中的文本内容。
- 读取服务器消息部分:在 ReadThread 中,当 socket 连接正常且 threadRunning 为 true 时,不断尝试从服务器读取传回的消息。
服务端的MainActivity.kt
代码与客户端的代码相对应,接下来给出服务端代码的关键部分,即与客户端实现连接的代码片段。
-
建立 ServerSocket
serverSocket = ServerSocket(8081)
这行代码在服务器端创建一个绑定到指定端口(此处为8081)的 ServerSocket。
-
接受客户端的连接请求:
socket = serverSocket!!.accept()
accept() 方法用于接受客户端发出的连接请求并返回一个 Socket 对象,这个对象就代表了服务器与客户端之间的连接。
-
开启服务器的代码:
val btnStartServer:Button = findViewById(R.id.startServer)
btnStartServer.setOnClickListener {
mThreadPool.execute {
try {
serverSocket = ServerSocket(8081)
while (true) {
socket = serverSocket!!.accept()
threadRunning = true
val readThread = ReadThread()
readThread.start()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
以上代码块在按钮btnStartServer的点击事件中开启一个新线程来开启服务器并接受客户端连接请求。
4. 关闭服务器的代码:
val btnStopServer:Button = findViewById(R.id.stopServer)
btnStopServer.setOnClickListener {
threadRunning = false
try {
outputStream!!.close()
br!!.close()
serverSocket!!.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
以上代码块在按钮btnStopServer的点击事件中尝试关闭服务器,同时关闭输入、输出流及socket连接。
2.2.2运行部分
运行的结果如下所示。可以实现两边的简单通讯
3.遇到的问题以及解决方法
问题一:客户机收到信息没有立即打印信息
解决:手动输入一个换行符解决
问题二:不知道如何实现两个安卓模拟器的通信,客户端直接连接服务端的IP不起作用
解决:问题二的解决方法:需要把模拟器的端口映射到PC的端口,通过连接PC端的端口来把请求重定向发送给模拟器。
4.学习感悟
本次实验中,在 Android 平台上实现 TCP 客户端和服务器的基本框架。这两个例子都使用了 Socket 编程和多线程技术,主要涉及了以下的知识点:
- Socket 编程:通过 Socket 对象进行网络通信。客户端使用 Socket(ip, port) 方法连接到服务器,服务器使用 ServerSocket(port) 创建一个服务器 Socket,并使用 accept() 方法接受客户端的连接。
- 多线程技术:由于网络操作不能在主线程(UI线程)中进行,因此我们需要使用多线程技术。在这里,一个新的线程被创建来等待并处理网络连接。
- 输入输出流:使用 getInputStream() 和 getOutputStream() 获取输入和输出流,从而能够读取和发送数据。
- 在Android中,更新UI需要在主线程中进行。但是我们不能直接在其他线程中更新UI,可以借助Handler实现线程间的通信来更新UI。
通过进行Android平台上Socket编程的实验,我对Socket编程的基本概念有了更深入的理解。在实验过程中,我学会了如何在Android平台上使用Kotlin语言来实现Socket服务端和客户端,加深了我对于Socket编程的基本原理的理解,包括Socket的建立、连接、通信和关闭等过程。这为我今后在Android开发中处理网络通信提供了重要的基础