当前位置: 首页 > news >正文

直面原理:5 张图彻底了解 Android TextToSpeech 机制

ChatGPT 如此火爆,但它的强悍在于 NLU(自然语言理解)、DM(对话管理)和 NLG (自然语言生成)这三块,而 Recognition 识别和 TTS 播报这两块是缺失的。假使你的 App 接入了 ChatGPT,但如果需要播报出来的话,TextToSpeech 机制就可以派上用场了。

1 前言

关于语音方面的交互,Android SDK 提供了用于语音交互的 VoiceInteraction 机制、语音识别的 Recognition 接口、语音播报的 TTS 接口。

前者已经介绍过,本次主要聊聊第 3 块即 TTS,后续会分析下第 2 块即 Android 标准的 Recognition 机制。

通过 TextToSpeech 机制,任意 App 都可以方便地采用系统内置或第三方提供的 TTS Engine 进行播放铃声提示、语音提示的请求,Engine 可以由系统选择默认的 provider 来执行操作,也可由 App 具体指定偏好的目标 Engine 来完成。

默认 TTS Engine 可以在设备设置的路径中找到,亦可由用户手动更改:Settings -> Accessibility -> Text-to-speech ouput -> preferred engine

TextToSpeech 机制的优点有很多:

  • 对于需要使用 TTS 的请求 App 而言:无需关心 TTS 的具体实现,通过 TextToSpeech API 即用即有
  • 对于需要对外提供 TTS 能力的实现 Engine 而言,无需维护复杂的 TTS 时序和逻辑,按照 TextToSpeechService 框架的定义对接即可,无需关心系统如何将实现和请求进行衔接

本文将会阐述 TextToSpeech 机制的调用、Engine 的实现以及系统调度这三块,彻底梳理清楚整个流程。

2 TextToSpeech 调用

TextToSpeech API 是为 TTS 调用准备,总体比较简单。

最主要的是提供初始化 TTS 接口的 TextToSpeech() 构造函数和初始化后的回调 OnInitListener,后续的播放 TTS 的 speak() 和播放铃声的 playEarcon()

比较重要的是处理播放请求的 4 种回调结果,需要依据不同结果进行 TTS 播报开始的状态记录、播报完毕后的下一步动作、抑或是在播报出错时对音频焦点的管理等等。

之前的 OnUtteranceCompletedListener 在 API level 18 时被废弃,可以使用回调更为精细的 UtteranceProgressListener

// TTSTest.kt
class TTSTest(context: Context) {private val tts: TextToSpeech = TextToSpeech(context) { initResult -> ... }init {tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {override fun onStart(utteranceId: String?) { ... }override fun onDone(utteranceId: String?) { ... }override fun onStop(utteranceId: String?, interrupted: Boolean)  { ... }override fun onError(utteranceId: String?)  { ... }})}fun testTextToSpeech(context: Context) {tts.speak("你好,汽车",TextToSpeech.QUEUE_ADD,Bundle(),"xxdtgfsf")tts.playEarcon(EARCON_DONE,TextToSpeech.QUEUE_ADD,Bundle(),"yydtgfsf")}companion object {const val EARCON_DONE = "earCon_done"}
}

3 TextToSpeech 系统调度

3.1 init 绑定

首先从 TextToSpeech() 的实现入手,以了解在 TTS 播报之前,系统和 TTS Engine 之间做了什么准备工作。

  1. 其触发的 initTTS() 将按照如下顺序查找需要连接到哪个 Engine:

    • 如果构造 TTS 接口的实例时指定了目标 Engine 的 package,那么首选连接到该 Engine
    • 反之,获取设备设置的 default Engine 并连接,设置来自于 TtsEngines 从系统设置数据 SettingsProvider 中 读取 TTS_DEFAULT_SYNTH 而来
    • 如果 default 不存在或者没有安装的话,从 TtsEngines 获取第一位的系统 Engine 并连接。第一位指的是从所有 TTS Service 实现 Engine 列表里获得第一个属于 system image 的 Engine
  2. 连接的话均是调用 connectToEngine(),其将依据调用来源来采用不同的 Connection 内部实现去 connect()

    • 如果调用不是来自 system,采用 DirectConnection

      • 其 connect() 实现较为简单,封装 Action 为 INTENT_ACTION_TTS_SERVICE 的 Intent 进行 bindService(),后续由 AMS 执行和 Engine 的绑定,这里不再展开
    • 反之,采用 SystemConnection,原因在于系统的 TTS 请求可能很多,不能像其他 App 一样总是创建一个新的连接,而是需要 cache 并复用这种连接

      • 具体是直接获取名为 texttospeech 、管理 TTS Service 的系统服务 TextToSpeechManagerService 的接口代理并直接调用它的 createSession() 创建一个 session,同时暂存其指向的 ITextToSpeechSession 代理接口。

        该 session 实际上还是 AIDL 机制,TTS 系统服务的内部会创建专用的 TextToSpeechSessionConnection 去 bind 和 cache Engine,这里不再赘述

    • 无论是哪种方式,在 connected 之后都需要将具体的 TTS Eninge 的 ITextToSpeechService 接口实例暂存,同时将 Connection 实例暂存到 mServiceConnection,给外部类接收到 speak() 的时候使用。而且要留意,此刻还会启动一个异步任务 SetupConnectionAsyncTask 将自己作为 Binder 接口 ITextToSpeechCallback 返回给 Engine 以处理完之后回调结果给 Request

  3. connect 执行完毕并结果 OK 的话,还要暂存到 mConnectingServiceConnection,以在结束 TTS 需求的时候释放连接使用。并通过 dispatchOnInit() 传递 SUCCESS 给 Request App

    • 实现很简单,将结果 Enum 回调给初始化传入的 OnInitListener 接口
  4. 如果连接失败的话,则调用 dispatchOnInit() 传递 ERROR

