Android Socket通讯

Posted 初学者-Study

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Socket通讯相关的知识,希望对你有一定的参考价值。

Socket通讯

前言

  Socket通讯在很多地方都会用到,android上同样不例外,Socket不是一种协议,而是一个编程调用接口(API),属于传输层,通过Socket,我们才能在Andorid平台上通过 TCP/IP协议进行开发。先看看效果图:

正文

先说明一下流程:

① 准备两台Android手机(真机)。
② 连接同一个WIFI网络 。
③ 服务端开启服务。
④ 客户端连接服务。
⑤ 服务端与客户端进行消息发送接收。

那么根据这个流程我们开始写代码。

一、创建项目

创建一个名为SocketDemo的项目,使用Kotlin。


因为涉及到网络通讯,所以需要在AndroidManifest.xml配置网络权限。

	<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"/>

然后再配置一下app的build.gradle,在android闭包下添加:

	buildFeatures 
        viewBinding true
    

这里开启项目的viewBinding,其他的就没啥好配置的了,进入正式的编码环节。

二、构建主页面布局

  创建项目会默认有一个MainActivity,这个页面既是服务端,又是客户端。修改一下activity_main.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=".MainActivity">

    <RadioGroup
        android: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">

        <RadioButton
            android:id="@+id/rb_server"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:checked="true"
            android:text="Socket服务端" />

        <RadioButton
            android:id="@+id/rb_client"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Socket客户端" />
    </RadioGroup>

    <LinearLayout
        android:id="@+id/lay_server"
        android:layout_width="match_parent"
        android:layout_height="110dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_ip_address"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp"
            android:text="Ip地址:" />

        <Button
            android: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>

    <LinearLayout
        android:id="@+id/lay_client"
        android:layout_width="match_parent"
        android:layout_height="110dp"
        android:orientation="vertical"
        android:visibility="gone">

        <com.google.android.material.textfield.TextInputLayout
            android: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.TextInputEditText
                android: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>

        <Button
            android: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>

    <TextView
        android:id="@+id/tv_info"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:padding="16dp"
        android:text="信息" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center_vertical"
        android:paddingStart="16dp"
        android:paddingEnd="16dp">

        <androidx.appcompat.widget.AppCompatEditText
            android: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.MaterialButton
            android: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"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="20dp" />
    <solid android:color="#EEE" />
</shape>

  说明一下页面的内容,首先是RadioButton切换服务端和客户端,服务端则显示当前手机的IP地址和开启服务按钮,客户端则显示一个输入框,和连接服务按钮。中间主要内容就是服务端和客户端交互的信息,底部是一个输入框和发送消息按钮。预览的效果如图所示:

三、服务端

在com.llw.socket包下新建一个server包,我们服务端的代码就写在这个server包下。新建一个ServerCallback接口,代码如下:

interface ServerCallback 
    //接收客户端的消息
    fun receiveClientMsg(success: Boolean, msg: String)
    //其他消息
    fun otherMsg(msg: String)

下面就是主要的服务端代码了,在server包下新建一个SocketServer类,代码如下:

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
    /**
     * 开启服务
     */
    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()
    

    /**
     * 发送到客户端
     */
    fun sendToClient(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()
    

    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) 
                        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 
    //接收服务端的消息
    fun receiveServerMsg(msg: String)
    //其他消息
    fun otherMsg(msg: String)

下面就是主要的客户端代码了,在client包下新建一个SocketClient类,代码如下:

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

    /**
     * 连接服务
     */
    fun connectServer(ipAddress: String, callback: ClientCallback) 
        mCallback = callback
        Thread 
            try 
                socket = Socket(ipAddress, SOCKET_PORT)
                ClientThread(socket!!, mCallback).start()
             catch (e: IOException) 
                e.printStackTrace()
            
        .start()
    

    /**
     * 关闭连接
     */
    fun closeConnect() 
        inputStreamReader?.close()
        outputStream?.close()
        socket?.apply 
            shutdownInput()
            shutdownOutput()
            close()
        
        Log.d(TAG, "关闭连接")
    

    /**
     * 发送数据至服务器
     * @param msg 要发送至服务器的字符串
     */
    fun sendToServer(msg: String) 以上是关于Android Socket通讯的主要内容,如果未能解决你的问题,请参考以下文章

Android Socket通讯

天地币:所用到的 Android Socket 通讯编程技术试验

Android轻量级Socket通讯框架

android socket 通讯(客户端) 发送数据

Android Socket UDP 点对点,或者广播通讯,包含发送端和接收端

Android Socket UDP 点对点,或者广播通讯,包含发送端和接收端