Intel® QAT 加速卡之IPSec示例

Intel® QAT 加速卡之IPSec示例


在IPSec的使用过程中需要频繁的加解密操作,而加解密操作会极大的消耗CPU的资源。因此很多提供IPSec服务的设备厂商尝试用多种方式来提高加解密性能,从而缓解CPU的压力,提高设备的IPSec的性能和吞吐量。其中既有软件加速方式,也有硬件加速方式。而Intel® QAT 加速卡便是之中的一款,这是Intel推出的硬件加速设备(Intel也推出过很多其他牛逼的加速设备)。使用专门的硬件加解密卡,从而将CPU解放出来处理其他的任务,这便是加速卡的基本思路。

下面我们就在IPSec中如何使用QAT加速技术流程做一个简单示例。(示例是人家Intel官方文档中提供的)

1. QAT处理IPSec入站报文

在这里插入图片描述

2. QAT处理IPSec出站报文

在这里插入图片描述

3. 组织架构

在这里插入图片描述
QAT相关的重要数据结构组织关系:
在这里插入图片描述

在这里插入图片描述

4. 示例源码

  • 对于出站方向,此示例将使用对称API来执行链式密码和哈希操作。 它在CBC模式下使用高级加密标准(AES)算法对一些纯文本进行加密,然后对密文,初始化向量和ESP头部执行SHA1 HMAC操作,并在密文之后立即将完整性检查值(ICV)写入缓冲区。
  • 对于入站方向,此示例将再次使用对称API来执行链式哈希和密码操作。 它对密文,初始化向量和ESP头部执行SHA1 HMAC操作,并将结果与输入的ICV进行比较,从而实现了完整性校验的功能。 然后,它在CBC模式下使用AES算法解密密文。
#include "cpa.h"
#include "cpa_cy_im.h"
#include "cpa_cy_sym.h"

#include "cpa_sample_utils.h"

#define TIMEOUT_MS 5000

#define ICV_LENGTH 12

/* For IPSec outbound direction we encrypt the payload and then
   generate the ICV. For IPSec inbound direction we compare the
   ICV and decrypt the payload
*/
#define IPSEC_OUTBOUND_DIR 0
#define IPSEC_INBOUND_DIR 1


extern int gDebugParam;

static Cpa8U sampleCipherKey[] = {
        0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
        0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55
};

static Cpa8U sampleCipherIv[] = {
        0xca, 0xfe, 0xba, 0xbe, 0xfa, 0xce, 0xdb, 0xad,
        0xde, 0xca, 0xf8, 0x88, 0x3d, 0x11, 0x59, 0x04
};

static Cpa8U sampleAuthKey[] = {
        0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
        0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
        0xDE, 0xAD, 0xBE, 0xEF
};

static Cpa8U sampleEspHdrData[] = {
        0x00, 0x00, 0x01, 0x2c, 0x00, 0x00, 0x00, 0x05
};

/* Payload padded to a multiple of the cipher block size
   (16), 2nd last byte gives pad length, last byte gives
   next header info */
static Cpa8U samplePayload[] = {
        0xd9, 0x31, 0x32, 0x25, 0xf8, 0x84, 0x06, 0xe5,
        0xa5, 0x59, 0x09, 0xc5, 0xaf, 0xf5, 0x26, 0x9a,
        0x86, 0xa7, 0xa9, 0x53, 0x15, 0x34, 0xf7, 0xda,
        0x2e, 0x4c, 0x30, 0x3d, 0x8a, 0x31, 0x8a, 0x72,
        0x1c, 0x3c, 0x0c, 0x95, 0x95, 0x68, 0x09, 0x53,
        0x2f, 0xcf, 0x0e, 0x24, 0x49, 0xa6, 0xb5, 0x25,
        0xb1, 0x6a, 0xed, 0xf5, 0xaa, 0x0d, 0xe6, 0x57,
        0xba, 0x63, 0x7b, 0x39, 0x01, 0x02, 0x02, 0x61
};


