Python向带有SSL/TSL认证服务器发送网络请求小实践(附并发http请求实现asyncio+aiohttp)

1. 写在前面

最近工作中遇到这样的一个场景:给客户发送文件的时候,为保证整个过程中,文件不会被篡改,需要在发送文件之间, 对发送的文件进行签名, 而整个签名系统是另外一个团队做的, 提供了一个接口服务完成签名,但访问这个接口需要提供他们团队提供的证书链先进行认证,所以需要和该服务端建立安全的链路,这里是用ssl双向认证的方式实现。

本篇文章主要是记录下如果是用Python给这种ssl双向认证的服务器发送post请求的时候,应该怎样携带证书去双向认证? 整个过程怎么实现的,如果需要处理的文件太多, 请求响应超时的情况下, 又怎么把同步http请求改成并发的http请求以减少响应时间?

ok, let’s go!

2. 基础知识

2.1 SSL/TSL

SSL是安全套接层(secure sockets layer),而TLS是SSL的继任者,叫传输层安全(transport layer security),是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。比如HTTP协议是明文传输,加上SSL层之后,就有了雅称HTTPS。

SSL/TLS双向认证,也被叫做客户端证书认证,是一个验证客户端和服务器双方身份的过程,用来确保双向的通信安全性。
在单向SSL/TLS认证中,只有服务器需要向客户端提供证书用以证明自己的身份。而在双向认证中,客户端也需要使用证书来证明自己的身份,服务器才会允许连接和交换信息。

这种认证流程增加了安全性,确保通信双方都可信。

整个握手认证流程大概是这样:
在这里插入图片描述
双向认证的流程通常如下:

  1. 握手开始:客户端向服务器发送一个连接请求,告知服务器它支持的SSL/TLS版本和加密算法等。
  2. 服务器证书:服务器响应客户端的请求,并发送它的SSL/TLS证书给客户端。这个证书中包含了服务器的公钥和一些身份信息,并且由可信的证书颁发机构(CA)签发。
  3. 证书验证:客户端验证服务器证书的合法性,它会检查证书是否过期、是否被CA真实签发、域名是否匹配等。
  4. 客户端密钥交换:客户端根据服务器提供的公钥加密一个随机生成的对称加密密钥,并发送给服务器。
  5. 客户端证书请求:服务器向客户端发送一条请求消息,要求客户端也提供一份证书。
  6. 客户端证书:客户端发送它的SSL/TLS证书给服务器。如同服务器证书一样,客户端证书也包含了客户端的公钥和身份信息,并且由CA签发。
  7. 服务器验证客户端证书:服务端将验证客户端提供的证书,检查是否由信任的CA签发、是否过期、是否被吊销等。
  8. 预主密钥生成:客户端根据之前的握手过程生成一个预主密钥(Pre-Master Secret)。
  9. 安全参数生成:客户端和服务器都使用预主密钥和一些已经交换的信息计算出会话密钥。
  10. 完成握手:双方利用生成的会话密钥进行加密通讯。

在整个过程中,SSL/TLS的加密算法保证了传输内容的隐私和完整性。双向认证增加了一层安全性,因为不仅要验证服务器的身份,客户端的身份也同样需要验证。这在需要高安全性的场合,比如金融交易、企业系统访问中非常有用。

当然这个过程简单了解下即可,主要是能在实际场景中会使用。

2.2 证书

CA: 证书授权中心( certificate authority)。类似于国家出入境管理处一样,给别人颁发护照;也类似于国家工商管理局一样,给公司企业颁发营业执照。 它有两大主要性质:

  1. CA本身是受信任的 // 国际认可的
  2. 给他受信任的申请对象颁发证书

