您的当前位置:首页正文

Android studio 简易MQTT客户端

2024-10-31 来源:个人技术集锦

一、Android studio版本 

2023.1.1  

使用 new UI

二、MQTT依赖添加及其网络配置

1、settings.gradle.kts配置

在settings.gradle.kts中添加如下语句

maven { url = uri ("https://jitpack.io/")}

下图为上下文(不同版本该语句格式可能不同)

点击提示 sync now 或者点击该按钮

2、build.gradle.kts(Module:app)配置

在build.gradle.kts中添加如下语句

//MQTT
    implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
    implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")

下图为上下文(不同版本该语句格式可能不同)

3、AndroidManifest.xml 清单文件配置

3.1在AndroidManifest.xml 中添加联网权限

    <!--    网络权限-->
    <uses-permission android:name="android.permission.INTERNET" />
    <!--    网络状态权限-->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!--    保持唤醒状态-->
    <uses-permission android:name="android.permission.WAKE_LOCK" />

下图为上下文

 

3.2  在AndroidManifest.xml 中添加服务

        <!-- Mqtt Service -->
        <service android:name="org.eclipse.paho.android.service.MqttService">
        </service>

下图为上下文 

记得在清单文件中添加对应的activity

三、代码

1、activity_main.xml 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:layout_editor_absoluteY="10dp">
        <TextView
            android:id="@+id/tv_mqtt_server"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/mqtt_server"
            android:textSize="30sp"
            android:layout_marginBottom="30dp"/>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="15dp">

            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/address"
                android:gravity="right"
                android:textSize="30sp"
                tools:ignore="RtlHardcoded" />

            <EditText
                android:id="@+id/et_address"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_address"
                android:textSize="25sp"
                android:inputType="text" />

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="15dp">

            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/port"
                android:gravity="right"
                android:textSize="30sp" />

            <EditText
                android:id="@+id/et_port"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_port"
                android:textSize="25sp"
                android:inputType="text" />

        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="15dp">

            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/client_id"
                android:gravity="right"
                android:textSize="30sp" />

            <EditText
                android:id="@+id/et_client_id"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
               android:hint="@string/hint_client_id"
                android:textSize="25sp"
                android:inputType="text"/>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="15dp">
            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/keep_alive"
                android:gravity="right"
                android:textSize="30sp" />
            <EditText
                android:id="@+id/et_keep_alive"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_keep_alive"
                android:textSize="25sp"
                android:inputType="number"/>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="15dp">

            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/username"
                android:gravity="right"
                android:textSize="30sp" />
            <EditText
                android:id="@+id/et_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_username"
                android:textSize="25sp"
                android:inputType="text"/>
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal"
            android:layout_marginBottom="30dp">

            <TextView
                android:layout_width="120dp"
                android:layout_height="50dp"
                android:text="@string/password"
                android:gravity="right"
                android:textSize="30sp" />
            <EditText
                android:id="@+id/et_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/hint_password"
                android:inputType="textPassword"
                android:textSize="25sp"/>
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btn_connect"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/connect"
                android:textSize="25sp"
                android:layout_marginEnd="20dp"/>

            <Button
                android:id="@+id/btn_test"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/test"
                android:textSize="25sp"
                android:layout_marginStart="20dp"/>

        </LinearLayout>

    </LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

下图为预览图

2、activity_subscribe.xml 订阅主题

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/subscribe"
            android:textSize="30sp"
            android:layout_marginBottom="20dp"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginBottom="20dp"
            >
            <TextView
                android:layout_width="110dp"
                android:layout_height="wrap_content"
                android:text="@string/topic"
                android:textSize="25sp"
                android:gravity="right"
                tools:ignore="RtlHardcoded" />
            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/et_topic"
                android:hint="@string/hint_topic"
                android:textSize="25sp"
                android:inputType="text"
                />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginBottom="20dp"
            >
            <TextView
                android:layout_width="110dp"
                android:layout_height="wrap_content"
                android:text="@string/qos"
                android:textSize="25sp"
                android:gravity="right"
                tools:ignore="RtlHardcoded" />
            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/et_qos"
                android:hint="@string/hint_qos"
                android:textSize="25sp"
                android:inputType="number"
                />
        </LinearLayout>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:id="@+id/tv_message"
            android:text="@string/tv_message"
            android:textSize="25sp"
            android:layout_marginBottom="20dp"
            />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/subscribe"
            android:id="@+id/btn_subscribe"
            android:layout_marginBottom="20dp"
            />
       <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/publish"
            android:id="@+id/btn_go_publish"
            android:layout_marginBottom="20dp"
            />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