// TextToSpeech.java
public class TextToSpeech {public TextToSpeech(Context context, OnInitListener listener) {this(context, listener, null);}private TextToSpeech( ... ) {...initTts();}private int initTts() {// Step 1: Try connecting to the engine that was requested.if (mRequestedEngine != null) {if (mEnginesHelper.isEngineInstalled(mRequestedEngine)) {if (connectToEngine(mRequestedEngine)) {mCurrentEngine = mRequestedEngine;return SUCCESS;}...} else if (!mUseFallback) {...dispatchOnInit(ERROR);return ERROR;}}// Step 2: Try connecting to the user's default engine.final String defaultEngine = getDefaultEngine();...// Step 3: Try connecting to the highest ranked engine in the system.final String highestRanked = mEnginesHelper.getHighestRankedEngineName();...dispatchOnInit(ERROR);return ERROR;}private boolean connectToEngine(String engine) {Connection connection;if (mIsSystem) {connection = new SystemConnection();} else {connection = new DirectConnection();}boolean bound = connection.connect(engine);if (!bound) {return false;} else {mConnectingServiceConnection = connection;return true;}}
}

Connection 内部类和其两个子类的实现:

// TextToSpeech.java
public class TextToSpeech {...private abstract class Connection implements ServiceConnection {private ITextToSpeechService mService;...private final ITextToSpeechCallback.Stub mCallback =new ITextToSpeechCallback.Stub() {public void onStop(String utteranceId, boolean isStarted)throws RemoteException {UtteranceProgressListener listener = mUtteranceProgressListener;if (listener != null) {listener.onStop(utteranceId, isStarted);}};@Overridepublic void onSuccess(String utteranceId) { ... }@Overridepublic void onError(String utteranceId, int errorCode) { ... }@Overridepublic void onStart(String utteranceId) { ... }...};@Overridepublic void onServiceConnected(ComponentName componentName, IBinder service) {synchronized(mStartLock) {mConnectingServiceConnection = null;mService = ITextToSpeechService.Stub.asInterface(service);mServiceConnection = Connection.this;mEstablished = false;mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask();mOnSetupConnectionAsyncTask.execute();}} ...}private class DirectConnection extends Connection {@Overrideboolean connect(String engine) {Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE);intent.setPackage(engine);return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);}...}private class SystemConnection extends Connection {...boolean connect(String engine) {IBinder binder = ServiceManager.getService(Context.TEXT_TO_SPEECH_MANAGER_SERVICE);...try {manager.createSession(engine, new ITextToSpeechSessionCallback.Stub() {...});return true;} ...}...}
}

3.2 speak 播报

后面看看重要的 speak(),系统做了什么具体实现。

  1. 首先将 speak() 对应的调用远程接口的操作封装为 Action 接口实例,并交给 init() 时暂存的已连接的 Connection 实例去调度。

    // TextToSpeech.java
    public class TextToSpeech {...private Connection mServiceConnection;public int speak(final CharSequence text, ... ) {return runAction((ITextToSpeechService service) -> {...}, ERROR, "speak");}private <R> R runAction(Action<R> action, R errorResult, String method) {return runAction(action, errorResult, method, true, true);}private <R> R runAction( ... ) {synchronized (mStartLock) {...return mServiceConnection.runAction(action, errorResult, method, reconnect,onlyEstablishedConnection);}}private abstract class Connection implements ServiceConnection {public <R> R runAction( ... ) {synchronized (mStartLock) {try {...return action.run(mService);}...}}}
    }
    
  2. Action 的实际内容是先从 mUtterances Map 里查找目标文本是否有设置过本地的 audio 资源:

    • 如有设置的话,调用用 TTS Engine 的 playAudio() 直接播放
    • 反之调用 text 转 audio 的接口 speak()
    // TextToSpeech.java
    public class TextToSpeech {...public int speak(final CharSequence text, ... ) {return runAction((ITextToSpeechService service) -> {Uri utteranceUri = mUtterances.get(text);if (utteranceUri != null) {return service.playAudio(getCallerIdentity(), utteranceUri, queueMode,getParams(params), utteranceId);} else {return service.speak(getCallerIdentity(), text, queueMode, getParams(params),utteranceId);}}, ERROR, "speak");}...
    }
    

    后面即是 TextToSpeechService 的实现环节。