static Cpa8U expectedOutput[] = {
        /* ESP header unmodified */
        0x00, 0x00, 0x01, 0x2c, 0x00, 0x00, 0x00, 0x05,
        /* IV unmodified */
        0xca, 0xfe, 0xba, 0xbe, 0xfa, 0xce, 0xdb, 0xad,
        0xde, 0xca, 0xf8, 0x88, 0x3d, 0x11, 0x59, 0x04,
        /* Ciphertext */
        0x39, 0x8E, 0x4C, 0x1B, 0x7B, 0x28, 0x94, 0x52,
        0x97, 0xAD, 0x95, 0x97, 0xD7, 0xF9, 0xB9, 0x4A,
        0x49, 0x03, 0x51, 0x47, 0x45, 0xC7, 0x58, 0x6A,
        0x9A, 0x48, 0xB6, 0x38, 0xB4, 0xD5, 0xEE, 0x42,
        0x4F, 0x39, 0x09, 0x3D, 0xAB, 0x1E, 0xB3, 0x6A,
        0x71, 0x0B, 0xFC, 0x80, 0xAD, 0x2E, 0x4C, 0xA5,
        0xAB, 0x78, 0xB8, 0xAB, 0x87, 0xCC, 0x37, 0xF0,
        0xB9, 0x61, 0xDC, 0xB1, 0xA7, 0x24, 0x26, 0x23,
        /* ICV */
        0xE6, 0x55, 0xBD, 0x90, 0x33, 0x2D, 0x04, 0x8C,
        0x34, 0x06, 0xE3, 0x2D
};

CpaStatus
algChainSample(void);


/*
 * Callback function
 *
 * This function is "called back" (invoked by the implementation of
 * the API) when the asynchronous operation has completed.  The
 * context in which it is invoked depends on the implementation, but
 * as described in the API it should not sleep (since it may be called
 * in a context which does not permit sleeping, e.g. a Linux bottom
 * half).
 *
 * This function can perform whatever processing is appropriate to the
 * application.  For example, it may free memory, continue processing
 * of a decrypted packet, etc.  In this example, the function checks
 * verifyResult returned and sets the complete variable to indicate it
 * has been called.
 */
static void
symCallback(void *pCallbackTag,
        CpaStatus status,
        const CpaCySymOp operationType,
        void *pOpData,
        CpaBufferList *pDstBuffer,
        CpaBoolean verifyResult)
{
    PRINT_DBG("Callback called with status = %d.\n", status);

    /* For this implementation verifyResult is true by default. In
       the digest generate case verifyDigest will never be false. In
       the digest verify case verifyDigest can be false if digest
       verification fails */
    if(CPA_FALSE == verifyResult)
    {
       PRINT_ERR("Callback verify result error\n");
    }

    if (NULL != pCallbackTag)
    {
        /** indicate that the function has been called */
        COMPLETE((struct COMPLETION_STRUCT *)pCallbackTag);
    }
}

/*
 * Perform an algorithm chaining operation
 */
