Browse Source

first commit

master
邝卫东 3 years ago
commit
c1e3aae62d
18 changed files with 635 additions and 0 deletions
  1. +25
    -0
      .dockerignore
  2. +8
    -0
      .gitignore
  3. +25
    -0
      AmqpTest.sln
  4. +177
    -0
      AmqpTest/Amqp/AmqpSubscribe.cs
  5. +20
    -0
      AmqpTest/AmqpTest.csproj
  6. +25
    -0
      AmqpTest/Configs/IotConfig.cs
  7. +24
    -0
      AmqpTest/Dockerfile
  8. +30
    -0
      AmqpTest/Logs/ThreadInfoEnricher.cs
  9. +72
    -0
      AmqpTest/Program.cs
  10. +13
    -0
      AmqpTest/Properties/launchSettings.json
  11. +51
    -0
      AmqpTest/Worker.cs
  12. +17
    -0
      AmqpTest/appsettings.Development.json
  13. +17
    -0
      AmqpTest/appsettings.debug.json
  14. +68
    -0
      AmqpTest/appsettings.json
  15. +0
    -0
      README.md
  16. +29
    -0
      amqp_test_run.sh
  17. +17
    -0
      setup_development.sh
  18. +17
    -0
      setup_test.sh

+ 25
- 0
.dockerignore View File

@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

+ 8
- 0
.gitignore View File

@@ -0,0 +1,8 @@
*.user
*.suo
.vs/
*.log
obj/
bin/
.idea/
.vscode/

