[Android]AsyncChannel介绍

news2024/11/17 21:18:28

背景

WifiManager.java中,随处可见这样的方法调用实现:
代码路径:frameworks/base/wifi/java/android/net/wifi/WifiManager.java

    public void connect(int networkId, ActionListener listener) {
		...
        getChannel().sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
    }

    public void save(WifiConfiguration config, ActionListener listener) {
		...
        getChannel().sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
    }

继续追踪getChannel()的实现不难发现,这个方法返回的是一个AsyncChanel的实例对象:

    private synchronized AsyncChannel getChannel() {
        if (mAsyncChannel == null) {
            Messenger messenger = getWifiServiceMessenger();
			...
            mAsyncChannel = new AsyncChannel();
            mConnected = new CountDownLatch(1);

            Handler handler = new ServiceHandler(mLooper);
            mAsyncChannel.connect(mContext, handler, messenger);
            try {
                mConnected.await();
            } catch (InterruptedException e) {
                Log.e(TAG, "interrupted wait at init");
            }
        }
        return mAsyncChannel;
    }

虽然可以进一步快速定位到AsyncChannel.sendMessage()的接收端是getWifiServiceMessenger()返回的Messenger实例对象,并通过Messenger.send()方法实现跨进程调用,从而由WifiServiceImpl.ClientHandler接收并处理消息;

但是由于有如下疑问:

  • Handler本身是不可跨进程的,因此AsyncChannel的跨进程是如何实现的;
  • 为何要使用AsyncChannel,而不是直接使用AIDL调用?

因此决定好好理一理这个Android Framework的Internal工具类;

整体介绍

根据类声明处的注释可知,AsyncChannel可用于实现如下四种通信之一:

  • 进程两个线程Handler的单向通信;
  • 进程两个线程Handler的双向通信;
  • 进程两个线程Handler的单向通信;
  • 进程两个线程Handler的双向通信;

同时,这4种通信方式还可以以同步异步两种连接、通信方式实现;
所以严格意义来说,一共是8种通信方式;
但是这里的同步异步差别不大,因此只会在需要区别的地方提出,其余部分就一概而论了;

进程和进程很好理解,事实上,无论是进程,还是进程的两个HandlerAsyncChannel的通信都是借助实现了Parcelable接口的Messenger类实现,因此区别仅是走不走Binder,对于上层表征来看是没有区别的。

这里重点解释下单向双向的含义:

单向指的是客户端可以向服务端发送消息,但是服务端无法知道客户端的任何情况(总共连接上的客户端个数,发送这个消息的客户端是哪一个等)

注意:这里说的单向并不是指数据流只能从ClientServer。事实上Server端是可以回执消息给Client的,只是Server端无法知晓Client,因此无法主动发送消息给Client;)

双向指的是客户端与服务端可以互发消息,因此彼此是可以了解对方情况的;

由于双向本质上就是两端都连接一次对端,各自持有一个AsyncChannel实例,向对端发送消息,互为Client/Server,并且的场景使用得比较少,因此本文后续若无特别说明,均以单向的连接、通信方式为例;

工作原理

概览

先上一张图:
在这里插入图片描述

简单解读下:

  1. Looper1Looper2表示两个线程,由于在讨论Handler的通信,因此用Looper更直观;
  2. Looper2可以与Looper1在不同进程,也可以在同一进程,甚至可以是同一个Looper(即同一线程);
  3. Looper1下的Handler1x构造AsyncChannel并发起对Looper2Handler2x的连接请求时,我们认为Handler1x是客户端(Client),Looper2中的目标Handler则为服务端(Server);
  4. 在单向连接情况下,只能由Client发送消息到ServerServer端只能在处理完Client发送过来的消息后,通过replyToMessage方法定向回执一个MessageClient,无法主动调用sendMessage/sendMessageSynchronouslyClient发送消息;
  5. 而在双向连接的状态下,双方均可调用sendMessage/sendMessageSynchronously方法向对端发送消息,也可通过replyToMessage方法定向回执一个Message给对端;
  6. 消息传递利用了IMessenger这一AIDL接口:如果Looper1Looper2在同一进程内,甚至为同一线程时,AIDL会直接走本地调用;如果Looper1Looper2在不同进程时,会利用Binder调用过去(详见AIDL接口文件生成的JAVA类中Stub.asInterface()方法实现)