生成和管理SSL/TLS证书通常涉及以下几个步骤:

  • 创建密钥对(公钥和私钥)
    • 使用密码学工具(例如OpenSSL)创建一对密钥。
    • 私钥必须安全保管,确保只有授权个体才能访问。
  • 创建证书签名请求(CSR):
    • 使用私钥生成CSR。
    • CSR中包括公钥和关于个人或组织身份的信息(如组织名称、常用名称(CN)、组织单位、城市、国家等)。
  • 提交CSR到证书颁发机构(CA):
    • 将CSR提交给CA。
    • CA验证请求者提供的信息并决定是否发放证书。
  • 获取证书:
    • CA签发证书后,它通常会提供一个X.509格式的证书文件(也就是SSL/TLS证书)。
  • 安装证书:
    • 将证书安装在服务器或者需要用到的设备上。
    • 一些服务可能还需要你安装中间证书,以建立证书链至根CA。
  • 配置使用SSL/TLS的服务:
    • 配置服务器使用安装的证书和私钥。
    • 调整服务器设置以启用SSL/TLS加密。
  • 证书的相关维护等

那假设有了这个证书,实际过程中应该怎么用呢? 从服务器的角度:

  1. SSL Server生成一个公钥/私钥对(server.key/server.pub), 私钥加密, 公钥解密。
  2. server.pub 生成一个请求文件 server.req. 请求文件中包含有 server 的一些信息,如域名/申请者/公钥等
  3. server 将请求文件 server.req 递交给 CA,CA验明正身后,将用 ca.key和请求文件加密生成 server.crt
  4. 由于 ca.key 和 ca.crt 是一对, 于是 ca.crt 可以解密 server.crt.


在实际应用中:如果 SSL Client 想要校验 SSL server.那么 SSL server 必须要将他的证书 server.crt 传给 client.然后 client 用 ca.crt 去校验 server.crt 的合法性。如果是一个钓鱼网站,那么CA是不会给他颁发合法server.crt证书的,这样client 用ca.crt去校验,就会失败。比如浏览器作为一个 client,你想访问合法的淘宝网站https://www.taobao.com, 结果不慎访问到 https://wwww.jiataobao.com ,那么浏览器将会检验到这个假淘宝钓鱼网站的非法性,提醒用户不要继续访问!这样就可以保证了client的所有https访问都是安全的

好了,上面的这些内容主要是先对SSL/TSL以及证书等有个大致印象, 下面看看在实际业务中是怎么使用的。

3. 业务落地实践

场景: 我有一个目录需要打成压缩包发给客户, 但是打包之前,为了防止网络传输过程中被篡改, 需要对里面的每个文件进行签名, 这样客户收到文件后再验签就知道是否被改动了。

签名是调用一个签名服务的接口完成,但这个签名服务是采用TSL认证的,如果想给它发请求去签名,需要申请client的证书,携带着证书去发送post请求(把文件内容发给服务器,服务器返回一个签名好的字符串)

这个过程用流程图表示如下:
在这里插入图片描述
流程就是:

  1. 客户端这边, 先生成一个CSR,可以理解成一个公钥一个私钥, 可以用openssl来实现

    # 生成私钥
    openssl ecparam -out private.key -name prime256v1 -genkey
    
    # 生成P8格式的pem私钥
    openssl pkcs8 -topk8 -inform PEM -in  private.key -outform pem -nocrypt -out  private.key.pem
    
    # 生成CSR文件
    openssl  req -new -out pki-tls-ADD.csr -key  private.key.pem -subj /CN=pki-tls-ADD
    
  2. 拿着这个pki-client-ADD.csr文件, 去申请证书

  3. 证书完成后, 会发回来一个server_chain.pem的证书链, 一个pki-tls-ADD.crt.pem的证书文件(PKI根据CSR颁发的客户端pem证书文件)

  4. 发送请求的时候,要在上下文里面带着3个文件去认证:

    cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"
    key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"
    ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"
    

下面是一个同步请求的代码:

import base64
import hashlib
import json
import os
import random
import ssl
import time
import urllib
import aiohttp
import asyncio
from urllib.parse import quote

import requests
from requests.adapters import HTTPAdapter
from urllib3 import PoolManager

class SSLAdapter(HTTPAdapter):
    def __init__(self, certificate, private_key, ca_file, *args, **kwargs):
        self._certificate = certificate
        self._private_key = private_key
        self._ca_file = ca_file
        super().__init__(*args, **kwargs)

    def init_poolmanager(self, *args, **kwargs):
        context = ssl.create_default_context()
        context.load_cert_chain(certfile=self._certificate, keyfile=self._private_key)
        context.load_verify_locations(cafile=self._ca_file)
        self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs)


