2023年3月

原文地址:
Java/Kotlin 使用Redis模拟发送邮件验证码 - Stars-One的杂货小窝

Java中常用语连接Redis的库有
lettuce

jredis
,一般是推荐
lettuce
,其具有异步性,下面两种都简单来使用如何实现功能

jredis

1.引入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>

脚本使用:

fun main() {
    //1.测试连接
    val jedis = Jedis("127.0.0.1", 6379)
    val resp = jedis.ping()
    //为pong即为可用的
    if (resp == "PONG") {
        val key = "mykey"
        val value = "hello world"

        //写入数据
        jedis[key]=value
        //读数据
        val result = jedis[key]
        println(result)
        //  删除指定key
        val row = jedis.del(key)
        //影响的行数
        println(row)
        
        //设置60s后过期
        jedis.setex(key,60,value)
        //设置60ms后过期
        jedis.psetex(key,60,value)
        
        //剩余的过期时间,ttl返回时间单位为s,pttl则是ms
        val time = jedis.ttl(key)
        val time = jedis.pttl(key)
    }
}

通过
setex

psetex
方法来设置过期时间后,当数据过期后,再次去查询该数据,就会得到null(即redis将数据删除了)

上述也是简单演示了redis数据库的增删改查功能,下面就利用此数据库来实现发送验证码的功能。

2.发送验证码

这里我是实现了邮箱发送验证码的功能,验证码定为6位纯数字随机数,当然,你也可以加上大小写字母来提高复杂性。

之后我们将邮箱和验证码存储到redis中,并设置十分钟过期时间,随后通过调用邮箱发送邮件的方法,将验证码发送出去(这里详见
JavaXMail发送邮件功能实现

下面是验证码生成方法:

//生成验证码
fun randomCode(): String {
    val sb = StringBuffer()
    repeat(6) {
        //0-9范围
        val num = Random.nextInt(0, 10)
        sb.append(num)
    }
    return sb.toString()
}

//发送验证码方法
fun sendCode(email: String) {
    val code = randomCode()
    
    //先判断redis是否有记录
    val oldCode = RedisUtil.getValue(email)
    val action = {
        RedisUtil.setKeyValue(email, code)
        //调用邮箱发送邮件方法
        sendEmail(email, code)
    }
    if (oldCode.isBlank()) {
        action.invoke()
    } else {
        //判断是否已过1分钟
        //已过一分钟,重新发送,否则不做操作
        val flag = RedisUtil.isGtOneMinutes(email)
        if (flag) {
            action.invoke()
        }
    }
}

object RedisUtil {
    private val url = "127.0.0.1"

    //10分钟
    private const val expiredTime = 10 * 60
    private val redis by lazy {
        val jedis = Jedis(url, 6379)
        //如果有设置密码
        //    jedis.auth("")
        jedis
    }

    /**
     * 获取数据
     */
    fun getValue(key: String): String {
        return redis[key] ?: ""
    }

    /**
     * 存储邮箱和验证码
     */
    fun setKeyValue(key: String, value: String) {
        redis.setex(key, 10 * 60, value)
    }

    /**
     * 获取指定key的剩余时间(s)
     */
    fun getSurplusTime(key: String): Long {
        return redis.ttl(key)
    }

    /**
     * key是否已过1分钟
     */
    fun isGtOneMinutes(key: String): Boolean {
        val time = getSurplusTime(key)
        //小于九分钟(说明已过1分钟)
        return time <= expiredTime - 60
    }

}

这里补充下,由于邮箱为用户输入,永远不要对用户输入抱有期待,用户可能输入不是个email地址或者输了个不存在的email地址,对于前者问题,我们可以通过在前端和后台增加一个邮箱格式验证,对于后者问题(不存在的email地址),没有什么验证办法,只有发送了才知道这个邮箱地址是否可用(可以使用try catch来捕获异常来处理)

所以如果发送邮件出现错误,我们需要进行对应的处理,把那条存储到redis数据删除,然后接口返回一个错误提示信息即可。

而且,为了考虑到恶意用户频繁操作,导致我们邮箱服务频繁发送邮件,我们也需要进行对应的考虑设置,这里只能顾全用户频繁输入单个邮箱的情况,如果是同个邮箱,我们设置验证码过了1分钟的时间,才给重新发送(即现在各大APP手机验证码的操作一样),前端和后台接口都是需要做限制。

如果是重新发送的话,我们需要重新
setex
方法设置一下验证码,同时这步也将过期时间重置了。

3.校验验证码

之后就是考虑校验验证码的情况了,这里也是比较简单,通过拿到用户输入的验证码和redis里面的进行比对就可校验。

但可能会有特殊情况,比如redis验证码已经过期了,需要进行判断,并自动重新发送邮件,且接口返回提示信息

fun checkCode(email: String, code: String):Boolean {
    val dbCode = RedisUtil.getValue(email)
    if (dbCode.isBlank()) {
        //重新发送邮件,并发送提示(这里省略了发送提示)
        sendCode(email)
        return false
    } else {
        if (dbCode==code) {
            //验证通过
            return true
        }
        return false
    }
}

lettuce

Lettuce是一个高性能基于Java编写的Redis驱动框架,底层集成了Project Reactor提供天然的反应式编程,通信框架集成了Netty使用了非阻塞IO,5.x版本之后融合了JDK1.8的异步编程特性,在保证高性能的同时提供了十分丰富易用的API,5.1版本的新特性如下:

  • 支持Redis的新增命令ZPOPMIN, ZPOPMAX, BZPOPMIN, BZPOPMAX。
  • 支持通过Brave模块跟踪Redis命令执行。
  • 支持Redis Streams。
  • 支持异步的主从连接。
  • 支持异步连接池。
  • 新增命令最多执行一次模式(禁止自动重连)。
  • 全局命令超时设置(对异步和反应式命令也有效)。

下面这里就稍微贴下代码就好,具体的思路上面已经都有提及了,就不再过多赘述了。

1.引入依赖

如果项目为Spring Boot,只需要引用spring-data-redis依赖即可,其内置默认使用lettuce此库来连接redis

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.0.5.RELEASE</version>
</dependency>

或者是单独使用,则直接引用lettuce库即可

<dependency>
  <groupId>io.lettuce</groupId>
  <artifactId>lettuce-core</artifactId>
  <version>5.0.2.RELEASE</version>
</dependency>

2.使用

val redisUri = RedisURI.builder() // <1> 创建单机连接的连接信息
    .withHost("localhost")
    .withPort(6379)
    .withTimeout(Duration.of(10, ChronoUnit.SECONDS))
    .build()

val redisClient = RedisClient.create(redisUri) // <2> 创建客户端
val connection = redisClient.connect() // <3> 创建线程安全的连接
val redisCommands = connection.sync() // <4> 创建同步命令


//这里的参数说明可以访问http://redis.io/commands/set查看
//ex就是设置5s的过期时间
val setArgs = SetArgs.Builder.nx().ex(5)

//获取剩余过期时间
redisCommands.ttl("name")

//设置数据
val result = redisCommands.set("name", "throwable", setArgs)
if (result.toLowerCase() == "ok") {
    println("成功插入数据")
}

connection.close() // <5> 关闭连接

redisClient.shutdown() // <6> 关闭客户端

Lettuce结构比较复杂,上面罗列的基本使用已经够用了,就没有深入研究下去了...

其他

不过最近找了一款后台框架,写的时候发现,它是用的RedisTemplate,似乎比Lettuce要早一些的技术栈了,稍微摸索了下也能使用,也没去替换了那个后台框架里的东西了

//存入数据并设置时间
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.HOURS);