static CpaStatus
algChainPerformOp(CpaInstanceHandle cyInstHandle, CpaCySymSessionCtx sessionCtx, int dir)
{
    CpaStatus status = CPA_STATUS_SUCCESS;
    Cpa8U  *pBufferMeta = NULL;
    Cpa32U bufferMetaSize = 0;
    CpaBufferList *pBufferList = NULL;
    CpaFlatBuffer *pFlatBuffer = NULL;
    CpaCySymOpData *pOpData = NULL;
    /* buffer size includes space for hdr, iv, payload and icv */
    Cpa32U bufferSize = sizeof(sampleEspHdrData) + sizeof(sampleCipherIv)
                            + sizeof(samplePayload) + ICV_LENGTH;
    Cpa32U numBuffers = 1;  /* only using 1 buffer in this case */
    /* allocate memory for bufferlist and array of flat buffers in a contiguous
     * area and carve it up to reduce number of memory allocations required. */
    Cpa32U bufferListMemSize = sizeof(CpaBufferList) +
        (numBuffers * sizeof(CpaFlatBuffer));
    Cpa8U  *pSrcBuffer = NULL;
    Cpa8U  *pIvBuffer = NULL;

    /* The following variables are allocated on the stack because we block
     * until the callback comes back. If a non-blocking approach was to be
     * used then these variables should be dynamically allocated */
    struct COMPLETION_STRUCT complete;

    /* get meta information size */
    status = cpaCyBufferListGetMetaSize( cyInstHandle,
                numBuffers, &bufferMetaSize);

    if (CPA_STATUS_SUCCESS == status)
    {
        status = PHYS_CONTIG_ALLOC(&pBufferMeta, bufferMetaSize);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        status = OS_MALLOC(&pBufferList, bufferListMemSize);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        status = PHYS_CONTIG_ALLOC(&pSrcBuffer, bufferSize);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        /* increment by sizeof(CpaBufferList) to get at the
         * array of flatbuffers */
        pFlatBuffer = (CpaFlatBuffer *) (pBufferList + 1);

        pBufferList->pBuffers = pFlatBuffer;
        pBufferList->numBuffers = 1;
        pBufferList->pPrivateMetaData = pBufferMeta;

        pFlatBuffer->dataLenInBytes = bufferSize;
        pFlatBuffer->pData = pSrcBuffer;

        /* copy source into buffer */
        if(IPSEC_OUTBOUND_DIR == dir)
        {
           memcpy(pSrcBuffer, sampleEspHdrData, sizeof(sampleEspHdrData));
           memcpy(pSrcBuffer+sizeof(sampleEspHdrData), sampleCipherIv, sizeof(sampleCipherIv));
           memcpy(pSrcBuffer+(sizeof(sampleEspHdrData)+sizeof(sampleCipherIv)),
                                  samplePayload, sizeof(samplePayload));
        }
        else
        {
           memcpy(pSrcBuffer, expectedOutput, sizeof(expectedOutput));
        }

        pIvBuffer = pSrcBuffer + sizeof(sampleEspHdrData);

        status = OS_MALLOC(&pOpData, sizeof(CpaCySymOpData));
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        if(IPSEC_OUTBOUND_DIR == dir)
        {
//<snippet name="opDataIPSecOut">
           /** Populate the structure containing the operational data that is
            * needed to run the algorithm in outbound direction */
           pOpData->sessionCtx = sessionCtx;
           pOpData->packetType = CPA_CY_SYM_PACKET_TYPE_FULL;
           pOpData->pIv = pIvBuffer;
           pOpData->ivLenInBytes = sizeof(sampleCipherIv);
           pOpData->cryptoStartSrcOffsetInBytes = sizeof(sampleEspHdrData)+sizeof(sampleCipherIv);
           pOpData->messageLenToCipherInBytes = sizeof(samplePayload);
           pOpData->hashStartSrcOffsetInBytes = 0;
           pOpData->messageLenToHashInBytes = sizeof(sampleEspHdrData)
                                   +sizeof(sampleCipherIv)+sizeof(samplePayload);
           /* Even though ICV follows immediately after the region to hash
           digestIsAppended is set to false in this case to workaround
           errata number IXA00378322 */
           pOpData->pDigestResult = pSrcBuffer+
                 (sizeof(sampleEspHdrData)+sizeof(sampleCipherIv)+sizeof(samplePayload));
//</snippet>
       }
       else
       {
//<snippet name="opDataIPSecIn">
           /** Populate the structure containing the operational data that is
            * needed to run the algorithm in inbound direction */
           pOpData->sessionCtx = sessionCtx;
           pOpData->packetType = CPA_CY_SYM_PACKET_TYPE_FULL;
           pOpData->pIv = pIvBuffer;
           pOpData->ivLenInBytes = sizeof(sampleCipherIv);
           pOpData->cryptoStartSrcOffsetInBytes = sizeof(sampleEspHdrData)+sizeof(sampleCipherIv);
           pOpData->messageLenToCipherInBytes = bufferSize -
                                    (sizeof(sampleEspHdrData)+sizeof(sampleCipherIv)+ICV_LENGTH);
           pOpData->hashStartSrcOffsetInBytes = 0;
           pOpData->messageLenToHashInBytes = bufferSize - ICV_LENGTH;
//</snippet>
       }
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        /** initialisation for callback; the "complete" variable is used by the
         * callback function to indicate it has been called*/
        COMPLETION_INIT(&complete);

        PRINT_DBG("cpaCySymPerformOp\n");

        /** Perform symmetric operation */
        status = cpaCySymPerformOp(cyInstHandle,
                (void *)&complete, /* data sent as is to the callback function*/
                pOpData,           /* operational data struct */
                pBufferList,       /* source buffer list */
                pBufferList,       /* same src & dst for an in-place operation*/
                NULL);             /* pVerifyResult not required in async mode */

        if (CPA_STATUS_SUCCESS != status)
        {
            PRINT_ERR("cpaCySymPerformOp failed. (status = %d)\n", status);
        }

        if (CPA_STATUS_SUCCESS == status)
        {
            /** wait until the completion of the operation*/
            if (!COMPLETION_WAIT(&complete, TIMEOUT_MS))
            {
                PRINT_ERR("timeout or interruption in cpaCySymPerformOp\n");
                status = CPA_STATUS_FAIL;
            }
        }

        if (CPA_STATUS_SUCCESS == status)
        {
            if(IPSEC_OUTBOUND_DIR == dir)
            {
               if (0 == memcmp(pSrcBuffer, expectedOutput, bufferSize))
               {
                   PRINT_DBG("Output matches expected output encrypt generate\n");
               }
               else
               {
                   PRINT_DBG("Output does not match expected output encrypt generate\n");
                   status = CPA_STATUS_FAIL;
               }
            }
            else
            {
               if (0 == memcmp(pSrcBuffer+(sizeof(sampleEspHdrData)+sizeof(sampleCipherIv)),
                              samplePayload, sizeof(samplePayload)))
               {
                   PRINT_DBG("Output matches expected output decrypt verify\n");
               }
               else
               {
                   PRINT_DBG("Output does not match expected output decrypt verify\n");
                   status = CPA_STATUS_FAIL;
                }
            }
        }
    }


    /* at this stage, the callback function has returned, so it is sure that
     * the structures won't be needed any more*/
    PHYS_CONTIG_FREE(pSrcBuffer);
    OS_FREE(pBufferList);
    PHYS_CONTIG_FREE(pBufferMeta);
    OS_FREE(pOpData);

    COMPLETION_DESTROY(&complete);

    return status;
}