连接

最常用的连接方法是如下几个:

//异步
public void connect(Context srcContext, Handler srcHandler, Handler dstHandler);
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger);
//同步
public int connectSync(Context srcContext, Handler srcHandler, Handler dstHandler);
public int connectSync(Context srcContext, Handler srcHandler, Messenger dstMessenger);
public int fullyConnectSync(Context srcContext, Handler srcHandler, Handler dstHandler);

方法1、3都是简单地将最后一个类型为Handler参数封装成Messenger并调用重载方法,因此我们以方法2、4为例分析:

    public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // We are connected
        connected(srcContext, srcHandler, dstMessenger);
        // Tell source we are half connected
        replyHalfConnected(STATUS_SUCCESSFUL);
    }

    public int connectSync(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // We are connected
        connected(srcContext, srcHandler, dstMessenger);
        return STATUS_SUCCESSFUL;
    }


    public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // Initialize source fields
        mSrcContext = srcContext;
        mSrcHandler = srcHandler;
        mSrcMessenger = new Messenger(mSrcHandler);

        // Initialize destination fields
        mDstMessenger = dstMessenger;
    }

前面说过,AsyncChannel利用的是IMessenger实现的通信,那么所谓的“连接”实际上也就是把mSrcMessengermDstMessenger等相关成员变量初始化、赋值即可;这部分无论是connect还是connectSync方法,都是同步执行的;

那么所谓的异步是什么?

其实就是返回值是否同步返回:如果是connectSync方法,则在完成连接后直接return STATUS_SUCCESSFUL,而如果是connect方法,则没有返回值,连接成功的状态通过replyHalfConnected传递给mSrcMessenger,需要mSrcHandlerhandleMessage中处理:

    private void replyHalfConnected(int status) {
        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
        msg.arg1 = status;
        msg.obj = this;
        msg.replyTo = mDstMessenger;
        if (!linkToDeathMonitor()) {
            // Override status to indicate failure
            msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
        }

        mSrcHandler.sendMessage(msg);
    }

关于异步连接,考虑到跨进程的AsyncChannel需要传递Binder以建立连接,而这一操作普遍通过bindService后从ServiceConnection.onServiceConnected()回调方法处获取,因此针对这一使用场景,AsyncChannel提供了两个个封装方法:

public void connect(Context srcContext, Handler srcHandler, Class<?> klass);
public void connect(Context srcContext, Handler srcHandler, String dstPackageName, String dstClassName);

当然,由于需要等待ServiceConnection.onServiceConnected()回调方法,因此这两个方法没有同步的响应实现;

断开

断开方法很单一,只有一个:

    public void disconnect() {
    	/*
    	 * 如果mConnection不为null,表明使用了上方所述的两种封装方法进行bindService建立连接:
    	 *    - connect(Context srcContext, Handler srcHandler, Class<?> klass);
    	 *    - connect(Context srcContext, Handler srcHandler, String dstPackageName, String dstClassName);
    	 * 这种情况下需要调用unbindService断开与远端Service的连接;
    	 * 注意,unbindService不会调用到ServiceConnection.onServiceDisconnected()
    	 */
        if ((mConnection != null) && (mSrcContext != null)) {
        	/* 
        	 * 注意,unbindService不会调用到ServiceConnection.onServiceDisconnected(),因此
        	 * ServiceConnection.onServiceDisconnected()中的replyDisconnected(STATUS_SUCCESSFUL)不会执行,
        	 * 需要下方显式调用一次
        	 */        	
            mSrcContext.unbindService(mConnection);
            mConnection = null;
        }
        try {
			/*
			 * 尝试给服务端发送CMD_CHANNEL_DISCONNECTED信息,尽管可能收不到
			 */
            Message msg = Message.obtain();
            msg.what = CMD_CHANNEL_DISCONNECTED;
            msg.replyTo = mSrcMessenger;
            mDstMessenger.send(msg);
        } catch(Exception e) {
        }
        /*
         * 给客户端的Handler发送CMD_CHANNEL_DISCONNECTED消息;
         * 如果上面mConnection不为null,这里会重复发送
         */
        replyDisconnected(STATUS_SUCCESSFUL);
        mSrcHandler = null;
        // Unlink only when bindService isn't used
        if (mConnection == null && mDstMessenger != null && mDeathMonitor!= null) {
            mDstMessenger.getBinder().unlinkToDeath(mDeathMonitor, 0);
            mDeathMonitor = null;
        }
    }

