本文转载自微信公众号「老王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模板。
我是习惯从命令行创建,就一条命令:
- % dotnet new worker -o projectname
Dotnet会自动造成工程,并自动引用Microsoft.Extensions.Hosting包,因为这本身是一个Self-Hosting应用。
2. 加入Linux Service扩展包
其实这就是一个包:Microsoft.Extensions.Hosting.Systemd。这个包为应用提供了在Linux下使用Systemd守护进程的基础配置。
还是命令行:
- % dotnet add package Microsoft.Extensions.Hosting.Systemd
3. 修改Program.cs
其实就是一行代码,把第二步引入的包加入应用。修改Program.cs
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSystemd() // 加入的就是这一行。
- .ConfigureServices((hostContext, services) =>
- {
- services.AddHostedService<Worker>();
- });
到这儿,套路性的工作已经完成。简单吧?
我们来看一下现在的工程:
- ├── Program.cs
- ├── Properties
- │ └── launchSettings.json
- ├── Worker.cs
- ├── appsettings.Development.json
- ├── appsettings.json
- └── workerdemo.csproj
大家会注意到,里面多了一个Worker.cs的类文件。
看一下这个文件:
- public class Worker : BackgroundService
- {
- private readonly ILogger<Worker> _logger;
- public Worker(ILogger<Worker> logger)
- {
- _logger = logger;
- }
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- while (!stoppingToken.IsCancellationRequested)
- {
- _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
- await Task.Delay(1000, stoppingToken);
- }
- }
- }
这其实就是加载到Systemd里的服务的模板。我们需要的服务代码,需要加到ExecuteAsync(CancellationToken stoppingToken)方法中。
我简单做个例子,在里面加入UDP服务,看代码:
- public class Worker : BackgroundService
- {
- private readonly ILogger<Worker> _logger;
- private readonly IConfiguration _configuration;
- public Worker(ILogger<Worker> logger, IConfiguration configuration)
- {
- _logger = logger;
- _configuration = configuration;
- }
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
- UdpClient udpClient = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000));
- while (!stoppingToken.IsCancellationRequested)
- {
- UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
- string message = Encoding.UTF8.GetString(udpReceiveResult.Buffer);
- Console.WriteLine($"{udpReceiveResult.RemoteEndPoint.ToString()} - {message}");
- await udpClient.SendAsync(Encoding.Default.GetBytes("Got"), 3, udpReceiveResult.RemoteEndPoint);
- }
- }
- }
这个代码中,有两件事需要注意:
- 在前边Program.cs中加入UseSystemd()时,已经注入了IConfiguration。因此,可以在这个方法中直接引入并使用。换句话说,就是可以直接读取例如appsetting.json的内容;
- 是上边提到的,真正的服务响应在ExecuteAsync(CancellationToken stoppingToken)中。这儿没什么特别的,就是正常的写法。
上面这个,是服务端的程序,是响应。
下面我简单做个客户端的请求,供测试用。就不解释了,只列出步骤:
创建一个工程
- % dotnet new console -o democlient
修改Program.cs
- static async Task Main(string[] args)
- {
- UdpClient udpClient = new UdpClient();
- for (int i = 0; i < 10000; i++)
- {
- byte[] buffer = new byte[8 * 1024];
- await Task.Run(() =>
- {
- udpClient.SendAsync(buffer, buffer.Length, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8000));
- });
- }
- while (true)
- {
- UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync();
- string message = Encoding.UTF8.GetString(udpReceiveResult.Buffer);
- Console.WriteLine($"{udpReceiveResult.RemoteEndPoint.ToString()} - {message}");
- }
- Console.ReadKey();
- }
运行一下,看看效果。
到这里,Service应用开发的工作已经完成。
下面是部署。
部署Service应用
Linux下面部署一个Service应用,只有两个步骤:
1. 创建Service定义
Linux下的每个Service,都会有个定义文件。这个文件存在于/etc/systemd/system目录下。
下面我给出一个简单的Service模板:
- [Unit]
- Description=DemoProject
- [Service]
- Type=notify
- ExecStart=dotnet /yourfolder/yourproject.dll
- [Install]
- 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守护
- % systemctl daemon-reload
刷新守护时,守护进程会去/etc/systemd/system目录下,寻找新加入的Service文件,并启动。
第二种是单独启动,有一系列命令:
启动
- % systemctl start demo.service
停止
- % systemctl stop demo.service
重启
- % systemctl restart demo.service
查询状态
- % systemctl status demo.service
嗯。这就是服务加载和停止了。
注意,这种方式加载的Service,是完全系统的服务,会没有任何输出。
如果需要调试,一种方式是加文件日志,另一种方式是用另一个命令启动:
- % journalctl -u dnsserver.service
当然,这种方式只用于调试。正式运行时,还应该是上面的方式。