4 TextToSpeechService 实现

  1. TextToSpeechService 内接收的实现是向内部的 SynthHandler 发送封装的 speak 或 playAudio 请求的 SpeechItem

    SynthHandler 绑定到 TextToSpeechService 初始化的时候启动的、名为 “SynthThread” 的 HandlerThread。

    • speak 请求封装给 Handler 的是 SynthesisSpeechItem
    • playAudio 请求封装的是 AudioSpeechItem
    // TextToSpeechService.java
    public abstract class TextToSpeechService extends Service {private final ITextToSpeechService.Stub mBinder =new ITextToSpeechService.Stub() {@Overridepublic int speak(IBinder caller,CharSequence text,int queueMode,Bundle params,String utteranceId) {SpeechItem item =new SynthesisSpeechItem(caller,Binder.getCallingUid(),Binder.getCallingPid(),params,utteranceId,text);return mSynthHandler.enqueueSpeechItem(queueMode, item);}@Overridepublic int playAudio( ... ) {SpeechItem item =new AudioSpeechItem( ... );...}...};...
    }
    
  2. SynthHandler 拿到 SpeechItem 后根据 queueMode 的值决定是 stop() 还是继续播放。播放的话,是封装进一步 play 的操作 Message 给 Handler。

    // TextToSpeechService.javaprivate class SynthHandler extends Handler {...public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) {UtteranceProgressDispatcher utterenceProgress = null;if (speechItem instanceof UtteranceProgressDispatcher) {utterenceProgress = (UtteranceProgressDispatcher) speechItem;}if (!speechItem.isValid()) {if (utterenceProgress != null) {utterenceProgress.dispatchOnError(TextToSpeech.ERROR_INVALID_REQUEST);}return TextToSpeech.ERROR;}if (queueMode == TextToSpeech.QUEUE_FLUSH) {stopForApp(speechItem.getCallerIdentity());} else if (queueMode == TextToSpeech.QUEUE_DESTROY) {stopAll();}Runnable runnable = new Runnable() {@Overridepublic void run() {if (setCurrentSpeechItem(speechItem)) {speechItem.play();removeCurrentSpeechItem();} else {speechItem.stop();}}};Message msg = Message.obtain(this, runnable);msg.obj = speechItem.getCallerIdentity();if (sendMessage(msg)) {return TextToSpeech.SUCCESS;} else {if (utterenceProgress != null) {utterenceProgress.dispatchOnError(TextToSpeech.ERROR_SERVICE);}return TextToSpeech.ERROR;}}...}
    
  3. play() 具体是调用 playImpl() 继续。对于 SynthesisSpeechItem 来说,将初始化时创建的 SynthesisRequest 实例和 SynthesisCallback 实例(此处的实现是 PlaybackSynthesisCallback)收集和调用 onSynthesizeText() 进一步处理,用于请求和回调结果。

    // TextToSpeechService.javaprivate abstract class SpeechItem {...public void play() {synchronized (this) {if (mStarted) {throw new IllegalStateException("play() called twice");}mStarted = true;}playImpl();}}class SynthesisSpeechItem extends UtteranceSpeechItemWithParams {public SynthesisSpeechItem(...String utteranceId,CharSequence text) {mSynthesisRequest = new SynthesisRequest(mText, mParams);...}...@Overrideprotected void playImpl() {AbstractSynthesisCallback synthesisCallback;mEventLogger.onRequestProcessingStart();synchronized (this) {...mSynthesisCallback = createSynthesisCallback();synthesisCallback = mSynthesisCallback;}TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback);if (synthesisCallback.hasStarted() && !synthesisCallback.hasFinished()) {synthesisCallback.done();}}...}
    
  4. onSynthesizeText() 是 abstract 方法,需要 Engine 复写以将 text 合成 audio 数据,也是 TTS 功能里最核心的实现。

    • Engine 需要从 SynthesisRequest 中提取 speak 的目标文本、参数等信息,针对不同信息进行区别处理。并通过 SynthesisCallback 的各接口将数据和时机带回:
    • 在数据合成前,通过 start() 告诉系统生成音频的采样频率,多少位 pcm 格式音频,几通道等等。PlaybackSynthesisCallback 的实现将会创建播放的 SynthesisPlaybackQueueItem 交由 AudioPlaybackHandler 去排队调度
    • 之后,通过 audioAvailable() 接口将合成的数据以 byte[] 形式传递回来,会取出 start() 时创建的 QueueItem put 该 audio 数据开始播放
    • 最后,通过 done() 告知合成完毕
    // PlaybackSynthesisCallback.java
    class PlaybackSynthesisCallback extends AbstractSynthesisCallback {...@Overridepublic int start(int sampleRateInHz, int audioFormat, int channelCount) {mDispatcher.dispatchOnBeginSynthesis(sampleRateInHz, audioFormat, channelCount);int channelConfig = BlockingAudioTrack.getChannelConfig(channelCount);synchronized (mStateLock) {...SynthesisPlaybackQueueItem item = new SynthesisPlaybackQueueItem(mAudioParams, sampleRateInHz, audioFormat, channelCount,mDispatcher, mCallerIdentity, mLogger);mAudioTrackHandler.enqueue(item);mItem = item;}return TextToSpeech.SUCCESS;}@Overridepublic int audioAvailable(byte[] buffer, int offset, int length) {SynthesisPlaybackQueueItem item = null;synchronized (mStateLock) {...item = mItem;}final byte[] bufferCopy = new byte[length];System.arraycopy(buffer, offset, bufferCopy, 0, length);mDispatcher.dispatchOnAudioAvailable(bufferCopy);try {item.put(bufferCopy);}...return TextToSpeech.SUCCESS;}@Overridepublic int done() {int statusCode = 0;SynthesisPlaybackQueueItem item = null;synchronized (mStateLock) {...mDone = true;if (mItem == null) {if (mStatusCode == TextToSpeech.SUCCESS) {mDispatcher.dispatchOnSuccess();} else {mDispatcher.dispatchOnError(mStatusCode);}return TextToSpeech.ERROR;}item = mItem;statusCode = mStatusCode;}if (statusCode == TextToSpeech.SUCCESS) {item.done();} else {item.stop(statusCode);}return TextToSpeech.SUCCESS;}...
    }
    

    上述的 QueueItem 的放置 audio 数据和消费的逻辑如下,主要是 put 操作触发 Lock 接口的 take Condition 恢复执行,最后调用 AudioTrack 去播放。

    // SynthesisPlaybackQueueItem.java
    final class SynthesisPlaybackQueueItem ... {void put(byte[] buffer) throws InterruptedException {try {mListLock.lock();long unconsumedAudioMs = 0;...mDataBufferList.add(new ListEntry(buffer));mUnconsumedBytes += buffer.length;mReadReady.signal();} finally {mListLock.unlock();}}private byte[] take() throws InterruptedException {try {mListLock.lock();while (mDataBufferList.size() == 0 && !mStopped && !mDone) {mReadReady.await();}...ListEntry entry = mDataBufferList.poll();mUnconsumedBytes -= entry.mBytes.length;mNotFull.signal();return entry.mBytes;} finally {mListLock.unlock();}}public void run() {...final UtteranceProgressDispatcher dispatcher = getDispatcher();dispatcher.dispatchOnStart();if (!mAudioTrack.init()) {dispatcher.dispatchOnError(TextToSpeech.ERROR_OUTPUT);return;}try {byte[] buffer = null;while ((buffer = take()) != null) {mAudioTrack.write(buffer);}} ...mAudioTrack.waitAndRelease();dispatchEndStatus();}void done() {try {mListLock.lock();mDone = true;mReadReady.signal();mNotFull.signal();} finally {mListLock.unlock();}}
    }
    
  5. 上述 PlaybackSynthesisCallback 在通知 QueueItem 的同时,会通过 UtteranceProgressDispatcher 接口将数据、结果一并发送给 Request App。

    // TextToSpeechService.javainterface UtteranceProgressDispatcher {void dispatchOnStop();void dispatchOnSuccess();void dispatchOnStart();void dispatchOnError(int errorCode);void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount);void dispatchOnAudioAvailable(byte[] audio);public void dispatchOnRangeStart(int start, int end, int frame);}
    

    事实上该接口的实现就是 TextToSpeechService 处理 speak 请求的 UtteranceSpeechItem 实例,其通过缓存着各 ITextToSpeechCallback 接口实例的 CallbackMap 发送回调给 TTS 请求的 App。(这些 Callback 来自于 TextToSpeech 初始化时候通过 ITextToSpeechService 将 Binder 接口传递来和缓存起来的。)

        private abstract class UtteranceSpeechItem extends SpeechItemimplements UtteranceProgressDispatcher  {...@Overridepublic void dispatchOnStart() {final String utteranceId = getUtteranceId();if (utteranceId != null) {mCallbacks.dispatchOnStart(getCallerIdentity(), utteranceId);}}@Overridepublic void dispatchOnAudioAvailable(byte[] audio) {final String utteranceId = getUtteranceId();if (utteranceId != null) {mCallbacks.dispatchOnAudioAvailable(getCallerIdentity(), utteranceId, audio);}}@Overridepublic void dispatchOnSuccess() {final String utteranceId = getUtteranceId();if (utteranceId != null) {mCallbacks.dispatchOnSuccess(getCallerIdentity(), utteranceId);}}@Overridepublic void dispatchOnStop() { ... }@Overridepublic void dispatchOnError(int errorCode) { ... }@Overridepublic void dispatchOnBeginSynthesis(int sampleRateInHz, int audioFormat, int channelCount) { ... }@Overridepublic void dispatchOnRangeStart(int start, int end, int frame) { ... }}private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> {...public void dispatchOnStart(Object callerIdentity, String utteranceId) {ITextToSpeechCallback cb = getCallbackFor(callerIdentity);if (cb == null) return;try {cb.onStart(utteranceId);} ...}public void dispatchOnAudioAvailable(Object callerIdentity, String utteranceId, byte[] buffer) {ITextToSpeechCallback cb = getCallbackFor(callerIdentity);if (cb == null) return;try {cb.onAudioAvailable(utteranceId, buffer);} ...}public void dispatchOnSuccess(Object callerIdentity, String utteranceId) {ITextToSpeechCallback cb = getCallbackFor(callerIdentity);if (cb == null) return;try {cb.onSuccess(utteranceId);} ...}...}
    
  6. ITextToSpeechCallback 的执行将通过 TextToSpeech 的中转抵达请求 App 的 Callback,以执行“TextToSpeech 调用”章节提到的进一步操作

    // TextToSpeech.java
    public class TextToSpeech {...private abstract class Connection implements ServiceConnection {...private final ITextToSpeechCallback.Stub mCallback =new ITextToSpeechCallback.Stub() {@Overridepublic void onStart(String utteranceId) {UtteranceProgressListener listener = mUtteranceProgressListener;if (listener != null) {listener.onStart(utteranceId);}}...};}
    }// TTSTest.kt
    class TTSTest(context: Context) {init {tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {override fun onStart(utteranceId: String?) { ... }override fun onDone(utteranceId: String?) { ... }override fun onStop(utteranceId: String?, interrupted: Boolean) { ... }override fun onError(utteranceId: String?)  { ... }})}....
    }
    