下图为预览图

3、activity_publish.xml 发布

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/publish"
            android:textSize="30sp"
            android:layout_marginBottom="20dp"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginBottom="20dp"
            >
            <TextView
                android:layout_width="110dp"
                android:layout_height="wrap_content"
                android:text="@string/topic"
                android:textSize="25sp"
                android:gravity="right"
                />
            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/et_ptopic"
                android:hint="@string/hint_topic"
                android:textSize="25sp"
                android:inputType="text"
                />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginBottom="20dp"
            >
            <TextView
                android:layout_width="110dp"
                android:layout_height="wrap_content"
                android:text="@string/qos"
                android:textSize="25sp"
                android:gravity="right"
                />
            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/et_pqos"
                android:hint="@string/hint_qos"
                android:textSize="25sp"
                android:inputType="number"
                />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            android:layout_marginBottom="20dp"
            >
            <TextView
                android:layout_width="110dp"
                android:layout_height="wrap_content"
                android:text="@string/retained"
                android:textSize="25sp"
                android:gravity="right"
                />
            <EditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:id="@+id/et_retained"
                android:hint="@string/hint_retained"
                android:textSize="25sp"
                android:inputType="text"
                />
        </LinearLayout>
        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/et_pmessage"
            android:hint="@string/hint_message"
            android:textSize="25sp"
            android:inputType="text"
            />
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/publish"
            android:id="@+id/btn_publish"
            android:layout_marginBottom="20dp"
            />
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

预览图

4、MainAcitvity.java 实现连接MQTT服务器

package com.example.demo_mqttserver;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.google.android.material.snackbar.Snackbar;

