서버프로그래밍/gRPC 미니 프로젝트

C# gRPC 최소 개발환경 구축하기 3(서버 구현)

codehunter 2022. 12. 18. 15:01

메인으로 가기

 

아무래도 서버니까 비동기 기반으로 프로젝트를 구성할까 한다.

 

참조 블로그

https://medium.com/@nikhilajayk/creating-your-first-grpc-net-core-client-and-server-app-using-visual-studio-or-visual-studio-code-293a6a5a5f7

 

메인프로그램은 가볍게 무한루프로 구성하고 RunServer()에서 gRPC서버를 구성하면 된다.

 

gmfServer > Program.cs

using Grpc.Core;

namespace gmfServer
{
    internal static class Program
    {
    	private static Server? _server;
        private static readonly CancellationTokenSource _tokenSource = new();
        private static readonly int port = 1234;
        
        private static async Task Initialize()
        {
            await Task.Delay(1);
        }
        
        private static void OnShutdown()
        {
            // TODO
        }

        private static void ShutdownServer()
        {
            _server?.ShutdownAsync().Wait();

            System.Console.WriteLine("서버 종료");

            OnShutdown();
        }
        
        private static async Task<Server> RunServer()
        {
        	// 상세코드는 밑에 있다.
            ...
        }
        
        private static async Task Main(string[] args)
        {
            await Initialize();
            _server = await RunServer();
            if (_server == null)
            {
                System.Console.WriteLine("서버 구동에 실패했습니다. 아무키나 누르면 종료합니다.");
                Console.ReadKey();
                return;
            }

            System.Console.WriteLine("Ctrl + C키를 누르면 종료합니다.");
            while (true)
            {
                System.Console.WriteLine($"서버 구동중... [ Port : 1234 ]");
                await Task.Delay(TimeSpan.FromMinutes(5));
            }

            var token = _tokenSource.Token;
            while (!token.IsCancellationRequested)
            {
                try
                {
                    await Task.Delay(TimeSpan.FromMilliseconds(100), token);
                }
                catch (OperationCanceledException)
                {
                    break;
                }
            }

            ShutdownServer();
        }
    }
 }

 

위의 Initialize(), RunServer()는 일단 빈 함수로 세팅하고 메인프로그램 구성이 끝났다면(다 주석처리하고 먼저 빌드만 되게 해도 된다.) 프로토파일을 다룰 순서인데 앞으로 프로토파일은 다음과 같은 순서로 작업하게 된다. 특히 여기서 vscode와 visual studio 의 세팅이 달라지게 된다.

 

우선 vscode 는

1. 프로토파일 정의 ( 예 : protos/ServerMain.proto)

2. 프로젝트 파일안에 프로토파일 위치 기입

3. 빌드

4. 빌드후 나온 파일을 상속받아 사용자가 쓸수있는 클래스 정의

 

저 위의 순서대로 작업해 보자

1. 프로토파일 정의

간단히 로그인 패킷구성을 한다고 가정하고 구성해보자. (프로토 파일을 구성할때 사실 중요한 몇가지 규칙이 있는데 자세한건 아래 링크를 참조하자).

https://lelecoder.com/15

간단히 함수나 변수이름은 모두 대문자로 시작하고 열거형은 0부터, 각 값마다 세미콜론(;)을 구분한다는 정도만 알아두자.

그리고 프로토파일 이름과 service 이름이 중요하기 때문에 이름짓기를 잘해야 한다.

 

proto > ServerMain.proto

syntax = "proto3";

package ServerMain;

service ServerMainService
{
    rpc Login(LoginRequest) returns (LoginResponse);
}

message LoginRequest
{
    string Id = 1;
    string passwd = 2;
}

message LoginResponse
{
    enum LoginResult
    {
        Success = 0;
        Fail = 1;
    }

    LoginResult Result = 1;
}

 

2.프로젝트 파일안에 프로토파일 위치 기입

아래처럼 프로젝트 파일안에 프로토파일의 위치를 잘 적어주자. 참고로 현재까지 프로젝트의 폴더 구성은 다음과 같다.