有几个点需要澄清一下:

  • 无论是通过自己实现bindService,或是通过AsyncChannel提供的内部封装bindService实现的connect方法,其本质只是通过ServiceConnection.onServiceConnected()回调方法获取到服务端的Messenger(以IMessenger实例对象传递);此后无论Service是否断开,均与AsyncChannel的连接没有必然联系;
  • disconnect()方法内之所以当mConnection不为null时要调用unbindService(mConnection),并不是为了彻底断开AsyncChannel的连接,而是由于mConnection仅在内部封装了bindService逻辑的connect方法调用时创建,因此在断开连接后,mConnection维系的与服务端Service的绑定关系也就没有必要了,因此需要断开;
  • 由上可得,unbindService后,只要服务端的Handler还在,AsyncChannel连接没有断开,则依旧可以通过AsyncChannel发送消息到服务端;
  • 同理,当AsyncChannel断开后,如果是自行建立的绑定Service并不需要立即解绑(unbindService);

通信

发送消息

最常用的发送消息方法是如下几个:

//异步
public void sendMessage(Message msg);
public void sendMessage(int what);
public void sendMessage(int what, int arg1);
public void sendMessage(int what, int arg1, int arg2);
public void sendMessage(int what, int arg1, int arg2, Object obj);
public void sendMessage(int what, Object obj);

//同步
public Message sendMessageSynchronously(Message msg);
public Message sendMessageSynchronously(int what);
public Message sendMessageSynchronously(int what, int arg1);
public Message sendMessageSynchronously(int what, int arg1, int arg2);
public Message sendMessageSynchronously(int what, int arg1, int arg2, Object obj);
public Message sendMessageSynchronously(int what, Object obj);

无论是同步还是异步,大部分方法都是重载方法,用于适用于各种场景的Message对象封装,只是同步与异步在发送消息后,是否等待对端返回结果:

//frameworks/base/core/java/com/android/internal/util/AsyncChannel.java

    public void sendMessage(Message msg) {
    	//重要:这个字段决定了回执消息发送给谁,此处主要回执给自己,所以必然是mSrcMessenger
        msg.replyTo = mSrcMessenger;
        try {
            mDstMessenger.send(msg);
        } catch (RemoteException e) {
            replyDisconnected(STATUS_SEND_UNSUCCESSFUL);
        }
    }
	
	...
	
    public Message sendMessageSynchronously(Message msg) {
        Message resultMsg = SyncMessenger.sendMessageSynchronously(mDstMessenger, msg);
        return resultMsg;
    }

    ...

	//专门用于同步等待返回消息的工具类
    private static class SyncMessenger {
        //用一个栈来管理已经构造出来的SyncMessenger对象,用于复用
        private static Stack<SyncMessenger> sStack = new Stack<SyncMessenger>();
        private static int sCount = 0;
        //每个SyncMessenger运行在一个独立的线程,该线程有一个Handler用于获取返回的Message
        private HandlerThread mHandlerThread;
        private SyncHandler mHandler;
        //用于接收返回消息的Messenger,即利用上方mHandler构造的Messenger
        private Messenger mMessenger;

		...

        /** Synchronous Handler class */
        private class SyncHandler extends Handler {
            /** The object used to wait/notify */
            private Object mLockObject = new Object();
            /** The resulting message */
            private Message mResultMsg;

            /** Constructor */
            private SyncHandler(Looper looper) {
                super(looper);
            }

            /** Handle of the reply message */
            @Override
            public void handleMessage(Message msg) {
            	//将收到的消息生拷贝一份,并唤醒等待线程
                Message msgCopy = Message.obtain();
                msgCopy.copyFrom(msg);
                synchronized(mLockObject) {
                    mResultMsg = msgCopy;
                    mLockObject.notify();
                }
            }
        }

		//尝试复用栈内已经构造,且可用的SyncMessenger对象
		//如果栈是空的,说明没有可用的SyncMessenger对象,需要构造一个
        private static SyncMessenger obtain() {
            SyncMessenger sm;
            synchronized (sStack) {
                if (sStack.isEmpty()) {
                    sm = new SyncMessenger();
                    sm.mHandlerThread = new HandlerThread("SyncHandler-" + sCount++);
                    sm.mHandlerThread.start();
                    sm.mHandler = sm.new SyncHandler(sm.mHandlerThread.getLooper());
                    sm.mMessenger = new Messenger(sm.mHandler);
                } else {
                    sm = sStack.pop();
                }
            }
            return sm;
        }
		
		//使用完毕的SyncMessenger对象压入栈内,供下次obtain方法获取
        private void recycle() {
            synchronized (sStack) {
                sStack.push(this);
            }
        }

 		//发送同步消息
        private static Message sendMessageSynchronously(Messenger dstMessenger, Message msg) {
        	//尝试从栈内获取SyncMessenger对象,如果栈内没有,构造一个返回
            SyncMessenger sm = SyncMessenger.obtain();
            Message resultMsg = null;
            try {
                if (dstMessenger != null && msg != null) {
                	//替换监听回执消息的Messenger为本地临时构造的mMessenger
                	//这样mHandler就可以收到返回消息,并唤醒下方的wait,将回执消息同步返回给调用方
                    msg.replyTo = sm.mMessenger;
                    synchronized (sm.mHandler.mLockObject) {
                        if (sm.mHandler.mResultMsg != null) {
                            Slog.wtf(TAG, "mResultMsg should be null here");
                            sm.mHandler.mResultMsg = null;
                        }
                        //发送消息依然是异步的
                        dstMessenger.send(msg);
                        //然后调用线程等待mHandler收到回执消息并notify
                        sm.mHandler.mLockObject.wait();
                        //走到这里即表示回执消息已经收到,并且mHandler内部已经调用notify唤醒了该线程
                        //此时获取到mResultMsg返回给调用方即可;
                        resultMsg = sm.mHandler.mResultMsg;
                        sm.mHandler.mResultMsg = null;
                    }
                }
            } catch (InterruptedException e) {
                Slog.e(TAG, "error in sendMessageSynchronously", e);
            } catch (RemoteException e) {
                Slog.e(TAG, "error in sendMessageSynchronously", e);
            }
            //回收SyncMessenger对象,将其压栈,方便后续复用
            sm.recycle();
            return resultMsg;
        }
    }

从上面代码片段以及注释讲解可以知道,异步发送消息,srcHandler这边要么不关心该消息的回执情况;要么需要在内部的handleMessage中监听回执结果;
而同步发送消息,则会在调用线程等待,并临时构造了一个SyncMessenger,通过其内部的SyncMessenger来接受回执消息,并通知调用线程唤醒,获取回执消息并返回;

回执消息

回执消息也有大量重载方法,且全为异步操作:

public void replyToMessage(Message srcMsg, Message dstMsg);
public void replyToMessage(Message srcMsg, int what);
public void replyToMessage(Message srcMsg, int what, int arg1);
public void replyToMessage(Message srcMsg, int what, int arg1, int arg2);
public void replyToMessage(Message srcMsg, int what, int arg1, int arg2, Object obj);
public void replyToMessage(Message srcMsg, int what, Object obj);