//删除
stringRedisTemplate.delete(key);

//获取剩余到期时间
redisTemplate.getExpire(key, TimeUnit.MINUTES);

参考

信号槽连接

信号槽的连接,其实内部本质还是一个回调函数,主要是维护了信号发送Object的元对象里一个连接的列表。调用
connect
函数时,将槽的一系列信息,封装成一个
Connection
,在发送信号时,通过这个列表,去回调槽函数。

1. 信号的连接

下面列举一种信号的连接方式,来大致讲解一下信号的连接过程。

//Connect a signal to a pointer to qobject member function
    // QtPrivate::FunctionPointer<Func1>::Object返回发送信号的对象类型
    template <typename Func1, typename Func2>
    static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
                                     const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
                                     Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::FunctionPointer<Func1> SignalType;
        typedef QtPrivate::FunctionPointer<Func2> SlotType;

        Q_STATIC_ASSERT_X(QtPrivate::HasQ_OBJECT_Macro<typename SignalType::Object>::Value,
                          "No Q_OBJECT in the class with the signal");

        //compilation error if the arguments does not match.
        // 检查信号和槽参数是否一致
        Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount),
                          "The slot requires more arguments than the signal provides.");
		// 检查信号和槽参数是否兼容
        Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<typename SignalType::Arguments, typename SlotType::Arguments>::value),
                          "Signal and slot arguments are not compatible.");
		// 检查信号和槽的返回值是否兼容
		Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType, typename SignalType::ReturnType>::value),
                          "Return type of the slot is not compatible with the return type of the signal.");

        const int *types = nullptr;
		// SignalType -> QtPrivate::FunctionPointer<Func1>
		// QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types() 返回信号参数的值对应的元类型id列表
        if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)
            types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();

        return connectImpl(sender, reinterpret_cast<void **>(&signal),
                           receiver, reinterpret_cast<void **>(&slot),
                           new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
                                           typename SignalType::ReturnType>(slot),
                            type, types, &SignalType::Object::staticMetaObject);
    }

上面主要都是一些基本的信号连接的判断,主要是:

  1. 信号和槽的参数数量
  2. 信号和槽的参数是否兼容
  3. 信号和槽的返回值是否兼容

然后获取信号参数所对应的元类型Id,再就到了一个信号连接的具体内部实现中

QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signal,
                                             const QObject *receiver, void **slot,
                                             QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                             const int *types, const QMetaObject *senderMetaObject)
{
    if (!signal) {
        qWarning("QObject::connect: invalid nullptr parameter");
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

    int signal_index = -1;
    void *args[] = { &signal_index, signal };
	// 根据调用来判断是否存在信号,如果当前类没有就去父类中寻找
	// 直到找到信号或者是最基层的类
	// 找到信号的index和信号的对象
    for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
        senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
        if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
            break;
    }
    if (!senderMetaObject) {
        qWarning("QObject::connect: signal not found in %s", sender->metaObject()->className());
        slotObj->destroyIfLastRef();
        return QMetaObject::Connection(nullptr);
    }
	// 信号下标
    signal_index += QMetaObjectPrivate::signalOffset(senderMetaObject);
    return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj, type, types, senderMetaObject);
}

同样,我们对这个函数进行分析,第一个片段是对信号发送者是否为空指针的一个判断

if (!signal) {
    qWarning("QObject::connect: invalid nullptr parameter");
    if (slotObj)
        slotObj->destroyIfLastRef();
    return QMetaObject::Connection();
}

第二个片段是去找到信号发送者(sender)的元对象类型(Meta Object)以及信号在对象信号中的位置。如果当前对象没有该信号,就去其父类对象去找。直到找到为止。

for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
    senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
    if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
        break;
}

然后就是进一步调用其内部实现:

QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,
                                             const QObject *receiver, void **slot,
                                             QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                             const int *types, const QMetaObject *senderMetaObject)
{
	// 发送对象、接收对象、槽函数对象、信号发送的元对象都不为空 2023-3-11
    if (!sender || !receiver || !slotObj || !senderMetaObject) {
		// 任意一个为空,报错且清理空间,并返回
        const char *senderString = sender ? sender->metaObject()->className()
                                          : senderMetaObject ? senderMetaObject->className()
                                          : "Unknown";
        const char *receiverString = receiver ? receiver->metaObject()->className()
                                              : "Unknown";
        qWarning("QObject::connect(%s, %s): invalid nullptr parameter", senderString, receiverString);
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

	// 去掉const的发送和接受对象
    QObject *s = const_cast<QObject *>(sender);
    QObject *r = const_cast<QObject *>(receiver);

	// 顺序锁,按照顺序依次去对mutex去上锁
	// 这里依次对发送和接收者的信号去上锁
    QOrderedMutexLocker locker(signalSlotLock(sender),
                               signalSlotLock(receiver));

    if (type & Qt::UniqueConnection && slot && QObjectPrivate::get(s)->connections.loadRelaxed()) {
		// ObjectPrivate::get(s) 获取s对应的d指针
		// connections 维护了所有的信号槽连接
        QObjectPrivate::ConnectionData *connections = QObjectPrivate::get(s)->connections.loadRelaxed();
        if (connections->signalVectorCount() > signal_index) {
			// 获取信号的连接
            const QObjectPrivate::Connection *c2 = connections->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed();

			// 循环遍历
            while (c2) {
				// 如果已经存在信号和槽的连接,且为uniqueConnection,则返回
                if (c2->receiver.loadRelaxed() == receiver && c2->isSlotObject && c2->slotObj->compare(slot)) {
                    slotObj->destroyIfLastRef();
                    return QMetaObject::Connection();
                }
                c2 = c2->nextConnectionList.loadRelaxed();
            }
        }
		// 将type与UniqueConnection进行异或,去掉UniqueConnection
        type = static_cast<Qt::ConnectionType>(type ^ Qt::UniqueConnection);
    }

	// 创建一个新的连接
    std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
    c->sender = s;
    c->signal_index = signal_index;
    QThreadData *td = r->d_func()->threadData;
    td->ref();
    c->receiverThreadData.storeRelaxed(td);
    c->receiver.storeRelaxed(r);
    c->slotObj = slotObj;
    c->connectionType = type;
    c->isSlotObject = true;
    if (types) {
        c->argumentTypes.storeRelaxed(types);
        c->ownArgumentTypes = false;
    }

	// 将新创建的连接加到连接列表中
    QObjectPrivate::get(s)->addConnection(signal_index, c.get());
    QMetaObject::Connection ret(c.release());
    locker.unlock();

    QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
    Q_ASSERT(method.isValid());
    s->connectNotify(method);

    return ret;
}

同样第一个部分也是对一些个空值的判断

	// 发送对象、接收对象、槽函数对象、信号发送的元对象都不为空 2023-3-11
    if (!sender || !receiver || !slotObj || !senderMetaObject) {
		// 任意一个为空,报错且清理空间,并返回
        const char *senderString = sender ? sender->metaObject()->className()
                                          : senderMetaObject ? senderMetaObject->className()
                                          : "Unknown";
        const char *receiverString = receiver ? receiver->metaObject()->className()
                                              : "Unknown";
        qWarning("QObject::connect(%s, %s): invalid nullptr parameter", senderString, receiverString);
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

然后就是一个if判断,主要是对
Qt::UniqueConnection
连接的一些处理,获取当前对象的信号连接列表,并判断当前要连接的信号和槽,之前有没有被连接过,如果有过连接,就直接返回。

if (type & Qt::UniqueConnection && slot && QObjectPrivate::get(s)->connections.loadRelaxed()) {
		// ObjectPrivate::get(s) 获取s对应的d指针
		// connections 维护了所有的信号槽连接
        QObjectPrivate::ConnectionData *connections = QObjectPrivate::get(s)->connections.loadRelaxed();
        if (connections->signalVectorCount() > signal_index) {
			// 获取信号的连接
            const QObjectPrivate::Connection *c2 = connections->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed();

			// 循环遍历
            while (c2) {
				// 如果已经存在信号和槽的连接,且为uniqueConnection,则返回
                if (c2->receiver.loadRelaxed() == receiver && c2->isSlotObject && c2->slotObj->compare(slot)) {
                    slotObj->destroyIfLastRef();
                    return QMetaObject::Connection();
                }
                c2 = c2->nextConnectionList.loadRelaxed();
            }
        }
		// 将type与UniqueConnection进行异或,去掉UniqueConnection
        type = static_cast<Qt::ConnectionType>(type ^ Qt::UniqueConnection);
    }

最后才是创建一个
Connection
并将连接的信息以及信号的参数设置进去,然后保存到对象的信号连接容器里。

// 创建一个新的连接
    std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
    c->sender = s;
    c->signal_index = signal_index;
    QThreadData *td = r->d_func()->threadData;
    td->ref();
    c->receiverThreadData.storeRelaxed(td);
    c->receiver.storeRelaxed(r);
    c->slotObj = slotObj;
    c->connectionType = type;
    c->isSlotObject = true;
    if (types) {
        c->argumentTypes.storeRelaxed(types);
        c->ownArgumentTypes = false;
    }

	// 将新创建的连接加到连接列表中
    QObjectPrivate::get(s)->addConnection(signal_index, c.get());
    QMetaObject::Connection ret(c.release());
    locker.unlock();

    QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
    Q_ASSERT(method.isValid());
    s->connectNotify(method);

    return ret;

2 槽的调用

定义一个信号,使用
moc
生成moc文件之后,我们可以看到信号函数的定义如下:

// SIGNAL 0
void MainWindow::sgnTestFor()
{
    QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}

我们发射一个信号的时候,我们会这样写:

emit sgnTestFor();

我们可以看关于
emit
的定义:

其实
emit
关键字什么都没有做,只是标识了一下当前发射了信号。所以本质上,发射一个信号实际上就是直接调用了这个信号的函数,也就是调用了
QMetaObject
中的
activate
函数。

函数如下:

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
                           void **argv)
{
    int signal_index = local_signal_index + QMetaObjectPrivate::signalOffset(m);

    if (Q_UNLIKELY(qt_signal_spy_callback_set.loadRelaxed()))
        doActivate<true>(sender, signal_index, argv);
    else
        doActivate<false>(sender, signal_index, argv);
}

上面的
qt_signal_spy_callback_set
暂时不清楚是什么玩意,所以我们不管,直接看具体的
doActive
函数

template <bool callbacks_enabled>
void doActivate(QObject *sender, int signal_index, void **argv)
{
	// 首先获取QObject的private对象
    QObjectPrivate *sp = QObjectPrivate::get(sender);

	// 判断信号是否阻塞
    if (sp->blockSig)
        return;

    Q_TRACE_SCOPE(QMetaObject_activate, sender, signal_index);

    if (sp->isDeclarativeSignalConnected(signal_index)
            && QAbstractDeclarativeData::signalEmitted) {
        Q_TRACE_SCOPE(QMetaObject_activate_declarative_signal, sender, signal_index);
        QAbstractDeclarativeData::signalEmitted(sp->declarativeData, sender,
                                                signal_index, argv);
    }

    const QSignalSpyCallbackSet *signal_spy_set = callbacks_enabled ? qt_signal_spy_callback_set.loadAcquire() : nullptr;

    void *empty_argv[] = { nullptr };
    if (!argv)
        argv = empty_argv;

    if (!sp->maybeSignalConnected(signal_index)) {
        // The possible declarative connection is done, and nothing else is connected
        if (callbacks_enabled && signal_spy_set->signal_begin_callback != nullptr)
            signal_spy_set->signal_begin_callback(sender, signal_index, argv);
        if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
            signal_spy_set->signal_end_callback(sender, signal_index);
        return;
    }

    if (callbacks_enabled && signal_spy_set->signal_begin_callback != nullptr)
        signal_spy_set->signal_begin_callback(sender, signal_index, argv);

    bool senderDeleted = false;
    {
    Q_ASSERT(sp->connections.loadAcquire());
    QObjectPrivate::ConnectionDataPointer connections(sp->connections.loadRelaxed());
    QObjectPrivate::SignalVector *signalVector = connections->signalVector.loadRelaxed();

	// 信号连接列表,因为一个信号可能连接了多个槽	
    const QObjectPrivate::ConnectionList *list;
    if (signal_index < signalVector->count())
        list = &signalVector->at(signal_index);
    else
        list = &signalVector->at(-1);

	// 判断当前线程是不是信号发送者的线程
    Qt::HANDLE currentThreadId = QThread::currentThreadId();
    bool inSenderThread = currentThreadId == QObjectPrivate::get(sender)->threadData.loadRelaxed()->threadId.loadRelaxed();

	// 
    // We need to check against the highest connection id to ensure that signals added
    // during the signal emission are not emitted in this emission.
    uint highestConnectionId = connections->currentConnectionId.loadRelaxed();
	// 此处也就代表着,一个信号连接的多个槽函数,或者多个连接,会以连接的顺序被触发
    do {
        QObjectPrivate::Connection *c = list->first.loadRelaxed();
        if (!c)
            continue;

        do {
            QObject * const receiver = c->receiver.loadRelaxed();
            if (!receiver)
                continue;

            QThreadData *td = c->receiverThreadData.loadRelaxed();
            if (!td)
                continue;

            bool receiverInSameThread;
			// 判断发送和接受是不是同一个线程
            if (inSenderThread) {
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            } else {
                // need to lock before reading the threadId, because moveToThread() could interfere
                QMutexLocker lock(signalSlotLock(receiver));
                receiverInSameThread = currentThreadId == td->threadId.loadRelaxed();
            }

			// 判断连接方式是否是队列连接,是队列连接就要丢入事件循环队列中处理
            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv);
                continue;
#if QT_CONFIG(thread)
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
            	// 如果发送对象和接受对象在一个线程,使用BlockingQueuedConnection会导致死锁
                if (receiverInSameThread) {
                    qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                    "Sender is %s(%p), receiver is %s(%p)",
                    sender->metaObject()->className(), sender,
                    receiver->metaObject()->className(), receiver);
                }
                QSemaphore semaphore;
                {
                    QBasicMutexLocker locker(signalSlotLock(sender));
                    if (!c->receiver.loadAcquire())
                        continue;
                    QMetaCallEvent *ev = c->isSlotObject ?
                        new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
                        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
                                           sender, signal_index, argv, &semaphore);
                    QCoreApplication::postEvent(receiver, ev);
                }
				// 阻塞直至函数执行完成
                semaphore.acquire();
                continue;
#endif
            }
			// 下面是普通连接,
			// 如果不在一个线程,并且使用直连,那么接收者就为空
            QObjectPrivate::Sender senderData(receiverInSameThread ? receiver : nullptr, sender, signal_index);

			// 如果是槽函数对象
            if (c->isSlotObject) {
                c->slotObj->ref();

                struct Deleter {
                    void operator()(QtPrivate::QSlotObjectBase *slot) const {
                        if (slot) slot->destroyIfLastRef();
                    }
                };
                const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
                    obj->call(receiver, argv);
                }
            } else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
                //we compare the vtable to make sure we are not in the destructor of the object.
                const int method_relative = c->method_relative;
                const auto callFunction = c->callFunction;
                const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr)
                    signal_spy_set->slot_begin_callback(receiver, methodIndex, argv);

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
                }

                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, methodIndex);
            } else {
                const int method = c->method_relative + c->method_offset;

                if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr) {
                    signal_spy_set->slot_begin_callback(receiver, method, argv);
                }

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                    QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
                }

                if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                    signal_spy_set->slot_end_callback(receiver, method);
            }
		// 此处while是循环遍历信号所连接的槽/信号
        } while ((c = c->nextConnectionList.loadRelaxed()) != nullptr && c->id <= highestConnectionId);

	// 循环两次
    } while (list != &signalVector->at(-1) &&
        //start over for all signals;
        ((list = &signalVector->at(-1)), true));

        if (connections->currentConnectionId.loadRelaxed() == 0)
            senderDeleted = true;
    }
    if (!senderDeleted) {
        sp->connections.loadRelaxed()->cleanOrphanedConnections(sender);

        if (callbacks_enabled && signal_spy_set->signal_end_callback != nullptr)
            signal_spy_set->signal_end_callback(sender, signal_index);
    }
}

