安卓网络通信(多线程、HTTP访问、图片加载、即时通信)

本章介绍App开发常用的以下网络通信技术,主要包括:如何以官方推荐的方式使用多线程技术,如何通过okhttp实现常见的HTTP接口访问操作,如何使用Dlide框架加载网络图片,如何分别运用SocketIO和WebSocket实现及时通信功能等。

多线程

本节介绍App开发对多线程的几种进阶用法,内容包括如何利用Message配合Handler完成主线程与分线程之间的简单通信,如何通过runOnUiThread方法简化分线程与处理器的通信机制,日和使用工作管理器代替IntentService实现后台任务管理。

分线程通过Handler操作界面

为了使App运行得更流畅,多线程技术被广泛应用于App开发。由于Android规定只有主线程(UI线程)才能直接操作界面,因此分线程若想修改界面就得另想办法,这要求有一种线程之间相互通信得机制。如果时主线程向分线程传递消息,可以在分线程的构造方法中传递参数,然而分线程向主线程传递消息并无捷径,为此Android设计了一个消息工具Message,通过结合Handler与Message能够实现线程间通信。
由分线程向主线程传递消息的过程主要有4个步骤,分别说明如下。

1.在主线程中构造一个处理器对象,并启动分线程

在Android中启动分线程有两种方式:一种是直接调用线程实例的start方法,另一种是通过处理器Handler对象的post方法启动线程实例。

2.在分线程中构造一个Message类型的消息包

Message是线程间通信存放消息的包裹,其作用类似于Intent机制的Bundle工具。消息实例可通过Message的obtain方法获得,比如下面这行代码:

Message message = Message.obtain(); // 获得默认的消息对象

也可以通过处理器对象的obtainMessage方法获得,比如下面这行代码:

Message message = mHandler.obtainMessage(); // 获得处理器的消息对象

获得消息实例之后,再给它补充详细的包裹信息,下面是Message工具的属性说明。
what:整型数,可存放本次消息的唯一标识。
arg1:整形数,可存放消息的处理结果。
arg2:整型数,可存放消息的处理代码。
obj:Object类型,可存放返回消息的数据结构。
replyTo:Messager(回应信使)类型,在跨进程通信中使用,在线程间通信用不着。

3.在分线程中通过处理器对象将Message消息发出去

处理器的消息操作主要包括各种send***方法和remove***方法,下面是这些消息操作方法的使用说明。

  • obtainMessage:获取当前的消息对象。
  • sendMessage:立即发送指定消息。
  • sendMessageDelayed:延迟一段时间后发送指定消息。
  • sendMessageAtTime:在设置的时间点发送指定消息。
  • sendEmptyMessage:立即发送空消息。
  • sendEmptyMessageDelayed:延迟一段时间后发送空消息。
  • sendEmptyMessageAtTime:在设置的时间点发送空消息。
  • removeMessages:从消息队列移除指定标识的消息。
  • hasMessages:判断消息队列是否存在指定标识的消息。

4.主线程的handler对象处理接收到的消息

主线程收到分线程发出的消息之后,需要实现处理器对象的handleMessage方法,在该方法中根据消息内容分别进行相应的处理,因为handleMessage方法在主线程(UI线程)中调用,所以方法内部可以直接操作界面元素。
综合上面的4个线程通信步骤,接下来通过一个实验观察线程间通信的效果。下面便是利用多线程技术实现新闻滚动的活动代码例子,其中结合了Handler与Message。

public class HandlerMessageActivity extends AppCompatActivity implements View.OnClickListener {private TextView tv_message; // 声明一个文本视图对象private boolean isPlaying = false; // 是否正在播放新闻private int BEGIN = 0, SCROLL = 1, END = 2; // 0为开始,1为滚动,2为结束private String[] mNewsArray = { "北斗导航系统正式开通,定位精度媲美GPS","黑人之死引发美国各地反种族主义运动", "印度运营商禁止华为中兴反遭诺基亚催债","贝鲁特发生大爆炸全球紧急救援黎巴嫩", "日本货轮触礁毛里求斯造成严重漏油污染"};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_handler_message);tv_message = findViewById(R.id.tv_message);findViewById(R.id.btn_start).setOnClickListener(this);findViewById(R.id.btn_stop).setOnClickListener(this);}@Overridepublic void onClick(View v) {if (v.getId() == R.id.btn_start) { // 点击了开始播放新闻的按钮if (!isPlaying) { // 如果不在播放就开始播放isPlaying = true;new PlayThread().start(); // 创建并启动新闻播放线程}} else if (v.getId() == R.id.btn_stop) { // 点击了结束播放新闻的按钮isPlaying = false;}}// 定义一个新闻播放线程private class PlayThread extends Thread {@Overridepublic void run() {mHandler.sendEmptyMessage(BEGIN); // 向处理器发送播放开始的空消息while (isPlaying) { // 正在播放新闻try {sleep(2000); // 睡眠两秒(2000毫秒)} catch (InterruptedException e) {e.printStackTrace();}Message message = Message.obtain(); // 获得默认的消息对象//Message message = mHandler.obtainMessage(); // 获得处理器的消息对象message.what = SCROLL; // 消息类型message.obj = mNewsArray[new Random().nextInt(5)]; // 消息描述mHandler.sendMessage(message); // 向处理器发送消息}mHandler.sendEmptyMessage(END); // 向处理器发送播放结束的空消息isPlaying = false;}}// 创建一个处理器对象private Handler mHandler = new Handler(Looper.myLooper()) {// 在收到消息时触发public void handleMessage(Message msg) {String desc = tv_message.getText().toString();if (msg.what == BEGIN) { // 开始播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), "开始播放新闻");} else if (msg.what == SCROLL) { // 滚动播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), msg.obj);} else if (msg.what == END) { // 结束播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), "新闻播放结束");}tv_message.setText(desc);}};
}

运行App,先点击“开始播放新闻”按钮,此时分线程每隔两秒添加一条新闻,正在播放新闻的界面如下图所示。
在这里插入图片描述

稍等片刻再点击“停止播放新闻”按钮,此时主线程收到分线程的END消息,在界面上提示用户“新闻播放结束”,如下图所示。
在这里插入图片描述
根据以上的新闻播放效果,可知分线程的播放开始和播放结束指令都成功送到了主线程。

通过runOnUiThread快速操作界面

因为Android规定分线程不能直接操纵界面,所以它设计了处理程序(Handler)工具,由处理程序负责在主线程和分线程之间传递数据。如果分线程想刷新界面,就得向处理程序发送消息,由处理程序在handleMessage方法中操作控件。举个例子,上一小节“分线程通过Handler操作界面”讲到的通过分线程播报新闻便是经由处理程序操纵文本视图。分线程与处理程序交互的代码片段如下:

// 是否正在播放新闻
private boolean isPlaying = false; // 定义一个新闻播放线程
private class PlayThread extends Thread {@Overridepublic void run() {mHandler.sendEmptyMessage(BEGIN); // 向处理器发送播放开始的空消息while (isPlaying) { // 正在播放新闻try {sleep(2000); // 睡眠两秒(2000毫秒)} catch (InterruptedException e) {e.printStackTrace();}Message message = Message.obtain(); // 获得默认的消息对象//Message message = mHandler.obtainMessage(); // 获得处理器的消息对象message.what = SCROLL; // 消息类型message.obj = mNewsArray[new Random().nextInt(5)]; // 消息描述mHandler.sendMessage(message); // 向处理器发送消息}mHandler.sendEmptyMessage(END); // 向处理器发送播放结束的空消息isPlaying = false;}
}// 创建一个处理器对象
private Handler mHandler = new Handler(Looper.myLooper()) {// 在收到消息时触发public void handleMessage(Message msg) {String desc = tv_message.getText().toString();if (msg.what == BEGIN) { // 开始播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), "开始播放新闻");} else if (msg.what == SCROLL) { // 滚动播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), msg.obj);} else if (msg.what == END) { // 结束播放desc = String.format("%s\n%s %s", desc, DateUtil.getNowTime(), "新闻播放结束");}tv_message.setText(desc);}
};

以上代码定义了一个新闻播放线程,接着主线程启动该线程,启动代码如下:

new PlayThread().start(); // 创建并启动新闻播放线程

上述代码处理分线程与处理程序的交互甚是繁琐,既要区分消息类型,又要来回类型。为此Android提供了一种简单的交互方式,分线程若想操纵界面控件,在线程内部调用runOnUiThread方法即可,调用代码如下:

// 回到主线程(UI线程)操作界面
runOnUiThread(new Runnable() {@Overridepublic void run() {// 操作界面的代码放这里}
});

由于Runnable属于函数式接口,因此调用代码可简化如下:

// 回到主线程(UI线程)操作界面
runOnUiThread(()->{// 操作界面代码放这里
});

倘若Runnable的运行代码只有一行,那么Lambda表达式允许进一步简化,也就是省略外面的花括号,于是精简的代码编程以下这样:

// 回到主线程(UI线程)操作界面
runOnUiThread(()-> /* 如果只有一行代码,那么连花括号也可省掉 */ );

回看之前的新闻播报线程,把原来的消息发送代码系统统统改成runOnUiThread方法,修改后的播放代码如下:

// 是否正在播放新闻
private boolean isPlaying = false; // 播放新闻
private void broadcastNews() {String startDesc = String.format("%s\n%s %s", tv_message.getText().toString(),DateUtil.getNowTime(), "开始播放新闻");// 回到主线程(UI线程)操纵界面runOnUiThread(() -> tv_message.setText(startDesc));while (isPlaying) { // 正在播放新闻try {Thread.sleep(2000); // 睡眠两秒(2000毫秒)} catch (InterruptedException e) {e.printStackTrace();}String runDesc = String.format("%s\n%s %s", tv_message.getText().toString(),DateUtil.getNowTime(), mNewsArray[new Random().nextInt(5)]);// 回到主线程(UI线程)操纵界面runOnUiThread(() -> tv_message.setText(runDesc));}String endDesc = String.format("%s\n%s %s", tv_message.getText().toString(),DateUtil.getNowTime(), "新闻播放结束,谢谢观看");// 回到主线程(UI线程)操纵界面runOnUiThread(() -> tv_message.setText(endDesc));isPlaying = false;
}

从以上代码可见,处理程序的相关代码不见了,取而代之的是一行又一行runOnUiThread方法。
主线程启动播放器线程也只需要下面一行代码就够了:

new Thread(() -> broadcastNews()).start(); // 启动新闻播放线程

改造完毕后运行测试App,可观察到开始新闻播报效果如下图所示:
在这里插入图片描述

停止播放新闻效果如下如图所示:
在这里插入图片描述

工作管理器WorkManager

Android 11不光废弃了AsyncTask,还把IntentService一起废弃了,对于后台的异步服务,官方建议改为使用工作管理器WorkManager。
除了IntentService之外,Android也提供了其他后台任务工具,例如工作调用器JobScheduler、闹钟管理器AlarmManager等。当然,这些后台工具的用法各不相同,徒增开发者的学习时间而已,所以谷歌索性把它们统一起来,在Jetpack库中推出了工作管理器WorkManager。这个WorkManager的兼容性很强,对于Android 6.0或更高版本的系统,它通过JobScheduler完成后台任务;对于Android 6.0以下版本的系统(不含Android 6.0),通过AlarmManager和广播接收器组合完成后台任务。无论采取哪种方案,后台任务最终都是由线程池Executor执行的。
因为WorkManager来自Jetpack库,所以使用之前要修改build.gradle.kts,增加下面一行以来配置:

implementation("androidx.work:work-runtime:2.9.0")

接着定义一个处理后台业务逻辑的工作者,该工作继承自Worker抽象类,就像异步任务需要从IntentService派生而来那样。自定义的工作者必须实现构造方法,并重写doWork方法,其中构造方法可获得外部传来的请求数据,而doWork方法处理具体的业务逻辑。特别注意,由于doWork方法运行于分线程,因此该方法内部不能操作界面控件。自定义工作者的示例代码如下:

public class CollectWork extends Worker {private final static String TAG = "CollectWork";private Data mInputData; // 工作者的输入数据public CollectWork(Context context, WorkerParameters workerParams) {super(context, workerParams);mInputData = workerParams.getInputData();}// doWork内部不能操纵界面控件@Overridepublic Result doWork() {String desc = String.format("请求参数包括:姓名=%s,身高=%d,体重=%f",mInputData.getString("name"),mInputData.getInt("height", 0),mInputData.getDouble("weight", 0));Log.d(TAG, "doWork "+desc);Data outputData = new Data.Builder().putInt("resultCode", 0).putString("resultDesc", "处理成功").build();//Result.success();//Result.failure();return Result.success(outputData); // success表示成功,failure表示失败}
}

然后在活动页面中构建并启动工作任务,详细过程主要分为下列4个步骤:

  1. 构建约束条件
    该步骤说明在哪些情况下才能执行后台任务,也就是运行后台任务的前提条件,此时用到了约束工具Constraints。约束条件的构建代码如下:
// 1、构建约束条件
Constraints constraints = new Constraints.Builder()//.setRequiresBatteryNotLow(true) // 设备电量充足//.setRequiresCharging(true) // 设备正在充电.setRequiredNetworkType(NetworkType.CONNECTED) // 已经连上网络.build();
  1. 构建输入数据
    该步骤把后台任务需要的参数封装到一个数据对象,此时用到了数据工具Data,构建输入数据的示例代码如下:
// 2、构建输入数据
Data inputData = new Data.Builder().putString("name", "小明").putInt("height", 180).putDouble("weight", 80).build();
  1. 构建工作请求
    该步骤把约束条件、输入数据等请求内容组装起来。此时用到了工作请求工具OneTimeWorkRequest,构建工作请求的示例代码如下:
// 3、构建一次性任务的工作请求。OneTimeWorkRequest表示一次性任务,PeriodicWorkRequest表示周期性任务
String workTag = "OnceTag";
OneTimeWorkRequest onceRequest = new OneTimeWorkRequest.Builder(CollectWork.class).addTag(workTag) // 添加工作标签.setConstraints(constraints) // 设置触发条件.setInputData(inputData) // 设置输入参数.build();
UUID workId = onceRequest.getId(); // 获取工作请求的编号
  1. 执行工作请求
    该步骤生成工作管理器实例,并将步骤3的工作请求对象加入管理器的执行队列中,由管理器调度并执行请求任务,执行工作请求的实例代码如下:
// 4、执行工作请求
WorkManager workManager = WorkManager.getInstance(this);
workManager.enqueue(onceRequest); // 将工作请求加入执行队列

工作管理器不知拥有enqueue方法,还有其他的调度方法,常用的几个方法分别说明如下:

  • enqueue:将工作请求加入执行队列中。
  • cancelWorkById:取消指定编号(步骤3 getId方法返回workId)的工作。
  • cancelAllWorkByTag:取消指定标签(步骤3设置的workTag)的所有工作。
  • cancelAllWork:取消所有工作。
  • getWorkInfoByIdLiveData:获取指定编号的工作信息。

鉴于后台任务是异步执行的,因此若想知晓工作任务的处理结果,就得调用getWorkInfoByIdLiveData方法,获取工作信息并实时监听它的运行情况。查询工作结果的示例代码:

// 获取指定编号的工作信息,并实时监听工作的处理结果
workManager.getWorkInfoByIdLiveData(workId).observe(this, workInfo -> {Log.d(TAG, "workInfo:" + workInfo.toString());if (workInfo.getState() == WorkInfo.State.SUCCEEDED) { // 工作处理成功Data outputData = workInfo.getOutputData(); // 获得工作信息的输出数据int resultCode = outputData.getInt("resultCode", 0);String resultDesc = outputData.getString("resultDesc");String desc = String.format("工作处理结果为:resultCode=%d,resultDesc=%s",resultCode, resultDesc);tv_result.setText(desc);}
});

至此,工作管理器的任务操作步骤都过了一遍。有的读者可能会发现,步骤3的工作请求类的名称为OneTimeWorkRequest,读起来像是一次性工作。其实工作管理器不止支持设定依次性工作,也支持设定周期性工作,此时用到的工作请求名为PeriodicWorkRequest,构建的示例代码如下:

// 构建周期性任务的工作请求。周期性任务的间隔时间不能小于15分钟
String workTag = "PeriodTag";
PeriodicWorkRequest periodRequest = new PeriodicWorkRequest.Builder(CollectWork.class, 15, TimeUnit.MINUTES).addTag(workTag) // 添加工作标签.setConstraints(constraints) // 设置触发条件.setInputData(inputData) // 设置输入参数.build();
UUID workId = periodRequest.getId(); // 获取工作请求的编号

最后在活动页面中继承工作管理器,运行App后点击启动按钮,执行结果如下图所示。
在这里插入图片描述

HTTP访问

本节介绍okhttp在App接口访问中的详细用法,内容包括如何利用移动数据格式JSON封装结构信息,以及如何从JSON串解析得结构对象;通过okhttp调用HTTP接口得三种方式(GET方式、表单格式得POST请求、JSON格式得POST请求);如何使用okhttp下载网络文件,以及如何将本地文件上传到服务器。

移动数据格式JSON

网络通信的交互数据格式有两大类,分别是JSON和XML,前者短小精悍,后者表现力丰富。对于App来说,基本采用JSON格式于服务器通信。原因很多,一个是手机流量很贵,表达同样的信息,JSON串比XML串短很多,在节省流量方面占了上风;另一个是JSON串解析得很快,也更省电,XML不但慢而且耗电。于是,JSON格式成了移动端事实上的网络数据格式标准。
先来看个购物订单的JSON串例子:

{"user_info": {"name": "思无邪","address": "桃花岛水帘洞123号","phone": "12345678901"},"goods_list": [{"goods_name": "Mate70","goods_number": 1,"goods_price": 10086},{"goods_name": "小米15","goods_number": 1,"goods_price": 8888},{"goods_name": "oneplus13","goods_number": 3,"goods_price": 6666}]
}

从以上JSON串的内容可以梳理出它的基本格式定义,详细说明如下:

  1. 整个JSON串由一对花括号包裹,并且内部的每个结构都以花括号包起来。
  2. 参数格式类似键值对,其中键名与键值以冒号分隔,形如“键名:键值”。
  3. 两个键值对之间以逗号分隔。
  4. 键名需要用双引号引起来,键值为数字的话则无需双引号,为字符串的话仍需双引号。
  5. JSON数组通过方括号表达,方括号内部依次罗列各个元素,具体格式形如“数组的键名:[元素1,元素2,元素3]”。

针对JSON字符串,Android提供了JSON解析工具,支持JSONObject(JSON对象)和
(JSON数组)的解析处理。

1.JSONObject

下面是JSONObject的常用方法。

  • JSONObject构造函数:从指定字符串构造一个JSONObject对象。
  • getJSONObject:获取指定名称的JSONObject对象。
  • getString:获取指定名称的字符串。
  • getInt:获取指定名称的整型数。
  • getDouble:获取指定名称的双精度数。
  • getBoolean:获取指定名称的布尔数。
  • getJSONArray:获取指定名称的JSONArray数组对象。
  • put:添加一个JSONObject对象。
  • toString:把当前的JSONObject对象输出为一个JSON字符串。

2.JSONArray

下面是JSONArray的常用方法。

  • length:获取JSONArray数组长度。
  • getJSONObject:获取JSONArray数组在指定位置的JSONObject对象。
  • put:往JSONArray数组中添加一个JSONObject对象。

虽然Android自带的JSONObject和JSONArray能够解析JSON串,但是这种手工解析实在太麻烦,费时费力还容易犯错,故而谷歌公司推出了专门的GSon支持库,方便开发者快速处理JSON串。
由于Gson是第三方库,因此首先要修改build.gradle.kts文件,往dependencies节点添加下面一行配置,表示导入指定版本的Gson库:

implementation("com.google.code.gson:gson:2.10")

接着在Java代码文件的头部添加如下一行导入语句,表示后面会用到Gson工具:

import com.google.gson.Gson;

完成上述两个步骤,就能在代码中调用Gson的各种处理方法了。Gson常见的应用场合主要有下列两个:

  1. 将数据对象转换为JSON字符串。此时可调用Gson工具的toJson方法,把指定的数据对象转换为JSON字符串。
  2. 从JSON字符串解析出数据对象。此时可调用Gson工具的fromJson方法,从JSON字符串解析得到指定类型的数据对象。

下面是通过Gson库封装与解析JSON串的活动代码例子:

public class JsonConvertActivity extends AppCompatActivity {private TextView tv_json; // 声明一个文本视图对象private UserInfo mUser; // 声明一个用户信息对象private String mJsonStr; // JSON格式的字符串@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_json_convert);mUser = new UserInfo("阿四", 25, 165L, 50.0f); // 创建用户实例mJsonStr = new Gson().toJson(mUser); // 把用户实例转换为JSON串tv_json = findViewById(R.id.tv_json);findViewById(R.id.btn_origin_json).setOnClickListener(v -> {mJsonStr = new Gson().toJson(mUser); // 把用户实例转换为JSON字符串tv_json.setText("JSON串内容如下:\n" + mJsonStr);});findViewById(R.id.btn_convert_json).setOnClickListener(v -> {// 把JSON串转换为UserInfo类型的对象UserInfo newUser = new Gson().fromJson(mJsonStr, UserInfo.class);String desc = String.format("\n\t姓名=%s\n\t年龄=%d\n\t身高=%d\n\t体重=%f",newUser.name, newUser.age, newUser.height, newUser.weight);tv_json.setText("从JSON串解析而来的用户信息如下:" + desc);});}
}

运行App,先点击“原始JSON串”按钮,把用户对象转换为JSON字符串,此时JSON界面如下图所示,可见包含用户信息的JSON字符串。
在这里插入图片描述
接着点击“转换JSON串”按钮,将JSON字符串转换为用户对象,此时JSON界面如下图所示,可见用户对象的各字段值。
在这里插入图片描述

通过okhttp调用HTTP

尽管使用HttpURLConnection能够实现大多数的网络访问操作,但是它的用法实在繁琐,很多细节都要开发者关注,一不留神就可能导致访问异常。于是各种网络开源框架纷纷涌现,比如声明显赫的Apache的HttpClient、Square的okhttp。Android从9.0开始正式弃用HttpClient,使得okhttp成为App开发流行的网络框架。
因为okhttp属于第三方框架,所以使用之前要修改build.gradle.kts,增加下面一行依赖配置:

implementation("com.squareup.okhttp3:okhttp:4.9.3")

当然访问网络之前得先申请上网权限,也就是在AndroidManifest.xml里面补充以下权限:

<uses-permission android:name="android.permission.INTERNET" />

除此之外,从Android 9开始默认只能访问https开头的安全地址,不能直接访问以http开头的网络地址。如果应用仍想访问http开头的普通地址,就是修改AndroidManifest.xml,给application节点添加如下属性,表示继续使用http明文地址:

android:usesCleartextTraffic="true"

okhttp的网络访问功能十分强大,单就HTTP接口调用而言,它就支持三种访问方式:GET方式的请求,表单格式的POST请求、JSON格式的POST请求,下面分别进行说明。

1.GET方式的请求

不管是GET方式还是POST方式,okhttp在访问网络时都离不开下面4个步骤:

  1. 使用OkHttpClient类创建一个okhttp客户端对象。创建客户端对象的示例代码如下:
OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象
  1. 使用Request类创建一个GET和POST方式的请求结构。采取GET方式时调用get方法,采取POST方法时调用post方法。此外,需要指定本次请求的网络地址,还可以添加个性化HTTP头部信息。
    创建请求结构的示例代码如下:
// 创建一个GET方式的请求结构
Request request = new Request.Builder()//.get() // 因为OkHttp默认采用get方式,所以这里可以不调get方法.header("Accept-Language", "zh-CN") // 给http请求添加头部信息.header("Referer", "https://finance.sina.com.cn") // 给http请求添加头部信息.url(URL_STOCK) // 指定http请求的调用地址.build();
  1. 调用步骤1中客户端对象的newCall方法,方法参数为步骤2中的请求结构,从而创建Call类型的调用对象。创建调用对象的实例代码如下:
Call call = client.newCall(request); // 根据请求结构创建调用对象
  1. 调用步骤3中Call对象的enqueue方法,将本次请求加入HTTP访问的执行队列中,并编写请求失败与请求成功两种情况的处理代码。加入执行队列的示例代码如下:
// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法
call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n"+resp));}
});

综合上述4个步骤,接下来以查询上证指数为例,来熟悉okhttp的完整使用过程。上证指数的查询接口来自新浪网的证券板块,具体的接口调用代码如下:

// 发起GET方式的HTTP请求
private void doGet() {OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request = new Request.Builder()//.get() // 因为OkHttp默认采用get方式,所以这里可以不调get方法.header("Accept-Language", "zh-CN") // 给http请求添加头部信息.header("Referer", "https://finance.sina.com.cn") // 给http请求添加头部信息.url(URL_STOCK) // 指定http请求的调用地址.build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用股指接口返回:\n"+resp));}});
}

运行测试App,可观察到上证指数的查询结果如下图所示。
在这里插入图片描述

2.表单格式的POST请求

对于okhttp来说,POST方式与GET方式的调用过程大同小异,主要区别于如何让创建请求结构。除了通过post方法表示本次请求采取POST方式外,还要给post方法填入请求参数,比如表单格式的请求参数放在FormBody结构中,示例代码如下:

String username = et_username.getText().toString();
String password = et_password.getText().toString();
// 创建一个表单对象
FormBody body = new FormBody.Builder().add("username", username).add("password", password).build();
// 创建一个POST方式的请求结构
Request request = new Request.Builder().post(body).url(URL_LOGIN).build();

以登录功能为例,用户在界面上输入用户名和密码,然后点击登录按钮时,App会把用户名和密码封装进FormBody结构后提交给后端服务器。采取表单格式的登录代码如下:

// 发起POST方式的HTTP请求(报文为表单格式)
private void postForm() {String username = et_username.getText().toString();String password = et_password.getText().toString();// 创建一个表单对象FormBody body = new FormBody.Builder().add("username", username).add("password", password).build();OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个POST方式的请求结构Request request = new Request.Builder().post(body).url(URL_LOGIN).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));}});
}

确保服务端的登录接口正常开启(点击查看服务端程序),并且手机和计算机连接同一个WiFi,再运行测试App。打开登录页面,填入登录信息然后点击“发起接口调用”按钮,接收到服务器端返回的数据,如下图所示,可见表单格式的POST请求被正常调用。
在这里插入图片描述

3.JSON格式的POST请求结果

由于表单格式不能传递复杂的数据,因此App在与服务端交互时经常使用JSON格式。设定好JSON串的字符编码后再放入RequestBody结构中,示例代码如下:

// 创建一个POST方式的请求结构
RequestBody body = RequestBody.create(jsonString, MediaType.parse("text/plain;charset=utf-8"));
Request request = new Request.Builder().post(body).url(URL_LOGIN).build();

仍以登录功能为例,App先将用户名和密码组装进JSON对象,再把JSON对象转为字符串,后续便是常规的okhttp调用过程了。采取JSON格式的登录代码示例如下:

// 发起POST方式的HTTP请求(报文为JSON格式)
private void postJson() {String username = et_username.getText().toString();String password = et_password.getText().toString();String jsonString = "";try {JSONObject jsonObject = new JSONObject();jsonObject.put("username", username);jsonObject.put("password", password);jsonString = jsonObject.toString();} catch (Exception e) {e.printStackTrace();}// 创建一个POST方式的请求结构RequestBody body = RequestBody.create(jsonString, MediaType.parse("text/plain;charset=utf-8"));OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象Request request = new Request.Builder().post(body).url(URL_LOGIN).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用登录接口返回:\n"+resp));}});
}

同样确保服务端的登录接口正常开启(点击查看服务端程序),并且手机和计算机连接同一个WiFi,再运行测试该App。打开登陆界面,填入登录信息后点击“发起接口调用”按钮,接收到服务端返回的数据,如下图所示,可见JSON格式的POST请求被正常调用。
在这里插入图片描述

使用okhttp下载和上传文件

okhttp不但简化了HTTP接口的调用过程,连下载文件都变简单了。对于一般的文件下载,按照常规的GET方式调用流程,只要重写回调方法onResponse,在该方法中通过应答对象的body方法即可获得应答的数据包对象,调用数据包对象的string方法即可获得到文本形式的字符串,调用数据包对象的byteStream方法即可得到InputStream类型的输入流对象,从输入流就能读出原始的二进制数据。
以下载网络图片为例,位图工具BitmapFactory刚好提供了decodeStream方法,允许直接从输入流中解码获取位图对象。此时通过okhttp下载图片的示例代码如下:

private final static String URL_IMAGE = "https://img-blog.csdnimg.cn/2018112123554364.png";// 下载网络图片
private void downloadImage() {tv_progress.setVisibility(View.GONE);iv_result.setVisibility(View.VISIBLE);OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request = new Request.Builder().url(URL_IMAGE).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("下载网络图片报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) { // 请求成功InputStream is = response.body().byteStream();// 从返回的输入流中解码获得位图数据Bitmap bitmap = BitmapFactory.decodeStream(is);String mediaType = response.body().contentType().toString();long length = response.body().contentLength();String desc = String.format("文件类型为%s,文件大小为%d", mediaType, length);// 回到主线程操纵界面runOnUiThread(() -> {tv_result.setText("下载网络图片返回:"+desc);iv_result.setImageBitmap(bitmap);});}});
}

回到活动代码中调用downloadImage方法,再运行并测试App,可观察到图片下载结果如下图所示,可见网络图片成功下载并显示了出来。
在这里插入图片描述
当然,网络文件不只是图片,还有其他各式各样的文件,这些文件没有专门的解码工具,只能从输入流老老实实地读取字节数据。不过读取字节数据有个好处,就是能够根据自己读写的数据长度计算下载进度,特别是在下载大文件的时候,实际展示当前的下载进度非常有用。下面是通过okhttp下载普通文件的示例代码:

// 下载网络文件
private void downloadFile() {tv_progress.setVisibility(View.VISIBLE);iv_result.setVisibility(View.GONE);OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request = new Request.Builder().url(URL_MP4).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("下载网络文件报错:"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) { // 请求成功String mediaType = response.body().contentType().toString();long length = response.body().contentLength();String desc = String.format("文件类型为%s,文件大小为%d", mediaType, length);// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("下载网络文件返回:"+desc));String path = String.format("%s/%s.mp4",getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),DateUtil.getNowDateTime());// 下面从返回的输入流中读取字节数据并保存为本地文件try (InputStream is = response.body().byteStream();FileOutputStream fos = new FileOutputStream(path)) {byte[] buf = new byte[100 * 1024];int sum=0, len=0;while ((len = is.read(buf)) != -1) {fos.write(buf, 0, len);sum += len;int progress = (int) (sum * 1.0f / length * 100);String detail = String.format("文件保存在%s。已下载%d%%", path, progress);// 回到主线程操纵界面runOnUiThread(() -> tv_progress.setText(detail));}} catch (Exception e) {e.printStackTrace();}}});
}

回到活动代码调用downloadFile方法,再运行测试该App,可观察到文件下载结果如下图所示。
在这里插入图片描述
okhttp不仅让下载文件变简单了,还让上传文件变得更加灵活易用。修改个人资料上传头像图片、在朋友圈发动态视频等都用到了文件上传功能,并且上传文件常常带着文字说明,比如上传头像时可能一并修改了昵称、发布视频时附加了视频描述,甚至可能同时上传多个文件等。
像这种组合上传的业务场景,倘若使用HttpUTLConnection编码就难了,有了okhttp就好办多了。它引入分段结构MultipartyBody及其建造器,并提供了名为addFormDataPart的两种重载方法,分别适用于文本格式与文件格式的数据。带两个参数的addFormDataPart方法,它的第一个参数是字符串的键名,第二个参数是字符串的键值,该方法用来传递文本消息。带三个参数的addFormDataPart方法,它的第一个参数是文件类型,第二个参数是文件名,第三个参数是文件体。
举个带头像进行用户注册的例子,既要把用户和密码发送给服务端,也要把头像图片传给服务端,此时需要多次调用addFormDataPart方法,并通过POST方式提交数据。虽然存在文件上传的交互操作,但整体操作流程与POST方式调用接口保持一致,唯一啥区别在于请求结构由MultipartyBody生成,下面是上传文件之时根据MultipartyBody构建请求结构的代码模板:

// 创建分段内容的建造器对象
MultipartBody.Builder builder = new MultipartBody.Builder();
// 往建造器对象添加文本格式的分段数据
builder.addFormDataPart("username", username);
builder.addFormDataPart("password", password);
File file = new File(path); // 根据文件路径创建文件对象
// 往建造器对象添加图像格式的分段数据
builder.addFormDataPart("image", file.getName(),RequestBody.create(file, MediaType.parse("image/*")));
RequestBody body = builder.build(); // 根据建造器生成请求结构
// 创建一个POST方式的请求结构
Request request = new Request.Builder().post(body).url(URL_REGISTER).build();

合理的文件上传代码要求具备容错机制,譬如判断文本内容是否为空、不能上传空文件、支持上传多个文件等。综合考虑之后,重新编写文件上传部分的示例代码如下:

private List<String> mPathList = new ArrayList<>(); // 头像文件的路径列表// 执行文件上传动作
private void uploadFile() {// 创建分段内容的建造器对象MultipartBody.Builder builder = new MultipartBody.Builder();String username = et_username.getText().toString();String password = et_password.getText().toString();if (!TextUtils.isEmpty(username)) {// 往建造器对象添加文本格式的分段数据builder.addFormDataPart("username", username);builder.addFormDataPart("password", password);}for (String path : mPathList) { // 添加多个附件File file = new File(path); // 根据文件路径创建文件对象// 往建造器对象添加图像格式的分段数据builder.addFormDataPart("image", file.getName(),RequestBody.create(file, MediaType.parse("image/*")));}RequestBody body = builder.build(); // 根据建造器生成请求结构OkHttpClient client = new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个POST方式的请求结构Request request = new Request.Builder().post(body).url(URL_REGISTER).build();Call call = client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用,并设置接口应答的回调方法call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用注册接口报错:\n"+e.getMessage()));}@Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp = response.body().string();// 回到主线程操纵界面runOnUiThread(() -> tv_result.setText("调用注册接口返回:\n"+resp));}});
}

确保服务端的注册接口正常开启(点击查看服务端程序),并且手机和计算机连接同一个WiFi,再运行测试该App。打开初始的注册界面,如下图所示。
在这里插入图片描述
依次输入用户名称和密码,跳转到相册选择头像图片,然后点击“注册”按钮,接收到服务器的数据,如谢图所示,可见服务端正常收到了注册信息与头像图片。
在这里插入图片描述

图片加载

本节介绍App加载网络图片的相关技术:首先描述如何利用第三方的Glide库加载网络图片;然后阐述图片加载框架的三级缓存机制,以及如何有效地运用Glide地缓存功能;最后讲述如何使用Glide加载特殊图像(GIF动图、视频封面等)。

使用Glide加载网络图片

上一小节通过异步任务获取网络图片,尽管能够实现图片加载功能,但是编码过程仍显繁琐。如果方便而又快速地显示网络图片,一直是安卓网络编程的热门课题,前些年图片加载框架Piecasso、Fresco等大行其道,以至于谷歌也按耐不住开发了自己的Glide来源库。由于Android本身就是谷歌开发的,Glide与Android系出同门,因此Glide成为事实上的官方推荐图片加载框架。不过Glide并未集成到Android的SDK中,开发者需要另外给App工程导入Glide库,也就是修改模块的build.gradle.kts,在dependencies节点内部添加如下一行依赖库配置:

implementation("com.github.bumptech.glide:glide:4.13.0")

导包完成之后,即可在代码中正常使用Glide。当然Glide的用法确实简单,默认情况下只要以下这行代码就够了:

Glide.with(活动实例).load(网址字符串).into(图像视图);

可见Glide的图片加载代码至少需要3个参数,说明如下:

  1. 当前页面的活动实例,参数类型为Activity。如果是在页面代码内部调用,则填写this表示当前活动即可。
  2. 网络图片的谅解地址,以http或者https大头,参数类型为字符串。
  3. 准备显示网络图片的图像视图实例,参数类型为ImageView。

假设在Activity内部调用Glide,且图片链接放在mImageUrl,演示的图像视图名为iv_network,那么实际的Glide加载代码是下面这样的:

Glide.with(this).load(mImageUrl).into(iv_network);

如果不指定图像视图的缩放类型,Glide默认采用FIT_CENTER方式显示图片,相当于在load方法和into方法中间增加调用fitCenter方法,迹象如下代码这般:

// 显示方式为容纳居中fitCenter
Glide.with(this).load(mImageUrl).fitCenter().into(iv_network);

除了fitCenter方法,Glide还提供了centerCrop方法对应CENTER_CROP,提供了centerInside方法对应CENTER_INSIDE,其中增加centerCrop方法的加载代码如下:

// 显示方式为居中剪裁centerCrop
Glide.with(this).load(mImageUrl).centerCrop().into(iv_network);

增加centerInside方法的加载代码如下:

// 显示方式为居中入内centerInside
Glide.with(this).load(mImageUrl).centerInside().into(iv_network);

另外,Glide还支持圆形裁剪,也就是只显示图片中央的圆形区域,此时方法调用改成零零circleCrop,具体代码实例如下:

// 显示方式为圆形剪裁circleCrop
Glide.with(this).load(mImageUrl).circleCrop().into(iv_network);

以上四种显示效果如下图所示。
在这里插入图片描述
虽然Glide支持上述4种显示类型,但它无法设定FIT_XY对应的平铺方式,若想让图片平铺至充满整个图像视图,还得调用图像视图的setScaleType方法,将缩放类型设置为ImageView.ScaleType.FIT_XY。
一旦把图像视图的缩放类型改为FIT_XY,则之前的4种显示方式也将呈现不一样的景象,缩放类型变更后的界面分别如下图所示。
在这里插入图片描述

利用Glide实现图片的三级缓存

图片加载框架之所以高效,是因为它不但封装了访问网络的步骤,而且引入了三级缓存机制。具体来说,是先到内存(运存)中查找图片,有找到就直接显示内存图片,没找到的话再去磁盘(闪存)查找图片;在磁盘能找到就直接显示磁盘图片,没找到的话再去请求网络;如此便形成“内存->磁盘->网络”的三级缓存,完整的缓存流程如下图:
在这里插入图片描述

对于Glide而言,默认已经开启了三级缓存机制,当然也可以根据实际情况另行调整。除此之外,Glide还提供了一些个性化的功能,方便开发者定制不同场景的需求。具体到编码上,则需想办法将个性化选项告知Glide,比如下面这段图片加载代码:

Glide.with(this).load(mImageUrl).into(iv_network);

可以拆分为以下两行代码:

// 构建一个加载网络图片的建造器
RequestBuilder<Drawable> builder = Glide.with(this).load(mImageUrl);
builder.into(iv_network);

原来load方法返回的是请求建造器,调用建造器对象的into方法,方能在图像视图上展示网络图片。除了into方法,建造器RequestBuilder还提供了apply方法,该方法表示启用指定的请求选项。于是添加了请求选项的完整代码示例如下:

// 构建一个加载网络图片的建造器
RequestBuilder<Drawable> builder = Glide.with(this).load(mImageUrl);
RequestOptions options = new RequestOptions(); // 创建Glide的请求选项
// 在图像视图上展示网络图片。apply方法表示启用指定的请求选项
builder.apply(options).into(iv_network);

可见请求选项为RequestOptions类型,详细的选项参数就交给它的下列方法了:

  • placeholder:设置加载开始的占位图。在得到网络图片之前,会先在图像视图上展现占位图。

  • error:设置发生错误的提示图。网络图片获取失败之时,会在图像视图上展现提示图。

  • override:设置图片的尺寸。注意该方法有多个重载方法,倘若调用只有一个参数的方法并设置Target.SIZE_ORIGINAL,表示展示原始图片;倘若调用拥有两个参数的方法,表示先将图片缩放到指定的宽度和高度,再展示缩放后的图片。

  • diskCacheStrategy:设置指定的缓存策略。各种缓存策略的取值见下表。
    | DiskCacheStrategy类的缓存策略 | 说明 |
    |–|–|
    | AUTOMATIC | 自动选择缓存策略 |
    | NONE | 不缓存图片 |
    | DATA | 只缓存原始图片 |
    | RESOURCE | 只缓存压缩后的图片 |
    | ALL | 同时缓存原始图片和压缩图片 |

  • skipMemoryCache:设置是否跳过内存(但不影响硬盘缓存)。为true表示跳过,为false则表示不跳过。

  • disallowHardwareConfig:关闭硬件加速,防止过大尺寸的图片加载报错。

  • fitCenter:保持图片的宽高比例并居中显示,图片需要顶到某个方向的边界但不能越过边界,对应缩放类型FIT_CENTER。

  • centerCrop:把排斥图片的宽高比例,充满整个图像视图,裁剪之后居中显示,对应缩放类型CENTER_CROP。

  • centerInside:保持图片的宽高比例,在图像视图内部居中显示,图片只能拉小不能拉大,对应缩放类型CENTER_INSIDE。

  • circleCrop:展示圆形裁剪之后的图片。

另外,Glide允许播放器加载过程的渐变动画,让图片从迷雾中逐渐变得清晰,有助于提高用户体验。这个渐变动画通过建造器的transition方法设置,调用代码示例如下:

// 设置时长3秒的渐变动画
builder.transition(DrawableTransitionOptions.withCrossFade(3000)); 

加载网络图片的渐变效果如下图所示。
在这里插入图片描述

使用Glide加载特殊图像

从Android 9.0开始增加了新的图像解码器ImageDecoder,该解码器支持直接读取GIF文件的图形数据,结合图形工具Animatable即可在图像视图上显示GIF动图。虽然通过ImageDecoder能够在界面上播放GIF动画,但是一方面实现代码有些臃肿,另一方面在Android 9.0之后才支持,显然不太好用。现在有了Glide,轻松加载GIF动图不在话下,简简单单只需下面一行代码:

Glide.with(this).load(R.drawable.happy).into(iv_cover);

使用Glide播放GIF动画的效果如下图所示:
在这里插入图片描述
除了支持GIF动画之外,Glide甚至还能自动加载视频封面,也就是把某个视频文件的首帧画面渲染到图像视图上。这个功能可谓是非常实在,先展示视频封面,等用户点击再开始播放,可以有效防止资源浪费。以加载本地视频的封面为例,首先到系统视频库中挑选某个视频,得到该视频的Uri对象后采用Glide加载,即可在图像上显示视频封面。视频挑选与封面加载代码示例如下:

// 注册一个善后工作的活动结果启动器,获取指定类型的内容
ActivityResultLauncher launcher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {if (uri != null) { // 视频路径非空,则加载视频封面Glide.with(this).load(uri).into(iv_cover);}
});
findViewById(R.id.btn_local_cover).setOnClickListener(v -> launcher.launch("video/*"));

使用Glide加载视频封面的效果如下图:
在这里插入图片描述
Glide不仅能加载本地视频的封面,还能加载网络视频的封面。当然,由于下载网络视频很消耗带宽,因此要事先指定视频帧所处的时间点,这样Glide只会加载该位置的视频画面,无需下载整个视频。指定视频的时间点,用到了RequestOptions类的frameOf方法,具体的请求参数构建代码如下:

// 获取指定时间点的请求参数
private RequestOptions getOptions(int position) {// 指定某个时间位置的帧,单位微秒RequestOptions options = RequestOptions.frameOf(position*1000*1000);// 获取最近的视频帧options.set(VideoDecoder.FRAME_OPTION, MediaMetadataRetriever.OPTION_CLOSEST);// 执行从视频帧到位图对象的转换操作options.transform(new BitmapTransformation() {@Overrideprotected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {return toTransform;}@Overridepublic void updateDiskCacheKey(MessageDigest messageDigest) {try {messageDigest.update((getPackageName()).getBytes(StandardCharsets.UTF_8));} catch (Exception e) {e.printStackTrace();}}});return options;
}

接着调用Glide的apply方法设置请求参数,并加载网络视频的封面图片,详细的加载代码示例如下:

// 加载第10秒处的视频画面
findViewById(R.id.btn_network_one).setOnClickListener(v -> {// 获取指定时间点的请求参数RequestOptions options = getOptions(10);// 加载网络视频的封面图片Glide.with(this).load(URL_MP4).apply(options).into(iv_cover);
});
// 加载第45秒处的视频画面
findViewById(R.id.btn_network_nine).setOnClickListener(v -> {// 获取指定时间点的请求参数RequestOptions options = getOptions(45);// 加载网络视频的封面图片Glide.with(this).load(URL_MP4).apply(options).into(iv_cover);
});

Glide加载网络视频封面的效果如下图所示。
在这里插入图片描述

即时通信

本节介绍App开发即时通信方面的几种进阶用法,内容包括:如何通过SocketIO在两台设备之间传输文本消息;如何通过Socket IO在两台设备之间传输图片消息;SocketIO的局限性和WebSocket协议,以及如何利用WebSocket更方便在设备之间传输各类消息。

通过SocketIO传输文本消息

虽然HTTP协议能够满足多数常见的接口交互,但是它属于短链接,每次调用完就自动断开连接,并且HTTP协议区分了服务端和客户端,双方的通信过程是单向的,只有客户端可以请求服务端,服务端无法向客户端推送消息。基于这些特点,HTTP协议仅能用于一次性的接口访问,而不适用于点对点的即时通信功能。
即时通信技术需要满足两方面的基本条件:一方面是长连接,以便在两台设备间持续通信,避免频繁的”连接-断开“再”连接-断开“如此反复而造成资源浪费;另一方面支持双向交流,既允许A设备主动向B设备发消息,又允许B设备主动向A设备发消息。这要求在套接字Socket层面进行通信,Socket连接一旦成功连上,便默认维持连接,直到有一方主动断开。而且Socket服务端支持向客户端的套接字推送消息,从而实现双向通信功能。
可是Java的Socket百年城比较繁琐,不仅要自行编写线程通信与IO处理的代码,还要自己定义数据包的内部格式以及编解码。为此,出现了第三方Socket通信框架SocketIO,该框架提供服务端和客户端的依赖包,大大简化了SocketIO,要先引入相关JAR包(点击查看服务端程序),接着编写如下的main方法监听文本发送事件:

public static void main(String[] args) {Configuration config = new Configuration();// 如果调用了setHostname方法,就只能通过主机名访问,不能通过IP访问//config.setHostname("localhost");config.setPort(9010); // 设置监听端口final SocketIOServer server = new SocketIOServer(config);// 添加连接连通的监听事件server.addConnectListener(client -> {System.out.println(client.getSessionId().toString()+"已连接");});// 添加连接断开的监听事件server.addDisconnectListener(client -> {System.out.println(client.getSessionId().toString()+"已断开");});// 添加文本发送的事件监听器server.addEventListener("send_text", String.class, (client, message, ackSender) -> {System.out.println(client.getSessionId().toString()+"发送文本消息:"+message);client.sendEvent("receive_text", "不开不开我不开,妈妈没回来谁来也不开。");});// 添加图像发送的事件监听器server.addEventListener("send_image", JSONObject.class, (client, json, ackSender) -> {String desc = String.format("%s,序号为%d", json.getString("name"), json.getIntValue("seq"));System.out.println(client.getSessionId().toString()+"发送图片消息:"+desc);client.sendEvent("receive_image", json);});server.start(); // 启动Socket服务
}

然后服务端执行main方法即可启动Socket服务进行监听。
在客户端继承SocketIO的话,要先修改build.gradle.kts,增加下面一行依赖配置:

implementation("io.socket:socket.io-client:1.0.1")

接着适用SocketIO提供的Socket工具完成消息的收发操作,Socket对象是由IO工具的socket方法获得的,它的常用方法分别说明如下:

  1. connect:建立Socket连接。
  2. connected:判断是否连上Socket。
  3. emit:向服务器提交指定事件的消息。
  4. on:开始监听服务器端推送的事件消息。
  5. off:取消监听服务端的推送的事件消息。
  6. disconnect:断开Socket连接。
  7. close:关闭Socket连接。关闭之后要重新获取新的Socket对象才能连接。

在两部手机之间Socket通信依旧区分发送方与接收方,且二者的消息收发通过Socket服务器中转。对于发送方的App来说,发消息的Socket操作流程:获取Socket对象->调用connect方法->调用emit方法往Socekt服务器发送消息。遂于接收方的App来说,收消息的Sokcet操作流程:获取Socket对象->调用connect方法->调用on方法从服务器接收消息。若想把Socket消息的收发功能集中在一个App上,让它既然充当发送方又充当接收方,则整理后的App消息收发流程如下图所示。
在这里插入图片描述
上图的实线表示代码的调用顺序,虚线表示异步的事件触发,例如用户的点击事件以及服务器的消息推送等。根据这个收发流程编写代码逻辑,具体实现代码如下:

public class SocketioTextActivity extends AppCompatActivity {private static final String TAG = "SocketioTextActivity";private EditText et_input; // 声明一个编辑框对象private TextView tv_response; // 声明一个文本视图对象private Socket mSocket; // 声明一个套接字对象@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_socketio_text);et_input = findViewById(R.id.et_input);tv_response = findViewById(R.id.tv_response);findViewById(R.id.btn_send).setOnClickListener(v -> {String content = et_input.getText().toString();if (TextUtils.isEmpty(content)) {Toast.makeText(this, "请输入聊天消息", Toast.LENGTH_SHORT).show();return;}mSocket.emit("send_text", content); // 往Socket服务器发送文本消息});initSocket(); // 初始化套接字}// 初始化套接字private void initSocket() {// 检查能否连上Socket服务器SocketUtil.checkSocketAvailable(this, NetConst.BASE_IP, NetConst.BASE_PORT);try {String uri = String.format("http://%s:%d/", NetConst.BASE_IP, NetConst.BASE_PORT);mSocket = IO.socket(uri); // 创建指定地址和端口的套接字实例} catch (URISyntaxException e) {throw new RuntimeException(e);}mSocket.connect(); // 建立Socket连接// 等待接收传来的文本消息mSocket.on("receive_text", (args) -> {String desc = String.format("%s 收到服务端消息:%s",DateUtil.getNowTime(), (String) args[0]);runOnUiThread(() -> tv_response.setText(desc));});}@Overrideprotected void onDestroy() {super.onDestroy();mSocket.off("receive_text"); // 取消接收传来的文本消息if (mSocket.connected()) { // 已经连上Socket服务器mSocket.disconnect(); // 断开Socket连接}mSocket.close(); // 关闭Socket连接}
}

确保服务器的SocketServer正在运行(点击查看服务端代码),再运行测试该App,在编辑框输入待发送的文本,此时交互界面如下图所示。
在这里插入图片描述
接着点击“发送文本消息”按钮,向Socket服务器发送文本消息;随后接收到服务器推送的应答消息,应答内容展示在按钮下方,此时交互界面如下图所示,可见文本消息的收发流程成功走通。
在这里插入图片描述

通过SocketIO传输图片消息

上一小节借助SocketIO成功实现了文本消息的即时通信,然而文本内容只用到字符串,本来就比较简单。倘若让SocketIO实时传输图片,便步那么容易了。因为SocketIO不支持直接传输二进制数据,使得位图对象的字节数据无法作为emit方法的参数。除了字符串类型,SocketIO还支持JSONObject类型的数据,所以可以考虑利用JSON对象封装图像信息,把图像的字节数据通过BASE64编码成字符串保存起来。
鉴于JSON格式允许容纳多个字段,同时图片很有可能很大,因此建议将图片拆开分段传输,每段标明本次的分段序号、分段长度以及分段数据,由接收方在收到后重新拼成完整的图像。为此需要将原来的Socket收发过程改造一番,使之支持图片数据的即时通信,改造步骤说明如下。

  1. 给服务端的Socket监听程序添加以下代码,表示新增图像发送事件:
// 添加图像发送的事件监听器
server.addEventListener("send_image", JSONObject.class, (client, json, ackSender) -> {client.sendEvent("receive_image", json);
});
  1. 在App模块中定义一个图像分段结构,用于存放分段名称、分段数据、分段序号、分段长度等信息,该结构的关键代码如下:
public class ImagePart {private String name; // 分段名称private String data; // 分段数据private int seq; // 分段序号private int length; // 分段长度public ImagePart(String name, String data, int seq, int length) {this.name = name;this.data = data;this.seq = seq;this.length = length;}
}
  1. 回到App的活动代码,补充实现图像的分段传输功能。先将位图数据转为字节数组,再将字节数组分段编码为BASE64字符串,再组装成JSON对象传给Socket服务器。发送图像的示例代码如下:
private int mBlock = 50*1024; // 每段的数据包大小
// 分段传输图片数据
private void sendImage() {ByteArrayOutputStream baos = new ByteArrayOutputStream();// 把位图数据压缩到字节数组输出流mBitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);byte[] bytes = baos.toByteArray();int count = bytes.length/mBlock + 1;// 下面把图片数据经过BASE64编码后发给Socket服务器for (int i=0; i<count; i++) {String encodeData = "";if (i == count-1) { // 是最后一段图像数据int remain = bytes.length % mBlock;byte[] temp = new byte[remain];System.arraycopy(bytes, i*mBlock, temp, 0, remain);encodeData = Base64.encodeToString(temp, Base64.DEFAULT);} else { // 不是最后一段图像数据byte[] temp = new byte[mBlock];System.arraycopy(bytes, i*mBlock, temp, 0, mBlock);encodeData = Base64.encodeToString(temp, Base64.DEFAULT);}// 往Socket服务器发送本段的图片数据ImagePart part = new ImagePart(mFileName, encodeData, i, bytes.length);SocketUtil.emit(mSocket, "send_image", part); // 向服务器提交图像数据}
}
  1. 除了要实现发送方的图像发送功能,还需实现接收方的图像接收功能。先从服务器获取各段图像数据,等所有分段都接收完毕再按照分段序号依次凭借图像的字节数组,再从拼接好的字节数组解码得到位图对象,接收图像的示例代码如下:
private String mLastFile; // 上次的文件名
private int mReceiveCount; // 接收包的数量
private byte[] mReceiveData; // 收到的字节数组
// 接收对方传来的图片数据
private void receiveImage(Object... args) {JSONObject json = (JSONObject) args[0];ImagePart part = new Gson().fromJson(json.toString(), ImagePart.class);if (!part.getName().equals(mLastFile)) { // 与上次文件名不同,表示开始接收新文件mLastFile = part.getName();mReceiveCount = 0;mReceiveData = new byte[part.getLength()];}mReceiveCount++;// 把接收到的图片数据通过BASE64解码为字节数组byte[] temp = Base64.decode(part.getData(), Base64.DEFAULT);System.arraycopy(temp, 0, mReceiveData, part.getSeq()*mBlock, temp.length);// 所有数据包都接收完毕if (mReceiveCount >= part.getLength()/mBlock+1) {// 从字节数组中解码得到位图对象Bitmap bitmap = BitmapFactory.decodeByteArray(mReceiveData, 0, mReceiveData.length);String desc = String.format("%s 收到服务端消息:%s", DateUtil.getNowTime(), part.getName());runOnUiThread(() -> { // 回到主线程展示图片与描述文字tv_response.setText(desc);iv_response.setImageBitmap(bitmap);});}
}

在App代码中记得调用Socket对象的on方法,这样App才能正常接收服务器传来的图像数据。下面是on方法的调用代码:

// 等待接收传来的图片数据
mSocket.on("receive_image", (args) -> receiveImage(args));

完成上述几个步骤之后,确保服务器的SocketServer正在运行(点击查看服务器端代码),再运行测试该App,从系统相册中选择待发送的图片,此时交互界面如下图所示。
在这里插入图片描述
接着点击“发送图片”按钮,向Socket服务器发送图片消息;随后接收到服务器推送的应答消息,应答消息内容显示再按钮下方(包含文本和图片),此时交互界面如下图所示。可见图片消息发送流程成功完成。
在这里插入图片描述

利用WebSocket传输消息

在前面两小节中,文本与图片的即时通信都可以由SocketIO实现,看似它要统一即时通信了,可是深究起来会发现SocektIO存在很多局限,包括但不限以下几点:

  1. SocketIO不能直接传输字节数据,只能重新编码成字符串(比如BASE64)后再传输,造成了额外的系统开销。
  2. SokcetIO不能保证前后发送的数据被接收到时仍然是同样顺序,如果业务要求实现分段数据的有序性,开发者就得自己采取某种机制确保这种有序性。
  3. SocketIO服务器只有一个main程序,不可避免地会产生性能瓶颈。倘若有许多通信请求奔涌过来,一个main程序很难应对。

为了解决上述几点问题,业界提出了一种互联网时代的Socket协议,名叫WebSocket。它支持在TCP连接上进行全双工通信,这个协议在2011年被定义为互联网的标准之一,并纳入HTML5的规范体系。相对于传统的HTTP与Socket来说,WebSocket具备以下几点优势:

  1. 实时性更强,无须轮询即可实时获得对方设备的消息推送。
  2. 利用率更高,连接创建之后,基于相同的控制协议,每次交互的数据包头较小,节省了数据处理的开销。
  3. 功能更强大,WebSocket定义了二进制帧,使得传输二进制的字节数组十分容易。
  4. 扩展更方便,WebSocekt接口被托管在普通的Web服务至上,跟着Web服务扩容方便,有效规避了性能瓶颈。

WebSocket不仅拥有如此丰富的特性,而且用起来也特别简单。先说服务器的WebSocekt编程,除了引入它的依赖包javaee-api-8.0.1.jar,就只需添加如下的服务器代码:

@ServerEndpoint("/testWebSocket")
public class WebSocketServer {// 存放每个客户端对应的WebSocket对象private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();private Session mSession; // 当前的连接会话// 连接成功后调用@OnOpenpublic void onOpen(Session session) {System.out.println("WebSocket连接成功");this.mSession = session;webSocketSet.add(this);}// 连接关闭后调用@OnClosepublic void onClose() {System.out.println("WebSocket连接关闭");webSocketSet.remove(this);}// 连接异常时调用@OnErrorpublic void onError(Throwable error) {System.out.println("WebSocket连接异常");error.printStackTrace();}// 收到客户端消息时调用@OnMessagepublic void onMessage(String msg) throws Exception {System.out.println("接收到客户端消息:" + msg);for(WebSocketServer item : webSocketSet){item.mSession.getBasicRemote().sendText("我听到消息啦“"+msg+"”");}}
}

接着启动服务器Web工程,便能通过形如http://192.168.10.121:8000/HttpServer/testWebSocket这样的地址访问WebSocket。
再说App端的WebSocket编程,由于WebSocket协议尚未纳入JDK,因此要引入它所依赖的JAR包tyrus-standalone-client-1.17.jar。代码方面则需要自定义客户端的连接任务,注意给任务类添加注解@ClientEndpoint,表示该类属于WebSocket的客户端任务。任务内部需要重写onOpen(连接成功后调用)、processMessage(收到服务端消息时调用)、processError(收到服务端错误时调用)三个方法,还得定义一个向服务端发消息方法,消息内容支持文本与二进制两种格式。下面是处理客户端消息交互工作的示例代码:

@ClientEndpoint
public class AppClientEndpoint {private final static String TAG = "AppClientEndpoint";private Activity mAct; // 声明一个活动实例private OnRespListener mListener; // 消息应答监听器private Session mSession; // 连接会话public AppClientEndpoint(Activity act, OnRespListener listener) {mAct = act;mListener = listener;}// 向服务器发送请求报文public void sendRequest(String req) {Log.d(TAG, "发送请求报文:"+req);try {if (mSession != null) {RemoteEndpoint.Basic remote = mSession.getBasicRemote();remote.sendText(req); // 发送文本数据// remote.sendBinary(buffer); // 发送二进制数据}} catch (Exception e) {e.printStackTrace();}}// 连接成功后调用@OnOpenpublic void onOpen(final Session session) {mSession = session;Log.d(TAG, "成功创建连接");}// 收到服务端消息时调用@OnMessagepublic void processMessage(Session session, String message) {Log.d(TAG, "WebSocket服务端返回:" + message);if (mListener != null) {mAct.runOnUiThread(() -> mListener.receiveResponse(message));}}// 收到服务端错误时调用@OnErrorpublic void processError(Throwable t) {t.printStackTrace();}// 定义一个WebSocket应答的监听器接口public interface OnRespListener {void receiveResponse(String resp);}
}

回到App的活动代码,依次执行下述步骤就能向WebSocket服务器发送消息:获取WebSocket容器->连接WebSocekt服务器->调用WebSocket任务的发送方法。其中前两步涉及的初始化代码如下:

// 初始化WebSocket的客户端任务
private void initWebSocket() {// 创建文本传输任务,并指定消息应答监听器mAppTask = new AppClientEndpoint(this, resp -> {String desc = String.format("%s 收到服务端返回:%s",DateUtil.getNowTime(), resp);tv_response.setText(desc);});// 获取WebSocket容器WebSocketContainer container = ContainerProvider.getWebSocketContainer();try {URI uri = new URI(SERVER_URL); // 创建一个URI对象// 连接WebSocket服务器,并关联文本传输任务获得连接会话Session session = container.connectToServer(mAppTask, uri);// 设置文本消息的最大缓存大小session.setMaxTextMessageBufferSize(1024 * 1024 * 10);// 设置二进制消息的最大缓存大小//session.setMaxBinaryMessageBufferSize(1024 * 1024 * 10);} catch (Exception e) {e.printStackTrace();}
}

因为WebSocket接口任为网络操作,所以必须在分线程中初始化WebSocekt,启动初始化线程的代码如下:

new Thread(() -> initWebSocket()).start(); // 启动线程初始化WebSocket客户端

同理,发送WebSocket消息也要在分线程中操作,启动消息发送线程的代码如下:

new Thread(() -> mAppTask.sendRequest(content)).start(); // 启动线程发送文本消息

最后确保后端的Web服务正在运行(点击查看服务端代码),再运行测试该App,在编辑框输入待发送的文本,此时交互界面如下图所示。
在这里插入图片描述
接着点击“发送WEBSOCKET消息”按钮,向WebSocket服务器发送文本消息;随后接收到服务器推送的应答消息,应答内容显示在按钮下方,此时监护界面如下图所示。
在这里插入图片描述

工程源码

文章涉及所有代码可点击工程源码下载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/28408.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP协议 快速入门

http概述 无状态性&#xff1a;HTTP是一个无状态协议&#xff0c;这意味着服务器不会在请求之间保存任何会话信息。每个请求都是独立的&#xff0c;服务器不会记住之前的请求。 请求-响应模型&#xff1a;HTTP通信是基于客户端发送请求和服务器返回响应的模型。客户端&#xf…

Spark常见的可以优化的点

Shuffle 复用 # 1.以下操作会复用的shuffle结果&#xff0c;只会读一遍数据源 val rdd1 sc.textFile("hdfs://zjyprc-hadoop/tmp/hive-site.xml").flatMap(_.split(" ")).map(x > (x,1)).reduceByKey(_ _).filter(_._2 > 1) rdd1.count() rdd1.fil…

华为od-C卷200分题目2 - 找城市

华为od-C卷200分题目2 - 找城市 题目描述 一个城市规划问题&#xff0c;一个地图有很多城市&#xff0c;两个城市之间只有一种路径&#xff0c;切断通往一 个城市i的所有路径之后&#xff0c;其他的城市形成了独立的城市群&#xff0c;这些城市群里最大的城 市数量&#xff0…

会声会影色彩校正在哪里 会声会影色彩素材栏在哪 会声会影中文免费版下载

会声会影是一款功能强大的视频编辑软件&#xff0c;它可以帮助用户轻松地编辑和制作视频。在进行视频编辑时&#xff0c;色彩校正是一个重要的步骤&#xff0c;它可以调整视频的色调、亮度和对比度等参数&#xff0c;使视频更加生动和鲜明。在会声会影中&#xff0c;色彩校正功…

【Python/Pytorch - 网络模型】-- TV Loss损失函数

文章目录 文章目录 00 写在前面01 基于Pytorch版本的TV Loss代码02 论文下载 00 写在前面 在医学图像重建过程中&#xff0c;经常在代价方程中加入TV 正则项&#xff0c;该正则项作为去噪项&#xff0c;对于重建可以起到很大帮助作用。但是对于一些纹理细节要求较高的任务&am…

MongoDB~分片数据存储Chunk;其迁移原理、影响,以及避免手段

分片数据存储&#xff1a;Chunk存储 Chunk&#xff08;块&#xff09; 是 MongoDB 分片集群的一个核心概念&#xff0c;其本质上就是由一组 Document 组成的逻辑数据单元。每个 Chunk 包含一定范围片键的数据&#xff0c;互不相交且并集为全部数据。 分片集群不会记录每条数据…

Python 基础:类

目录 一、类的概念二、定义类三、创建对象并进行访问四、修改属性的值方法一&#xff1a;句点表示法直接访问并修改方法二&#xff1a;通过方法进行修改 五、继承继承父类属性和方法重写父类方法 六、将实例用作属性七、导入类导入单个类从一个模块中导入多个类导入整个模块导入…

C语言的基本输入输出函数+构造类型数据——数组

C语言的基本输入输出函数 1. 字符输入输出函数 getchar()、putchar() getchar()&#xff1a;从标准输入&#xff08;通常是键盘&#xff09;读取一个字符&#xff0c;并返回其ASCII值。putchar()&#xff1a;将指定的字符&#xff08;由其ASCII值表示&#xff09;写入标准输出…

10_Transformer预热---注意力机制(Attention)

1.1 什么是注意力机制(attention) 注意力机制&#xff08;Attention Mechanism&#xff09;是一种在神经网络中用于增强模型处理特定输入特征的能力的技术。它最早被应用于自然语言处理&#xff08;NLP&#xff09;任务中&#xff0c;特别是在机器翻译中&#xff0c;如Google的…

python14 字典类型

字典类型 键值对方式&#xff0c;可变数据类型&#xff0c;所以有增删改功能 声明方式1 {} 大括号&#xff0c;示例 d {key1 : value1, key2 : value2, key3 : value3 ....} 声明方式2 使用内置函数 dict() 创建1)通过映射函数创建字典zip(list1,list2) 继承了序列的所有操作 …

Linux基础I/O之文件描述符fd 重定向(上)

目录 一、预备知识 二、C语言中的文件接口 三、系统调用中的文件接口 一、预备知识 首先我们要明确的一个观点是 --- 文件 内容 属性。而且我们之前也还将过一个概念&#xff0c;那就是Linux下一切皆文件。 内容是数据&#xff0c;属性也是数据 --- 那么也就是说我…

使用STL算法函数有效提升STL列表的搜索速度(附源码)

STL(Standard Templete Library)活动模板库已被广泛地应用于各种C++程序的开发中,STL中vector、list、map等列表极大地方便了我们日常的开发,不再需要我们去实现链表等数据结构,使用这些列表能基本能解决开发过程中遇到的各种问题。网上关于STL的文章比较多,今天我们就来…

代码随想录——组合总和Ⅱ(Leetcode 40)需要回顾

题目链接 回溯 本题的难点在于&#xff1a;集合&#xff08;数组candidates&#xff09;有重复元素&#xff0c;但还不能有重复的组合。 思想&#xff1a;元素在同一个组合内是可以重复的&#xff0c;怎么重复都没事&#xff0c;但两个组合不能相同。所以要去重的是同一树…

统计套利—配对交易策略

配对交易是一种基于统计学的交易策略&#xff0c;通过两只股票的差价来获取收益&#xff0c;因而与很多策略不同&#xff0c;它是一种中性策略&#xff0c;理论上可以做到和大盘走势完全无关。 配对交易的基本原理是&#xff0c;两个相似公司的股票&#xff0c;其股价走势虽然在…

[Linux] TCP协议介绍(3): TCP协议的“四次挥手“过程、状态分析...

TCP协议是面向连接的 上一篇文章简单分析了TCP通信非常重要的建立连接的"三次握手"的过程 本篇文章来分析TCP通信中同样非常重要的断开连接的"四次挥手"的过程 TCP的"四次挥手" TCP协议建立连接 需要"三次握手". "三次挥手&q…

基于STM32和人工智能的自动驾驶小车系统

目录 引言环境准备自动驾驶小车系统基础代码实现&#xff1a;实现自动驾驶小车系统 4.1 数据采集模块4.2 数据处理与分析4.3 控制系统4.4 用户界面与数据可视化应用场景&#xff1a;自动驾驶应用与优化问题解决方案与优化收尾与总结 1. 引言 随着人工智能和嵌入式系统技术的…

稀疏矩阵是什么 如何求

稀疏矩阵是一种特殊类型的矩阵&#xff0c;其中大多数元素都是零。由于稀疏矩阵中非零元素的数量远少于零元素&#xff0c;因此可以使用特定的数据结构和算法来高效地存储和处理它们&#xff0c;从而节省存储空间和计算时间。 RowPtr 数组中的每个元素表示对应行的第一个非零元…

变压器纵联差动保护的Simulink仿真

利用Simulink在变压器空载合闸励磁涌流的仿真模型的基础上将变压器改为采用Yd11联结且不考虑饱和特性,增加外部故障模块Fault2,得到新的仿真模型如图1所示。 图1 变压器的Simulink仿真模型 在建立模型时,请注意三相电压电流测量模块Um,UN的方向。比率制动特性纵差保护…

目标检测算法SSD与FasterRCNN

目标检测算法SSD与FasterRCNN SSD:&#xff08; Single Shot MultiBox Detector&#xff09;特点是在不同特征尺度上预测不同尺度的目标。 SSD网络结构 首先对网络的特征进行说明&#xff1a;输入的图像是300x300的三通道彩色图像。 网络的第一个部分贯穿到Vgg16模型 Conv5的…

工厂方法模式实战之某商场一次促销活动

目录 1.5.1、前言1.5.2、实战场景简介1.5.3、开发环境1.5.4、用传统的if-else语句实现1.5.4.1、工程结构1.5.4.2、if-else需求实现1.5.4.3、测试验证 1.5.5、工厂模式优化代码1.5.5.1、工程结构1.5.5.2、代码实现1.5.5.2.1、定义各种商品发放接口及接口实现1.5.5.2.2、定义工厂…