5 使用和实现上的注意

对于 TTS 请求方有几点使用上的建议:

  1. TTS 播报前记得申请对应 type 的音频焦点
  2. TTS Request App 的 Activity 或 Service 生命周期销毁的时候,比如 onDestroy() 等时候,需要调用 TextToSpeech 的 shutdown() 释放连接、资源
  3. 可以通过 addSpeech() 指定固定文本的对应 audio 资源(比如说语音里常用的几套唤醒后的欢迎词 audio),在后续的文本请求时直接播放该 audio,免去文本转语音的过程、提高效率

对于 TTS Engine 提供方也有几点实现上的建议:

  1. TTS Engine 的各实现要和 TTS 的 SynthesisCallback 做好对接,要留意只能在该 callback 已经执行了 start() 并未结束的条件下调用 done()。否则 TTS 会发生如下两种错误:

    • Duplicate call to done()
    • done() was called before start() call
  2. TTS Engine 核心作用是将 text 文本合成 speech 音频数据,合成到数据之后 Engine 当然可以选择直接播报,甚至不回传音频数据。但建议将音频数据回传,交由系统 AudioTrack 播报。一来交由系统统一播报;二来 Request App 亦可以拿到音频数据进行 cache 和分析

6 结语

可以看到 Request App 不关心实现、只需通过 TextToSpeech 几个 API 便可完成 TTS 的播报操作。而且 TTS 的实现也只需要按照 TextToSpeechService 约定的框架、回调实现即可,和 App 的对接工作由系统完成。