class SignClient(object):

    IP = "https://wuzhongqiang.net"
    SIGN_URL = "/api/v1/sign_file"
    APP_KEY = os.environ.get("APP_KEY", "xxx")
    APP_SECRET = os.environ.get("APP_SECRET", "xxx")

    @staticmethod
    def gen_app_key_sign(method: str, url: str, header: dict, security_key: str, body: str):
        http_str = (f"{method}{url}"
                    f"es-auth-appkey:{header['es-auth-appkey']}"
                    f"es-auth-nonce:{header['es-auth-nonce']}"
                    f"es-auth-timestamp:{header['es-auth-timestamp']}"
                    f"json={body}"
                    f"{security_key}")
        try:
            urlCodeStr = urllib.parse.quote(http_str, safe='')
            hashHex = hashlib.sha256(urlCodeStr.encode('utf-8')).hexdigest()
        except Exception as e:
            print(e)
            hashHex = ""
        return hashHex

    @staticmethod
    def sign_file(data: str, cert_file_path: str, key_file_path: str, ca_file_path: str) -> str:
        target_data = {"data": data}
        es_auth_noce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16))
        es_auth_timestamp = str(int(time.time() * 1000))
        auth_info = {
            "es-auth-appkey": SignClient.APP_KEY,
            "es-auth-nonce": es_auth_noce,
            "es-auth-timestamp": es_auth_timestamp
        }
        payload = json.dumps(target_data, separators=(',', ':'))
        signature_value = SignClient.gen_app_key_sign(method="POST",
                                                      url=SignClient.SIGN_URL,
                                                      header=auth_info,
                                                      security_key=SignClient.APP_SECRET,
                                                      body=payload)

        # print(f"target_data: {target_data}\n"
        #       f"es-auth-appkey:{SignClient.APP_KEY}\n"
        #       f"es-auth-nonce:{es_auth_noce}\n"
        #       f"es-auth-timestamp:{es_auth_timestamp}\n"
        #       f"es-auth-sign: {signature_value}")
        headers = {
            "Content-Type": "application/json",
            "es-auth-appkey": SignClient.APP_KEY,
            "es-auth-nonce": es_auth_noce,
            "es-auth-timestamp": es_auth_timestamp,
            "es-auth-sign": signature_value
        }
        session = requests.Session()
        session.mount('https://', SSLAdapter(certificate=cert_file_path,
                                             private_key=key_file_path,
                                             ca_file=ca_file_path))
        response = session.post(url=f"{SignClient.IP}{SignClient.SIGN_URL}", data=payload, headers=headers)
        # requests method
        # response = requests.request("POST",
        #                             url=f"{SignClient.IP}{SignClient.SIGN_URL}",
        #                             headers=headers,
        #                             data=payload,
        #                             cert=(cert_file_path, key_file_path),
        #                             verify=ca_file_path)

        try:
            response_body = response.text
            json_object = json.loads(response_body)
            sign_data = json_object["signData"]
            return sign_data
        except Exception as e:
            print(e)
            return ""


if __name__ == '__main__':
    base_dir = "/home/wuzhongqiang/Downloads/test_dir"
    cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"
    key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"
    ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"
    start = time.time()
    for root, _, files in os.walk(base_dir):
        for file in files:
            file_path = os.path.join(root, file)
            with open(file_path, 'rb') as f:
                base64_data = base64.b64encode(f.read())
            base64_data_str = base64_data.decode()
            sign_data = SignClient.sign_file(data=base64_data_str,
                                             cert_file_path=cert_file_path,
                                             key_file_path=key_file_path,
                                             ca_file_path=ca_file_path)
            dir_name = os.path.dirname(file_path)
            file_name = os.path.basename(file_path)
            sign_file_name = f"{dir_name}/{file_name}.sign"
            print(f"target file: {file_path}, sign file: {sign_file_name}")
            with open(sign_file_name, 'wb') as f:
                f.write(sign_data.encode())
    print(f"use time: {time.time() - start} s")   # 46s