前面的一些基本的判断,我们就忽略,直接找到重要的地方,循环遍历信号所连接的部分。

  1. 当信号槽为队列连接,我们需要将信号丢到事件循环里,待事件循环将该信号发送出去。

    if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                    || (c->connectionType == Qt::QueuedConnection)) {
                    queued_activate(sender, signal_index, c, argv);
                    continue;
    #if QT_CONFIG(thread)
    } 
    
  2. 当信号槽为阻塞队列连接(
    BlockingQueuedConnection
    )时,首先,我们需要判断发送和接收者是不是在一个线程,因为如果连接类型为
    BlockingQueuedConnection
    ,发送者和接收者在一个线程,会导致死锁。

    else if (c->connectionType == Qt::BlockingQueuedConnection) {
                	// 如果发送对象和接受对象在一个线程,使用BlockingQueuedConnection会导致死锁
                    if (receiverInSameThread) {
                        qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                        "Sender is %s(%p), receiver is %s(%p)",
                        sender->metaObject()->className(), sender,
                        receiver->metaObject()->className(), receiver);
                    }
                    QSemaphore semaphore;
                    {
                        QBasicMutexLocker locker(signalSlotLock(sender));
                        if (!c->receiver.loadAcquire())
                            continue;
                        QMetaCallEvent *ev = c->isSlotObject ?
                            new QMetaCallEvent(c->slotObj, sender, signal_index, argv, &semaphore) :
                            new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction,
                                               sender, signal_index, argv, &semaphore);
                        QCoreApplication::postEvent(receiver, ev);
                    }
    				// 阻塞直至函数执行完成
                    semaphore.acquire();
                    continue;
    #endif
    }
    