我们再花点时间梳理下整个过程:

流程:

  1. TTS Request App 调用 TextToSpeech 构造函数,由系统准备播报工作前的准备,比如通过 Connection 绑定和初始化目标的 TTS Engine
  2. Request App 提供目标 text 并调用 speak() 请求
  3. TextToSpeech 会检查目标 text 是否设置过本地的 audio 资源,没有的话回通过 Connection 调用 ITextToSpeechService AIDL 的 speak() 继续
  4. TextToSpeechService 收到后封装请求 SynthesisRequest 和用于回调结果的 SynthesisCallback 实例
  5. 之后将两者作为参数调用核心实现 onSynthesizeText(),其将解析 Request 并进行 Speech 音频数据合成
  6. 此后通过 SynthesisCallback 将合成前后的关键回调告知系统,尤其是 AudioTrack 播放
  7. 同时需要将 speak 请求的结果告知 Request App,即通过 UtteranceProgressDispatcher 中转,实际上是调用 ITextToSpeechCallback AIDL
  8. 最后通过 UtteranceProgressListener 告知 TextToSpeech 初始化时设置的各回调

7 语音文章推荐

  • 如何打造车载语音交互:Google Voice Interaction 给你答案

8 参考 / 源码

  • OnUtteranceCompletedListener 已废弃
  • TextToSpeech.java
  • TextToSpeechManagerService.java
  • TextToSpeechService.java