requests库是同步阻塞的,必须等到结果才会发第二个请求。 这个代码如果部署成服务的方式,是会发生接口访问请求超时,所以下面通过asyncio+aiohttp实现并发http请求, 来减少请求响应时间, 下面是一个异步并发http请求的代码实现。

class SignClient(object):

    IP = "https://wuzhongqiang.net"
    SIGN_URL = "/api/v1/sign_file"
    APP_KEY = os.environ.get("APP_KEY", "xxx")
    APP_SECRET = os.environ.get("APP_SECRET", "xxx")

    @staticmethod
    def gen_app_key_sign(method: str, url: str, header: dict, security_key: str, body: str):
        http_str = (f"{method}{url}"
                    f"es-auth-appkey:{header['es-auth-appkey']}"
                    f"es-auth-nonce:{header['es-auth-nonce']}"
                    f"es-auth-timestamp:{header['es-auth-timestamp']}"
                    f"json={body}"
                    f"{security_key}")
        try:
            urlCodeStr = urllib.parse.quote(http_str, safe='')
            hashHex = hashlib.sha256(urlCodeStr.encode('utf-8')).hexdigest()
        except Exception as e:
            print(e)
            hashHex = ""
        return hashHex

    @staticmethod
    async def sign_file(session: aiohttp.ClientSession, data: str, url: str, file_path: str, ssl_context) -> typing.Tuple[str, str]:
        target_data = {"data": data}
        es_auth_noce = ''.join(random.choices('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', k=16))
        es_auth_timestamp = str(int(time.time() * 1000))
        auth_info = {
            "es-auth-appkey": SignClient.APP_KEY,
            "es-auth-nonce": es_auth_noce,
            "es-auth-timestamp": es_auth_timestamp
        }
        payload = json.dumps(target_data, separators=(',', ':'))
        signature_value = SignClient.gen_app_key_sign(method="POST",
                                                      url=SignClient.SIGN_URL,
                                                      header=auth_info,
                                                      security_key=SignClient.APP_SECRET,
                                                      body=payload)
        headers = {
            "Content-Type": "application/json",
            "es-auth-appkey": SignClient.APP_KEY,
            "es-auth-nonce": es_auth_noce,
            "es-auth-timestamp": es_auth_timestamp,
            "es-auth-sign": signature_value
        }

        dir_name = os.path.dirname(file_path)
        file_name = os.path.basename(file_path)
        sign_file_name = f"{dir_name}/{file_name}.sign"
        async with session.post(url=f"{SignClient.IP}{SignClient.SIGN_URL}", data=payload, headers=headers, ssl_context=ssl_context) as response:
            try:
                response_body = await response.text()
                json_object = json.loads(response_body)
                sign_data = json_object["signData"]
                return sign_file_name, sign_data
            except Exception as e:
                print(e)
                return "", ""

    @staticmethod
    async def sign_files(base_dir: str,
                         cert_file_path: str,
                         key_file_path: str,
                         ca_file_path):
        ssl_context = ssl.create_default_context()
        # 加载私钥及对应证书
        ssl_context.load_cert_chain(certfile=cert_file_path, keyfile=key_file_path)
        # 加载一组用于验证其他对等方证书的 "证书颁发机构" (CA) 证书
        ssl_context.load_verify_locations(cafile=ca_file_path)

        async with aiohttp.ClientSession() as session:
            tasks = []
            for root, _, files in os.walk(base_dir):
                for file in files:
                    file_path = os.path.join(root, file)
                    with open(file_path, 'rb') as f:
                        base64_data = base64.b64encode(f.read())
                    base64_data_str = base64_data.decode()
                    # 创建一个签名的任务并添加到任务列表
                    task = SignClient.sign_file(
                        session=session,
                        data=base64_data_str,
                        url=f"{SignClient.IP}{SignClient.SIGN_URL}",
                        file_path=file_path,
                        ssl_context=ssl_context
                    )
                    tasks.append(task)

            results = await asyncio.gather(*tasks)
            return results


