Dotnet创建Linux下的Service应用,


本文转载自微信公众号「老王Plus」,作者老王Plus的老王 。转载本文请联系老王Plus公众号。

前言

说到服务端应用,最常见的就是API服务。

除此之外,还有一类应用,比方一个Socket的服务器。这类型的应用,本身没有Web层,当然也不属于API服务。

通常大家会怎么做?

不讲究的做法,就是做一个Console应用,加载到后台一直跑着。

其实,还有另外一种做法,就是把应用加载到Services里,使应用以一个Service来做响应。这样可以依托操作系统的Services管理器来进行统一管理,自动运行和故障处理。

Dotnet做Window Service的内容,网上有很多。我今天写一个在Linux下做Service的方法。

创建Linux下的Service应用

创建一个LInux下的Service应用其实很简单,就分这么几步:

1. 用 Worker 模板创建工程

如果习惯用VS上创建,就找一下Worker Service模板。

我是习惯从命令行创建,就一条命令:

  1. % dotnet new worker -o projectname 

Dotnet会自动造成工程,并自动引用Microsoft.Extensions.Hosting包,因为这本身是一个Self-Hosting应用。

2. 加入Linux Service扩展包

其实这就是一个包:Microsoft.Extensions.Hosting.Systemd。这个包为应用提供了在Linux下使用Systemd守护进程的基础配置。

还是命令行:

  1. % dotnet add package Microsoft.Extensions.Hosting.Systemd 

3. 修改Program.cs

其实就是一行代码,把第二步引入的包加入应用。修改Program.cs

  1. public static IHostBuilder CreateHostBuilder(string[] args) => 
  2.     Host.CreateDefaultBuilder(args) 
  3.         .UseSystemd()  // 加入的就是这一行。 
  4.         .ConfigureServices((hostContext, services) => 
  5.         { 
  6.             services.AddHostedService<Worker>(); 
  7.         }); 

到这儿,套路性的工作已经完成。简单吧?

我们来看一下现在的工程:

  1. ├── Program.cs 
  2. ├── Properties 
  3. │   └── launchSettings.json 
  4. ├── Worker.cs 
  5. ├── appsettings.Development.json 
  6. ├── appsettings.json 
  7. └── workerdemo.csproj 

大家会注意到,里面多了一个Worker.cs的类文件。

看一下这个文件:

  1. public class Worker : BackgroundService 
  2.     private readonly ILogger<Worker> _logger; 
  3.  
  4.     public Worker(ILogger<Worker> logger) 
  5.     { 
  6.         _logger = logger; 
  7.     } 
  8.  
  9.     protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
  10.     { 
  11.         while (!stoppingToken.IsCancellationRequested) 
  12.         { 
  13.             _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 
  14.             await Task.Delay(1000, stoppingToken); 
  15.         } 
  16.     } 

这其实就是加载到Systemd里的服务的模板。我们需要的服务代码,需要加到ExecuteAsync(CancellationToken stoppingToken)方法中。

我简单做个例子,在里面加入UDP服务,看代码:

  1. public class Worker : BackgroundService 
  2.     private readonly ILogger<Worker> _logger; 
  3.     private readonly IConfiguration _configuration; 
  4.  
  5.     public Worker(ILogger<Worker> logger, IConfiguration configuration) 
  6.     { 
  7.         _logger = logger; 
  8.         _configuration = configuration; 
  9.     } 
  10.  
  11.     protected override async Task ExecuteAsync(CancellationToken stoppingToken) 
  12.     { 
  13.         _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); 
  14.  
  15.         UdpClient udpClient = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000)); 
  16.  
  17.         while (!stoppingToken.IsCancellationRequested) 
  18.         { 
  19.             UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); 
  20.  
  21.             string message = Encoding.UTF8.GetString(udpReceiveResult.Buffer); 
  22.             Console.WriteLine($"{udpReceiveResult.RemoteEndPoint.ToString()} - {message}"); 
  23.  
  24.             await udpClient.SendAsync(Encoding.Default.GetBytes("Got"), 3, udpReceiveResult.RemoteEndPoint); 
  25.         } 
  26.     } 

这个代码中,有两件事需要注意:

上面这个,是服务端的程序,是响应。

下面我简单做个客户端的请求,供测试用。就不解释了,只列出步骤:

创建一个工程

  1. % dotnet new console -o democlient 

修改Program.cs

  1. static async Task Main(string[] args) 
  2.     UdpClient udpClient = new UdpClient(); 
  3.  
  4.     for (int i = 0; i < 10000; i++) 
  5.     { 
  6.         byte[] buffer = new byte[8 * 1024]; 
  7.  
  8.         await Task.Run(() => 
  9.         { 
  10.             udpClient.SendAsync(buffer, buffer.Length, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000)); 
  11.         }); 
  12.  
  13.     } 
  14.     while (true) 
  15.     { 
  16.         UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); 
  17.  
  18.         string message = Encoding.UTF8.GetString(udpReceiveResult.Buffer); 
  19.         Console.WriteLine($"{udpReceiveResult.RemoteEndPoint.ToString()} - {message}"); 
  20.     } 
  21.     Console.ReadKey(); 

运行一下,看看效果。

到这里,Service应用开发的工作已经完成。

下面是部署。

部署Service应用

Linux下面部署一个Service应用,只有两个步骤:

1. 创建Service定义

Linux下的每个Service,都会有个定义文件。这个文件存在于/etc/systemd/system目录下。

下面我给出一个简单的Service模板:

  1. [Unit] 
  2. Description=DemoProject 
  3.  
  4. [Service] 
  5. Type=notify 
  6. ExecStart=dotnet /yourfolder/yourproject.dll 
  7.  
  8. [Install] 
  9. WantedBy=multi-user.target 

把这个内容保存为一个文件,例如叫demo.service。然后把这个文件复制到/etc/systemd/system下,并改为可执行。

简单说一下这个文件的一些项:

  • Description,是服务的名字。不重要,启动时,你用到的是文件名demo.service;
  • Type,服务类型,使用Dotnet加载时,只能是这种类型。如果把程序编译为自包含程序,这个类型可以是simple;
  • ExecStart,启动程序的命令,是全路径的,要确保能找得到这个程序。上面例子中,dotnet /yourfolder/yourproject.dll,是因为dotnet命令是有PATH变量支持的。

这个文件的配置项有很多,包括定义是否需要自动重启、重启间隔等。如果需要,可以去这里查询。

2. 启动Service

有两种方法。

第一种是刷新Service守护

  1. % systemctl daemon-reload 

刷新守护时,守护进程会去/etc/systemd/system目录下,寻找新加入的Service文件,并启动。

第二种是单独启动,有一系列命令:

启动

  1. % systemctl start demo.service 

停止

  1. % systemctl stop demo.service 

重启

  1. % systemctl restart demo.service 

查询状态

  1. % systemctl status demo.service 

嗯。这就是服务加载和停止了。

注意,这种方式加载的Service,是完全系统的服务,会没有任何输出。

如果需要调试,一种方式是加文件日志,另一种方式是用另一个命令启动:

  1. % journalctl -u dnsserver.service 

当然,这种方式只用于调试。正式运行时,还应该是上面的方式。

相关内容