相关文章:

直面原理:5 张图彻底了解 Android TextToSpeech 机制

ChatGPT 如此火爆&#xff0c;但它的强悍在于 NLU&#xff08;自然语言理解&#xff09;、DM&#xff08;对话管理&#xff09;和 NLG &#xff08;自然语言生成&#xff09;这三块&#xff0c;而 Recognition 识别和 TTS 播报这两块是缺失的。假使你的 App 接入了 ChatGPT&…...

Ruby Socket 编程

Ruby提供了两个级别访问网络的服务&#xff0c;在底层你可以访问操作系统&#xff0c;它可以让你实现客户端和服务器为面向连接和无连接协议的基本套接字支持。 Ruby 统一支持应用程序的网络协议&#xff0c;如FTP、HTTP等。 不管是高层的还是底层的。ruby提供了一些基本类&a…...

Vue3+ElementPlus+koa2实现本地图片的上传

一、示例图二、实现过程利用Koa2书写提交图片的后台接口这个模块是我写的项目中的其中一个板块——上传图片&#xff0c;这个项目的后台接口主要是是使用了后端的Koa2框架&#xff0c;前端小伙伴想要试着自己书写一些增删改查的接口可以从这个入手&#xff0c;Koa2用来了解后端…...

常见漏洞之 Fastjson

数据来源 01 Fastjson相关介绍 》Fastjson概述 》Fastjson历史漏洞 02 Fastson的识别与漏洞发现 》Fastjson寻找 》Fastjson漏洞发现&#xff08;利用 dnslog&#xff09; 03 修复建议 建议1&#xff1a;使用fastjson1.2.83版本&#xff1b; Github地址&#xff1a;https:…...

绕过Nginx Host限制

目录绕过Nginx Host限制SNI第三种方法&#xff1a;总结绕过Nginx Host限制 SNI SNI&#xff08;Server Name Indication&#xff09;是 TLS 的扩展&#xff0c;这允许在握手过程开始时通过客户端告诉它正在连接的服务器的主机名称。 作用&#xff1a;用来解决一个服务器拥有…...

Visual Studio 2022 常用快捷键,记录一下别忘记~

Visual Studio 2022 常用快捷键&#xff0c;记录一下别忘记~ CtrlEC 注释代码 CtrlEU 取消注释代码 CtrlED 格式化全部代码 CtrlShiftA 新建类 CtrlRG 删除无效Using CtrlH 批量替换 CtrlG 跳转到指定行 CtrlEE 在交互窗口中运行选中代码(很实用) AltEnter 快速引用 …...

软件测试回顾---重点知识

软件测试重点知识回顾 8.1.1软件测试的目的是 尽可能的发现程序中的错误并不是发现所有的错误并不是证明程序是错误的也不是为了调试程序8.1.2白盒测试根据什么设置测试用例&#xff1f;黑盒测试根据什么设置测试用例&#xff1f; 白盒测试根据内部逻辑来设计的黑盒测试根据的是…...

2D图像处理:2D Shape_Base_Matching_缩放_旋转_ICP_显示ROI

文章目录 调试结果参考调试说明问题0:并行运行问题问题1:模板+Mask大小问题问题2:组合缩放和旋转问题3:可以直接将计算边缘的代码删除问题4:如何在原始图像上显示匹配到的ROI问题5:计算的原始旋转角度不需要判断,直接可以在ICP中使用问题6:绘制坐标轴问题7:绘制ROI调试…...

HTTP、HTTPS

目录 1.HTTP 1.1.概述 1.2.报文结构 1.2.1.请求报文 1.2.2.响应报文 1.3.方法 2.HTTPS 1.HTTP 1.1.概述 HTTP&#xff0c;超文本传输协议&#xff0c;WEB体系选用了该协议作为应用层协议。 1.2.报文结构 1.2.1.请求报文 HTTP的请求报文&#xff08;request&#xff0…...

计算机网络之http03:HTTPS RSA握手解析

不同的秘钥交换算法,握手过程可能略有差别 上文对HTTPS四次握手的学习 SSL/TLS Secure Sockets Layer/Transport Layer Security 协议握手过程 四次通信&#xff1a;请求服务端公钥 2次 秘钥协商 2次 &#xff08;1&#xff09;ClientHello请求 客户端向服务端发送client…...

一款针对EF Core轻量级分表分库、读写分离的开源项目

更多开源项目请查看&#xff1a;一个专注推荐.Net开源项目的榜单 在项目开发中&#xff0c;如果数据量比较大&#xff0c;比如日志记录&#xff0c;我们往往会采用分表分库的方案&#xff1b;为了提升性能&#xff0c;把数据库查询与更新操作分开&#xff0c;这时候就要采用读写…...