CpaStatus
algChainSample(void)
{
    CpaStatus status = CPA_STATUS_FAIL;
    CpaCySymSessionCtx sessionCtx = NULL;
    Cpa32U sessionCtxSize = 0;
    CpaInstanceHandle cyInstHandle = NULL;
    CpaCySymSessionSetupData sessionSetupData = {0};
    CpaCySymStats64 symStats = {0};
    /*
     * In this simplified version of instance discovery, we discover
     * exactly one instance of a crypto service.
     */
    sampleCyGetInstance(&cyInstHandle);
    if (cyInstHandle == NULL)
    {
        PRINT_DBG("No crypto instances available\n");
        return CPA_STATUS_FAIL;
    }

    /* Start Cryptographic component */
    PRINT_DBG("cpaCyStartInstance\n");
    status = cpaCyStartInstance(cyInstHandle);

    if(CPA_STATUS_SUCCESS == status)
    {
      /*
       * Set the address translation function for the instance
       */
       status = cpaCySetAddressTranslation(cyInstHandle, sampleVirtToPhys);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
       /*
        * If the instance is polled start the polling thread. Note that
        * how the polling is done is implementation-dependant.
        */
        sampleCyStartPolling(cyInstHandle);

        PRINT_DBG("Encrypt-Generate ICV\n");

        /* populate symmetric session data structure */
        sessionSetupData.sessionPriority =  CPA_CY_PRIORITY_HIGH;
//<snippet name="initSessionIPSecEnc">
        sessionSetupData.symOperation = CPA_CY_SYM_OP_ALGORITHM_CHAINING;
        sessionSetupData.algChainOrder =
                                    CPA_CY_SYM_ALG_CHAIN_ORDER_CIPHER_THEN_HASH;

        sessionSetupData.cipherSetupData.cipherAlgorithm =
                                    CPA_CY_SYM_CIPHER_AES_CBC;
        sessionSetupData.cipherSetupData.pCipherKey = sampleCipherKey;
        sessionSetupData.cipherSetupData.cipherKeyLenInBytes =
                                    sizeof(sampleCipherKey);
        sessionSetupData.cipherSetupData.cipherDirection =
                                    CPA_CY_SYM_CIPHER_DIRECTION_ENCRYPT;

        sessionSetupData.hashSetupData.hashAlgorithm =  CPA_CY_SYM_HASH_SHA1;
        sessionSetupData.hashSetupData.hashMode = CPA_CY_SYM_HASH_MODE_AUTH;
        sessionSetupData.hashSetupData.digestResultLenInBytes = ICV_LENGTH;
        sessionSetupData.hashSetupData.authModeSetupData.authKey = sampleAuthKey;
        sessionSetupData.hashSetupData.authModeSetupData.authKeyLenInBytes =
                                            sizeof(sampleAuthKey);

        /* Even though ICV follows immediately after the region to hash
           digestIsAppended is set to false in this case to workaround
           errata number IXA00378322 */
        sessionSetupData.digestIsAppended = CPA_FALSE;
        /* Generate the ICV in outbound direction */
        sessionSetupData.verifyDigest = CPA_FALSE;
//</snippet>

        /* Determine size of session context to allocate */
        status = cpaCySymSessionCtxGetSize(cyInstHandle,
                    &sessionSetupData, &sessionCtxSize);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        /* Allocate session context */
        status = PHYS_CONTIG_ALLOC(&sessionCtx, sessionCtxSize);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        /* Initialize the session */
        status = cpaCySymInitSession(cyInstHandle,
                    symCallback, &sessionSetupData, sessionCtx);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        CpaStatus sessionStatus = CPA_STATUS_SUCCESS;

        /* Perform algchaining operation */
        status = algChainPerformOp(cyInstHandle, sessionCtx,
                                               IPSEC_OUTBOUND_DIR);

        /* Remove the session - session init has already succeeded */
        sessionStatus = cpaCySymRemoveSession(
                            cyInstHandle, sessionCtx);

        /* maintain status of remove session only when status of all operations
         * before it are successful. */
        if (CPA_STATUS_SUCCESS == status)
        {
            status = sessionStatus;
        }
    }

    if(CPA_STATUS_SUCCESS == status)
    {
        PRINT_DBG("Decrypt-Verify ICV\n");

        /* populate symmetric session data structure */
        sessionSetupData.sessionPriority =  CPA_CY_PRIORITY_HIGH;
//<snippet name="initSessionIPSecDec">
        sessionSetupData.symOperation = CPA_CY_SYM_OP_ALGORITHM_CHAINING;
        sessionSetupData.algChainOrder =
                                    CPA_CY_SYM_ALG_CHAIN_ORDER_HASH_THEN_CIPHER;

        sessionSetupData.cipherSetupData.cipherAlgorithm =
                                    CPA_CY_SYM_CIPHER_AES_CBC;
        sessionSetupData.cipherSetupData.pCipherKey = sampleCipherKey;
        sessionSetupData.cipherSetupData.cipherKeyLenInBytes =
                                    sizeof(sampleCipherKey);
        sessionSetupData.cipherSetupData.cipherDirection =
                                    CPA_CY_SYM_CIPHER_DIRECTION_DECRYPT;

        sessionSetupData.hashSetupData.hashAlgorithm =  CPA_CY_SYM_HASH_SHA1;
        sessionSetupData.hashSetupData.hashMode = CPA_CY_SYM_HASH_MODE_AUTH;
        sessionSetupData.hashSetupData.digestResultLenInBytes = ICV_LENGTH;
        sessionSetupData.hashSetupData.authModeSetupData.authKey = sampleAuthKey;
        sessionSetupData.hashSetupData.authModeSetupData.authKeyLenInBytes
                                                         = sizeof(sampleAuthKey);

        /* ICV follows immediately after the region to hash */
        sessionSetupData.digestIsAppended = CPA_TRUE;
        /* Verify the ICV in the inbound direction */
        sessionSetupData.verifyDigest = CPA_TRUE;
//</snippet>

    }
    if (CPA_STATUS_SUCCESS == status)
    {
        /* Initialize the session */
        status = cpaCySymInitSession(cyInstHandle,
                    symCallback, &sessionSetupData, sessionCtx);
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        CpaStatus sessionStatus = CPA_STATUS_SUCCESS;

        /* Perform algchaining operation */
        status = algChainPerformOp(cyInstHandle, sessionCtx,
                                                      IPSEC_INBOUND_DIR);

        /* Remove the session - session init has already succeeded */
        sessionStatus = cpaCySymRemoveSession(
                            cyInstHandle, sessionCtx);

        /* maintain status of remove session only when status of all operations
         * before it are successful. */
        if (CPA_STATUS_SUCCESS == status)
        {
            status = sessionStatus;
        }
    }

    if (CPA_STATUS_SUCCESS == status)
    {
        /* Query symmetric statistics */
        status = cpaCySymQueryStats64(cyInstHandle, &symStats);

        if (CPA_STATUS_SUCCESS != status)
        {
            PRINT_ERR("cpaCySymQueryStats failed, status = %d\n", status);
        }
        else
        {
            PRINT_DBG("Number of symmetric operations completed: %llu\n",
                    (unsigned long long)symStats.numSymOpCompleted);
        }
    }

    /* Clean up */

    /* Free session Context */
    PHYS_CONTIG_FREE(sessionCtx);

    /* Stop the polling thread */
    sampleCyStopPolling();


    PRINT_DBG("cpaCyStopInstance\n");
    cpaCyStopInstance(cyInstHandle);

    if (CPA_STATUS_SUCCESS == status)
    {
        PRINT_DBG("Sample code ran successfully\n");
    }
    else
    {
        PRINT_DBG("Sample code failed with status of %d\n", status);
    }

    return status;
}


