• 「Spring」Boot Docker 认证指南(下)


    构建插件

    如果你不想docker在你的构建中直接调用,有一套丰富的 Maven 和 Gradle 插件可以为你完成这项工作。这里仅仅是少数。

    Spring Boot Maven 和 Gradle 插件

    您可以使用Maven和Gradle的 Spring Boot 构建插件来创建容器映像。docker build这些插件使用Cloud Native Buildpacks创建一个 OCI 映像(与创建的格式相同) 。您不需要Dockerfile,但您确实需要 Docker 守护程序,可以在本地(使用 docker 构建时使用)或通过DOCKER_HOST环境变量远程进行。默认构建器针对 Spring Boot 应用程序进行了优化,并且图像像上面的示例一样有效地分层。

    以下示例在不更改pom.xml文件的情况下使用 Maven:

    ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp复制

    以下示例适用于 Gradle,无需更改build.gradle文件:

    ./gradlew bootBuildImage --imageName=myorg/myapp复制

    第一次构建可能需要很长时间,因为它必须下载一些容器镜像和 JDK,但后续构建应该很快。

    然后您可以运行映像,如以下清单所示(带输出):

    1. docker run -p 8080:8080 -t myorg/myapp
    2. Setting Active Processor Count to 6
    3. Calculating JVM memory based on 14673596K available memory
    4. Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 14673596K, Thread Count: 50, Loaded Class Count: 13171, Headroom: 0%)
    5. Adding 129 container CA certificates to JVM truststore
    6. Spring Cloud Bindings Enabled
    7. Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M -Dorg.springframework.cloud.bindings.boot.enable=true
    8. ....
    9. 2015-03-31 13:25:48.035 INFO 1 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
    10. 2015-03-31 13:25:48.037 INFO 1 --- [ main] hello.Application复制

    您可以看到应用程序正常启动。您可能还注意到 JVM 内存需求是在容器内计算并设置为命令行选项的。这与多年来在 Cloud Foundry 构建包中使用的内存计算相同。它代表了对一系列 JVM 应用程序(包括但不限于 Spring Boot 应用程序)的最佳选择的重要研究,结果通常比 JVM 的默认设置好得多。您可以自定义命令行选项并通过设置环境变量覆盖内存计算器,如Paketo buildpacks 文档中所示。

    Spotify Maven 插件

    Spotify Maven 插件是一个受欢迎的选择。它要求您编写 aDockerfile然后docker为您运行,就像您在命令行上执行它一样。docker 镜像标签和其他东西有一些配置选项,但它使您的应用程序中的 docker 知识集中在一个Dockerfile很多人喜欢的 .

    对于真正的基本用法,它无需额外配置即可开箱即用:

    1. mvn com.spotify:dockerfile-maven-plugin:build
    2. ...
    3. [INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp
    4. [INFO]
    5. [INFO] Image will be built without a name
    6. [INFO]
    7. ...
    8. [INFO] BUILD SUCCESS
    9. [INFO] ------------------------------------------------------------------------
    10. [INFO] Total time: 7.630 s
    11. [INFO] Finished at: 2018-11-06T16:03:16+00:00
    12. [INFO] Final Memory: 26M/595M
    13. [INFO] ------------------------------------------------------------------------复制

    这将构建一个匿名 docker 映像。我们现在可以在命令行上标记它docker或使用 Maven 配置将其设置为repository. 以下示例在不更改pom.xml文件的情况下工作:

    $ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp复制

    或者,您更改pom.xml​文件:

    pom.xml

    1. <build>
    2. <plugins>
    3. <plugin>
    4. <groupId>com.spotify</groupId>
    5. <artifactId>dockerfile-maven-plugin</artifactId>
    6. <version>1.4.8</version>
    7. <configuration>
    8. <repository>myorg/${project.artifactId}</repository>
    9. </configuration>
    10. </plugin>
    11. </plugins>
    12. </build>复制

    Palantir Gradle 插件

    Palantir Gradle 插件与 a 一起使用,Dockerfile并且还可Dockerfile以为您生成 a。然后它docker就像在命令行上运行它一样运行。

    首先,您需要将插件导入您的build.gradle:

    build.gradle

    1. buildscript {
    2. ...
    3. dependencies {
    4. ...
    5. classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
    6. }
    7. }复制

    然后,最后,您可以应用插件并调用它的任务:

    build.gradle

    1. apply plugin: 'com.palantir.docker'
    2. group = 'myorg'
    3. bootJar {
    4. baseName = 'myapp'
    5. version = '0.1.0'
    6. }
    7. task unpack(type: Copy) {
    8. dependsOn bootJar
    9. from(zipTree(tasks.bootJar.outputs.files.singleFile))
    10. into("build/dependency")
    11. }
    12. docker {
    13. name "${project.group}/${bootJar.baseName}"
    14. copySpec.from(tasks.unpack.outputs).into("dependency")
    15. buildArgs(['DEPENDENCY': "dependency"])
    16. }复制

    在本例中,我们选择将 Spring Boot fat JAR 解压到build目录中的特定位置,该位置是 docker build 的根目录。Dockerfile然后早期显示的多层(不是多阶段)起作用。

    Jib Maven 和 Gradle 插件

    Google 有一个名为Jib的开源工具,它相对较新,但出于多种原因非常有趣。可能最有趣的是您不需要 docker 来运行它。Jib 使用与您获得的相同标准输出来构建映像,docker build但除非您要求它,否则它不会使用docker,因此它可以在未安装 docker 的环境中工作(在构建服务器中很常见)。您也不需要Dockerfile(无论如何都会被忽略)或任何东西pom.xml来获得在 Maven 中构建的图像(Gradle 将要求您至少在 中安装插件build.gradle)。

    DockerfileJib 的另一个有趣的特性是它对层有意见,并且它以与上面创建的多层略有不同的方式优化它们。与胖 JAR 中一样,Jib 将本地应用程序资源与依赖项分开,但它更进一步,还将快照依赖项放入单独的层,因为它们更有可能发生变化。有用于进一步自定义布局的配置选项。

    以下示例在不更改 Maven 的情况下使用pom.xml:

    $ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp复制

    myorg要运行该命令,您需要具有在存储库前缀下推送到 Dockerhub 的权限。如果您已docker在命令行上进行了身份验证,则可以在本地~/.docker配置中使用。~/.m2/settings.xml您还可以在您的(id存储库的重要)中设置 Maven“服务器”身份验证:

    settings.xml

    1. <server>
    2. <id>registry.hub.docker.com</id>
    3. <username>myorg</username>
    4. <password>...</password>
    5. </server>复制

    还有其他选项——例如,您可以docker使用dockerBuild目标而不是build. 还支持其他容器注册表。对于每一项,您都需要通过 Docker 或 Maven 设置来设置本地身份验证。

    gradle 插件具有类似的功能,一旦你在你的build.gradle:.

    build.gradle

    1. plugins {
    2. ...
    3. id 'com.google.cloud.tools.jib' version '1.8.0'
    4. }复制

    以下清单使用入门指南中使用的旧 Gradle 样式:

    build.gradle

    1. buildscript {
    2. repositories {
    3. maven {
    4. url "https://plugins.gradle.org/m2/"
    5. }
    6. mavenCentral()
    7. }
    8. dependencies {
    9. classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE')
    10. classpath('com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:1.8.0')
    11. }
    12. }复制

    然后,您可以通过运行以下命令来构建映像:

    ./gradlew jib --image=myorg/myapp复制

    与 Maven 构建一样,如果您已docker在命令行上进行了身份验证,则图像推送将从您的本地~/.docker配置进行身份验证。

    持续集成

    如今,自动化(或应该是)是每个应用程序生命周期的一部分。人们用来进行自动化的工具往往非常擅长从源代码调用构建系统。因此,如果这为您提供了一个 docker 映像,并且构建代理中的环境与开发人员自己的环境充分一致,那可能就足够了。对 docker 注册表进行身份验证可能是最大的挑战,但所有自动化工具中都有一些功能可以帮助解决这个问题。

    但是,有时最好将容器创建完全留给自动化层,在这种情况下,可能不需要污染用户的代码。容器创建很棘手,开发人员有时不需要真正关心它。如果用户代码更干净,则不同的工具更有可能“做正确的事”(应用安全修复、优化缓存等)。自动化有多种选择,如今它们都带有一些与容器相关的功能。我们将看一对夫妇。

    大厅

    Concourse是一个基于管道的自动化平台,可用于 CI 和 CD。它在 VMware 内部使用,该项目的主要作者在那里工作。Concourse 中的所有内容都是无状态的,并且在容器中运行,CLI 除外。由于运行容器是自动化管道的主要业务顺序,因此很好地支持创建容器。Docker Image Resource负责保持构建的输出状态是最新的,如果它是一个容器镜像的话。

    以下示例管道为前面显示的示例构建了一个 docker 映像,假设它位于 github 中myorg/myapp,Dockerfile在根中有一个,并且在 中有一个构建任务声明src/main/ci/build.yml:

    1. resources:
    2. - name: myapp
    3. type: git
    4. source:
    5. uri: https://github.com/myorg/myapp.git
    6. - name: myapp-image
    7. type: docker-image
    8. source:
    9. email: {{docker-hub-email}}
    10. username: {{docker-hub-username}}
    11. password: {{docker-hub-password}}
    12. repository: myorg/myapp
    13. jobs:
    14. - name: main
    15. plan:
    16. - task: build
    17. file: myapp/src/main/ci/build.yml
    18. - put: myapp-image
    19. params:
    20. build: myapp复制

    管道的结构是非常具有声明性的:您定义“资源”(输入、输出或两者)和“作业”(使用资源并将操作应用于资源)。如果任何输入资源发生更改,则会触发新的构建。如果任何输出资源在作业期间发生更改,则会对其进行更新。

    管道可以在与应用程序源代码不同的地方定义。此外,对于通用构建设置,任务声明也可以集中或外部化。这允许在开发和自动化之间分离一些关注点,这适合一些软件开发组织。

    詹金斯

    Jenkins是另一个流行的自动化服务器。它具有大量功能,但最接近此处其他自动化示例的是管道功能。下面Jenkinsfile使用 Maven 构建一个 Spring Boot 项目,然后使用 aDockerfile构建一个镜像并将其推送到存储库:

    Jenkinsfile

    1. node {
    2. checkout scm
    3. sh './mvnw -B -DskipTests clean package'
    4. docker.build("myorg/myapp").push()
    5. }复制

    对于需要在构建服务器中进行身份验证的(实际)docker 存储库,您可以docker使用docker.withCredentials(…​).

    构建包

    packSpring Boot Maven 和 Gradle 插件使用构建包的方式与CLI 在以下示例中的使用方式完全相同。给定相同的输入,生成的图像是相同的。

    Cloud Foundry在内部使用容器已经很多年了,用于将用户代码转换为容器的部分技术是 Build Packs,这个想法最初是从Heroku借来的。当前一代的 buildpacks (v2) 生成由平台组装到容器中的通用二进制输出。新一代构建包(v3) 是 Heroku 与其他公司(包括 VMware)的合作,它直接明确地构建容器镜像。这对开发人员和运营商来说很有趣。开发人员不需要太关心如何构建容器的细节,但如果需要,他们可以轻松创建一个。Buildpacks 还具有许多用于缓存构建结果和依赖项的功能。通常,构建包的运行速度比原生 Docker 构建快得多。操作员可以扫描容器以审核其内容并将其转换为修补它们以进行安全更新。此外,您可以在本地(例如,在开发人员机器或 CI 服务中)或在 Cloud Foundry 等平台中运行构建包。

    buildpack 生命周期的输出是容器映像,但您不需要Dockerfile. 输出映像中的文件系统层由 buildpack 控制。通常,许多优化都是在开发人员不必知道或关心它们的情况下进行的。在较低层(例如包含操作系统的基础映像)和较高层(包含中间件和语言特定依赖项)之间还有一个应用程序二进制接口。这使得 Cloud Foundry 等平台可以在有安全更新的情况下修补较低层,而不会影响应用程序的完整性和功能。

    为了让您了解 buildpack 的功能,以下示例(显示其输出)从命令行使用Pack CLI(它可以与我们在本指南中使用的示例应用程序一起使用 - 不需要Dockerfile或任何特殊的构建配置):

    1. pack build myorg/myapp --builder=paketobuildpacks/builder:base --path=.
    2. base: Pulling from paketobuildpacks/builder
    3. Digest: sha256:4fae5e2abab118ca9a37bf94ab42aa17fef7c306296b0364f5a0e176702ab5cb
    4. Status: Image is up to date for paketobuildpacks/builder:base
    5. base-cnb: Pulling from paketobuildpacks/run
    6. Digest: sha256:a285e73bc3697bc58c228b22938bc81e9b11700e087fd9d44da5f42f14861812
    7. Status: Image is up to date for paketobuildpacks/run:base-cnb
    8. ===> DETECTING
    9. 7 of 18 buildpacks participating
    10. paketo-buildpacks/ca-certificates 2.3.2
    11. paketo-buildpacks/bellsoft-liberica 8.2.0
    12. paketo-buildpacks/maven 5.3.2
    13. paketo-buildpacks/executable-jar 5.1.2
    14. paketo-buildpacks/apache-tomcat 5.6.1
    15. paketo-buildpacks/dist-zip 4.1.2
    16. paketo-buildpacks/spring-boot 4.4.2
    17. ===> ANALYZING
    18. Previous image with name "myorg/myapp" not found
    19. ===> RESTORING
    20. ===> BUILDING
    21. Paketo CA Certificates Buildpack 2.3.2
    22. https://github.com/paketo-buildpacks/ca-certificates
    23. Launch Helper: Contributing to layer
    24. Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper
    25. Paketo BellSoft Liberica Buildpack 8.2.0
    26. https://github.com/paketo-buildpacks/bellsoft-liberica
    27. Build Configuration:
    28. $BP_JVM_VERSION 11 the Java version
    29. Launch Configuration:
    30. $BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation
    31. $BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation
    32. $BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation
    33. $JAVA_TOOL_OPTIONS the JVM launch flags
    34. BellSoft Liberica JDK 11.0.12: Contributing to layer
    35. Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jdk11.0.12+7-linux-amd64.tar.gz
    36. Verifying checksum
    37. Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jdk
    38. Adding 129 container CA certificates to JVM truststore
    39. Writing env.build/JAVA_HOME.override
    40. Writing env.build/JDK_HOME.override
    41. BellSoft Liberica JRE 11.0.12: Contributing to layer
    42. Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jre11.0.12+7-linux-amd64.tar.gz
    43. Verifying checksum
    44. Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
    45. Adding 129 container CA certificates to JVM truststore
    46. Writing env.launch/BPI_APPLICATION_PATH.default
    47. Writing env.launch/BPI_JVM_CACERTS.default
    48. Writing env.launch/BPI_JVM_CLASS_COUNT.default
    49. Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default
    50. Writing env.launch/JAVA_HOME.default
    51. Writing env.launch/MALLOC_ARENA_MAX.default
    52. Launch Helper: Contributing to layer
    53. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/active-processor-count
    54. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts
    55. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns
    56. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator
    57. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader
    58. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer
    59. Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9
    60. JVMKill Agent 1.16.0: Contributing to layer
    61. Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so
    62. Verifying checksum
    63. Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill
    64. Writing env.launch/JAVA_TOOL_OPTIONS.append
    65. Writing env.launch/JAVA_TOOL_OPTIONS.delim
    66. Java Security Properties: Contributing to layer
    67. Writing env.launch/JAVA_SECURITY_PROPERTIES.default
    68. Writing env.launch/JAVA_TOOL_OPTIONS.append
    69. Writing env.launch/JAVA_TOOL_OPTIONS.delim
    70. Paketo Maven Buildpack 5.3.2
    71. https://github.com/paketo-buildpacks/maven
    72. Build Configuration:
    73. $BP_MAVEN_BUILD_ARGUMENTS -Dmaven.test.skip=true package the arguments to pass to Maven
    74. $BP_MAVEN_BUILT_ARTIFACT target/*.[jw]ar the built application artifact explicitly. Supersedes $BP_MAVEN_BUILT_MODULE
    75. $BP_MAVEN_BUILT_MODULE the module to find application artifact in
    76. Creating cache directory /home/cnb/.m2
    77. Compiled Application: Contributing to layer
    78. Executing mvnw --batch-mode -Dmaven.test.skip=true package
    79. [ ... Maven build output ... ]
    80. [INFO] ------------------------------------------------------------------------
    81. [INFO] BUILD SUCCESS
    82. [INFO] ------------------------------------------------------------------------
    83. [INFO] Total time: 53.474 s
    84. [INFO] Finished at: 2021-07-23T20:10:28Z
    85. [INFO] ------------------------------------------------------------------------
    86. Removing source code
    87. Paketo Executable JAR Buildpack 5.1.2
    88. https://github.com/paketo-buildpacks/executable-jar
    89. Class Path: Contributing to layer
    90. Writing env/CLASSPATH.delim
    91. Writing env/CLASSPATH.prepend
    92. Process types:
    93. executable-jar: java org.springframework.boot.loader.JarLauncher (direct)
    94. task: java org.springframework.boot.loader.JarLauncher (direct)
    95. web: java org.springframework.boot.loader.JarLauncher (direct)
    96. Paketo Spring Boot Buildpack 4.4.2
    97. https://github.com/paketo-buildpacks/spring-boot
    98. Creating slices from layers index
    99. dependencies
    100. spring-boot-loader
    101. snapshot-dependencies
    102. application
    103. Launch Helper: Contributing to layer
    104. Creating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindings
    105. Spring Cloud Bindings 1.7.1: Contributing to layer
    106. Downloading from https://repo.spring.io/release/org/springframework/cloud/spring-cloud-bindings/1.7.1/spring-cloud-bindings-1.7.1.jar
    107. Verifying checksum
    108. Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings
    109. Web Application Type: Contributing to layer
    110. Reactive web application detected
    111. Writing env.launch/BPL_JVM_THREAD_COUNT.default
    112. 4 application slices
    113. Image labels:
    114. org.opencontainers.image.title
    115. org.opencontainers.image.version
    116. org.springframework.boot.version
    117. ===> EXPORTING
    118. Adding layer 'paketo-buildpacks/ca-certificates:helper'
    119. Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
    120. Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
    121. Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
    122. Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
    123. Adding layer 'paketo-buildpacks/executable-jar:classpath'
    124. Adding layer 'paketo-buildpacks/spring-boot:helper'
    125. Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
    126. Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
    127. Adding 5/5 app layer(s)
    128. Adding layer 'launcher'
    129. Adding layer 'config'
    130. Adding layer 'process-types'
    131. Adding label 'io.buildpacks.lifecycle.metadata'
    132. Adding label 'io.buildpacks.build.metadata'
    133. Adding label 'io.buildpacks.project.metadata'
    134. Adding label 'org.opencontainers.image.title'
    135. Adding label 'org.opencontainers.image.version'
    136. Adding label 'org.springframework.boot.version'
    137. Setting default process type 'web'
    138. Saving myorg/myapp...
    139. *** Images (ed1f92885df0):
    140. myorg/myapp
    141. Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
    142. Adding cache layer 'paketo-buildpacks/maven:application'
    143. Adding cache layer 'paketo-buildpacks/maven:cache'
    144. Successfully built image 'myorg/myapp'复制

    这--builder是一个运行 buildpack 生命周期的 Docker 镜像。通常,它将是所有开发人员或单个平台上的所有开发人员的共享资源。您可以在命令行上设置默认构建器(在 中创建一个文件~/.pack),然后从后续构建中省略该标志。

    构建器
    paketobuildpacks/builder:base还知道如何从可执行 JAR 文件构建映像,因此您可以先使用 Maven 构建,然后将其指向--pathJAR 文件以获得相同的结果。

    原生

    容器和平台领域的另一个新项目是Knative。如果您不熟悉它,可以将其视为构建无服务器平台的构建块。它建立在Kubernetes 之上,因此最终它会使用容器镜像并将它们转化为平台上的应用程序或“服务”。不过,它的主要功能之一是能够使用源代码并为您构建容器,使其对开发人员和操作员更加友好。Knative Build是执行此操作的组件,它本身就是一个灵活的平台,用于将用户代码转换为容器——您几乎可以以任何您喜欢的方式进行操作。一些模板提供了通用模式(例如 Maven 和 Gradle 构建)和多阶段 docker 构建使用卡尼科。还有一个模板使用了Buildpacks,这对我们来说很有趣,因为 buildpacks 一直对 Spring Boot 有很好的支持。

    结束

    本指南提供了许多用于为 Spring Boot 应用程序构建容器映像的选项。所有这些都是完全有效的选择,现在由您决定您需要哪一个。您的第一个问题应该是“我真的需要构建容器映像吗?” 如果答案是“是”,那么您的选择可能会受到效率、可缓存性和关注点分离的驱动。您是否想让开发人员无需过多了解容器镜像的创建方式?当需要修补操作系统和中间件漏洞时,您是否想让开发人员负责更新映像?或者,开发人员可能需要完全控制整个过程,并且他们拥有所需的所有工具和知识。


    文末备注:

    Spring中国教育管理中心

    Spring Boot Docker来源:Spring中国教育管理中心

     

  • 相关阅读:
    Dockerd搭建redis三主三从&&扩容&&缩容
    golang使用channel通道实现非阻塞队列和超时阻塞队列
    市场拓展招聘:完整指南
    VSCode远程连接服务器报错:Could not establish connection to
    Linux内核驱动开发的需要掌握的知识点
    【每日一题】ABC301D - Bitmask | 贪心 | 简单
    Linux - 系统调用
    C++《面向对象程序设计课程设计》
    计算机视觉发展和应用浅谈
    无胁科技-TVD每日漏洞情报-2022-11-28
  • 原文地址:https://blog.csdn.net/CIPSDN/article/details/125503938