Linux环境变量讲解

目录 环境变量 alias命令 type命令 变量分类 Linux最主要的全局环境变量 环境变量 变量是计算机系统用于保存可变数值的数据类型 在Linux中&#xff0c;一般变量都是大写&#xff0c;命令是小写 在Linux中&#xff0c;变量直接使用&#xff0c;不需要定义&#xff08;更快…...

iptables和nftables的使用

文章目录前言iptable简介iptable命令使用iptables的四表五链nftables简介nftables命令的时候nftables与iptables的区别iptables-legacy和iptables-nft实例将指定protocol:ip:port的流量转发到本地指定端口前言 本文展示了&#xff0c;iptables和nftable命令的使用。 # 实验环…...

中小学信息学相关编程比赛清单及报名网站汇总(C++类)

1、NOI系列比赛(CSP-J CSP-S NOIP NOI APIO CTSC IOI ISIJ等) NOI官网 NOI全国青少年信息学奥林匹克竞赛https://www.noi.cn/ 2、蓝桥杯青少年创意编程大赛 https://www.lanqiaoqingshao.cn/home 3、中国电子协会考评中心...

06Makefile

Makefile 1、Makefile简介 一个工程中的源文件不计其数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的规则来指定哪些文件需要先编译&#xff0c;哪些文件需要后编译&#xff0c;哪些文件需要重新编译&#xff0c;甚至于进行更复杂…...

【C++】模板初阶

&#x1f345;讨厌废话&#xff0c;直接上车 ☃️1.泛型编程 void Swap(int& left, int& right) { int temp left; left right; right temp; } void Swap(double& left, double& right) { double temp left; left right; right temp; } void Swap(char&…...

vue+nodejs考研资料分享系统vscode - Visual Studio Code

前端技术&#xff1a;nodejsvueelementui,视图层其实质就是vue页面&#xff0c;通过编写vue页面从而展示在浏览器中&#xff0c;编写完成的vue页面要能够和控制器类进行交互&#xff0c;从而使得用户在点击网页进行操作时能够正常。 Express 框架于Node运行环境的Web框架, 目 …...

LeetCode_单周赛_332

6354. 找出数组的串联值 题意 将数组首尾元素接在一起&#xff0c;就是串联值。 串联之后删除&#xff0c;如果只剩下一个元素&#xff0c;加上这个元素即可 双指针&#xff0c;从首和尾向中间移动即可 code **注意&#xff1a;**用 long 没看题目用了 int wa了一发 clas…...

[LeetCode周赛复盘] 第 332 场周赛20230212

[LeetCode周赛复盘] 第 332 场周赛20230212 一、本周周赛总结二、 [Easy] 6354. 找出数组的串联值1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6355. 统计公平数对的数目1. 题目描述2. 思路分析3. 代码实现四、[Medium] 6356. 子字符串异或查询1. 题目描述2. 思路分析3. 代…...

C++轻量级RPC库RpcCore

C轻量级的RPC库&#xff0c;可用于任何项目中&#xff0c;甚至单片机。 方便平台直接相互进行功能调用。 基于asio的实现 asio_net 也可用在esp32适用于ESP32/ESP8266的实现 esp_rpc 目前也有一些轻量的库&#xff0c;参考了protobuf&#xff08;或者依赖它&#xff09;&…...

Mysql的视图

视图的特点&#xff1a; 1.视图可以看做一个虚拟的表&#xff0c;本身是不存储数据的。 视图的本质可以看作是存储起来的select语句 2.视图中涉及到的表都统称为基表 3.针对视图多DML操作&#xff0c;会影响到对应基表中的数据。反之亦然 4.视图本身的删除&#xff0c;不会…...

2/12考试总结

时间安排 8:30–8:50 读题&#xff0c;T1 不知道是个啥,T2是个dp &#xff0c;T3可能也是 dp 之类的。 8:50–9:30 T1&#xff0c;读了好几遍才理解了题意&#xff0c;对于部分分有爆搜。考虑正解&#xff0c;想到预处理后O(1) 查询&#xff0c;问题是如何由已知的信息得到所有…...

第三章虚拟机的克隆,快照,迁移删除

1.虚拟机的克隆 如果你已经安装了一台linux操作系统&#xff0c;你还想再更多的&#xff0c;没有必要再重新安装&#xff0c;你只需要克 隆就可以&#xff0c;看演示。 方式1&#xff0c;直接拷贝一份安装好的虚拟机文件,再用虚拟机打开这个文件方式2&#xff0c;使用vmware的…...

华为OD机试 - 任务总执行时长(Python)| 真题含思路

任务总执行时长 题目 任务编排服务负责对任务进行组合调度。 参与编排的任务又两种类型, 其中一种执行时长为taskA, 另一种执行时长为taskB。 任务一旦开始执行不能被打断,且任务可连续执行。 服务每次可以编排 num 个任务。 请编写一个方法,生成每次编排后的任务所有可…...