其他类型的连接如下:

  1. 信号的连接是一个槽函数对象
    QSlotObject
    ,就直接调用
    call
    函数

    if (c->isSlotObject) {
                    c->slotObj->ref();
    
                    struct Deleter {
                        void operator()(QtPrivate::QSlotObjectBase *slot) const {
                            if (slot) slot->destroyIfLastRef();
                        }
                    };
                    const std::unique_ptr<QtPrivate::QSlotObjectBase, Deleter> obj{c->slotObj};
    
                    {
                        Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.get());
                        obj->call(receiver, argv);
                    }
                } 
    
  2. 如果是其他类型,就通过
    QMetaObject::InvokeMetaMethod
    来调用

    else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
                    //we compare the vtable to make sure we are not in the destructor of the object.
                    const int method_relative = c->method_relative;
                    const auto callFunction = c->callFunction;
                    const int methodIndex = (Q_HAS_TRACEPOINTS || callbacks_enabled) ? c->method() : 0;
                    if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr)
                        signal_spy_set->slot_begin_callback(receiver, methodIndex, argv);
    
                    {
                        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                        callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv);
                    }
    
                    if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                        signal_spy_set->slot_end_callback(receiver, methodIndex);
                } else {
                    const int method = c->method_relative + c->method_offset;
    
                    if (callbacks_enabled && signal_spy_set->slot_begin_callback != nullptr) {
                        signal_spy_set->slot_begin_callback(receiver, method, argv);
                    }
    
                    {
                        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                        QMetaObject::metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv);
                    }
    
                    if (callbacks_enabled && signal_spy_set->slot_end_callback != nullptr)
                        signal_spy_set->slot_end_callback(receiver, method);
    }
    

并且遍历整个列表,将所有相关的连接都调用一遍。

然后我们看
QueuedConnection
的连接函数:

代码里,揭示了一点,就是如果我们使用信号槽连接的方式,而信号的参数不是一个元类型或者没用
qRegisterMetaType
来注册类型,那么队列连接是不行的,槽函数是不会触发的。

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv)
{
	// 存储元类型参数(meta-type argument)
    const int *argumentTypes = c->argumentTypes.loadRelaxed();
    if (!argumentTypes) {
		// 获取对应的信号
        QMetaMethod m = QMetaObjectPrivate::signal(sender->metaObject(), signal);
		// 获取信号的参数,并检查是否所有参数均为元类型(meta-type)
        argumentTypes = queuedConnectionTypes(m.parameterTypes());
        if (!argumentTypes) // cannot queue arguments
            argumentTypes = &DIRECT_CONNECTION_ONLY;
        if (!c->argumentTypes.testAndSetOrdered(nullptr, argumentTypes)) {
            if (argumentTypes != &DIRECT_CONNECTION_ONLY)
                delete [] argumentTypes;
            argumentTypes = c->argumentTypes.loadRelaxed();
        }
    }
	// 参数不符合要求,返回
    if (argumentTypes == &DIRECT_CONNECTION_ONLY) // cannot activate
        return;
    int nargs = 1; // include return type
    while (argumentTypes[nargs-1])
        ++nargs;

    QBasicMutexLocker locker(signalSlotLock(c->receiver.loadRelaxed()));
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected before we got the lock
        return;
    }
    if (c->isSlotObject)
        c->slotObj->ref();
    locker.unlock();

	// 然后通过post一个QMetaCallEvent事件到事件循环队列中去
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs);

    void **args = ev->args();
    int *types = ev->types();

    types[0] = 0; // return type
    args[0] = nullptr; // return value

    if (nargs > 1) {
        for (int n = 1; n < nargs; ++n)
            types[n] = argumentTypes[n-1];

        for (int n = 1; n < nargs; ++n)
            args[n] = QMetaType::create(types[n], argv[n]);
    }

    locker.relock();
    if (c->isSlotObject)
        c->slotObj->destroyIfLastRef();
    if (!c->receiver.loadRelaxed()) {
        // the connection has been disconnected while we were unlocked
        locker.unlock();
        delete ev;
        return;
    }

    QCoreApplication::postEvent(c->receiver.loadRelaxed(), ev);
}