+ 25
- 0
AmqpTest.sln View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31005.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmqpTest", "AmqpTest\AmqpTest.csproj", "{305F4286-AD47-48F4-89DD-395A5B05801C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{305F4286-AD47-48F4-89DD-395A5B05801C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{305F4286-AD47-48F4-89DD-395A5B05801C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{305F4286-AD47-48F4-89DD-395A5B05801C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{305F4286-AD47-48F4-89DD-395A5B05801C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5E935C3-96EC-425E-AAB8-D9409563B328}
EndGlobalSection
EndGlobal

+ 177
- 0
AmqpTest/Amqp/AmqpSubscribe.cs View File

@@ -0,0 +1,177 @@
using Amqp;
using Amqp.Framing;
using Amqp.Sasl;
using AmqpTest.Configs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace AmqpTest.Amqp
{
/// <summary>
/// 用于处理阿里云平台amqp消息队列的数据(负责接收)
/// </summary>
public class AmqpSubscribe
{
private readonly ILogger<AmqpSubscribe> _logger;
private readonly IotConfig _configIot;

public AmqpSubscribe(ILogger<AmqpSubscribe> logger, IOptions<IotConfig> optConfigIot)
{
_logger = logger;
_configIot = optConfigIot.Value;
}

/// <summary>
/// 开始接收iot平台amqp推送的数据
/// </summary>
/// <returns></returns>
public async Task BeginListen()
{
try
{
long timestamp = GetCurrentMilliseconds();
string param = "authId=" + _configIot.AccessKey + "&timestamp=" + timestamp;
string password = DoSign(param, _configIot.AccessSecret, "HmacMD5");
string iotInstanceId = !string.IsNullOrEmpty(_configIot.IotInstanceId) ? "iotInstanceId=" + _configIot.IotInstanceId + "," : "";

string username = _configIot.ClientId + "|" + iotInstanceId + "authMode=aksign,signMethod=hmacmd5,consumerGroupId=" + _configIot.ConsumerGroupId
+ ",authId=" + _configIot.AccessKey + ",timestamp=" + timestamp + "|";

await DoConnectAmqpAsync(username, password);
}
catch (Exception ex)
{
_logger.LogError($"BeginListen 处理AMQP数据发生异常 {ex.Message}\n{ex.StackTrace}");
}
}

/// <summary>
/// 获取当前时间的时间戳
/// </summary>
/// <returns></returns>
private long GetCurrentMilliseconds()
{
DateTime dt1970 = new DateTime(1970, 1, 1);
DateTime current = DateTime.Now;
return (long)(current - dt1970).TotalMilliseconds;
}

public string DoSign(string param, string accessSecret, string signMethod)
{
byte[] key = Encoding.UTF8.GetBytes(accessSecret);
byte[] signContent = Encoding.UTF8.GetBytes(param);
var hmac = new HMACMD5(key);
byte[] hashBytes = hmac.ComputeHash(signContent);
return Convert.ToBase64String(hashBytes);
}

private async Task DoConnectAmqpAsync(string username, string password)
{
string host = $"{_configIot.Uid}.iot-amqp.{_configIot.RegionId}.aliyuncs.com";
int port = Convert.ToInt32(_configIot.Port);

var address = new Address(host, port, username, password);
//Create Connection
ConnectionFactory cf = new ConnectionFactory();
//use local tls if neccessary
//cf.SSL.ClientCertificates.Add(GetCert());
//cf.SSL.RemoteCertificateValidationCallback = ValidateServerCertificate;
cf.SASL.Profile = SaslProfile.External;
cf.AMQP.IdleTimeout = 120000;
cf.AMQP.ContainerId = "client.1.2";
cf.AMQP.HostName = "contoso.com";
cf.AMQP.MaxFrameSize = 8 * 1024;

var connection = await cf.CreateAsync(address);
if (connection.ConnectionState.Equals(ConnectionState.OpenSent))
{
_logger.LogInformation("Open frame was received.");
}
//Connection Exception Closed
connection.AddClosedCallback(ConnClosed);

//Receive Message
await DoReceiveAsync(connection);
}

private async Task DoReceiveAsync(Connection connection)
{
var session = new Session(connection);
var receiver = new ReceiverLink(session, "queueName", null);
_logger.LogInformation("开始接收设备信息!");
int maxCount = Convert.ToInt32(_configIot.MaxDegreeOfParallelism);
//LimitedConcurrencyLevelTaskScheduler lcts = new LimitedConcurrencyLevelTaskScheduler(maxCount);
//TaskFactory taskFactory = new TaskFactory(lcts);

try
{
int count = 0;
var sw = new Stopwatch();
sw.Start();
//var tasks = new List<Task>();
//receiver.SetCredit(10000, false);

//只是控制该线程不退出
while (true)
{
receiver.Start(100, (link, message) =>
{
var messageId = message.ApplicationProperties["messageId"].ToString();
var topic = message.ApplicationProperties["topic"].ToString();
var body = Encoding.UTF8.GetString((byte[])message.Body);

link.Accept(message);

_logger.LogInformation($"message arrived, topic= {topic}, messageId= {messageId}, body= {body}");
count++;
});


//var message = await receiver.ReceiveAsync(new TimeSpan(0, 0, 5));
//if (message != null)
//{
// var messageId = message.ApplicationProperties["messageId"].ToString();
// var topic = message.ApplicationProperties["topic"].ToString();
// var body = Encoding.UTF8.GetString((byte[])message.Body);

// _logger.LogInformation($"message arrived, topic= {topic}, messageId= {messageId}, body= {body}");

// receiver.Accept(message);

// count++;
//}


if (sw.ElapsedMilliseconds > 60000)
{
_logger.LogWarning($"约1分钟处理 {count} 个请求");
sw.Restart();
count = 0;
}

await Task.Delay(100).ConfigureAwait(false); //睡眠值用于控制计时的准确性,和iot消息处理效率无关
}
}
catch (Exception)
{
throw;
}
finally
{
connection?.Close();
}
}

private void ConnClosed(IAmqpObject _, Error e)
{
_logger.LogError($"连接关闭:{e?.ToString()}");
}
}
}

+ 20
- 0
AmqpTest/AmqpTest.csproj View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>dotnet-AmqpTest-E73E3A37-DF5F-44BE-A32D-9A5FF9DA0BD9</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AMQPNetLite" Version="2.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.11" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
</ItemGroup>
</Project>

+ 25
- 0
AmqpTest/Configs/IotConfig.cs View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace AmqpTest.Configs
{
/// <summary>
/// IOTConfig配置模板类
/// </summary>
public class IotConfig
{
public string AccessKey { get; set; }
public string AccessSecret { get; set; }
public string IotInstanceId { get; set; }
public string ConsumerGroupId { get; set; }
public string RegionId { get; set; }
public string ProductKey { get; set; }
public string Uid { get; set; }
public string ClientId { get; set; }
public string Port { get; set; }
public string ReconnectionTime { get; set; }
public string MaxDegreeOfParallelism { get; set; }
public string SmsSignName { get; set; }
}
}

+ 24
- 0
AmqpTest/Dockerfile View File

@@ -0,0 +1,24 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
WORKDIR /src
COPY ["AmqpTest/AmqpTest.csproj", "AmqpTest/"]
RUN dotnet restore "AmqpTest/AmqpTest.csproj"
COPY . .
WORKDIR "/src/AmqpTest"
RUN dotnet build "AmqpTest.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "AmqpTest.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENV environment=Development
ENV TimeZone=Asia/Shanghai
ENV LANG C.UTF-8
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
ENTRYPOINT dotnet AmqpTest.dll --environment=$environment

+ 30
- 0
AmqpTest/Logs/ThreadInfoEnricher.cs View File

@@ -0,0 +1,30 @@
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace AmqpTest.Logs
{
public class ThreadInfoEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{

logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadId", Thread.CurrentThread.ManagedThreadId));
}
}

public static class EnricherExtensions
{
public static LoggerConfiguration WithThreadInfo(this LoggerEnrichmentConfiguration enrich)
{
if (enrich == null)
throw new ArgumentNullException(nameof(enrich));
return enrich.With<ThreadInfoEnricher>();
}
}
}

+ 72
- 0
AmqpTest/Program.cs View File

@@ -0,0 +1,72 @@
using AmqpTest.Configs;
using AmqpTest.Logs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace AmqpTest
{
public class Program
{
public Program(IConfiguration configuration)
{
Configuration = configuration;
}

public static IConfiguration Configuration { get; set; }

public static void Main(string[] args)
{
//Ñ¡ÔñÅäÖÃÎļþappsetting.json
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(config).Enrich.WithThreadInfo()
.CreateLogger();

try
{
Log.Information("Starting up");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureServices((hostContext, services) =>
{
var configuration = hostContext.Configuration;
services.Configure<IotConfig>(configuration.GetSection("IOTConfig"));

JsonSerializerSettings setting = new JsonSerializerSettings();
JsonConvert.DefaultSettings = () =>
{
setting.DateFormatString = "yyyy-MM-dd HH:mm:ss";
setting.ContractResolver = new CamelCasePropertyNamesContractResolver();
setting.NullValueHandling = new NullValueHandling();

return setting;
};
});
}
}


+ 13
- 0
AmqpTest/Properties/launchSettings.json View File

@@ -0,0 +1,13 @@
{
"profiles": {
"AmqpTest": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker"
}
}
}

+ 51
- 0
AmqpTest/Worker.cs View File

@@ -0,0 +1,51 @@
using AmqpTest.Amqp;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AmqpTest
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly AmqpSubscribe _subscribe;

public Worker(ILogger<Worker> logger, AmqpSubscribe subscribe)
{
_logger = logger;
_subscribe = subscribe;

}

public override Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("------StartAsync");

return base.StartAsync(cancellationToken);
}

public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("------StopAsync");

return base.StopAsync(cancellationToken);
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("------ExecuteAsync");
await _subscribe.BeginListen();
_logger.LogWarning("与阿里云AMQP的连接结束,即将休眠30秒,然后重新连接...");
await Task.Delay(30000, stoppingToken);
}
}
}
}