if __name__ == '__main__':
    base_dir = "/home/wuzhongqiang/Downloads/test_dir"
    cert_file_path = "/home/wuzhongqiang/csr_file/pki-tls-ADD.crt.pem"
    key_file_path = "/home/wuzhongqiang/csr_file/private.key.pem"
    ca_file_path = "/home/wuzhongqiang/csr_file/server_chain.pem"
    start = time.time()
    # 如果已经开启了event, 也就是在服务里面调用的话,这里直接await 后面的签名函数即可
    results = asyncio.run(SignClient.sign_files(base_dir, cert_file_path, key_file_path, ca_file_path))
    for sign_file_name, sign_data in results:
        if sign_data:
            print(f"sign file: {sign_file_name}")
            with open(sign_file_name, 'wb') as f:
                f.write(sign_data.encode())
        else:
            print(f"Signed filed written to: {sign_file_name}")
    print(f"use time: {time.time() - start} s")   # 0.58s

改成并发http请求之后, 处理速度从之前的46s降到了0.58s, 效果还是非常可观的。

参考:

  • python关于SSL/TLS认证的实现
  • SSL接口文档

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

银行数字化转型导师坚鹏:银行数字化转型必知的3大客户分析维度

银行数字化转型需要进行客户分析,如何进行客户分析呢?银行数字化转型导师坚鹏认为至少从客户需求分析、客户画像分析、客户购买行为分析3个维度进行客户分析。 1.客户需求分析 银行数字化转型需要了解客户需求,不同年龄段的客户有不同的需求…

游戏APP如何提高广告变现收益的同时,保证用户留存率?

APP广告变现对接第三方聚合广告平台主要通过SDK文档对接,一些媒体APP不具备专业运营广告变现的对接能力和资源沉淀,导致APP被封控,设置列入黑名单,借助第三方聚合广告平台进行商业化变现是最佳选择。#APP广告变现# 接入第三方平台…

VGG网络模型

VGG网络模型 VGG的网络架构VGG16VGG19 特点总结时间关系AlexNet和VGG相似之处AlexNet和VGG不同之处启发与影响总结 VGG(Visual Geometry Group)是由牛津大学的 Visual Geometry Group 提出的一个深度卷积神经网络模型,它在2014年的ImageNet大…

哲♂学家带你深♂入了解动态顺序表

前言: 最近本哲♂学家学习了顺序表,下面我给大家分享一下关于顺序表的知识。 一、什么是顺序表 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组 上完成数据的增删查改。 顺序表&#xff…

动态规划刷题(算法竞赛、蓝桥杯)--乌龟棋(线性DP)

1、题目链接&#xff1a;[NOIP2010 提高组] 乌龟棋 - 洛谷 #include <bits/stdc.h> using namespace std; const int M41; int f[M][M][M][M],num[351],g[5],n,m,x; //f[a][b][c][d]表示放a个1b个2c个3d个4的总得分 int main(){scanf("%d %d",&n,&m)…

创新指南|贝恩的产品经理RAPID框架:解决问题的分步指南,使决策过程既高效又民主

您是否曾发现自己陷入项目的阵痛之中&#xff0c;决策混乱、角色不明确、团队成员之间的冲突不断升级&#xff1f;作为产品经理&#xff0c;驾驭这艘船穿过如此汹涌的水域可能是令人畏惧的。应对这些挑战的关键在于采用清晰、结构化的决策方法。输入贝恩的 RAPID 框架&#xff…

软件测试用例(2)

具体的设计方法 -- 黑盒测试 因果图 因果图是一种简化的逻辑图, 能直观地表明程序的输入条件(原因)和输出动作(结果)之间的相互关系. 因果图法是借助图形来设计测试用例的一种系统方法, 特别适用于被测试程序具有多种输入条件, 程序的输出又依赖于输入条件的各种情况. 因果图…

Linux-进程概念

1. 进程基本概念 书面概念&#xff1a;程序的一个执行实例&#xff0c;正在执行的程序等 内核概念&#xff1a;担当分配系统资源&#xff08;CPU时间&#xff0c;内存&#xff09;的实体。 2. 描述和组织进程-PCB PCB&#xff08;process contral block&#xff09;&#xff0…

【讲解下如何Stable Diffusion本地部署】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

20240324-2-频繁模式FrequentPattern

