一、实验目的
实验目标
掌握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编程的基本概念和技术,还让我学会了如何在移动平台上实现网络通信。
五、参考资料
- 获取android模拟器的IP地址和访问网络_如何查看手机模拟器ip-CSDN博客
- Android Studio模拟器联网_android studio 模拟器怎么联网-CSDN博客
- android socket客户端与服务器通信_哔哩哔哩_bilibili
- https://mbd.baidu.com/ug_share/mbox/4a83aa9e65/share?product=smartapp&tk=87714a0d6e3bc0ec41cec32142914bec&share_url=https%3A%2F%2Fyebd1h.smartapps.cn%2Fpages%2Fblog%2Findex%3FblogId%3D103793265%26_swebfr%3D1%26_swebFromHost%3Dbaiduboxapp&domain=mbd.baidu.com