import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MainActivity extends AppCompatActivity {

    protected static MqttClient mqttClient;//客户端
    private MqttConnectOptions options;//配置  保存控制客户端连接到服务器的方式的选项集。
    private TextView tv_mqtt_server;
    private EditText et_address,et_port,et_clientId,et_username,et_password,et_keep_alive;

    private Button btn_connect,btn_test;
    private String host,clientId,username,password,message;
    private int keep_alive;

    private Intent intent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initview();

        btn_connect.setOnClickListener(v -> {
            if (isOnline()){
                if (isequals()) {
                    host = "tcp://" + et_address.getText().toString() + ":" + et_port.getText().toString();
                    clientId = et_clientId.getText().toString();
                    keep_alive = Integer.parseInt(et_keep_alive.getText().toString());
                    username = et_username.getText().toString();
                    password = et_password.getText().toString();
                    initMqtt(host, clientId, username, password, keep_alive);
                    connect_mqtt(true);
                } else {
                    Snackbar.make(tv_mqtt_server, "请填入完整信息!", Snackbar.LENGTH_SHORT).show();
                }
            }else {
                Snackbar.make(tv_mqtt_server, "请检查网络连接!", Snackbar.LENGTH_SHORT).show();
            }
        });
        btn_test.setOnClickListener(v -> {
            initMqtt("tcp://192.168.123.183:1883","app-0A:0B:0C","app-test","app123456",60);
            connect_mqtt(true);
        });
    }
    /**
     * 初始化控件*/
    public void initview(){
        intent = new Intent(MainActivity.this,SubscribeActivity.class);
        tv_mqtt_server = findViewById(R.id.tv_mqtt_server);
        et_address = findViewById(R.id.et_address);
        et_port = findViewById(R.id.et_port);
        et_clientId = findViewById(R.id.et_client_id);
        et_keep_alive= findViewById(R.id.et_keep_alive);
        et_username = findViewById(R.id.et_username);
        et_password = findViewById(R.id.et_password);
        btn_connect = findViewById(R.id.btn_connect);
        btn_test=findViewById(R.id.btn_test);
    }
    /**
     * 判断输入框是否为空
     * @return true不为空,false为空*/
    private boolean isequals(){
        return !et_address.getText().toString().equals("") && !et_port.getText().toString().equals("") && !et_clientId.getText().toString().equals("") && !et_username.getText().toString().equals("") && !et_password.getText().toString().equals("");
    }
    /**
     * 初始化Mqtt
     * @param host 服务器地址+端口号
     * @param clientId 客户端ID
     * @param username 用户名
     * @param password 密码
     * @param keep_alive 保持连接时间*/
    private void initMqtt(String host,String clientId,String username,String password,int keep_alive){
        try {
            //(1)主机地址(2)客户端ID,一般以客户端唯一标识符(不能够和其它客户端重名)(3)最后一个参数是指数据保存在内存(具体保存什么数据,以后再说,其实现在我也不是很确定)
            mqttClient = new MqttClient(host, clientId, new MemoryPersistence());
        } catch (MqttException e) {
            e.printStackTrace();
        }
        options = new MqttConnectOptions();//MQTT的连接设置
        options.setCleanSession(true);//设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
        options.setUserName(username);//设置连接的用户名(自己的服务器没有设置用户名)
        options.setPassword(password.toCharArray());//设置连接的密码(自己的服务器没有设置密码)
        options.setKeepAliveInterval(keep_alive);
        options.setConnectionTimeout(10);
        mqttClient.setCallback(new MqttCallback() {
            @Override//连接丢失后,会执行这里
            public void connectionLost(Throwable throwable) {
            }
            @Override//获取的消息会执行这里--arg0是主题,arg1是消息
            public void messageArrived(final String ssr, MqttMessage mqttMessage) throws Exception {
                message = mqttMessage.toString();//消息
                runOnUiThread(new Runnable() {//
                    public void run() {
                        Log.d("mqtt", "收到消息");
                        Log.d("主题", ssr);
                        Log.d("消息", message);
                        SubscribeActivity.tv_message.setText(message);
                    }
                });
            }
            @Override//订阅主题后会执行到这里
            public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
            }
        });
}
    /**
    * 连接mqtt
    * @param bloConn true连接,false断开连接*/
    private void connect_mqtt(boolean bloConn) {
        // 必须开启新的线程执行
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (bloConn) {
                    try {
                        if (!mqttClient.isConnected() ) {
                            mqttClient.connect(options);//连接服务器,连接不上会阻塞在这
                        }
                        Snackbar.make(tv_mqtt_server, "连接成功!", Snackbar.LENGTH_SHORT).show();
                        startActivity(intent);
                    } catch (MqttException e) {
                        e.printStackTrace();
                        Snackbar.make(tv_mqtt_server, "连接失败!", Snackbar.LENGTH_SHORT).show();
                    }
                } else {
                    try {
                        if (mqttClient.isConnected()) {
                            mqttClient.disconnect();
                        }
                        Snackbar.make(tv_mqtt_server, "断开成功!", Snackbar.LENGTH_SHORT).show();
                    } catch (MqttException e) {
                        e.printStackTrace();
                        Snackbar.make(tv_mqtt_server, "断开失败!", Snackbar.LENGTH_SHORT).show();
                    }
                }
            }
        }).start();
    }
    /**
     * 判断网络是否连接
     * @return true为连接,false为未连接*/
    public boolean isOnline() {
        ConnectivityManager connMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        try {
            mqttClient.disconnect();
        }catch (MqttException e) {
            e.printStackTrace();
        }
    }
}

5、SubscribeActivity.java 实现订阅主题

package com.example.demo_mqttserver;

import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.google.android.material.snackbar.Snackbar;

import org.eclipse.paho.client.mqttv3.MqttException;

public class SubscribeActivity extends AppCompatActivity {
    protected static TextView tv_message;
    private String topic;
    private EditText et_topic, et_qos;
    private int qos;
    private Button btn_subscribe, btn_go_publish;

    @Override
    public void onCreate(android.os.Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_subscribe);