+ 17
- 0
AmqpTest/appsettings.Development.json View File

@@ -0,0 +1,17 @@
{
"AllowedHosts": "*",
"IOTConfig": {
"AccessKey": "LTAI4FdXhwy1evoHXingMaiZ",
"AccessSecret": "CGmGpzta6ro8Bta4RLiQD18EF8m6Bm",
"iotInstanceId": "", //iot-cn-nif1vosz501
"ConsumerGroupId": "SKb6q4caxeXfRGQFiLO6000100", // "hJBxHrxY51jZVhu6cCsQ000100", //"8GwMMBJUWz5PWaW0jtb6000100",
"RegionId": "cn-shanghai",
"ProductKey": "a18mXM6Cvx8",
"UId": "1111649216405698",
"clientId": "gateway",
"Port": "5671",
"ReconnectionTime": "10000",
"MaxDegreeOfParallelism": "8",
"SmsSignName": "随手精灵"
}
}

+ 17
- 0
AmqpTest/appsettings.debug.json View File

@@ -0,0 +1,17 @@
{
"AllowedHosts": "*",
"IOTConfig": {
"AccessKey": "LTAI4FdXhwy1evoHXingMaiZ",
"AccessSecret": "CGmGpzta6ro8Bta4RLiQD18EF8m6Bm",
"iotInstanceId": "iot-cn-nif1vosz501",
"ConsumerGroupId": "0ZQFQv0QreC7WALTEWad000100", //"bbQdnXQJIx2eCDjVbCIZ000100", // "hJBxHrxY51jZVhu6cCsQ000100", //"8GwMMBJUWz5PWaW0jtb6000100",
"RegionId": "cn-shanghai",
"ProductKey": "a18mXM6Cvx8",
"UId": "1111649216405698",
"clientId": "gateway",
"Port": "5671",
"ReconnectionTime": "10000",
"MaxDegreeOfParallelism": "8",
"SmsSignName": "随手精灵"
}
}