示例路径:qat1.5.l.1.13.0-19\quickassist\lookaside\access_layer\src\sample_code\functional\sym\ipsec_sample

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
OpenSSL QAT是指OpenSSL与英特尔快速加密技术(Intel QuickAssist Technology)的结合。通过将OpenSSL与QAT驱动程序结合使用,可以加速SSL/TLS协议的性能。在配置OpenSSL QAT时,通常需要设置一些参数。 引用中提到了一个设置OpenSSL QAT安装目录的参数`--with-openssl_install_dir=/root/openssl-1.1.1b/.openssl`。这个参数指定了OpenSSL的安装目录,也就是QAT驱动程序的安装路径。 引用中提到了一个设置OpenSSL配置文件路径的参数`export OPENSSL_CONF=/root/openssl-1.1.1b/.openssl/ssl/openssl.cnf`。这个参数指定了OpenSSL的配置文件路径,其中包含了QAT驱动程序的配置信息。 通过配置好这些参数,可以使OpenSSL能够正确使用QAT驱动程序,从而提高SSL/TLS协议的性能。 除了配置参数,还可以通过对比不同的实现方式来评估OpenSSL QAT的性能。引用提到了针对TLS1.2 RSA卸载的例子,比较了普通的OpenSSL、同步QAT OpenSSL和异步QAT OpenSSL的每秒连接数。这个比较可以帮助我们了解QAT加速对性能的影响。 总之,OpenSSL QAT是通过将OpenSSL与英特尔快速加密技术结合使用,来提高SSL/TLS协议的性能的。配置OpenSSL QAT需要设置安装目录和配置文件路径,并可以通过比较不同实现方式来评估性能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [nginx 异步openssl Intel QAT硬件加速方案](https://blog.csdn.net/realmardrid/article/details/117456917)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叨陪鲤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值