频繁模式(frequent pattern) 频繁模式一般是指频繁地出现在数据集中的模式。这种频繁模式和关联规则是数据挖掘中想要挖掘的知识。我们都知道一个很有趣的故事&#xff0c;就是啤酒和尿布的故事&#xff0c; 在某些特定的情况下&#xff0c;“啤酒”与“尿布”两件看上去毫无关…

SCP 从Linux快速下载文件到Windows本地

需求&#xff1a;通过mobaxterm将大文件拖动到windows本地速度太慢。 环境&#xff1a;本地是Windows&#xff0c;安装了Git。 操作&#xff1a;进入文件夹内&#xff0c;鼠标右键&#xff0c;点击Git Bash here&#xff0c;然后输入命令即可。这样的话&#xff0c;其实自己本…

java农家乐旅游管理系统springboot+vue

实现了一个完整的农家乐系统&#xff0c;其中主要有用户表模块、关于我们模块、收藏表模块、公告信息模块、酒店预订模块、酒店信息模块、景区信息模块、景区订票模块、景点分类模块、会员等级模块、会员模块、交流论坛模块、度假村信息模块、配置文件模块、在线客服模块、关于…

基于深度学习的番茄成熟度检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)

摘要&#xff1a;在本博客中&#xff0c;我们深入探讨了基于YOLOv8/v7/v6/v5的番茄成熟度检测系统。核心技术基于YOLOv8&#xff0c;同时融合了YOLOv7、YOLOv6、YOLOv5的算法&#xff0c;对比了它们在性能指标上的差异。本文详细介绍了国内外在此领域的研究现状、数据集的处理方…

9.图像中值腐蚀膨胀滤波的实现

1 简介 在第七章介绍了基于三种卷积前的图像填充方式&#xff0c;并生成了3X3的图像卷积模板&#xff0c;第八章运用这种卷积模板进行了均值滤波的FPGA实现与MATLAB实现&#xff0c;验证了卷积模板生成的正确性和均值滤波算法的MATLAB算法实现。   由于均值滤波、中值滤波、腐…

Flask Python:如何获取不同请求方式的参数

目录 前言 1. 获取GET请求中的查询参数 2. 获取POST请求中的表单数据 3. 获取JSON数据 总结 前言 在使用Flask开发Web应用时&#xff0c;我们经常需要获取不同请求方式的参数。Flask提供了多种方式来获取不同请求方式的参数&#xff0c;包括GET请求中的查询参数、POST请求…

Spring Boot Mockito (二)

Spring Boot Mockito (二) 基于第一篇Spring Boot Mockito (一) 这篇文章主要是讲解Spring boot 与 Mockito 集成持久层接口层单元测试。 1. 引入数据库 h2及其依赖包 <dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId…

JavaScript基础代码练习之冒泡排序

一、要求对一个数组进行冒泡排序&#xff0c;并将排序后的结果输出到控制台。在代码中&#xff0c;数组 arr 包含了一组数字&#xff0c;然后使用嵌套的循环来进行冒泡排序。 二、编写代码 <!DOCTYPE html> <html lang"en"><head><meta chars…

NOI - OpenJudge - 2.5基本算法之搜索 - 1490:A Knight‘s Journey - 超详解析(含AC代码)

点赞关注吧~ 1490:A Knights Journey 查看提交统计提问 总时间限制: 1000ms 内存限制: 65536kB 描述 Background The knight is getting bored of seeing the same black and white squares again and again and has decided to make a journey around the world. When…

《QT实用小工具·九》设备按钮控件

1、概述 源码放在文章末尾 该项目实现了设备按钮控件&#xff0c;主要包含如下功能&#xff1a; 可设置按钮样式 圆形、警察、气泡、气泡2、消息、消息2。可设置按钮颜色 布防、撤防、报警、旁路、故障。可设置报警切换及对应报警切换的颜色。可设置显示的防区号。可设置是否…

实验报告答案

基本任务&#xff08;必做&#xff09; 先用普通用户&#xff08;自己的姓名拼音&#xff09;登录再操作 编程有代码截图和执行过程结果截图 代写获取&#xff1a; https://laowangall.oss-cn-beijing.aliyuncs.com/studentall.pdf 1. Linux的Shell编程 &#xff08;1&am…