本质上都是调用的第一个:

    /**
     * Reply to srcMsg sending dstMsg
     *
     * @param srcMsg
     * @param dstMsg
     */
    public void replyToMessage(Message srcMsg, Message dstMsg) {
        try {
            dstMsg.replyTo = mSrcMessenger;
            srcMsg.replyTo.send(dstMsg);
        } catch (RemoteException e) {
            log("TODO: handle replyToMessage RemoteException" + e);
            e.printStackTrace();
        }
    }

这里可以看到,回执消息本质上是调用的srcMsg.replyTo.send()方法发送了一个Message
srcMsg即为ServerhandleMessage方法传入的参数;
srcMsg.replyTo是一个IMessenger,即为发送这个srcMsgHandler对应的Messenger
也就是说,通过这句调用srcMsg.replyTo.send(),即可向Client端发送一个Message,而在Client端会将其认为是刚才发送的那条消息的回执;

这里也反向印证了,上面sendMessage的时候,为何需要将mSrcMessenger赋值给msg.replyTo,而sendMessageSynchronously则需要将临时的SyncMessenger对象中mMessenger赋值给msg.replyTo,这样才能让Client端的对应Handler收到回执消息;

此外,细心的同学其实可以发现,单向通信模型下的AsyncChannelreplyToMessage调用会发送给哪个Handler,取决于接收到的这个Message对象(即srcMsg中封装的replyTo成员变量指向哪个Messenger),与AsyncChannel是不是同一个实例对象没有关系(这也是必然的,跨进程的场景下,两端不可能持有同一个AsyncChannel实例对象)

以至于我们可以在WifiStateMachine.java中看到,回执消息用的AsyncChannel只是随便new的一个:

//frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java

	...

    // Channel for sending replies.
    private AsyncChannel mReplyChannel = new AsyncChannel();

    ...

    private void replyToMessage(Message msg, int what) {
        if (msg.replyTo == null) return;
        Message dstMsg = obtainMessageWithWhatAndArg2(msg, what);
        mReplyChannel.replyToMessage(msg, dstMsg);
    }

    private void replyToMessage(Message msg, int what, int arg1) {
        if (msg.replyTo == null) return;
        Message dstMsg = obtainMessageWithWhatAndArg2(msg, what);
        dstMsg.arg1 = arg1;
        mReplyChannel.replyToMessage(msg, dstMsg);
    }

    private void replyToMessage(Message msg, int what, Object obj) {
        if (msg.replyTo == null) return;
        Message dstMsg = obtainMessageWithWhatAndArg2(msg, what);
        dstMsg.obj = obj;
        mReplyChannel.replyToMessage(msg, dstMsg);
    }

总结

整个AsyncChannel对于第一次接触的开发人员来说,还是比较难以理解的,这里我列几条总结性的描述,方便理解:

  1. AsyncChannel通常使用的是单向连接模式;
  2. AsyncChannel的单向连接模式,仅需要Client构造AsyncChannel实例对象,并调用connect/connectSync即可,前者在其收到CMD_CHANNEL_HALF_CONNECTED消息后表示连接成功,后者在方法返回值中即可确定连接是否成功;
  3. AsyncChannel的单向连接模式下,ClientServer端身份固定,不会变换:Client通过sendMessage/sendMessageSynchronously方法向Server端发送MessageServer端只能在接收到Message后处理并通过replyToMessage方法回执给Client端,无法主动发送其他MessageClient
  4. 进程内通信使用AsyncChannel比较冗余,可直接使用两个Handler即可,除非对发送消息的回执有一定要求;
  5. 因此AsyncChannel更多还是用于跨进程的通信上;

最后,解答一下一开始的两个疑问:

  • Handler本身是不可跨进程的,因此AsyncChannel的跨进程是如何实现的;

    答:进程间通信使用AsyncChannel,本质上是利用了IMessenger这个Parcelable接口的跨进程特性,本质上还是Binder调用;

  • 为何要使用AsyncChannel,而不是直接使用AIDL调用?
    答:Android Framework中定义AIDL接口如果需要增加、更改、删减,需要update API,同时通知APP适配。这通常是不被建议、甚至不被允许的。而使用AsyncChannel实现进程间通信,只需要定义一个获取对端MessengerAIDL接口,后续扩展只需要两端协商好Messagewhat取值即可,可扩展性方面较好;

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

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

