Dotnet創(chuàng)建Linux下的Service應(yīng)用
本文轉(zhuǎn)載自微信公眾號「老王Plus」,作者老王Plus的老王 。轉(zhuǎn)載本文請聯(lián)系老王Plus公眾號。
前言
說到服務(wù)端應(yīng)用,最常見的就是API服務(wù)。
除此之外,還有一類應(yīng)用,比方一個Socket的服務(wù)器。這類型的應(yīng)用,本身沒有Web層,當(dāng)然也不屬于API服務(wù)。
通常大家會怎么做?
不講究的做法,就是做一個Console應(yīng)用,加載到后臺一直跑著。
其實,還有另外一種做法,就是把應(yīng)用加載到Services里,使應(yīng)用以一個Service來做響應(yīng)。這樣可以依托操作系統(tǒng)的Services管理器來進(jìn)行統(tǒng)一管理,自動運行和故障處理。
Dotnet做Window Service的內(nèi)容,網(wǎng)上有很多。我今天寫一個在Linux下做Service的方法。
創(chuàng)建Linux下的Service應(yīng)用
創(chuàng)建一個LInux下的Service應(yīng)用其實很簡單,就分這么幾步:
1. 用 Worker 模板創(chuàng)建工程
如果習(xí)慣用VS上創(chuàng)建,就找一下Worker Service模板。
我是習(xí)慣從命令行創(chuàng)建,就一條命令:
- % dotnet new worker -o projectname
Dotnet會自動造成工程,并自動引用Microsoft.Extensions.Hosting包,因為這本身是一個Self-Hosting應(yīng)用。
2. 加入Linux Service擴(kuò)展包
其實這就是一個包:Microsoft.Extensions.Hosting.Systemd。這個包為應(yīng)用提供了在Linux下使用Systemd守護(hù)進(jìn)程的基礎(chǔ)配置。
還是命令行:
- % dotnet add package Microsoft.Extensions.Hosting.Systemd
3. 修改Program.cs
其實就是一行代碼,把第二步引入的包加入應(yīng)用。修改Program.cs
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSystemd() // 加入的就是這一行。
- .ConfigureServices((hostContext, services) =>
- {
- services.AddHostedService<Worker>();
- });
到這兒,套路性的工作已經(jīng)完成。簡單吧?
我們來看一下現(xiàn)在的工程:
- ├── 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里的服務(wù)的模板。我們需要的服務(wù)代碼,需要加到ExecuteAsync(CancellationToken stoppingToken)方法中。
我簡單做個例子,在里面加入UDP服務(wù),看代碼:
- 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()時,已經(jīng)注入了IConfiguration。因此,可以在這個方法中直接引入并使用。換句話說,就是可以直接讀取例如appsetting.json的內(nèi)容;
- 是上邊提到的,真正的服務(wù)響應(yīng)在ExecuteAsync(CancellationToken stoppingToken)中。這兒沒什么特別的,就是正常的寫法。
上面這個,是服務(wù)端的程序,是響應(yīng)。
下面我簡單做個客戶端的請求,供測試用。就不解釋了,只列出步驟:
創(chuàng)建一個工程
- % 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應(yīng)用開發(fā)的工作已經(jīng)完成。
下面是部署。
部署Service應(yīng)用
Linux下面部署一個Service應(yīng)用,只有兩個步驟:
1. 創(chuàng)建Service定義
Linux下的每個Service,都會有個定義文件。這個文件存在于/etc/systemd/system目錄下。
下面我給出一個簡單的Service模板:
- [Unit]
- Description=DemoProject
- [Service]
- Type=notify
- ExecStart=dotnet /yourfolder/yourproject.dll
- [Install]
- WantedBy=multi-user.target
把這個內(nèi)容保存為一個文件,例如叫demo.service。然后把這個文件復(fù)制到/etc/systemd/system下,并改為可執(zhí)行。
簡單說一下這個文件的一些項:
- Description,是服務(wù)的名字。不重要,啟動時,你用到的是文件名demo.service;
- Type,服務(wù)類型,使用Dotnet加載時,只能是這種類型。如果把程序編譯為自包含程序,這個類型可以是simple;
- ExecStart,啟動程序的命令,是全路徑的,要確保能找得到這個程序。上面例子中,dotnet /yourfolder/yourproject.dll,是因為dotnet命令是有PATH變量支持的。
這個文件的配置項有很多,包括定義是否需要自動重啟、重啟間隔等。如果需要,可以去這里查詢。
2. 啟動Service
有兩種方法。
第一種是刷新Service守護(hù)
- % systemctl daemon-reload
刷新守護(hù)時,守護(hù)進(jìn)程會去/etc/systemd/system目錄下,尋找新加入的Service文件,并啟動。
第二種是單獨啟動,有一系列命令:
啟動
- % systemctl start demo.service
停止
- % systemctl stop demo.service
重啟
- % systemctl restart demo.service
查詢狀態(tài)
- % systemctl status demo.service
嗯。這就是服務(wù)加載和停止了。
注意,這種方式加載的Service,是完全系統(tǒng)的服務(wù),會沒有任何輸出。
如果需要調(diào)試,一種方式是加文件日志,另一種方式是用另一個命令啟動:
- % journalctl -u dnsserver.service
當(dāng)然,這種方式只用于調(diào)試。正式運行時,還應(yīng)該是上面的方式。