LeetCode 热题 C++ 114. 二叉树展开为链表

给你二叉树的根结点 root &#xff0c;请你将它展开为一个单链表&#xff1a; 展开后的单链表应该同样使用 TreeNode &#xff0c;其中 right 子指针指向链表中下一个结点&#xff0c;而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。 示例 1&#xf…...

Spring的事务控制-基于AOP的声明式事务控制

Spring的事务控制-基于AOP的声明式事务控制 Spring事务编程概述 事务是开发中必不可少的东西&#xff0c;使用JDBC开发时&#xff0c;我们使用connection对事务进行控制&#xff0c;使用MyBatis时&#xff0c;我们使用SqlSession对事务进行控制&#xff0c;缺点就是&#xff…...

SSO(单点登陆)

Single Sign On 一处登陆、处处可用 0、前置概念&#xff1a; 1&#xff09;、单点登录业务介绍 早期单一服务器&#xff0c;用户认证。 缺点&#xff1a;单点性能压力&#xff0c;无法扩展 分布式&#xff0c; SSO(single sign on)模式 解决 &#xff1a; 用户身份信息独…...

线程和QObjects

QObject的可重入性&#xff1a; QThread继承了QObject&#xff0c;它发出信号以指示线程开始或完成执行&#xff0c;并提供一些插槽。 QObjects可以在多个线程中使用发出调用其他线程中槽的信号&#xff0c;并将事件发布到在其他线程中“活动”的对象。这是可能的&#xff0…...

最新中文版FL Studio21水果软件下载安装图文教程

FL Studio是目前流行广泛使用人数最多音乐编曲制作软件&#xff0c;这款软件相信广大网友并不陌生&#xff0c;今天带来的是FL中文版本&#xff0c;所有的功能都能在线编辑&#xff0c;用户直接就能操作&#xff0c;同时因为是21水果是最新版&#xff0c;所以增加了新的功能&am…...

pandas数据分析35——多个数据框实现笛卡尔积

什么是笛卡尔积。就是遍历所有组合的可能性。 比如第一个盒子有[1,2,3]三个号码球&#xff0c;第二个盒子有[4,5]两个号码球。那么从每个盒子里面分别拿一个球共有3*2两种可能性&#xff0c;其集合就是{[1,4],[2,4],[3,4],[1,5],[2,5],[3,5]},这个就是笛卡尔积。 三个盒子也是…...

wordpress旅行地图主题/成人职业培训机构

第九届北京高中数学知识应用竞赛初赛 第二题原题&#xff1a;一只老鼠为了躲避猫的追捕&#xff0c;跳入了半径为&#xff32;的圆形湖中&#xff0e;猫不会游泳&#xff0c;只能沿湖岸追击&#xff0c;并且总是试图使自己离老鼠最近(即猫总是试图使自己在老鼠离岸最近的点上)…...

做鸭网站/如何在百度做推广

一.RF之UI自动化测试环境 1&#xff1a;通过pip安装扩展库: pip install robotframework-seleniumlibrary 2.:下载谷歌游览器和对应驱动 https://www.cnblogs.com/loved-wangwei/p/8993013.html 3.将游览器驱动放在python的目录下 比如&#xff1a;我的python安装在D:\install…...

做移门图的 网站有哪些/seo北京公司

概要 电影文件有很多基本的组成部分。首先&#xff0c;文件本身被称为容器Container&#xff0c;容器的类型决定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接着&#xff0c;你有一组流&#xff0c;例如&#xff0c;你经常有的是一个音频流和一个视频流。&…...

电子销售网站模板/百度推广基木鱼

昨天&#xff0c;学习了Jar包的打包过程&#xff0c;现在打算记录一下&#xff0c;如何在Eclipse中导入外部Jar包。 第一步&#xff1a;在项目中鼠标右键>>New>>点击Folder。 第二步&#xff1a;在弹出窗口将Folder name命名为lib&#xff0c;点击确定。 第三步&am…...

谈谈网站的开发流程/做seo排名好的公司

CNN的经典结构始于 1998 年的 LeNet&#xff0c;成于 2012 年历史性的 AlexNet&#xff0c;从此大盛于图像相关领域&#xff0c;主要包括&#xff1a; LeNet&#xff0c;1998年AlexNet&#xff0c;2012年ZF-net&#xff0c;2013年GoogleNet&#xff0c;2014年VGG&#xff0c;2…...

深圳做网站服务公司/深圳竞价托管公司

纽约大学计算机科学专业提供一个高度适应性的计算机科学课程&#xff0c;让你在你的兴趣范围内完成学位。除了院校在计算机科学基础课程中的核心课程之外&#xff0c;你还有很多选修课可供选择。你可以把重点放在计算机和网络安全、分布式系统和网络、计算机图形学、网络搜索技…...