+ 68
- 0
AmqpTest/appsettings.json View File

@@ -0,0 +1,68 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Serilog": {
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Async", "Serilog.Sinks.Console" ],
"MinimumLevel": {
"Default": "Verbose",
"Override": {
"Microsoft": "Warning",
"HttpClient": "Information"
}
},
"WriteTo:Information": {
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "File",
"Args": {
"RestrictedToMinimumLevel": "Information",
"RollingInterval": "Day",
"RollOnFileSizeLimit": "true",
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"Path": "/var/amqp/logs/infos/info.log",
"RetainedFileCountLimit": 7 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null
// "FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G
// "RollOnFileSizeLimit": true //超过文件大小后创建新的

}
}
]
}
},
"WriteTo:Error": {
"Name": "Async",
"Args": {
"Configure": [
{
"Name": "File",
"Args": {
"RestrictedToMinimumLevel": "Error",
"RollingInterval": "Day",
"RollOnFileSizeLimit": "true",
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}][{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"Path": "/var/amqp/logs/errors/error.log",
"RetainedFileCountLimit": 15 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null
// "FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G
// "RollOnFileSizeLimit": true //超过文件大小后创建新的
}
}
]
}
},
"WriteTo:Console": {
"Name": "Console",
"Args": {
"restrictedToMinimumLevel": "Verbose",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}",
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
}
}
}
}

+ 0
- 0
README.md View File


+ 29
- 0
amqp_test_run.sh View File

@@ -0,0 +1,29 @@
#!/bin/bash
environment=$1
version=$2
echo "环境变量为${environment},版本为$version!"
if [[ ${environment} == 'production' ]]; then
echo "开始远程构建容器"
docker stop gps_gateway || true
docker rm gps_gateway || true
docker rmi -f $(docker images | grep registry.cn-shanghai.aliyuncs.com/gps_card/gps_gateway | awk '{print $3}')
#docker login --username=telpo_linwl@1111649216405698 --password=telpo#1234 registry.cn-shanghai.aliyuncs.com
docker login --username=telpo_fengjj@1111649216405698 --password=PWDaliyun123 registry.cn-shanghai.aliyuncs.com
docker pull registry.cn-shanghai.aliyuncs.com/gps_card/gps_gateway:$version
docker run -d -e environment=production -v /home/data/gps_gateway/log:/var/gateway/logs --restart=always --name gps_gateway registry.cn-shanghai.aliyuncs.com/gps_card/gps_gateway:$version;
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
docker ps -a

elif [[ ${environment} == 'test' ]]; then
echo "开始在测试环境远程构建容器"
docker stop gps_gateway || true
docker rm gps_gateway || true
docker rmi -f $(docker images | grep 139.224.254.18:5000/gps_gateway | awk '{print $3}')
docker pull 139.224.254.18:5000/gps_gateway:$version
docker run -d -e environment=test -v /home/data/gps_gateway/log:/var/gateway/logs --restart=always --name gps_gateway 139.224.254.18:5000/gps_gateway:$version;
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
docker ps -a

fi

+ 17
- 0
setup_development.sh View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
image_version=`date +%Y%m%d%H%M`;

docker stop amqp_test || true;
docker rm amqp_test || true;
# 删除镜像
docker rmi -f $(docker images | grep telpo/amqp_test | awk '{print $3}')

docker build -f ./AmqpTest/Dockerfile . -t telpo/amqp_test:$image_version;
# 启动容器
docker run -d -e environment=Development -v /home/data/amqp_test/log:/var/amqp/logs --name amqp_test telpo/amqp_test:$image_version;
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
# 查看镜像列表
docker images;
# 查看日志
docker logs amqp_test;

+ 17
- 0
setup_test.sh View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
image_version=$version
# 删除镜像
docker rmi -f $(
docker images | grep 139.224.254.18:5000/amqp_test | awk '{print $3}'
)
# 构建telpo/mrp:$image_version镜像
docker build -f ./AmqpTest/Dockerfile . -t telpo/amqp_test:$image_version
#TODO:推送镜像到私有仓库
echo '=================开始推送镜像======================='
docker tag telpo/amqp_test:$image_version 139.224.254.18:5000/amqp_test:$image_version
docker push 139.224.254.18:5000/amqp_test:$image_version
echo '=================推送镜像完成======================='
#删除产生的None镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')
# 查看镜像列表
docker images

Loading…
Cancel
Save