代码中我们可以看到,这里是通过post一个
QMetaCallEvent
的事件到事件循环中,然后由事件循环去触发槽函数的调用。

好了,对于信号和槽的分析,我们暂时就先分析到这,如果有问题是我上面没有说明的,可以在评论区给我评论,我看到了,看懂了,我就会更新这篇博客的。


谢谢观看

开发了一个Java库的Google Bard API,可以自动化与AI对话了

Google Bard
是Google提供的还在实验阶段的人工智能对话服务。这明显是对标
ChatGPT
来的,它可以提供更实时的答案,会基于Google强大的网页数据。

为了更方便的使用并实现自动化,我写了一个Java类库,GitHub仓库地址为:
https://github.com/LarryDpk/Google-Bard

欢迎大家STAR...

如何使用

使用是非常简单的,只要网络通就可以了。

引入依赖

<dependencies>
    <dependency>
        <groupId>com.pkslow</groupId>
        <artifactId>google-bard</artifactId>
        <version>0.0.1</version>
    </dependency>
</dependencies>

拿到验证Token

目前没有用户名密码的方式,直接有Token就行。但这个要从网页上拿。它其实就是一个Cookie,名为
__Secure-1PSID

据说Google未来会开放API接口,拭目以待吧。

Java代码

两行代码即可:

AIClient client = new GoogleBardClient(token);
List<String> answers = client.ask("How to be a good father?");

返回的是一个列表:
如果
Google Bard
能够回答问题,一般会给出三个答案。第一个是推荐的答案。

for (int i = 0; i < answers.size(); i++) {
    if (i == 0) {
        System.out.println("### Recommended Answer");
        System.out.println(answers.get(i));
    } else {
        System.out.println("### Answer " + i);
        System.out.println(answers.get(i));
    }
}

Google Bard
目前还是实验阶段,只在美国英国可用,所以你懂的:

NetworkUtils.setUpProxy("localhost", "7890");

这个要在程序最开始设置。

我尝试的问题如答案

我问了一个问题:
How to be a good father?
答案是
Markdown
格式的,我获得的答案如下:

Here are some tips on how to be a good father:

  • Spend time with your child.
    Quality time is essential for building a strong relationship with your child. Make time for activities that you both enjoy, such as playing games, going to the park, or reading stories.
  • Be a good role model.
    Children learn by watching the adults in their lives. Set a good example by being honest, kind, and respectful.
  • Be supportive.
    Let your child know that you are there for them, no matter what. Offer them your support and encouragement, especially when they are going through tough times.
  • Be a good communicator.
    Talk to your child about their day, their feelings, and their hopes and dreams. Let them know that they can come to you with anything.
  • Be patient.
    Parenting is a journey, not a destination. There will be times when you feel frustrated or overwhelmed. Remember to be patient with yourself and your child.
  • Be loving.
    The most important thing a father can do is to love his child unconditionally. Let your child know that you love them every day.

Being a good father is a lifelong journey. There is no one right way to do it. Just remember to be there for your child, be a good role model, and show them love.

Answer 1

Here are some tips on how to be a good father:

  • Spend time with your child.
    Quality time is essential for building a strong relationship with your child. Make time for activities that you both enjoy, such as playing games, going to the park, or reading stories.
  • Be a good role model.
    Children learn by watching the adults in their lives. Set a good example by being honest, kind, and respectful.
  • Be supportive.
    Let your child know that you are there for them, no matter what. Offer them your support and encouragement, especially when they are going through tough times.
  • Be a good communicator.
    Talk to your child about their day, their feelings, and their hopes and dreams. Let them know that they can come to you with anything.
  • Be patient.
    Parenting is a journey, not a destination. There will be times when you feel frustrated or overwhelmed. Remember to be patient with yourself and your child.
  • Be loving.
    The most important thing a father can do is to love his child unconditionally. Let your child know that you love them every day.

Being a good father is a lifelong journey. There is no one right way to do it. Just remember to be there for your child, be a good role model, and show them love.

Answer 2

Here are some tips on how to be a good father:

  • Spend time with your children.
    Make time for them, even if it's just for a few minutes each day. Talk to them, listen to them, and play with them.
  • Discipline with love and positive parenting.
    Set rules and boundaries, but be fair and consistent. Use positive reinforcement, such as praise and rewards, to encourage good behavior.
  • Be your child's role model.
    Show them what it means to be a good person by being honest, trustworthy, and kind.
  • Earn the right to be heard.
    Be a good listener and take your children's concerns seriously. When you give them advice, make sure it's based on your own experience and wisdom.
  • Be your child's teacher.
    Teach them about the world around them and help them to grow into responsible, independent adults.
  • Be there for your children, no matter what.
    Love them unconditionally and support them through thick and thin.