📦gmfServer
 ┣ 📂gmfCommon 
 ┃ ┗ 📜gmfCommon.csproj 
 ┣ 📂gmfServer 
 ┃ ┣ 📜gmfServer.csproj
 ┃ ┗ 📜Program.cs (실질적인 메인진입)
 ┣ 📂protos
 ┃ ┗ 📜ServerMain.proto (정의한 프로토파일)
 ┗ 📜gmfServer.sln (전체 솔루션)

 

gmfServer.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.21.12" />
    <PackageReference Include="Grpc" Version="2.46.5" />
    <PackageReference Include="Grpc.Tools" Version="2.51.0">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="../protos/ServerMain.proto" />
  </ItemGroup>

</Project>

 

3. 빌드

그리고 프로젝트 빌드(dotnet build --no-incremental -c Release) 를 하면 (gRPC.Tools NuGet 패키지로인해 자동 빌드된다. 수동으로 proto파일만 빌드할수도 있는데 무지 귀찮다 ) obj 폴더에 (Debug / Release 에 따라 달라짐) 폴더안에 보면 위에서 정의한 ServerMain이라는 프로토파일의 이름으로 2개파일이 생성된걸 볼수 있다. (프로토파일명을 ServerMain.proto 로 해서 그런거다.)

ServerMain.cs
ServerMainGrpc.cs

 

4. 빌드후 나온 파일을 상속받아 사용자가 쓸수있는 클래스 정의

이 두개의 파일은 자동으로 생성되는거라 사용자가 건드리면 안되고 ServerMainGrpc 파일에 정의된 ServerMainService 클래스를 상속받아 재정의해서 써야 한다. 파일이름은 클래스 이름하고 같게 하면 된다. 일단은 자동완성 기능으로 기본 기능만 완성하자.

이때 vscode에서 작업을 하다보면 제대로 했는데도 클래스이름 밑에 에러표시가 뜨면서 아래와 같은 메세지를 볼수 있는데 일단 파일을 저장하고 vscode를 껐다켜면 없어진다.

'ServerMainService' 네임스페이스에 이미 'gmfServer'에 대한 정의가 포함되어 있습니다. [gmfServer, gmfServer]",

 

 

ServerMainService.cs

using Grpc.Core;
using ServerMain;

namespace gmfServer
{
    class ServerMainService : ServerMain.ServerMainService.ServerMainServiceBase
    {
        public override Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
        {
            return base.Login(request, context);
        }
    }
}

 

그럼 외부와 연결할수 있는 인터페이스가 하나 생성된것이다.

 

 

계속해서 RunServce를 보자. 위에서 외부 인터페이스를 만들었으면 프로토파일에서 정의한 서비스와 인터페이스를 연결을 해야 한다. 그것이 바인드인데

 

gmfServer > Program.cs 

using Grpc.Core;

...

private static async Task<Server> RunServer()
{
    try
    {
         var server = new Server
        {
            Services = { ServerMain.ServerMasterService.BindService( new ServerMasterService() ) },
            Ports = { new ServerPort("[::]", port, ServerCredentials.Insecure) }
        };

        // gRPC 서버 시작
        server.Start();

        return server; 
    }
    catch (Exception e)
    {
        System.Console.WriteLine(e);
    }

    return await Task.FromException<Server>(null!);
}

...

위 코드에서 보이는 Server 라는 클래스는 Grpc.Core에 등록된 클래스이다. 그리고 BindService가 없다고 나올때가 있는데 ServerMainServer 의 정확한 위치를 못찾아서 그렇게 나올때가 있다. 이때는 네임스페이스까지 포함한 정확한 위치를 지정해주면 해결될때가 있다.

 

실행해보면 잘 작동하는걸 볼 수 있다. 참고로 해당 실행폴더까지 가서 실행시키는건 배치파일이나 쉘스크립트로 뽑아서 따로 실행시키는게 편하다.

Server\gmfServer> dotnet run --project .\gmfServer\gmfServer.csproj