在Android开发中,我们经常需要通过网络请求来获取数据或执行某些操作。但是,网络连接并不总是稳定的,有时候会出现延迟、丢包、断开等问题,这些问题会影响用户的体验和满意度。因此,我们需要对网络连接的稳定性进行测试,找出可能存在的问题,并采取相应的优化措施。
为了测试Android应用在不同网络环境下的表现,我们可以使用一些工具或方法来模拟网络的变化,比如使用[Network Link Conditioner]或[Android Emulator]等。但是,这些工具或方法都有一些局限性,比如不能覆盖所有的网络类型和场景,或者不能实时反映网络状态的变化。
因此,需要一种更灵活和全面的解决方案,来测试Android应用在真实网络环境下的稳定性。本文将介绍一种基于后台服务和定时器的解决方案,它可以在后台持续地发送HTTP请求到一些可靠的服务器,并记录响应的时间和状态码,从而评估网络连接的稳定性。同时,它还可以将测试结果保存到本地文件,并定期上传到远程服务器,方便我们进行分析和比较。
我开始本来想使用tomcat搭建的,但后来发现缺少后台,后面就考虑用scp技术,直接把结果push到服务器。
目标
目标是设计一个Android网络连接稳定性测试解决方案,具体要求如下:
- 可以对不同类型的网络(如WIFI、4G、以太网等)进行测试
- 可以对不同的服务器(如百度、必硬等)进行测试
- 可以记录每次请求的时间、状态码、时间戳等信息
- 可以将测试结果保存到本地文件,并定期上传到远程服务器
- 可以统计测试结果的总次数、成功次数、失败次数、成功率等指标
实现
为了实现这个目标,需要使用以下几个组件:
- NetworkMonitorService:一个后台服务,用来发送HTTP请求,并记录响应的时间和状态码
- FileUploadService:一个后台服务,用来将测试结果保存到本地文件,并上传到远程服务器
- NetworkMonitorReceiver:一个广播接收器,用来启动NetworkMonitorService
- FileUploadReceiver:一个广播接收器,用来启动FileUploadService
- DatabaseHelper:一个数据库操作类,用来插入和查询测试结果
- NetworkHelper:一个网络操作类,用来获取当前的网络类型和IP地址
下面我们分别介绍这些组件的具体实现。
NetworkMonitorService
NetworkMonitorService是一个继承自JobIntentService的后台服务,它可以在后台线程上处理传入的工作请求。重写了它的onHandleWork方法,在这个方法中,做了以下几件事:
- 获取一个OkHttpClient实例,用来发送HTTP请求
- 获取一个DatabaseHelper实例,用来操作本地数据库
- 获取当前的网络类型和IP地址
- 遍历可靠的服务器的URL,依次发送HTTP请求,并记录响应的时间和状态码
- 插入一条数据到数据库,并检查数据量
代码如下:
package com.xxx.factorytest.service; import android.content.Context; import android.content.Intent; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.JobIntentService; import com.xxx.factorytest.utils.DatabaseHelper; import com.xxx.factorytest.utils.NetworkHelper; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class NetworkMonitorService extends JobIntentService { // 定义一个工作ID,用来唯一标识这个服务 private static final int JOB_ID = 1; // 定义最大的数据量 private static final int MAX_DATA_COUNT = 1000; // 定义一些可靠的服务器的URL,用来发送HTTP请求 private static final String[] SERVER_URLS = { "https://www.baidu.com", }; // 定义一个静态方法,用来启动这个服务 public static void startService(Context context) { Log.d("NetworkMonitorService", "startService"); Intent intent = new Intent(context, NetworkMonitorService.class); enqueueWork(context, NetworkMonitorService.class, JOB_ID, intent); } // 重写onHandleWork方法,在后台线程上处理传入的工作请求 @Override protected void onHandleWork(@NonNull Intent intent) { Log.d("NetworkMonitorService", "onHandleWork"); // 获取一个OkHttpClient实例,用来发送HTTP请求 OkHttpClient client = new OkHttpClient(); // 获取一个DatabaseHelper实例,用来操作本地数据库 DatabaseHelper databaseHelper = new DatabaseHelper(this); // 获取当前的网络类型和IP地址 String networkType = NetworkHelper.getCurrentNetworkType(getApplicationContext()); String ipAddress = NetworkHelper.getCurrentIpAddress(getApplicationContext()); // 遍历可靠的服务器的URL,依次发送HTTP请求,并记录响应的时间和状态码 for (String url : SERVER_URLS) { // 创建一个Request对象,用来封装HTTP请求 Request request = new Request.Builder() .url(url) .build(); // 发送异步HTTP请求,并注册一个回调函数 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { // 获取当前时间戳 long timestamp = System.currentTimeMillis(); // 插入一条数据,并检查数据量,时间设置为-1,状态码设置为-1,表示请求失败 databaseHelper.insertData(url, -1, -1, timestamp, networkType + "_" + ipAddress); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { // 处理正常情况,获取响应的时间和状态码 // 计算请求花费的时间,单位为毫秒 long time = response.receivedResponseAtMillis() - response.sentRequestAtMillis(); // 获取响应的状态码 int status = response.code(); // 获取当前时间戳 long timestamp = System.currentTimeMillis(); // 打印日志,显示请求的URL,时间和状态码 Log.d("NetworkMonitorService", "URL: " + url + ", Time: " + time + " ms, Status: " + status); // 插入一条数据,并检查数据量 databaseHelper.insertData(url, time, status, timestamp, networkType + "_" + ipAddress); } }); } } }
FileUploadService
FileUploadService也是一个继承自JobIntentService的后台服务,它可以在后台线程上处理传入的工作请求。我们重写了它的onHandleWork方法,在这个方法中,我们做了以下几件事:
- 将测试结果从数据库中读取出来,并写入到本地文件中
- 使用JSch库,通过SSH协议,将本地文件上传到远程服务器上
代码如下:
package com.xxx.factorytest.service; import android.btf.VendorStorageManager; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Environment; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.JobIntentService; import com.xxx.factorytest.utils.DatabaseHelper; import com.xxx.factorytest.utils.NetworkHelper; import com.xxx.factorytest.utils.Utils; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import com.jcraft.jsch.SftpException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class FileUploadService extends JobIntentService { public static final String LOCAL_FILE_PATH = Environment.getExternalStorageDirectory() + "/"+ getCurrentSystemTime() +"_"+ getCPUSerial()+ ".log"; private static final int JOB_ID = 2; private static final String TAG = "FileUploadService"; public static void startService(Context context) { Intent intent = new Intent(context, FileUploadService.class); enqueueWork(context, FileUploadService.class, JOB_ID, intent); } @Override protected void onHandleWork(@NonNull Intent intent) { writeDataToFile(); uploadFile("username", "123456", "192.168.1.22", 22, LOCAL_FILE_PATH, "/home/work2/network_test/"); } // 获取当前系统时间的函数 private static String getCurrentSystemTime() { // 创建一个SimpleDateFormat对象,指定日期格式为yyyyMMdd SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); // 获取当前日期对象 Date date = new Date(System.currentTimeMillis()); // 格式化日期对象,返回字符串 return simpleDateFormat.format(date); } private void uploadFile(String username, String password, String host, int port, String localFile, String remoteDir) { JSch jsch = new JSch(); Session session = null; try { session = jsch.getSession(username, host, port); session.setPassword(password); session.setConfig("StrictHostKeyChecking", "no"); session.connect(); Log.d(TAG, "Connected to SSH server"); ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp"); sftp.connect(); Log.d(TAG, "Connected to SFTP channel"); sftp.put(localFile, remoteDir + new File(localFile).getName()); Log.d(TAG, "File upload success"); sftp.disconnect(); session.disconnect(); } catch (JSchException | SftpException e) { Log.e(TAG, "File upload failed", e); Log.e(TAG, "Exception: " + e.getMessage()); if (e.getCause() != null) { Log.e(TAG, "Caused by: " + e.getCause().getClass().getName() + ": " + e.getCause().getMessage()); } } } private void writeDataToFile() { Log.d(TAG, "writeDataToFile:"+LOCAL_FILE_PATH); DatabaseHelper databaseHelper = new DatabaseHelper(this); SQLiteDatabase db = databaseHelper.getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT * FROM " + DatabaseHelper.TABLE_NAME, null); File file = new File(LOCAL_FILE_PATH); try (FileOutputStream fos = new FileOutputStream(file)) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); int totalCount = 0; int successCount = 0; int failureCount = 0; while (cursor.moveToNext()) { String url = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_URL)); long time = cursor.getLong(cursor.getColumnIndex(DatabaseHelper.COLUMN_TIME)); int status = cursor.getInt(cursor.getColumnIndex(DatabaseHelper.COLUMN_STATUS)); long timestamp = cursor.getLong(cursor.getColumnIndex(DatabaseHelper.COLUMN_TIMESTAMP)); String network_ip = cursor.getString(cursor.getColumnIndex(DatabaseHelper.COLUMN_NETWORK_IP)); String datetime = sdf.format(new Date(timestamp)); String data = String.format(Locale.getDefault(), "Index:%d,Network:%s,URL:%s,Time:%d ms,Status:%d,Datetime:%s\n", totalCount + 1, network_ip, url, time, status, datetime); fos.write(data.getBytes()); totalCount++; if (status == 200) { successCount++; } else { failureCount++; } } double successRate = (double) successCount / totalCount * 100; String stats = String.format(Locale.getDefault(), "总请求次数: %d, 成功次数: %d, 失败次数: %d, 成功率: %.2f%%\n", totalCount, successCount, failureCount, successRate); fos.write(stats.getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { cursor.close(); db.close(); } } }
NetworkMonitorReceiver
NetworkMonitorReceiver是一个继承自BroadcastReceiver的广播接收器,它可以接收系统或应用发送的广播,并做出相应的处理。我们重写了它的onReceive方法,在这个方法中,我们做了以下一件事:
- 启动一个定时器和一个定时任务,每隔一定时间就启动NetworkMonitorService
代码如下:
package com.xxx.factorytest.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import com.xxx.factorytest.service.NetworkMonitorService; import java.util.Timer; import java.util.TimerTask; public class NetworkMonitorReceiver extends BroadcastReceiver { // 定义一个定时器对象,用来执行定时任务 private Timer timer; // 定义一个定时任务对象,用来启动后台服务 private TimerTask timerTask; // 定义一个常量,表示每隔多少毫秒执行一次 private static final long PERIOD = 1000; // 重写onReceive方法,在接收到广播时启动定时器和定时任务 @Override public void onReceive(Context context, Intent intent) { // Toast.makeText(context, "upload-->", Toast.LENGTH_SHORT).show(); // 创建一个定时器对象 timer = new Timer(); // 创建一个定时任务对象 timerTask = new TimerTask() { @Override public void run() { // 启动后台服务 NetworkMonitorService.startService(context); } }; // 启动定时器和定时任务,延迟0毫秒,每隔1秒执行一次 timer.schedule(timerTask, 0, PERIOD); } }
FileUploadReceiver
FileUploadReceiver也是一个继承自BroadcastReceiver的广播接收器,它可以接收系统或应用发送的广播,并做出相应的处理。我们重写了它的onReceive方法,在这个方法中,我们做了以下一件事:
- 启动一个定时器和一个定时任务,每隔一定时间就启动FileUploadService
代码如下:
package com.xxx.factorytest.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import com.btf.factorytest.service.FileUploadService; import java.util.Timer; import java.util.TimerTask; public class FileUploadReceiver extends BroadcastReceiver { // 定义一个定时器对象,用来执行定时任务 private Timer timer; // 定义一个定时任务对象,用来启动后台服务 private TimerTask timerTask; // 定义一个常量,表示每隔多少毫秒执行一次 private static final long PERIOD = 10000; // 重写onReceive方法,在接收到广播时启动定时器和定时任务 @Override public void onReceive(Context context, Intent intent) { // Toast.makeText(context, "upload-->", Toast.LENGTH_SHORT).show(); // 创建一个定时器对象 timer = new Timer(); // 创建一个定时任务对象 timerTask = new TimerTask() { @Override public void run() { // 启动后台服务 FileUploadService.startService(context); } }; // 启动定时器和定时任务,延迟0毫秒,每隔10秒执行一次 timer.schedule(timerTask, 0, PERIOD); } }
DatabaseHelper
DatabaseHelper是一个继承自SQLiteOpenHelper的数据库操作类,它可以创建和管理本地数据库。我们重写了它的onCreate和onUpgrade方法,在这些方法中,我们做了以下几件事:
- 创建一个名为network_test的数据库表,用来存储测试结果
- 定义了表中的五个字段:url、time、status、timestamp、network_ip,分别表示请求的URL、请求花费的时间、响应的状态码、请求的时间戳、请求的网络类型和IP地址
- 定义了一些常量,用来表示表名和字段名
我们还定义了两个方法:insertData和queryData,分别用来插入和查询数据。
代码如下:
public class DatabaseHelper extends SQLiteOpenHelper { // 定义数据库的名称和版本号 private static final String DATABASE_NAME = "network_test.db"; private static final int DATABASE_VERSION = 1; // 定义表名和字段名的常量 public static final String TABLE_NAME = "network_test"; public static final String COLUMN_URL = "url"; public static final String COLUMN_TIME = "time"; public static final String COLUMN_STATUS = "status"; public static final String COLUMN_TIMESTAMP = "timestamp"; public static final String COLUMN_NETWORK_IP = "network_ip"; // 定义创建表的SQL语句 private static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + "(" + COLUMN_URL + " TEXT," + COLUMN_TIME + " INTEGER," + COLUMN_STATUS + " INTEGER," + COLUMN_TIMESTAMP + " INTEGER," + COLUMN_NETWORK_IP + " TEXT" + ")"; // 定义删除表的SQL语句 private static final String DROP_TABLE_SQL = "DROP TABLE IF EXISTS " + TABLE_NAME; // 构造方法,调用父类的构造方法,并传入数据库的名称和版本号 public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } // 重写onCreate方法,在创建数据库时执行创建表的SQL语句 @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_TABLE_SQL); } // 重写onUpgrade方法,在升级数据库时执行删除表和创建表的SQL语句 @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP_TABLE_SQL); db.execSQL(CREATE_TABLE_SQL); } // 定义一个插入数据的方法,接收五个参数,分别表示请求的URL、请求花费的时间、响应的状态码、请求的时间戳、请求的网络类型和IP地址,并返回插入结果(true或false) public boolean insertData(String url, long time, int status, long timestamp, String network_ip) { // 获取一个可写的数据库对象 SQLiteDatabase db = getWritableDatabase(); // 创建一个ContentValues对象,用来存放要插入的数据 ContentValues values = new ContentValues(); values.put(COLUMN_URL, url); values.put(COLUMN_TIME, time); values.put(COLUMN_STATUS, status); values.put(COLUMN_TIMESTAMP, timestamp); values.put(COLUMN_NETWORK_IP, network_ip); // 调用insert方法,将数据插入到数据库中,并返回插入结果(-1表示失败,其他表示成功) long result = db.insert(TABLE_NAME, null, values); // 关闭数据库对象 db.close(); // 判断插入结果,如果不等于-1,表示成功,返回true,否则返回false return result != -1; } // 定义一个查询数据的方法,返回一个Cursor对象,用来遍历查询结果 public Cursor queryData() { // 获取一个可读的数据库对象 SQLiteDatabase db = getReadableDatabase(); // 调用query方法,查询所有的数据,并按照时间戳降序排序 Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, COLUMN_TIMESTAMP + " DESC"); // 返回Cursor对象 return cursor; } }
NetworkHelper
NetworkHelper是一个网络操作类,它提供了两个静态方法:getCurrentNetworkType和getCurrentIpAddress,分别用来获取当前的网络类型和IP地址。
代码如下:
public class NetworkHelper { // 定义一个获取当前网络类型的方法,接收一个Context对象作为参数,返回一个字符串表示网络类型 public static String getCurrentNetworkType(Context context) { // 获取一个ConnectivityManager对象,用来管理网络连接 ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); // 获取当前激活的网络信息 NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); // 判断网络信息是否为空,如果为空,表示没有网络连接,返回"NONE" if (networkInfo == null) { return "NONE"; } // 获取当前网络的类型 int type = networkInfo.getType(); // 根据不同的类型,返回不同的字符串 switch (type) { case ConnectivityManager.TYPE_WIFI: return "WIFI"; case ConnectivityManager.TYPE_MOBILE: return "MOBILE"; case ConnectivityManager.TYPE_ETHERNET: return "ETHERNET"; default: return "UNKNOWN"; } } // 定义一个获取当前IP地址的方法,接收一个Context对象作为参数,返回一个字符串表示IP地址 public static String getCurrentIpAddress(Context context) { // 获取一个WifiManager对象,用来管理WIFI连接 WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); // 获取当前WIFI连接的信息 WifiInfo wifiInfo = wifiManager.getConnectionInfo(); // 获取当前WIFI分配的IP地址,是一个整数 int ipAddress = wifiInfo.getIpAddress(); // 将整数转换为字符串,使用点号分隔四个字节 String ip = (ipAddress & 0xFF) + "." + ((ipAddress >> 8) & 0xFF) + "." + ((ipAddress >> 16) & 0xFF) + "." + ((ipAddress >> 24) & 0xFF); // 返回IP地址字符串 return ip; } }
注册组件和申请权限
在AndroidManifest.xml文件中,我们需要注册我们的四个组件:两个后台服务和两个广播接收器,并指定它们的名称和属性。还需要申请一些必要的权限,如访问网络、访问WIFI、访问外部存储等。代码如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.networktest"> <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" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 定义一个后台服务的组件,使用JobIntentService类 --> <service android:name=".service.NetworkMonitorService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" /> <!-- 定义一个后台服务的组件,使用JobIntentService类 --> <service android:name=".service.FileUploadService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false" /> <!-- 定义一个广播接收器的组件,使用FileUploadReceiver类 --> <receiver android:name=".receiver.FileUploadReceiver" android:exported="false"> <!-- 指定要接收的广播类型,比如开机启动,网络变化等 --> <intent-filter> <action android:name="com.example.fileupload.ACTION_START_UPLOAD" /> </intent-filter> </receiver> <receiver android:name=".receiver.NetworkMonitorReceiver" android:exported="false"> <!-- 指定要接收的广播类型,比如开机启动,网络变化等 --> <intent-filter> <action android:name="com.example.networkmonitor.ACTION_START_MONITOR" /> </intent-filter> </receiver>
测试
- 在远程服务器上查看上传的文件,并分析测试结果
总结
本文介绍了一个Android网络连接稳定性测试解决方案,它可以对不同类型的网络和服务器进行测试,并记录和上传测试结果。
希望本文对你有所帮助,如果你有任何问题或建议,欢迎在评论区留言。谢谢!