        initview();
        btn_subscribe.setOnClickListener(v -> {
            if (isequals()){
                topic = et_topic.getText().toString();
                qos = Integer.parseInt(et_qos.getText().toString());
                try {
                    MainActivity.mqttClient.subscribe(topic, qos);
                } catch (MqttException e) {
                    e.printStackTrace();
                    Snackbar.make(tv_message, "订阅失败!", Snackbar.LENGTH_SHORT).show();
                }
                Snackbar.make(tv_message, "订阅成功", Snackbar.LENGTH_SHORT).show();
            }else {
                Snackbar.make(tv_message, "请填入完整信息!", Snackbar.LENGTH_SHORT).show();
            }
        });
            btn_go_publish.setOnClickListener(v -> {
            startActivity(new Intent(this, PublishActivity.class));
        });
    }
    /**
     * 初始化组件*/
    public void initview() {
        tv_message =findViewById(R.id.tv_message);
        et_topic = findViewById(R.id.et_topic);
        et_qos = findViewById(R.id.et_qos);
        btn_subscribe=findViewById(R.id.btn_subscribe);
btn_go_publish = findViewById(R.id.btn_go_publish);
    }
    /**
     * 判断输入框是否为空
     * @return true:不为空 false:为空*/
    private boolean isequals(){
        return !et_topic.getText().toString().equals("")&&!et_qos.getText().toString().equals("");
    }

}

6、实现发布

package com.example.demo_mqttserver;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

import com.google.android.material.snackbar.Snackbar;

import org.eclipse.paho.client.mqttv3.MqttException;

public class PublishActivity extends AppCompatActivity {
    private EditText et_ptopic,et_pmsg,et_pqos,et_retained;
    private Button btn_publish;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_publish);
        initview();

        btn_publish.setOnClickListener(v -> {
            if (isequals()){
                String topic;
                byte []msg = et_pmsg.getText().toString().getBytes();
                int qos;
                boolean retained = false;
                if(et_retained.getText().toString().equals("true"))retained=true;
                else if (et_retained.getText().toString().equals("false")) retained=false;
                topic = et_ptopic.getText().toString();
                qos = Integer.parseInt(et_pqos.getText().toString());
                try {
                    MainActivity.mqttClient.publish(topic,msg,qos,retained);
                } catch (MqttException e) {
                    e.printStackTrace();
                    Snackbar.make(btn_publish, "发布!", Snackbar.LENGTH_SHORT).show();
                }
                Snackbar.make(btn_publish, "发布成功", Snackbar.LENGTH_SHORT).show();
            }else {
                Snackbar.make(btn_publish, "请填入完整信息!", Snackbar.LENGTH_SHORT).show();
            }
        });
    }

    private void initview()
    {
        et_ptopic = findViewById(R.id.et_ptopic);
        et_pmsg = findViewById(R.id.et_pmessage);
        et_pqos = findViewById(R.id.et_pqos);
        et_retained = findViewById(R.id.et_retained);
        btn_publish = findViewById(R.id.btn_publish);
    }

    /**
     * 判断输入框是否为空
     * @return true:不为空 false:为空*/
    private boolean isequals(){
        return !et_ptopic.getText().toString().equals("")&&!et_pqos.getText().toString().equals("")&&! et_pmsg.getText().toString().equals("");
    }
}

7、string.xml

<resources>
    <string name="app_name">MQTT客户端</string>
    <string name="disconnect">断开</string>
    <string name="connect">连接</string>
    <string name="hint_password">请输入密码</string>
    <string name="password">密码</string>
    <string name="hint_username">请输入用户名</string>
    <string name="username">用户名</string>
    <string name="hint_client_id">请输入客户端ID</string>
    <string name="client_id">客户端id</string>
    <string name="hint_address">请输入ip</string>
    <string name="address">IP地址</string>
    <string name="port">端口号</string>
    <string name="hint_port">请输入端口号</string>
    <string name="mqtt_server">MQTT 客户端</string>
    <string name="keep_alive">心跳</string>
    <string name="hint_keep_alive">请输入心跳</string>
    <string name="test">Test</string>
    <string name="tv_subscribe">主题订阅</string>
    <string name="hint_topic">请输入主题名称</string>
    <string name="subscribe">订阅</string>
    <string name="tv_message">消息显示区</string>
    <string name="topic">主题 </string>
    <string name="qos">服务质量</string>
    <string name="hint_qos">请输入服务质量</string>
    <string name="publish">发布</string>
    <string name="hint_message">请输入消息</string>
    <string name="retained">保留</string>
    <string name="hint_retained">true/false</string>

</resources>

四、测试案例

Top