Being a good father is a lifelong journey. There will be times when you make mistakes, but that's okay. The important thing is to learn from your mistakes and keep trying to be the best father you can be.

Answer 3

Here are some tips on how to be a good father:

  • Spend time with your child.
    This is one of the most important things you can do as a father. Make time for your child every day, even if it's just for a few minutes. Play with them, read to them, or just talk to them.
  • Discipline with love and positive parenting.
    Discipline is important, but it should be done in a loving and positive way. Avoid yelling or hitting your child. Instead, try to explain why they are being disciplined and what they can do to avoid being disciplined in the future.
  • Be your child's role model.
    Children learn by watching the adults in their lives. Show your child what it means to be a good person by being someone they can look up to.
  • Earn the right to be heard.
    Children are more likely to listen to their parents if they respect them. Earn your child's respect by being a good role model and by being fair and consistent with your discipline.
  • Be your child's teacher.
    Teach your child about right and wrong and encourage them to do their best. Use everyday examples to help them learn the basic lessons of life.
  • Be there for your child.
    Your child needs to know that you are there for them, no matter what. Be supportive and loving, and let them know that you will always be there for them.

Being a good father is one of the most important things you can do in life. By following these tips, you can help your child grow into a happy, healthy, and successful adult.

今天12点左右,它又来了,熟悉的恐怖身影(116开头IP段、山西的某个城市),不是cc攻击,是那只令人“敬畏”的蜘蛛。

2022年4月3日,它的首次来访在园子里掀起狂风暴浪,数据库连接数过万,负载均衡带宽跑满,带宽扩容50%依然跑满,当时我们通过限制同一个IP每天的请求数侥幸躲过。

2022年5月4日,它如同吃了兴奋剂般变得格外疯狂,由于触发IP请求数限制被重定向到登录站点,但它那龙卷风般的天量请求直接冲垮登录站点,无奈之下,我们屏蔽了这个IP网段。

它有成千上万的IP,我们屏蔽的IP网段只有255个IP,看索引量没有大的变化,我们天真地以为影响不大,却不知因此得罪了中文互联网的老大。

得罪老大的后果就是:园子遭遇2022年最悲惨的境遇——被它拉黑,在搜索结果中对园子全面降权,让我们切身体会老大的厉害。

后悔自己的天真,感叹老大的厉害,我们亡羊补牢以求老大网开一面——放开了被屏蔽的IP网段。

解除屏蔽后观察了1-2月,搜索流量并没有明显的回升,看来这次真的是惹怒了老大,我们会后悔莫及,见识了真正霸气的老大不会给你悔过的机会。

在观察期间,我们有了一个惊人的发现——每天光临园子最多的竟然是它,每天竟然访问1000多万次,园子的博文总数也就在1000万篇左右,日理万机的它竟然会每天看园子的每篇博文,难道准备以后改行当程序员?

接着,我们又有一个发现,每天千万次的访问竟然消耗了园子近一半的带宽费用,对于囊中羞涩的园子来说有点吃不消,每天让它看园子的每篇博文,还得给它倒贴带宽费,而且在搜索排名中继续被拉黑着。

于是,我们采取了一个应对之策,专门提供一个负载均衡给它访问并限制了峰值带宽,这样不仅可以控制带宽费用,而且在它发飙的时候可以减少宕机。

从去年到今天中午,它被我们这个对应之策通过专用负载均衡关进笼子里,即使被限制了带宽,它也会很多时候给园子带来3000-4000的QPS,所以来个cc攻击,园子就格外难以撑住。

今天中午,恐怖的事情发生了,它开始不遵守针对搜索引擎的dns解析,从笼子里跑出来了!

近一年后它依然如此彪悍,如此毫无忌惮,园子依然扛不住,除非屏蔽 IP 段,但又不敢再次得罪依然威风凛凛的老大,目前只是临时限制它所访问的负载均衡的带宽,但这会让访问这些负载均衡的正常用户受影响。

非常抱歉,这个突发问题给您带来了麻烦,我们正在进一步想办法,争取找到更好的解决方法。

【16:30】刚刚试着放开一些带宽,数据库连接数很快飙升,这种并发疯狂爬取不同博文,缓存发挥不了作用。

【16:50】这只蜘蛛今天已疯,我们不想陪着疯,已暂时屏蔽去年屏蔽过的那个网段,屏蔽后立马恢复正常。如此疯狂的爬取已经和cc攻击没什么区别了。

【19:15】今年2月,我们通过“百度搜索资源平台-反馈中心”反馈“抓取频次高的离谱,百度蜘蛛每秒请求数(QPS)竟然高达4000多”问题,百度客户竟然回复让我们“自查整改”,吓得我们再也不敢向百度客服反馈问题了。

【19:30】在被全面降权后,我们被自己的经历惊呆了,百度不打一声招呼随手一掐,每天几百万的搜索流量就没了。如果是一家依赖搜索流量的公司,百度可以随时要这家公司的命,园子因为不依赖百度流量而躲过一劫。