Socket通讯
前言
Socket通讯在很多地方都会用到,Android上同样不例外,Socket不是一种协议,而是一个编程调用接口(API),属于传输层,通过Socket,我们才能在Andorid平台上通过 TCP/IP协议进行开发。先看看效果图:
正文
先说明一下流程:
① 准备两台Android手机(真机)。
② 连接同一个WIFI网络 。
③ 服务端开启服务。
④ 客户端连接服务。
⑤ 服务端与客户端进行消息发送接收。
那么根据这个流程我们开始写代码。
一、创建项目
创建一个名为SocketDemo的项目,使用Kotlin。
因为涉及到网络通讯,所以需要在AndroidManifest.xml配置网络权限。
<uses-permissionandroid:name="android.permission.INTERNET"/><uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/><uses-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/>
然后再配置一下app的build.gradle,在android{}闭包下添加:
buildFeatures {
viewBinding true}
这里开启项目的viewBinding,其他的就没啥好配置的了,进入正式的编码环节。
二、构建主页面布局
创建项目会默认有一个MainActivity,这个页面既是服务端,又是客户端。修改一下activity_main.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns: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=".MainActivity"><RadioGroupandroid:id="@+id/rg"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginEnd="16dp"android:orientation="horizontal"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"><RadioButtonandroid:id="@+id/rb_server"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:checked="true"android:text="Socket服务端"/><RadioButtonandroid:id="@+id/rb_client"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:text="Socket客户端"/></RadioGroup><LinearLayoutandroid:id="@+id/lay_server"android:layout_width="match_parent"android:layout_height="110dp"android:orientation="vertical"><TextViewandroid:id="@+id/tv_ip_address"android:layout_width="match_parent"android:layout_height="wrap_content"android:padding="16dp"android:text="Ip地址:"/><Buttonandroid:id="@+id/btn_start_service"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginEnd="16dp"android:text="开启服务"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/radioGroup"/></LinearLayout><LinearLayoutandroid:id="@+id/lay_client"android:layout_width="match_parent"android:layout_height="110dp"android:orientation="vertical"android:visibility="gone"><com.google.android.material.textfield.TextInputLayoutandroid:id="@+id/op_code_layout"style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginEnd="16dp"><com.google.android.material.textfield.TextInputEditTextandroid:id="@+id/et_ip_address"android:layout_width="match_parent"android:layout_height="wrap_content"android:hint="连接Ip地址"android:inputType="text"android:lines="1"android:singleLine="true"/></com.google.android.material.textfield.TextInputLayout><Buttonandroid:id="@+id/btn_connect_service"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginEnd="16dp"android:text="连接服务"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/btn_start_service"/></LinearLayout><TextViewandroid:id="@+id/tv_info"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:padding="16dp"android:text="信息"/><LinearLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:gravity="center_vertical"android:paddingStart="16dp"android:paddingEnd="16dp"><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_msg"android:layout_width="0dp"android:gravity="center_vertical"android:layout_height="40dp"android:hint="发送给客户端"android:textSize="14sp"android:layout_weight="1"android:background="@drawable/shape_et_bg"android:padding="10dp"/><com.google.android.material.button.MaterialButtonandroid:id="@+id/btn_send_msg"android:layout_width="80dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:text="发送"app:cornerRadius="8dp"/></LinearLayout></LinearLayout>
这里面有一个输入框的背景样式,在drawable下新增shape_et_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><shapexmlns:android="http://schemas.android.com/apk/res/android"><cornersandroid:radius="20dp"/><solidandroid:color="#EEE"/></shape>
说明一下页面的内容,首先是RadioButton切换服务端和客户端,服务端则显示当前手机的IP地址和开启服务按钮,客户端则显示一个输入框,和连接服务按钮。中间主要内容就是服务端和客户端交互的信息,底部是一个输入框和发送消息按钮。预览的效果如图所示:
三、服务端
在com.llw.socket包下新建一个server包,我们服务端的代码就写在这个server包下。新建一个ServerCallback接口,代码如下:
interface ServerCallback {//接收客户端的消息funreceiveClientMsg(success: Boolean, msg: String)//其他消息funotherMsg(msg: String)}
下面就是主要的服务端代码了,在server包下新建一个SocketServer类,代码如下:
object SocketServer {privateval TAG = SocketServer::class.java.simpleName
privateconstval SOCKET_PORT =9527privatevar socket: Socket?=nullprivatevar serverSocket: ServerSocket?=nullprivatelateinitvar mCallback: ServerCallback
privatelateinitvar outputStream: OutputStream
var result =true/**
* 开启服务
*/funstartServer(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
}/**
* 关闭服务
*/funstopServer(){
socket?.apply{shutdownInput()shutdownOutput()close()}
serverSocket?.close()}/**
* 发送到客户端
*/funsendToClient(msg: String){
Thread {if(socket!!.isClosed){
Log.e(TAG,"sendToClient: Socket is closed")return@Thread}
outputStream = socket!!.getOutputStream()try{
outputStream.write(msg.toByteArray())
outputStream.flush()
mCallback.otherMsg("toClient: $msg")
Log.d(TAG,"发送到客户端成功")}catch(e: IOException){
e.printStackTrace()
Log.e(TAG,"向客户端发送消息失败")}}.start()}classServerThread(privateval socket: Socket,privateval callback: ServerCallback):Thread(){overridefunrun(){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){
callback.receiveClientMsg(true, receiveStr)
receiveStr =""}}}catch(e: IOException){
e.printStackTrace()
e.message?.let{ Log.e("socket error", it)}
callback.receiveClientMsg(false,"")}}}}
代码从上往下看,首先是初始化一些变量,然后就是startServer()函数,在这里进行回调接口的初始化然后开一个子线程进行ServerSocket的构建,构建成功之后会监听连接,得到一个socket,这个socket就是客户端,这里将连接客户端的地址显示出来。然后再开启一个子线程去处理客户端发送过来的消息。这个地方服务端和客户端差不多,下面看ServerThread中的代码。Socket通讯,发送和接收对应的是输入流和输入流,通过socket.getInputStream()得到输入流,获取字节数据然后转成String,通过接口回调,最后重置变量。关闭服务就没好说的,代码一目了然。最后就是发送到客户端的sendToClient()函数。接收发送字符串,开启子线程,获取输出流,写入字节数据然后刷新,最后回调到页面。
四、客户端
在com.llw.socket包下新建一个client包,我们客户端的代码就写在这个client包下。新建一个ClientCallback接口,代码如下:
interface ClientCallback {//接收服务端的消息funreceiveServerMsg(msg: String)//其他消息funotherMsg(msg: String)}
下面就是主要的客户端代码了,在client包下新建一个SocketClient类,代码如下:
object SocketClient {privateval TAG = SocketClient::class.java.simpleName
privatevar socket: Socket?=nullprivatevar outputStream: OutputStream?=nullprivatevar inputStreamReader: InputStreamReader?=nullprivatelateinitvar mCallback: ClientCallback
privateconstval SOCKET_PORT =9527/**
* 连接服务
*/funconnectServer(ipAddress: String, callback: ClientCallback){
mCallback = callback
Thread {try{
socket =Socket(ipAddress, SOCKET_PORT)ClientThread(socket!!, mCallback).start()}catch(e: IOException){
e.printStackTrace()}}.start()}/**
* 关闭连接
*/funcloseConnect(){
inputStreamReader?.close()
outputStream?.close()
socket?.apply{shutdownInput()shutdownOutput()close()}
Log.d(TAG,"关闭连接")}/**
* 发送数据至服务器
* @param msg 要发送至服务器的字符串
*/funsendToServer(msg: String){
Thread {if(socket!!.isClosed){
Log.e(TAG,"sendToServer: Socket is closed")return@Thread}
outputStream = socket?.getOutputStream()try{
outputStream?.write(msg.toByteArray())
outputStream?.flush()
mCallback.otherMsg("toServer: $msg")}catch(e: IOException){
e.printStackTrace()
Log.e(TAG,"向服务端发送消息失败")}}.start()}classClientThread(privateval socket: Socket,privateval callback: ClientCallback):Thread(){overridefunrun(){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){
callback.receiveServerMsg(receiveStr)
receiveStr =""}}}catch(e: IOException){
e.printStackTrace()
e.message?.let{ Log.e("socket error", it)}
callback.receiveServerMsg("")}}}}
客户端的代码和服务端其实很相似,这里我就简单说明一下,首先就是连接服务,需要输入服务端的ip地址,端口号则是写死的一个端口号,也可以动态去设置。其他的地方和服务端相似。
五、业务交互
现在核心功能代码都写好了,下面怎么样让这些功能和页面串起来,这里因为涉及到用户交互所以会说明的多一点。
① 接口回调
还记得之前的ServerCallback和ClientCallback吗?这两个回调接口因为我们是服务端和客户端在一起的,所以在同一个Activity中去实现接口。
然后实现接口中的方法,在MainActivity中新增如下代码:
overridefunreceiveClientMsg(success: Boolean, msg: String){}overridefunotherMsg(msg: String){}overridefunreceiveServerMsg(msg: String){}
这里的otherMsg()函数是服务端和客户端共用,因为函数名参数都一致,可以自行修改为不共用。这些函数里面后面会写代码,目前先不管,先实现页面的业务逻辑。
② 服务端和客户端切换
服务端和客户端的切换是会影响整个页面的,首先在MainActivity中定义变量,如下所示:
privateval TAG = MainActivity::class.java.simpleName
privatelateinitvar binding: ActivityMainBinding
privateval buffer =StringBuffer()//当前是否为服务端privatevar isServer =true//Socket服务是否打开privatevar openSocket =false//Socket服务是否连接privatevar connectSocket =false
然后修改一下onCreate()函数,代码如下:
overridefunonCreate(savedInstanceState: Bundle?){super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)initView()}
这里就是实现ViewBinding,然后创建了一个initView()函数,代码如下:
privatefuninitView(){
binding.tvIpAddress.text ="Ip地址:${getIp()}"//服务端和客户端切换
binding.rg.setOnCheckedChangeListener{ _, checkedId ->}//开启服务/关闭服务 服务端处理
binding.btnStartService.setOnClickListener{}//连接服务/断开连接 客户端处理
binding.btnConnectService.setOnClickListener{}//发送消息 给 服务端/客户端
binding.btnSendMsg.setOnClickListener{}}
在initView()函数中,首先要做的就是显示当前的Ip地址,不管你是服务端还是客户端,我都会获取Ip地址,因为在你切换时并不会重新获取Ip地址,这里有一个getIp()函数,代码如下:
privatefungetIp()=intToIp((applicationContext.getSystemService(WIFI_SERVICE)as WifiManager).connectionInfo.ipAddress)privatefunintToIp(ip: Int)="${(ip and0xFF)}.${(ip shr8and0xFF)}.${(ip shr16and0xFF)}.${(ip shr24and0xFF)}"
这里的WIFI_SERVICE就对应之前在AndroidManifest.xml中配置的WIFI状态读取权限。下面我们完成服务端和客户端切换对UI上的改变。代码如下:
binding.rg.setOnCheckedChangeListener{ _, checkedId ->
isServer =when(checkedId){
R.id.rb_server ->true
R.id.rb_client ->falseelse->true}
binding.layServer.visibility =if(isServer) View.VISIBLE else View.GONE
binding.layClient.visibility =if(isServer) View.GONE else View.VISIBLE
binding.etMsg.hint =if(isServer)"发送给客户端"else"发送给服务端"}
这里在对RadioGroup进行选中改变监听,点击RadioButton获取id设置是否为服务端,就是改变isServer的值,默认是服务端。然后就是根据isServer去设置服务端布局和客户端布局的显示状态,同时还需要设置底部输入框的提示文字。
③ 服务开启和关闭
如果当前是服务端,则会看到开启服务按钮,点击按钮的代码如下:
binding.btnStartService.setOnClickListener{
openSocket =if(openSocket){
SocketServer.stopServer();false}else SocketServer.startServer(this)//显示日志showInfo(if(openSocket)"开启服务"else"关闭服务")//改变按钮文字
binding.btnStartService.text =if(openSocket)"关闭服务"else"开启服务"}
这里根据当前是否开启服务条件去控制是开启服务还是关闭服务,还有一些不严谨,再往下就是一个显示日志的方法和修改按钮显示文字,这里就是页面中部的那个TextView。showInfo()函数代码很简单,如下所示:
privatefunshowInfo(info: String){
buffer.append(info).append("\n")
runOnUiThread { binding.tvInfo.text = buffer.toString()}}
就是字符串拼接,然后显示出来。
④ 服务连接和断开
如果当前是客户端,则会看到连接服务按钮,点击按钮的代码如下:
binding.btnConnectService.setOnClickListener{val ip = binding.etIpAddress.text.toString()if(ip.isEmpty()){showMsg("请输入Ip地址");return@setOnClickListener}
connectSocket =if(connectSocket){
SocketClient.closeConnect();false}else{
SocketClient.connectServer(ip,this);true}showInfo(if(connectSocket)"连接服务"else"关闭连接")
binding.btnConnectService.text =if(connectSocket)"关闭连接"else"连接服务"}
这里会先检查是否输入IP地址,没有就会提示一下,showMsg()函数代码如下:
privatefunshowMsg(msg: String)= Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
这里还缺少一步检查ip地址是否合规,我就先不做了。ip地址有了地址就会根据connectSocket状态得知当前点击按钮时执行连接还是断开。最后同样时显示日志和修改按钮文字。
⑤ 发送消息
终于到了底部的发送消息处理了,点击按钮的代码如下:
binding.btnSendMsg.setOnClickListener{val msg = binding.etMsg.text.toString()if(msg.isEmpty()){showMsg("请输入要发送的信息");return@setOnClickListener}//检查是否能发送消息val isSend =if(openSocket) openSocket elseif(connectSocket) connectSocket elsefalseif(!isSend){showMsg("当前未开启服务或连接服务");return@setOnClickListener}if(isServer) SocketServer.sendToClient(msg)else SocketClient.sendToServer(msg)
binding.etMsg.setText("")}
检查是否有消息输入,然后是根据当前是否为服务端进行消息发送,发送后清空输入框。
⑥ 显示消息内容
在服务端和客户端连接之后,服务端发送消息之后,客户端收到,客户端发送消息之后,服务端收到。在①中我们实现了接口,现在只要将接口返回的消息显示出来就行了。
overridefunreceiveClientMsg(success: Boolean, msg: String){showInfo("ClientMsg: $msg")}overridefunotherMsg(msg: String){showInfo(msg)}overridefunreceiveServerMsg(msg: String){showInfo("ServerMsg: $msg")}
那么现在所有的代码都写完了,因为页面的底部是一个输入框,当点击之后会弹出软键盘,此时页面会被顶上去,为了避免这样的问题,修改修改一下我们运行看看效果。
六、UI优化
既然现在消息通讯已经可以了,那么我们可不可以做成类似聊天的UI风格呢?当然可以。首先要改变一下UI,先把activity_main.xml中id为tv_info的控件控件删掉,换成RecyclerView,代码如下:
<androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/rv_msg"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"/>
现在MainActivity中肯定会有报错,不过我们先不管它,先写列表适配器的代码。
① 列表适配器
做适配器的话要考虑服务端和客户端的关系,因此和传统的聊天是有区别的。首先在layout下创建一个item_rv_msg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayoutxmlns: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:orientation="horizontal"android:paddingTop="8dp"android:paddingBottom="8dp"android:paddingStart="16dp"android:paddingEnd="16dp"><com.google.android.material.imageview.ShapeableImageViewandroid:id="@+id/iv_server"android:layout_width="60dp"android:layout_height="60dp"android:src="@drawable/icon_server"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:shapeAppearanceOverlay="@style/circleImageStyle"/><TextViewandroid:id="@+id/tv_server_msg"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_weight="1"android:background="@drawable/shape_left_msg_bg"android:text="123"android:textColor="@color/black"app:layout_constraintStart_toEndOf="@+id/iv_server"app:layout_constraintTop_toTopOf="@+id/iv_server"/><TextViewandroid:id="@+id/tv_client_msg"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginEnd="16dp"android:layout_weight="1"android:background="@drawable/shape_right_msg_bg"android:text="123"android:textColor="@color/white"app:layout_constraintEnd_toStartOf="@+id/iv_client"app:layout_constraintTop_toTopOf="@+id/iv_client"/><com.google.android.material.imageview.ShapeableImageViewandroid:id="@+id/iv_client"android:layout_width="60dp"android:layout_height="60dp"android:src="@drawable/icon_client"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"app:shapeAppearanceOverlay="@style/circleImageStyle"/></androidx.constraintlayout.widget.ConstraintLayout>
这里用到了两个头像图片。
可以直接去我源码里面拿,同时为了设置圆形头像,我在themes.xml中增加了一个样式,代码如下:
<stylename="circleImageStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
然后就是消息的背景样式了,因为是要区分服务端和客户端的,服务端在左,客户端在右。在drawable中新增shape_left_msg_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><shapexmlns:android="http://schemas.android.com/apk/res/android"><solidandroid:color="#EEE"/><cornersandroid:bottomLeftRadius="16dp"android:bottomRightRadius="16dp"android:topRightRadius="16dp"/><paddingandroid:bottom="16dp"android:left="16dp"android:right="16dp"android:top="16dp"/></shape>
shape_right_msg_bg.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?><shapexmlns:android="http://schemas.android.com/apk/res/android"><solidandroid:color="#4177F6"/><cornersandroid:bottomLeftRadius="16dp"android:bottomRightRadius="16dp"android:topLeftRadius="16dp"/><paddingandroid:bottom="16dp"android:left="16dp"android:right="16dp"android:top="16dp"/></shape>
适配器是需要数据的,因为我们在com.llw.socket包下新增一个数据类,代码如下:
dataclassMessage(val type:Int,val msg:String)
最后我们写一个适配器,在com.llw.socket包下新增一个MsgAdapter,代码如下:
classMsgAdapter(privateval messages: ArrayList<Message>):
RecyclerView.Adapter<MsgAdapter.ViewHolder>(){overridefunonCreateViewHolder(parent: ViewGroup, viewType: Int)=ViewHolder(ItemRvMsgBinding.inflate(LayoutInflater.from(parent.context), parent,false))overridefunonBindViewHolder(holder: ViewHolder, position: Int){val message = messages[position]if(message.type ==1){
holder.mView.tvServerMsg.text = message.msg
}else{
holder.mView.tvClientMsg.text = message.msg
}
holder.mView.ivServer.visibility =if(message.type ==1) View.VISIBLE else View.INVISIBLE
holder.mView.ivClient.visibility =if(message.type ==1) View.INVISIBLE else View.VISIBLE
holder.mView.tvServerMsg.visibility =if(message.type ==1) View.VISIBLE else View.GONE
holder.mView.tvClientMsg.visibility =if(message.type ==1) View.GONE else View.VISIBLE
}overridefungetItemCount()= messages.size
classViewHolder(itemView: ItemRvMsgBinding): RecyclerView.ViewHolder(itemView.root){var mView: ItemRvMsgBinding
init{
mView = itemView
}}}
这里就是RecyclerView+ViewBinding的使用方式。根据不同的消息类型设置控件状态就可以了。
② 修改页面逻辑
首先要将适配器和RV绑定起来,在MainActivity中新增如下代码:
//消息列表privateval messages = ArrayList<Message>()//消息适配器privatelateinitvar msgAdapter: MsgAdapter
然后在initView()函数中初始化,代码如下:
//初始化列表
msgAdapter =MsgAdapter(messages)
binding.rvMsg.apply{
layoutManager =LinearLayoutManager(this@MainActivity)
adapter = msgAdapter
}
代码添加位置如下图所示:
然后在MainActivity中新增一个updateList()函数,代码如下:
/**
* 更新列表
*/privatefunupdateList(type: Int, msg: String){
messages.add(Message(type, msg))
runOnUiThread {(if(messages.size ==0)0else messages.size -1).apply{
msgAdapter.notifyItemChanged(this)
binding.rvMsg.smoothScrollToPosition(this)}}}
添加数据到列表,然后刷新位置,滑动到新增数据位置,将showInfo()函数代码先去掉,然后将btnStartService和btnConnectService按钮的点击事件中的showInfo修改为showMsg。
然后修改一下这三个回调函数,代码如下:
overridefunreceiveClientMsg(success: Boolean, msg: String)=updateList(2, msg)overridefunreceiveServerMsg(msg: String)=updateList(1, msg)overridefunotherMsg(msg: String){
Log.d(TAG, msg)}
之前都是调用的showInfo()函数,现在都改了,为了让我们发送消息也能更新列表,在btnSendMsg按钮点击事件中,最后一行增加如下代码:
updateList(if(isServer)1else2, msg)
那么现在代码就写完了,看看运行的效果。
七、源码
如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~
源码地址:SocketDemo
版权归原作者 初学者-Study 所有, 如有侵权,请联系我们删除。