相关文章

你真的会跟 ChatGPT 聊天吗?(下)

接《你真的会跟 ChatGPT 聊天吗&#xff1f;&#xff08;上&#xff09;》&#xff0c;本文下半部分没有无毛猫那么搞笑的内容啦&#xff01;即使如此&#xff0c;哪怕你对文中提及的技术不大了解&#xff0c;也可以毫无压力地看完这篇&#xff0c;描述如何更好地获得 ChatGPT …

视频怎么压缩到20M以内

视频怎么压缩到20M以内&#xff1f;我们知道在很多聊天软件中是限制传输的内容大小的&#xff0c;比如说视频大小会限制20M以内。还有就是我们在发一些邮件时&#xff0c;我们在上传附件的时候也是限制视频大小在20M以内。所以说我们有需要把视频压缩到20M以内的情况。那么针对…

记录一次Windows7操作系统渗透测试

#本文档仅用于实验&#xff0c;请勿用来使用恶意攻击&#xff01; 《中华人民共和国网络安全法》中&#xff0c;恶意破坏计算机信息系统罪在第二十七条被明确规定&#xff0c;规定内容为&#xff1a; 第二十七条 任何单位和个人不得为达到破坏计算机信息系统安全的目的&#x…

Linux发行版新秀:基于Ubuntu、系统核心 “不可变”

Vanilla OS 是近期才公开发布的 Linux 发行版&#xff0c;基于 Ubuntu 构建&#xff0c;免费且开源&#xff0c;默认桌面环境是 GNOME。虽然 Vanilla OS 的底层是 Ubuntu&#xff0c;但它并不是简单地基于 Ubuntu 进行 "remix"&#xff0c;而且外观看起来也不像 Ubun…

FreeRTOS:列表和列表项

要想看懂 FreeRTOS 源码并学习其原理&#xff0c;有一个东西绝对跑不了&#xff0c;那就是 FreeRTOS 的列表和列表项。列表和列表项是FreeRTOS的一个数据结构&#xff0c; FreeRTOS 大量使用到了列表和列表项&#xff0c;它是 FreeRTOS 的基石。要想深入学习并理解 FreeRTOS&am…

【LeetCode】环形链表II+结论证明

题目链接&#xff1a;环形链表II 题目&#xff1a;给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表…

Grafana系列-统一展示-7-ElasticSearch数据源

系列文章 Grafana 系列文章 ElasticSearch 数据源 Grafana内置了对Elasticsearch的支持。你可以进行多种类型的查询&#xff0c;以可视化存储在Elasticsearch中的日志或指标&#xff0c;并使用存储在Elasticsearch中的日志事件对图表进行注释。 配置 ES 数据源 关键的几项…

在两道多线程基础题“顺序打印”中对比一下Java中的wait()和join()

目录 一、基础 二、进阶 一、基础 有三个线程&#xff0c;线程名称分别为&#xff1a;a&#xff0c;b&#xff0c;c&#xff0c;每个线程打印自己的名称。 需要让他们同时启动&#xff0c;并按 c&#xff0c;b&#xff0c;a的顺序打印。 这道题要求打印 cba&#xff0c;且只打…

开发中proc文件系统的简单使用

使用proc文件系统 文章目录 使用proc文件系统1.meminfo文件2. free命令3、创建 /proc 节点4、使用 file_operations 实现 proc 文件读写 导向内核信息5、使用 seq_file 实现 proc 文件的读取 在Linux系统中&#xff0c; “/proc”文件系统十分有用&#xff0c; 它被内核用于向用…

可视化图表API格式要求有哪些?Sugar BI详细代码示例(4)

Sugar BI中的每个图表可以对应一个数据 API&#xff0c;用户浏览报表时&#xff0c;选定一定的过滤条件&#xff0c;点击「查询」按钮将会通过 API 拉取相应的数据&#xff1b;前面说过&#xff0c;为了确保用户数据的安全性&#xff0c;Sugar BI上的所有数据请求都在Sugar BI的…

