@@ -0,0 +1,30 @@ | |||||
**/.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 | |||||
!**/.gitignore | |||||
!.git/HEAD | |||||
!.git/config | |||||
!.git/packed-refs | |||||
!.git/refs/heads/** |
@@ -0,0 +1,9 @@ | |||||
*.user | |||||
*.suo | |||||
.vs/ | |||||
*.log | |||||
obj/ | |||||
bin/ | |||||
.idea/ | |||||
.vscode/ | |||||
publish/ |
@@ -0,0 +1,7 @@ | |||||
namespace NearCardAttendance.Common | |||||
{ | |||||
public class Class1 | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,26 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Common | |||||
{ | |||||
public class Consts | |||||
{ | |||||
/// <summary> | |||||
/// HttpClient常用配置分组名称 | |||||
/// </summary> | |||||
public const string DEFAULT_HTTPCLIENT_NAME = "DEFAULT_HTTP"; | |||||
/// <summary> | |||||
/// 纪元时间(UTC时间戳起始计算时间) | |||||
/// </summary> | |||||
public static DateTime EraUtcTime = DateTime.Parse("1970/01/01"); | |||||
/// <summary> | |||||
/// 有效定位的半径阈值(大于该值则为无效定位) | |||||
/// </summary> | |||||
public static int RadiusThreshold = 150; | |||||
} | |||||
} |
@@ -0,0 +1,14 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" /> | |||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,128 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Net.Http.Headers; | |||||
using System.Net.Security; | |||||
using System.Net; | |||||
using System.Security.Cryptography.X509Certificates; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using Newtonsoft.Json; | |||||
using Microsoft.Extensions.Logging; | |||||
namespace NearCardAttendance.Common.helper | |||||
{ | |||||
public class HttpHelper | |||||
{ | |||||
private readonly IHttpClientFactory _httpClientFactory; | |||||
private readonly ILogger<HttpHelper> _logger; | |||||
public HttpHelper(IHttpClientFactory httpClientFactory, ILogger<HttpHelper> logger) | |||||
{ | |||||
_httpClientFactory = httpClientFactory; | |||||
_logger = logger; | |||||
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult!); | |||||
} | |||||
public static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) | |||||
{ | |||||
return true; | |||||
} | |||||
public async Task<string?> HttpToGetAsync(string url) | |||||
{ | |||||
return await HttpToGetAsync(url, new List<KeyValuePair<string, string>>()); | |||||
} | |||||
public async Task<string?> HttpToGetAsync(string url, List<KeyValuePair<string, string>> headers) | |||||
{ | |||||
var client = _httpClientFactory.CreateClient(Consts.DEFAULT_HTTPCLIENT_NAME); | |||||
if (headers != null && headers.Count > 0) //指定请求头 | |||||
{ | |||||
headers.ForEach(e => | |||||
{ | |||||
client.DefaultRequestHeaders.Add(e.Key, e.Value); | |||||
}); | |||||
} | |||||
try | |||||
{ | |||||
using (var response = await client.GetAsync(url).ConfigureAwait(false)) | |||||
{ | |||||
if (!response.IsSuccessStatusCode) return null; | |||||
else | |||||
{ | |||||
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"HttpToGetAsync发生异常, Msg:{ex.Message}\n{ex.StackTrace}"); | |||||
return null; | |||||
} | |||||
} | |||||
public async Task<string?> HttpToPostAsync(string url, object data) | |||||
{ | |||||
return await HttpToPostAsync(url, data, new List<KeyValuePair<string, string>>()); | |||||
} | |||||
public async Task<string?> HttpToPostAsync(string url, object data, List<KeyValuePair<string, string>> headers) | |||||
{ | |||||
if (data == null) return null; | |||||
var client = _httpClientFactory.CreateClient(Consts.DEFAULT_HTTPCLIENT_NAME); | |||||
if (headers != null && headers.Count > 0) //指定请求头 | |||||
{ | |||||
headers.ForEach(e => | |||||
{ | |||||
client.DefaultRequestHeaders.Add(e.Key, e.Value); | |||||
}); | |||||
} | |||||
string? body; | |||||
if (data is string) body = data as string; | |||||
else body = JsonConvert.SerializeObject(data); | |||||
try | |||||
{ | |||||
using (var content = new StringContent(body!)) | |||||
{ | |||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); | |||||
using (var response = await client.PostAsync(url, content).ConfigureAwait(false)) | |||||
{ | |||||
if (!response.IsSuccessStatusCode) return null; | |||||
else | |||||
{ | |||||
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogError($"HttpToPostAsync发生异常, Msg:{ex.Message}\n{ex.StackTrace}"); | |||||
return null; | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Authorization 的 Base64 加密 | |||||
/// </summary> | |||||
/// <param name="username"></param> | |||||
/// <param name="password"></param> | |||||
/// <returns></returns> | |||||
public string GetEncodedCredentials(string username, string password) | |||||
{ | |||||
string mergedCredentials = string.Format("{0}:{1}", username, password); | |||||
byte[] byteCredentials = UTF8Encoding.UTF8.GetBytes(mergedCredentials); | |||||
return Convert.ToBase64String(byteCredentials); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,7 @@ | |||||
namespace NearCardAttendance.Model | |||||
{ | |||||
public class Class1 | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,9 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
</Project> |
@@ -0,0 +1,7 @@ | |||||
namespace NearCardAttendance.Service | |||||
{ | |||||
public class Class1 | |||||
{ | |||||
} | |||||
} |
@@ -0,0 +1,19 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | |||||
<PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | |||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\NearCardAttendance.Common\NearCardAttendance.Common.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,91 @@ | |||||
using DotNetty.Buffers; | |||||
using DotNetty.Transport.Channels; | |||||
using Microsoft.Extensions.Logging; | |||||
using NearCardAttendance.Service.TcpServer.Protocol; | |||||
using System; | |||||
using System.Text; | |||||
namespace NearCardAttendance.Service.TcpServer.Handler | |||||
{ | |||||
public class ProtocolHandler : SimpleChannelInboundHandler<IByteBuffer> | |||||
{ | |||||
private IByteBuffer? buffer; | |||||
private readonly ILogger<ProtocolHandler> _logger; | |||||
public ProtocolHandler(ILogger<ProtocolHandler> logger) | |||||
{ | |||||
_logger = logger; | |||||
} | |||||
public override async void ChannelActive(IChannelHandlerContext context) | |||||
{ | |||||
// Handle channel active event | |||||
} | |||||
public override void ChannelInactive(IChannelHandlerContext context) | |||||
{ | |||||
ReleaseBuffer(); | |||||
base.ChannelInactive(context); | |||||
} | |||||
protected override void ChannelRead0(IChannelHandlerContext context, IByteBuffer message) | |||||
{ | |||||
try | |||||
{ | |||||
string content = message.ToString(Encoding.ASCII); | |||||
_logger.LogInformation($"{nameof(ProtocolHandler)} -- {nameof(ChannelRead0)} -- 最开始接受内容:{content}"); | |||||
ProcessMessage(context, content); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
ReleaseBuffer(); | |||||
_logger.LogInformation($"{nameof(ProtocolHandler)} --- {nameof(ChannelRead0)} 处理出错\n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
} | |||||
private void ProcessMessage(IChannelHandlerContext context, string content) | |||||
{ | |||||
buffer ??= Unpooled.Buffer(); | |||||
byte[] bytes = Encoding.ASCII.GetBytes(content); | |||||
buffer.WriteBytes(Unpooled.WrappedBuffer(bytes)); | |||||
if (int.TryParse(buffer.ToString(Encoding.ASCII).Substring(0, 4), out int messageLength)) | |||||
{ | |||||
Console.WriteLine(buffer.ToString(Encoding.ASCII)); | |||||
if (buffer.ToString(Encoding.ASCII).Length == messageLength) | |||||
{ | |||||
//var parser = buffer.ToString(Encoding.ASCII); | |||||
ProtocolParser parser = new(buffer.ToString(Encoding.ASCII)); | |||||
context.FireChannelRead(parser); | |||||
Console.WriteLine($"发送正常:{parser}"); | |||||
//ProtocolWrapper wrapper = new ProtocolWrapper("82", parser.SeqNo, DateTime.Now.ToString("yyyyMMddHHmmss")); | |||||
//Console.WriteLine(wrapper.GenerateProtocolString()); | |||||
Console.WriteLine($"length:{parser.MessageLength},func_no:{parser.FuncNo},seq_no:{parser.SeqNo},data:{ parser.Data}"); | |||||
ReleaseBuffer(); | |||||
} | |||||
else if (buffer.ToString(Encoding.ASCII).Length > messageLength) | |||||
{ | |||||
// var parser = buffer.ToString(Encoding.ASCII).Substring(0, messageLength); | |||||
ProtocolParser parser = new(buffer.ToString(Encoding.ASCII).Substring(0, messageLength)); | |||||
context.FireChannelRead(parser); | |||||
//ReleaseBuffer(); | |||||
var overLongbuffer = Unpooled.Buffer(); | |||||
Console.WriteLine($"过长消息:{buffer.ToString(Encoding.ASCII).Substring(messageLength)}"); | |||||
overLongbuffer.WriteBytes(Unpooled.WrappedBuffer(Encoding.ASCII.GetBytes(buffer.ToString(Encoding.ASCII).Substring(messageLength)))); | |||||
ReleaseBuffer(); | |||||
buffer = overLongbuffer; | |||||
Console.WriteLine($"剩余消息{buffer.ToString(Encoding.ASCII)}"); | |||||
} | |||||
} | |||||
} | |||||
private void ReleaseBuffer() | |||||
{ | |||||
buffer?.Release(); | |||||
buffer = null; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,142 @@ | |||||
using DotNetty.Buffers; | |||||
using DotNetty.Transport.Channels; | |||||
using Microsoft.Extensions.Logging; | |||||
using NearCardAttendance.Common.helper; | |||||
using NearCardAttendance.Service.TcpServer.Mapper; | |||||
using NearCardAttendance.Service.TcpServer.Protocol; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.TcpServer.Handler | |||||
{ | |||||
public class RegisterHandler : ChannelHandlerAdapter | |||||
{ | |||||
private readonly ILogger<RegisterHandler> _logger; | |||||
private readonly HttpHelper _httpHelper = default!; | |||||
private readonly IDisposable _loggerScope = default!; | |||||
private readonly TcpClientsManager _managerTcpClients; | |||||
private readonly ScheduleResendManager _managerScheduleResend; | |||||
public RegisterHandler(ILogger<RegisterHandler> logger,HttpHelper httpHelper) | |||||
{ | |||||
_logger = logger; | |||||
_httpHelper = httpHelper; | |||||
} | |||||
public override void ChannelActive(IChannelHandlerContext context) | |||||
{ | |||||
base.ChannelActive(context); // Pass the event to the next handler | |||||
} | |||||
public override async void ChannelInactive(IChannelHandlerContext context) | |||||
{ | |||||
try | |||||
{ | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogInformation($"{nameof(RegisterHandler)} --- {nameof(ChannelInactive)} 出错\n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
} | |||||
public override void HandlerRemoved(IChannelHandlerContext context) | |||||
{ | |||||
base.HandlerRemoved(context); | |||||
} | |||||
public override async void ChannelRead(IChannelHandlerContext context, object message) | |||||
{ | |||||
if (message is ProtocolParser parser) | |||||
{ | |||||
try | |||||
{ | |||||
using (_logger.BeginScope(new Dictionary<string, object> { ["RequestId"] = parser.SeqNo })) | |||||
{ | |||||
//认证 | |||||
if (parser.FuncNo.Equals("10")) | |||||
{ | |||||
#region 认证业务 | |||||
// PHONE_AUTHEN | |||||
ProtocolWrapper phoneAuthWrapper = new(parser.FuncNo, parser.SeqNo, "1"); | |||||
await SendToTcpClientAsync(phoneAuthWrapper, context.Channel); | |||||
// ABT_STATUS 延时3秒 给终端同步时间 | |||||
await context.Channel.EventLoop.Schedule(async () => | |||||
{ | |||||
ProtocolWrapper abtStatusWrapper = new("82", parser.SeqNo, DateTime.Now.ToString("yyyyMMddHHmmss")); | |||||
await SendToTcpClientAsync(abtStatusWrapper, context.Channel); | |||||
},TimeSpan.FromSeconds(3)); | |||||
#endregion | |||||
} | |||||
else if (parser.FuncNo.Equals("82")) | |||||
{ | |||||
// 认证成功 | |||||
_logger.LogInformation($"认证成功{parser.Data}."); | |||||
} | |||||
// 心跳 | |||||
else if (parser.FuncNo.Equals("05")) | |||||
{ | |||||
ProtocolWrapper stdtSignRecsWrapper = new(parser.FuncNo, parser.SeqNo,""); | |||||
await SendToTcpClientAsync(stdtSignRecsWrapper, context.Channel); | |||||
} // STDT_SIGN_RECS | |||||
else if (parser.FuncNo.Equals("04")) | |||||
{ | |||||
// 回应设备 | |||||
ProtocolWrapper stdtSignRecsWrapper = new(parser.FuncNo, parser.SeqNo,"1"); | |||||
await SendToTcpClientAsync(stdtSignRecsWrapper, context.Channel); | |||||
// 刷卡考勤信息,需要推送给第三方平台 | |||||
//var url = ""; | |||||
//await _httpHelper.HttpToPostAsync(url, new object()); | |||||
} | |||||
//switch (parser.FuncNo) | |||||
//{ | |||||
// case "10": | |||||
// break; | |||||
// case "82": | |||||
// break; | |||||
// default: | |||||
// break; | |||||
//} | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
_logger.LogInformation($"{nameof(RegisterHandler)} --- {nameof(ChannelRead)} 处理消息 {parser.SeqNo} 出错\n{ex.Message}\n{ex.StackTrace}"); | |||||
} | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// 发送到TCP客户端 | |||||
/// </summary> | |||||
/// <param name="wrapper"></param> | |||||
/// <param name="channel"></param> | |||||
private async Task SendToTcpClientAsync(ProtocolWrapper wrapper, IChannel channel) | |||||
{ | |||||
string protocolMsg = wrapper.GenerateProtocolString(); | |||||
// 发送protocolMsg到tcp客户端 | |||||
await channel | |||||
.WriteAndFlushAsync(Unpooled.WrappedBuffer(Encoding.UTF8.GetBytes(protocolMsg))) | |||||
.ContinueWith(Action => { | |||||
_logger.LogInformation($"{nameof(RegisterHandler)} -- {nameof(SendToTcpClientAsync)} -- 下发设备内容:\n{protocolMsg}"); | |||||
}); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,21 @@ | |||||
using DotNetty.Common.Concurrency; | |||||
using Microsoft.Extensions.Logging; | |||||
using System; | |||||
using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.TcpServer.Mapper | |||||
{ | |||||
public class ScheduleResendManager : ConcurrentDictionary<string, IScheduledTask> | |||||
{ | |||||
private readonly ILogger<ScheduleResendManager> _logger; | |||||
public ScheduleResendManager(ILogger<ScheduleResendManager> logger) | |||||
{ | |||||
_logger = logger; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
using DotNetty.Transport.Channels; | |||||
using Microsoft.Extensions.Logging; | |||||
using System; | |||||
using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.TcpServer.Mapper | |||||
{ | |||||
public class TcpClientsManager : ConcurrentDictionary<string, IChannel> | |||||
{ | |||||
private readonly ILogger<TcpClientsManager> _logger; | |||||
public TcpClientsManager(ILogger<TcpClientsManager> logger) | |||||
{ | |||||
_logger = logger; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,47 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.TcpServer.Protocol | |||||
{ | |||||
public class ProtocolParser | |||||
{ | |||||
public int MessageLength { get; private set; } = default!; | |||||
public string FuncNo { get; private set; } = default!; | |||||
public string SeqNo { get; private set; } = default!; | |||||
public string Data { get; private set; } = default!; | |||||
//private string _protocolString { get; set; } = default!; | |||||
public ProtocolParser(string protocolString) | |||||
{ | |||||
ParseProtocolString(protocolString); | |||||
//_protocolString = protocolString; | |||||
} | |||||
private void ParseProtocolString(string protocolString) | |||||
{ | |||||
try | |||||
{ | |||||
_ = int.TryParse(protocolString.AsSpan(0, 4), out int messageLength); | |||||
MessageLength = messageLength; | |||||
FuncNo = protocolString.Substring(4,2); | |||||
SeqNo = protocolString.Substring(6,4); | |||||
Data = protocolString.Substring(10).TrimEnd(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Console.WriteLine($"Error: {e.Message}"); | |||||
// throw new ArgumentException("Invalid protocol string format."); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,39 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.Service.TcpServer.Protocol | |||||
{ | |||||
public class ProtocolWrapper | |||||
{ | |||||
private string Length { get; } | |||||
private string FuncNo { get; } | |||||
private string SeqNo { get; } | |||||
private string Data { get; } | |||||
public ProtocolWrapper(string funcNo,string seqNo,string data) | |||||
{ | |||||
FuncNo =funcNo; | |||||
SeqNo=seqNo; | |||||
Data=data; | |||||
Length = CalculateMessageLength(); | |||||
} | |||||
private string CalculateMessageLength() | |||||
{ | |||||
return string.Format("{0:D4}", GenerateProtocolString().Length + 4); | |||||
} | |||||
public string GenerateProtocolString() | |||||
{ | |||||
return $"{Length}{FuncNo}{SeqNo}{Data}"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,31 @@ | |||||
using Serilog.Configuration; | |||||
using Serilog.Core; | |||||
using Serilog.Events; | |||||
using Serilog; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.TcpServer.Config | |||||
{ | |||||
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>(); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,33 @@ | |||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. | |||||
#FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base | |||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base | |||||
WORKDIR /app | |||||
EXPOSE 16662 | |||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build | |||||
ARG BUILD_CONFIGURATION=Release | |||||
WORKDIR /src | |||||
COPY ["NearCardAttendance.TcpServer/NearCardAttendance.TcpServer.csproj", "NearCardAttendance.TcpServer/"] | |||||
COPY ["NearCardAttendance.Common/NearCardAttendance.Common.csproj", "NearCardAttendance.Common/"] | |||||
COPY ["NearCardAttendance.Service/NearCardAttendance.Service.csproj", "NearCardAttendance.Service/"] | |||||
RUN dotnet restore "./NearCardAttendance.TcpServer/./NearCardAttendance.TcpServer.csproj" | |||||
COPY . . | |||||
WORKDIR "/src/NearCardAttendance.TcpServer" | |||||
RUN dotnet build "./NearCardAttendance.TcpServer.csproj" -c $BUILD_CONFIGURATION -o /app/build | |||||
FROM build AS publish | |||||
ARG BUILD_CONFIGURATION=Release | |||||
RUN dotnet publish "./NearCardAttendance.TcpServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false | |||||
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", "NearCardAttendance.TcpServer.dll"] | |||||
ENTRYPOINT ["sh", "-c", "dotnet NearCardAttendance.TcpServer.dll --environment=$environment"] |
@@ -0,0 +1,44 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<OutputType>Exe</OutputType> | |||||
<TargetFramework>net6.0</TargetFramework> | |||||
<ImplicitUsings>enable</ImplicitUsings> | |||||
<Nullable>enable</Nullable> | |||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<None Remove="appsettings.debug.json" /> | |||||
<None Remove="appsettings.json" /> | |||||
<None Remove="appsettings.test.json" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Include="appsettings.test.json"> | |||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
</Content> | |||||
<Content Include="appsettings.debug.json"> | |||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
</Content> | |||||
<Content Include="appsettings.json"> | |||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | |||||
</Content> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="DotNetty.Handlers" Version="0.7.5" /> | |||||
<PackageReference Include="DotNetty.Transport" Version="0.7.5" /> | |||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" /> | |||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" /> | |||||
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> | |||||
<PackageReference Include="Serilog.Expressions" Version="3.4.0" /> | |||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\NearCardAttendance.Common\NearCardAttendance.Common.csproj" /> | |||||
<ProjectReference Include="..\NearCardAttendance.Service\NearCardAttendance.Service.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,112 @@ | |||||
using DotNetty.Codecs; | |||||
using DotNetty.Transport.Bootstrapping; | |||||
using DotNetty.Transport.Channels; | |||||
using DotNetty.Transport.Channels.Sockets; | |||||
using Microsoft.Extensions.Configuration; | |||||
using Microsoft.Extensions.DependencyInjection; | |||||
using Microsoft.Extensions.Hosting; | |||||
using NearCardAttendance.Common; | |||||
using NearCardAttendance.Common.helper; | |||||
using NearCardAttendance.Service.TcpServer.Handler; | |||||
using NearCardAttendance.Service.TcpServer.Mapper; | |||||
using NearCardAttendance.TcpServer.Config; | |||||
using Serilog; | |||||
using System.Text; | |||||
namespace NearCardAttendance.TcpServer | |||||
{ | |||||
internal class Program | |||||
{ | |||||
static void Main(string[] args) | |||||
{ | |||||
var config = new ConfigurationBuilder() | |||||
.SetBasePath(Directory.GetCurrentDirectory()) | |||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) | |||||
.Build(); | |||||
Log.Logger = new LoggerConfiguration() | |||||
.ReadFrom.Configuration(config).Enrich.WithThreadInfo() | |||||
.Filter.ByExcluding(logEvent => logEvent.Level == Serilog.Events.LogEventLevel.Verbose) // 过滤掉VRB级别的日志 | |||||
.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; | |||||
#region 配置信息 | |||||
//services | |||||
// .Configure<ServiceConfig>(configuration.GetSection("ServiceConfig")) | |||||
// ; | |||||
#endregion | |||||
#region Http请求 | |||||
services | |||||
.AddSingleton<HttpHelper>() | |||||
.AddHttpClient(Consts.DEFAULT_HTTPCLIENT_NAME, c => | |||||
{ | |||||
c.Timeout = TimeSpan.FromSeconds(10); //超时限制 | |||||
c.DefaultRequestHeaders.Add("Accept", "application/json"); | |||||
}) | |||||
; | |||||
#endregion | |||||
#region TcpService | |||||
services | |||||
.AddSingleton<ServerBootstrap>(provider => | |||||
{ | |||||
var bossGroup = new MultithreadEventLoopGroup(); | |||||
var workerGroup = new MultithreadEventLoopGroup(); | |||||
var bootstrap = new ServerBootstrap(); | |||||
bootstrap.Group(bossGroup, workerGroup) | |||||
.Channel<TcpServerSocketChannel>() | |||||
.Option(ChannelOption.SoBacklog, 100) | |||||
.ChildOption(ChannelOption.TcpNodelay, true) // 低延迟 | |||||
.ChildHandler(new ActionChannelInitializer<IChannel>(channel => | |||||
{ | |||||
// var handler = provider.GetRequiredService<ProtocolHandler>(); | |||||
// var handler = provider.GetRequiredService<SomeServerHandler>(); | |||||
var pipeline = channel.Pipeline; | |||||
pipeline.AddLast(new StringEncoder(Encoding.ASCII)); | |||||
pipeline.AddLast( | |||||
provider.GetRequiredService<ProtocolHandler>(), | |||||
provider.GetRequiredService<RegisterHandler>() | |||||
//provider.GetRequiredService<HeartBeatHandler>(), | |||||
//provider.GetRequiredService<PassThroughHandler>() | |||||
); // Add the injected handler | |||||
})); | |||||
return bootstrap; | |||||
}) | |||||
.AddSingleton<TcpClientsManager>() | |||||
.AddSingleton<ScheduleResendManager>() | |||||
.AddTransient<ProtocolHandler>() | |||||
.AddTransient<RegisterHandler>() | |||||
//.AddTransient<HeartBeatHandler>() | |||||
//.AddTransient<PassThroughHandler>() | |||||
.AddHostedService<Server>() | |||||
; | |||||
#endregion | |||||
}); | |||||
} | |||||
} |
@@ -0,0 +1,10 @@ | |||||
{ | |||||
"profiles": { | |||||
"NearCardAttendance.TcpServer": { | |||||
"commandName": "Project", | |||||
"environmentVariables": { | |||||
"DOTNET_ENVIRONMENT": "debug" | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,77 @@ | |||||
using DotNetty.Transport.Bootstrapping; | |||||
using DotNetty.Transport.Channels; | |||||
using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace NearCardAttendance.TcpServer | |||||
{ | |||||
public class Server : BackgroundService | |||||
{ | |||||
private readonly ILogger<Server> _logger; | |||||
private readonly IServiceProvider _serviceProvider; | |||||
private readonly ServerBootstrap _serverBootstrap; | |||||
private IChannel _serverChannel = default!; | |||||
private CancellationTokenSource _tokenSource = null!; | |||||
public Server( | |||||
ILogger<Server> logger, | |||||
ServerBootstrap serverBootstrap, | |||||
IServiceProvider serviceProvider) | |||||
{ | |||||
_logger = logger; | |||||
_serviceProvider = serviceProvider; | |||||
_serverBootstrap = serverBootstrap; | |||||
} | |||||
public override Task StartAsync(CancellationToken cancellationToken) | |||||
{ | |||||
_logger.LogInformation("------StartAsync"); | |||||
_tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); | |||||
return base.StartAsync(cancellationToken); | |||||
} | |||||
public override Task StopAsync(CancellationToken cancellationToken) | |||||
{ | |||||
_logger.LogInformation("------StopAsync"); | |||||
_tokenSource.Cancel(); //停止工作线程 | |||||
return base.StopAsync(cancellationToken); | |||||
} | |||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | |||||
{ | |||||
_logger.LogInformation("DotNetty server starting..."); | |||||
//var address = new IPEndPoint(IPAddress.Any, 12345); | |||||
//IChannel _serverChannel = await _serverBootstrap.BindAsync(address); | |||||
string ipAddress = "0.0.0.0"; | |||||
int port = 16662; | |||||
var endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port); | |||||
IChannel _serverChannel = await _serverBootstrap.BindAsync(endPoint); | |||||
// _serverChannel.GetAttribute(MessageIdAttribute.Key).Set("SomeRequestId"); | |||||
//_logger.LogInformation("DotNetty server started on {0}.", address); | |||||
_logger.LogInformation("DotNetty server started on {0}.", endPoint); | |||||
// Wait until the service is stopped | |||||
stoppingToken.WaitHandle.WaitOne(); | |||||
_logger.LogInformation("DotNetty server stopping..."); | |||||
// Close the server channel and release resources | |||||
await _serverChannel.CloseAsync(); | |||||
_logger.LogInformation("DotNetty server stopped."); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,4 @@ | |||||
{ | |||||
"AllowedHosts": "*" | |||||
} |
@@ -0,0 +1,121 @@ | |||||
{ | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft": "Warning", | |||||
"Microsoft.Hosting.Lifetime": "Information", | |||||
"HttpClient": "Information" | |||||
} | |||||
}, | |||||
"Serilog": { | |||||
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Async", "Serilog.Sinks.Console", "Serilog.Expressions" ], | |||||
"MinimumLevel": { | |||||
"Default": "Verbose", | |||||
"Override": { | |||||
"Microsoft": "Warning", | |||||
"Microsoft.Hosting.Lifetime": "Information", | |||||
"HttpClient": "Information" | |||||
} | |||||
}, | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "Console", | |||||
"Args": { | |||||
"restrictedToMinimumLevel": "Verbose", | |||||
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console" | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Information", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"Path": "/var/near_card_attendance/logs/infos/info.log", | |||||
"RetainedFileCountLimit": 10 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null | |||||
//"FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G | |||||
//"RollOnFileSizeLimit": true //超过文件大小后创建新的 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Information'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Warning", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}] [{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}", | |||||
"Path": "/var/near_card_attendance/logs/warnings/warn.log", | |||||
"RetainedFileCountLimit": 10 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Warning'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
}, | |||||
{ | |||||
"Name": "Logger", | |||||
"Args": { | |||||
"ConfigureLogger": { | |||||
"WriteTo": [ | |||||
{ | |||||
"Name": "File", | |||||
"Args": { | |||||
"RestrictedToMinimumLevel": "Error", | |||||
"RollingInterval": "Day", | |||||
"RollOnFileSizeLimit": "true", | |||||
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff }[{Level:u3}] [Thread-{ThreadId}][{SourceContext:l}] [{RequestId}] {Message:lj}{NewLine}{Exception}", | |||||
"Path": "/var/near_card_attendance/logs/errors/error.log", | |||||
"RetainedFileCountLimit": 15 // "--设置日志文件个数最大值,默认31,意思就是只保留最近的31个日志文件", "等于null时永远保留文件": null | |||||
//"FileSizeLimitBytes": 20971520, //设置单个文件大小为3M 默认1G | |||||
//"RollOnFileSizeLimit": true //超过文件大小后创建新的 | |||||
} | |||||
} | |||||
], | |||||
"Filter": [ | |||||
{ | |||||
"Name": "ByIncludingOnly", | |||||
"Args": { | |||||
"Expression": "@l = 'Error'" | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} | |||||
} | |||||
] | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"AllowedHosts": "*" | |||||
} |
@@ -0,0 +1,43 @@ | |||||
| |||||
Microsoft Visual Studio Solution File, Format Version 12.00 | |||||
# Visual Studio Version 17 | |||||
VisualStudioVersion = 17.8.34330.188 | |||||
MinimumVisualStudioVersion = 10.0.40219.1 | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NearCardAttendance.TcpServer", "NearCardAttendance.TcpServer\NearCardAttendance.TcpServer.csproj", "{46A2FEAF-7333-4D41-83F2-5ACDDDD850F5}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NearCardAttendance.Service", "NearCardAttendance.Service\NearCardAttendance.Service.csproj", "{2394B5EA-8A6A-4FC4-9218-2EED87E3FC14}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NearCardAttendance.Common", "NearCardAttendance.Common\NearCardAttendance.Common.csproj", "{CC1A7D30-569C-497A-8D94-C23BA11C870D}" | |||||
EndProject | |||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NearCardAttendance.Model", "NearCardAttendance.Model\NearCardAttendance.Model.csproj", "{10C81D50-6839-4F3C-92C9-C4E6554B8566}" | |||||
EndProject | |||||
Global | |||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||||
Debug|Any CPU = Debug|Any CPU | |||||
Release|Any CPU = Release|Any CPU | |||||
EndGlobalSection | |||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | |||||
{46A2FEAF-7333-4D41-83F2-5ACDDDD850F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{46A2FEAF-7333-4D41-83F2-5ACDDDD850F5}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{46A2FEAF-7333-4D41-83F2-5ACDDDD850F5}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{46A2FEAF-7333-4D41-83F2-5ACDDDD850F5}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{2394B5EA-8A6A-4FC4-9218-2EED87E3FC14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{2394B5EA-8A6A-4FC4-9218-2EED87E3FC14}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{2394B5EA-8A6A-4FC4-9218-2EED87E3FC14}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{2394B5EA-8A6A-4FC4-9218-2EED87E3FC14}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{CC1A7D30-569C-497A-8D94-C23BA11C870D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{CC1A7D30-569C-497A-8D94-C23BA11C870D}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{CC1A7D30-569C-497A-8D94-C23BA11C870D}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{CC1A7D30-569C-497A-8D94-C23BA11C870D}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
{10C81D50-6839-4F3C-92C9-C4E6554B8566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||||
{10C81D50-6839-4F3C-92C9-C4E6554B8566}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||||
{10C81D50-6839-4F3C-92C9-C4E6554B8566}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||||
{10C81D50-6839-4F3C-92C9-C4E6554B8566}.Release|Any CPU.Build.0 = Release|Any CPU | |||||
EndGlobalSection | |||||
GlobalSection(SolutionProperties) = preSolution | |||||
HideSolutionNode = FALSE | |||||
EndGlobalSection | |||||
GlobalSection(ExtensibilityGlobals) = postSolution | |||||
SolutionGuid = {2A15CF2F-2E7D-42D5-B8D7-9A22592049A0} | |||||
EndGlobalSection | |||||
EndGlobal |
@@ -0,0 +1,29 @@ | |||||
#!/bin/bash | |||||
environment=$1 | |||||
version=$2 | |||||
echo "环境变量为${environment},版本为$version!" | |||||
if [[ ${environment} == 'production' ]]; then | |||||
echo "开始远程构建容器" | |||||
docker stop near_card_attendance || true | |||||
docker rm near_card_attendance || true | |||||
docker rmi -f $(docker images | grep registry.cn-shanghai.aliyuncs.com/gps_card/near_card_attendance | awk '{print $3}') | |||||
#docker login --username=telpo_linwl@1111649216405698 --password=telpo#1234 registry.cn-shanghai.aliyuncs.com | |||||
docker login --username=rzl_wangjx@1111649216405698 --password=telpo.123 registry.cn-shanghai.aliyuncs.com | |||||
docker pull registry.cn-shanghai.aliyuncs.com/gps_card/near_card_attendance:$version | |||||
docker run --network=host -p 16662:16662 -d -e environment=production -v /home/data/near_card_attendance/log:/var/near_card_attendance/logs --restart=always --name near_card_attendance registry.cn-shanghai.aliyuncs.com/gps_card/near_card_attendance:$version; | |||||
#删除产生的None镜像 | |||||
docker rmi -f $(docker images | grep none | awk '{print $3}') | |||||
docker ps -a | |||||
elif [[ ${environment} == 'test' || ${environment} == 'presure' ]]; then | |||||
echo "开始在测试环境远程构建容器" | |||||
docker stop near_card_attendance || true | |||||
docker rm near_card_attendance || true | |||||
docker rmi -f $(docker images | grep 139.224.254.18:5000/near_card_attendance | awk '{print $3}') | |||||
docker pull 139.224.254.18:5000/near_card_attendance:$version | |||||
docker run --network=host -d -p 16662:16662 -e environment=${environment} -v /home/data/near_card_attendance/log:/var/near_card_attendance/logs --restart=always --name near_card_attendance 139.224.254.18:5000/near_card_attendance:$version; | |||||
#删除产生的None镜像 | |||||
docker rmi -f $(docker images | grep none | awk '{print $3}') | |||||
docker ps -a | |||||
fi |
@@ -0,0 +1,17 @@ | |||||
#!/usr/bin/env bash | |||||
image_version=$version | |||||
# 删除镜像 | |||||
docker rmi -f $( | |||||
docker images | grep 139.224.254.18:5000/near_card_attendance | awk '{print $3}' | |||||
) | |||||
# 构建telpo/mrp:$image_version镜像 | |||||
docker build -f ./NearCardAttendance.TcpServer/Dockerfile . -t telpo/near_card_attendance:$image_version | |||||
#TODO:推送镜像到私有仓库 | |||||
echo '=================开始推送镜像=======================' | |||||
docker tag telpo/near_card_attendance:$image_version 139.224.254.18:5000/near_card_attendance:$image_version | |||||
docker push 139.224.254.18:5000/near_card_attendance:$image_version | |||||
echo '=================推送镜像完成=======================' | |||||
#删除产生的None镜像 | |||||
docker rmi -f $(docker images | grep none | awk '{print $3}') | |||||
# 查看镜像列表 | |||||
docker images |