进击数据基础设施新蓝海,厂商如何“择木而栖”?

文 | 螳螂观察 作者 | 李永华 多样的应用需求&#xff0c;倒逼底层存储能力不断升级&#xff1b; 复杂的数据状况&#xff0c;要求存储能够“兼容并蓄”&#xff1b; 客户企业在数字化方面的战略转型升级&#xff0c;总是触及到存储…… 当数据基础设施成为新的蓝海&#…

大学计算机基础-题库刷题-精选

题库刷题&#xff1a; 写在前面&#xff1a; 这个是我准备应对学校转专业考试而刷的题库&#xff0c; 也是大学计算机的题库&#xff0c;同样适用于大学计算机这门课的期末考试。 精选了一些重要的题目。 目录 题库刷题&#xff1a; 写在前面&#xff1a; 题目1&#x…

内卷时代,大厂产品经理仅用3步破局

本文首发自「慕课网」&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"&#xff01; 作者&#xff1a;申悦|慕课网讲师 在当下互联网环境下&#xff0c;产品经理究竟要如何破局&#xff1f; 我认为&#xff0c;既然要破局…

RocketMQ学习笔记:生产者Producer

DefaultMQProducer 根据上文&#xff1a;RocketMQ学习笔记&#xff1a;消息Message - 掘金 (juejin.cn)&#xff0c;我们定位到Producer中的这一行代码&#xff1a; java 复制代码 DefaultMQProducer producer new DefaultMQProducer("ProducerGroupName"); pro…

2023年3月GESP能力等级认证C++一级真题

一、单选题&#xff08;每题2分&#xff0c;共30分&#xff09; 1.以下不属于计算机输入设备的有&#xff08;B &#xff09;。(2分) A&#xff0e;键盘 B&#xff0e;音箱 C&#xff0e;鼠标 D&#xff0e;传感器 2.计算机系统中存储的基本单位用 B 来表示&#xff0c;它…

Git 常用命令笔记

下载安装这里就不赘述了&#xff0c;直接下一步就行&#xff01; 一、常用命令 1. 增加删除/文件 添加当前目录的所有文件到暂存区 git add .添加指定文件到暂存区 git add [file1] [file2] ...添加指定目录到暂存区&#xff0c;包括子目录 git add [dir]对于同一个文件的多…

多种方法解决There is no tracking information for the current branch的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误3.1 远程有分支3.2 远程无分支 4. 总结 1. 复现错误 今天发布某版本的项目&#xff0c;准备创建个v0point1分支&#xff0c;后期如果修改该版本&#xff0c;直接在该分支上修改即可。 首先&#xff0c;使用git branch v0point1命令…

问道游戏私人服务器架设+详细搭建教程+外网教程

搭建条件: 1、服务器一台, 2、下载服务端 搭建教程&#xff1a; 1.先安装宝塔 2、放行安全组的相应端口 具体要放行的端口有&#xff1a;3306、888、8888、5000、8101、8110、8120、8160-8168&#xff08;这个是范围之8160是一线&#xff0c;依次类推&#xff09; 3、安装数据库…

别点了!CAS登录对接,这个Bug让你反复登录!

目录 引言 背景描述 问题描述 问题排查 软件测试工程师发展规划路线 引言 你是否曾经在登录一个网站时&#xff0c;不断输入账号密码&#xff0c;却发现自己总是无法成功登录&#xff1f;或者你是否曾经遇到过跨域问题导致的登录失败&#xff1f; 今天我要和大家分享的就…

Speech and Language Processing之神经网络

上面这句话很好的解释了一件事&#xff0c;就是“大力出奇迹” &#xff0c;当神经元的数目足够足够多的时候&#xff0c;机器所能做到的事情就很复杂、很难理解了&#xff0c;这是不是说明chatgpt的成功也是因为大&#xff1f; 现代神经网络是一个由小型计算单元组成的网络&am…