반응형

Font Asset Creator 클릭

Character Set에 Custom Range 선택

32-126,44032-55203,12593-12643,8200-9900

 

영어 32-126

한글 44032-55203

한글 자음 모음 12593-12643

특수문자 8200-9000

 

 

Generate Font Atlas 클릭

 

기다렸다가 Save

 

TextMesh Pro Resource에서 TMP Settings 선택

위에서 생성한 폰트 Asset을 Default로 설정

 

 

반응형

'개발관련 > Unity' 카테고리의 다른 글

앱 이름 다국어 설정  (0) 2023.11.06
해상도 고정  (0) 2023.09.02
클리커 게임 단위 구하기  (0) 2023.03.15
Addressables 동기 사용법 및 주의점  (0) 2022.09.12
곡사체 관련 공식  (0) 2021.03.20
반응형

1000 = 1A

1000000 = 1B

namespace GameUtils
{
    public class BigNumberHelper
    {
        internal static readonly char[] MagnitudeUnits = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

        private static string _numberFormat = "{0:F3}";
        public static void Init(string format)
        {
            _numberFormat = format;
        }
        public static string BigNumberToString(BigNumber bigNumber)
        {
            if(bigNumber.Magnitude >=0)
            {
                return $"{string.Format(_numberFormat, bigNumber.Value)}{bigNumber.MagnitudeLabel}";
            }
            else
            {
                return bigNumber.Value.ToString();
            }
        }
    }
}
using System;
using System.Text;

namespace GameUtils
{
    public struct BigNumber
    {
        public double Value { get; private set; }
        public int Magnitude { get; private set; }
        public string MagnitudeLabel { get; private set; }

        public string DisplayValue
        {
            get
            {
                if (string.IsNullOrEmpty(_displayValue))
                {
                    BigNumberHelper.BigNumberToString(this);
                }
                return _displayValue;
            }
        }

        private readonly string _displayValue;
        public BigNumber(double value) : this(value, -1)
        {
        }
        public BigNumber(double value, int digit = -1)
        {
            while (value >= 1000)
            {
                value /= 1000;
                digit++;
            }
            Magnitude = digit;
            Value = value;

            var sb = new StringBuilder();
            while (digit >= 0)
            {
                sb.Insert(0, BigNumberHelper.MagnitudeUnits[digit % BigNumberHelper.MagnitudeUnits.Length]);
                digit = digit / BigNumberHelper.MagnitudeUnits.Length - 1;
            }
            MagnitudeLabel = sb.ToString();
            _displayValue = string.Empty;
        }
        public static BigNumber operator +(BigNumber number1, BigNumber number2)
        {
            double convertedValue1 = number1.Value;
            double convertedValue2 = number2.Value;
            int finalMagnitude = number1.Magnitude;
            if (number1.Magnitude > number2.Magnitude)
            {
                for (int i = 0; i < (number1.Magnitude - number2.Magnitude); i++)
                {
                    convertedValue2 /= 1000;
                }
            }
            else if (number1.Magnitude < number2.Magnitude)
            {
                for (int i = 0; i < (number2.Magnitude - number1.Magnitude); i++)
                {
                    convertedValue1 /= 1000;
                }
                finalMagnitude = number2.Magnitude;
            }
            var value = convertedValue1 + convertedValue2;

            return new BigNumber(value, finalMagnitude);

        }
        public static BigNumber operator -(BigNumber number1, BigNumber number2)
        {
            double convertedValue1 = number1.Value;
            double convertedValue2 = number2.Value;
            int finalMagnitude = number1.Magnitude;

            if (number1.Magnitude > number2.Magnitude)
            {
                for (int i = 0; i < (number1.Magnitude - number2.Magnitude); i++)
                {
                    convertedValue2 /= 1000;
                }
            }
            else if (number1.Magnitude < number2.Magnitude)
            {
                for (int i = 0; i < (number2.Magnitude - number1.Magnitude); i++)
                {
                    convertedValue1 /= 1000;
                }
                finalMagnitude = number2.Magnitude;
            }

            var value = convertedValue1 - convertedValue2;
            if (Math.Abs(value) < 1)
            {
                value *= 1000;
                finalMagnitude--;
            }
            return new BigNumber(value, finalMagnitude);
        }
    }
}
반응형

'개발관련 > Unity' 카테고리의 다른 글

해상도 고정  (0) 2023.09.02
TextMeshPro 폰트 생성  (0) 2023.07.31
Addressables 동기 사용법 및 주의점  (0) 2022.09.12
곡사체 관련 공식  (0) 2021.03.20
could not create asset from file could not be read  (0) 2021.01.06
반응형

System.IO.Pipelines 는 .NET에서 고성능 I/O 를 더 쉽게 수행할 수 있도록 설계된 라이브러리이다.

Kestrel 을 업계에서 가장 빠른 웹 서버 중 하나로 만들기 위해 수행한 작업에서 탄생했다.

웹 소켓 라이브러리인 SignalR 에도 포함이 되어있고 네트워크단에서 처리를 하는 SDK에는 포함이 대부분 되어 있는 것 같다.

해당 벤치마크에서 확인이 가능하다.

 

https://www.techempower.com/benchmarks/#section=data-r21&hw=ph&test=plaintext

 

TechEmpower Framework Benchmarks

 

www.techempower.com

 

소켓 버퍼로부터 유저 어플리케이션단으로 데이터를 가져오기 위해 해당 버퍼 만큼의 메모리를 할당하고 값을 복사하는 작업 등이 필요하고 각 개발자마다 이 부분을 속도나 메모리나 GC 등 단점 등을 보완하기 위해 각자의 방법으로 처리합니다.

 

아래는 패킷의 완성이 \r\n 으로 데이터의 끝을 알리는 MS형님의 예제이다.

  • \r\n을 찾을 때까지 들어오는 데이터를 버퍼링 해야 합니다.
  • 버퍼에 반환된 모든 줄 구문을 분석해야 한다.
  • 보낸 사이즈보다 버퍼가 작아 데이터가 덜 들어오는 경우엔 해당 데이터를 저장을 해야 하고 다음 데이터를 이어 받아야 한다.
async Task ProcessLinesAsync(NetworkStream stream)
{
    var buffer = new byte[1024];
    var bytesBuffered = 0;
    var bytesConsumed = 0;
 
    while (true)
    {
        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, buffer.Length - bytesBuffered);
        if (bytesRead == 0)
        {
            // EOF
            break;
        }
        // Keep track of the amount of buffered bytes
        bytesBuffered += bytesRead;
         
        var linePosition = -1;
 
        do
        {
            // Look for a EOL in the buffered data
            linePosition = Array.IndexOf(buffer, (byte)'\n', bytesConsumed, bytesBuffered - bytesConsumed);
 
            if (linePosition >= 0)
            {
                // Calculate the length of the line based on the offset
                var lineLength = linePosition - bytesConsumed;
 
                // Process the line
                ProcessLine(buffer, bytesConsumed, lineLength);
 
                // Move the bytesConsumed to skip past the line we consumed (including \n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}
  • 버퍼 풀을 이용해서 1024만큼의 버퍼 풀링을 시작했으나 1024 사이즈보다 작은 데이터들이 들어오는 경우 메모리를 더 많이 사용한다.
async Task ProcessLinesAsync(NetworkStream stream)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
    var bytesBuffered = 0;
    var bytesConsumed = 0;
 
    while (true)
    {
        // Calculate the amount of bytes remaining in the buffer
        var bytesRemaining = buffer.Length - bytesBuffered;
 
        if (bytesRemaining == 0)
        {
            // Double the buffer size and copy the previously buffered data into the new buffer
            var newBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length * 2);
            Buffer.BlockCopy(buffer, 0, newBuffer, 0, buffer.Length);
            // Return the old buffer to the pool
            ArrayPool<byte>.Shared.Return(buffer);
            buffer = newBuffer;
            bytesRemaining = buffer.Length - bytesBuffered;
        }
 
        var bytesRead = await stream.ReadAsync(buffer, bytesBuffered, bytesRemaining);
        if (bytesRead == 0)
        {
            // EOF
            break;
        }
         
        // Keep track of the amount of buffered bytes
        bytesBuffered += bytesRead;
         
        do
        {
            // Look for a EOL in the buffered data
            linePosition = Array.IndexOf(buffer, (byte)'\n', bytesConsumed, bytesBuffered - bytesConsumed);
 
            if (linePosition >= 0)
            {
                // Calculate the length of the line based on the offset
                var lineLength = linePosition - bytesConsumed;
 
                // Process the line
                ProcessLine(buffer, bytesConsumed, lineLength);
 
                // Move the bytesConsumed to skip past the line we consumed (including \n)
                bytesConsumed += lineLength + 1;
            }
        }
        while (linePosition >= 0);
    }
}
  • 메모리 낭비를 막기 위해 512보다 작은 경우엔 새로 메모리를 할당한다.
  • 처리량을 늘리기 위해 소켓에 들어온 데이터를 읽는 것과 유저 어플리케이션에 저장된 버퍼를 처리하는 로직을 분산해서 동시에 처리한다.
  • 기타 등등 최적화를 위한 로직이 들어간다면 코드가 복잡해진다.

System.IO.Pipelines가 있는 TCP 서버

async Task ProcessLinesAsync(Socket socket)
{
    var pipe = new Pipe();
    Task writing = FillPipeAsync(socket, pipe.Writer);
    Task reading = ReadPipeAsync(pipe.Reader);
 
    return Task.WhenAll(reading, writing);
}
 
async Task FillPipeAsync(Socket socket, PipeWriter writer)
{
    const int minimumBufferSize = 512;
 
    while (true)
    {
        // Allocate at least 512 bytes from the PipeWriter
        Memory<byte> memory = writer.GetMemory(minimumBufferSize);
        try
        {
            int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None);
            if (bytesRead == 0)
            {
                break;
            }
            // Tell the PipeWriter how much was read from the Socket
            writer.Advance(bytesRead);
        }
        catch (Exception ex)
        {
            LogError(ex);
            break;
        }
 
        // Make the data available to the PipeReader
        FlushResult result = await writer.FlushAsync();
 
        if (result.IsCompleted)
        {
            break;
        }
    }
 
    // Tell the PipeReader that there's no more data coming
    writer.Complete();
}
 
async Task ReadPipeAsync(PipeReader reader)
{
    while (true)
    {
        ReadResult result = await reader.ReadAsync();
 
        ReadOnlySequence<byte> buffer = result.Buffer;
        SequencePosition? position = null;
 
        do
        {
            // Look for a EOL in the buffer
            position = buffer.PositionOf((byte)'\n');
 
            if (position != null)
            {
                // Process the line
                ProcessLine(buffer.Slice(0, position.Value));
                 
                // Skip the line + the \n character (basically position)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);
 
        // Tell the PipeReader how much of the buffer we have consumed
        reader.AdvanceTo(buffer.Start, buffer.End);
 
        // Stop reading if there's no more data coming
        if (result.IsCompleted)
        {
            break;
        }
    }
 
    // Mark the PipeReader as complete
    reader.Complete();
}
  1. https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/BufferSegmentStack.cs
  2. https://github.com/dotnet/runtime/blob/main/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/Pipe.cs
    1. 읽기용 데이터의 시작 인덱스 _readHeadIndex 와 읽기용 데이터의 끝 _readTailIndex 
    2. 쓰기용 데이터의 시작 인덱스 _writingHead 와 바이트의 수 _writingHeadBytesBuffered
    3. 이 인덱스들이 내부에서 왔다갔다 하면서 카피 대신에 해당하는 버퍼의 index들의 데이터들을 스택에 올려 반환하면서 처리를 하게 된다.
    4. 스택을 사용함으로써 GC가 돌지 않게 만들 수 있다.

References

https://learn.microsoft.com/en-us/dotnet/standard/io/pipelines?irgwc=1&OCID=AID2200057_aff_7593_1243925&tduid=(ir__qusfgwwd1wkfbzylzocc1exah32xcv00chrqwasx00)(7593)(1243925)(je6NUbpObpQ-QF0.r_GFsgNy_qAr0H6row)()&irclickid=_qusfgwwd1wkfbzylzocc1exah32xcv00chrqwasx00#pipe-basic-usage?ranMID=24542&ranEAID=je6NUbpObpQ&ranSiteID=je6NUbpObpQ-QF0.r_GFsgNy_qAr0H6row&epi=je6NUbpObpQ-QF0.r_GFsgNy_qAr0H6row

https://github.com/davidfowl/TcpEcho

https://blog.naver.com/oidoman/221674992672

https://habr.com/en/post/466137/

반응형
반응형

서비스를 실행하려면 .service 파일이 필요하다.

 

서비스 파일이 없이 서비스를 등록하려고 하면 Unit not found. 해당 에러가 발생한다.

 

systemctl enable ba.internalserverd

 

파일을 생성해준다. 현재 개인 프로젝트의 서버 등록을 위해 ba.internalserverd.service 로 파일을 만들어준다.

 

vi /etc/systemd/system/ba.internalserverd.service

[Unit]
Description=BA.InterServer.dll

[Service]
WorkingDirectory=/ba/bin/interserver
ExecStart=/usr/bin/dotnet /ba/bin/interserver/BA.InterServer.dll
Restart=always
# Restart service after 10 seconds if the dotnet service crashes:
RestartSec=10
KillSignal=SIGINT
SyslogIdentifier=BA.InterServer

[Install]
WantedBy=multi-user.target

파일을 생성 후에 다시 등록

systemctl enable ba.internalserverd

서비스를 실행시킨다.
systemctl start ba.internalserverd

반응형

'개발관련 > ETC..' 카테고리의 다른 글

Https 적용하기  (0) 2024.03.11
aws ec2 프리티어 메모리 부족  (0) 2023.09.22
Apple revoke token 처리  (0) 2022.07.09
AWS CodeBuild, CodeDeploy  (0) 2019.07.25
객체 지향 설계의 원칙>SOLID  (0) 2019.05.08
반응형

환경은 amazon linux 2 ami OS 이다.

 

yum 업데이트

sudo yum update -y

도커 설치

sudo yum install docker -y

 

도커 버전 확인

docker -v

 

도커 시작

sudo service docker start

 

도커 그룹에 ec2-user 를 추가한다. 이렇게 하면 docker 명령어를 실행할 때 sudo를 사용하지 않아도 된다.

sudo usermod -aG docker ec2-user

 

아래 방식으로도 가능하다.

 

amazon-linux-extras install docker

 

다만 다른 블로그나 사이트에서 설명한대로 도커 저장소 등록을 해놓은 경우 

 

Error: Package: containerd.io-1.2.10-3.2.el7.x86_64 (docker-ce-stable)
           Requires: container-selinux >= 2:2.74

 

위의 명령어를 실행시에 해당 에러가 발생하게 된다.

 

/etc/yum.repos.d/docker-ce.repo 를 제거해준 다음

 

amazon-linux-extras install docker 를 다시 실행하면 된다.

 

도커 시작

service docker start

 

도커 서비스 등록

systemctl enable docker

 

https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/developerguide/create-container-image.html

 

※ centOS

 

1.기존 도커 삭제

설치되어 있지 않다면 무시해도 된다.

 

yum remove docker \

                  docker-client \

                  docker-client-latest \

                  docker-common \

                  docker-latest \

                  docker-latest-logrotate \

                  docker-logrotate \

                  docker-engine \

                  podman \

                  runc

 

2.도커 설치 방법

2.Docker Engine 설치

  • yum install docker-ce

설치시에 종속성 문제가 발생하게 되는 경우 
https://rhel.pkgs.org/7/docker-ce-x86_64/
여기에서 필요한 패키지를 검색해서 찾아야한다. 

반응형
반응형
//spin lock
public void Add(T item)
{
    bool locked = false;
    _spinLock.Enter(ref locked);
    _vector.Add(item);
    _spinLock.Exit();
}

//lock
public void Add(T item)
{
    lock(this.SyncRoot)
    {
        _vector.Add(item);
    }
}

사용가능한 쓰레드를 모두 사용했을때 결과 

lock을 사용한 결과 2.310, 1.876 ms

spin lock을 사용한 결과 47.241 ms, 62.261ms

 

public class Benchmark
{
    [Benchmark]
    public void ThreadSpinLock()
    {
        Kosher.Collections.SynchronizedVector2<int> synchronizedVector = new SynchronizedVector2<int>(10000);
        Parallel.For(0, 10000, (i) =>
        {
            synchronizedVector.Add(i);
        });
    }

    [Benchmark]
    public void ThreadLock()
    {
        Kosher.Collections.SynchronizedVector<int> synchronizedVector = new SynchronizedVector<int>(10000);
        Parallel.For(0, 10000, (i) =>
        {
            synchronizedVector.Add(i);
        });
    }
}

쓰레드의 경합이 많아질수록 SpinLock의 처리속도는 늦어진다.

public class Benchmark
{
        [Benchmark]
        public void ThreadSpinLock()
        {
            Kosher.Collections.SynchronizedVector2<int> synchronizedVector = new SynchronizedVector2<int>(10000);

            var option = new ParallelOptions();
            option.MaxDegreeOfParallelism = 2;
            Parallel.For(0, 10000, option, (i) =>
            {
                synchronizedVector.Add(i);
            });
        }

        [Benchmark]
        public void ThreadLock()
        {
            var option = new ParallelOptions();
            option.MaxDegreeOfParallelism = 2;
            Kosher.Collections.SynchronizedVector<int> synchronizedVector = new SynchronizedVector<int>(10000);
            Parallel.For(0, 10000, (i) =>
            {
                synchronizedVector.Add(i);
            });
        }
    }

다만 쓰레드의 경합이 없는 경우 Thread Context Switching 이 없기에 Spin Lock의 속도가 더 잘나온다.

반응형
반응형

클러스터 인덱스

  • 데이블의 데이터는 클러스터형 인덱스 키 열별로 정렬된 순서로 저장
  • 테이블이나 뷰는 하나의 클러스터형 인덱스만 있다.
  • 클러스터형 인덱스가 정의된 경우에만 데이터가 정렬
  • 데이터가 정렬되어 있기에 검색 속도가 빠르다
  • 데이터 삽입시 테이블의 모든 데이터들을 정렬해야한다.

논 클러스터 인덱스

  • 순서대로 정렬되어 있지 않다.
  • 한 테이블에 여러개 생성 가능하다.
  • 인덱스를 저장할 추가적인 공간이 필요하다.
  • 클러스터 인덱스와는 달리 삽입시 데이터 정렬을 하지 않고 인덱스 생성을 해야한다.
반응형

'개발관련 > DB' 카테고리의 다른 글

MYSQL>FIND_IN_SET  (0) 2019.03.29
MYSQL>집계함수 곱연산  (0) 2019.03.26
MYSQL>Explain  (0) 2019.03.26
MSSQL>페이징 처리  (0) 2017.07.04
반응형

생성과 릴리즈는 짝을 맞춘다.

Addressables.LoadAssetAsync = Addressables.Release
Addressables.InstantiateAsync = Addressables.ReleaseInstance

 

대부분 비동기로 이뤄짐으로 씬에서 여러가지 요청으로 비동기 관리가 힘들다면 

 

var op = Addressables.LoadAssetAsync<GameObject>(path);

var prefab = op.WaitForCompletion();

 

이런식으로 동기형으로 사용이 가능하다.

 

주의점

 

메모리 해제시 씬 객체가 참조를 물고 있다면 Addressables Release를 하더라도  메모리에 계속해서 상주하게 된다.

이때 Addressables Resource 릴리즈 관리는 씬으로 넘어가게 되고 씬에서 객체가 파괴가 되면 레퍼런스 카운트 체크를 통해 릴리즈가 된다.

 

AddressableAssetSettings 클릭해서 프로파일링 설정

 

 

반응형

'개발관련 > Unity' 카테고리의 다른 글

해상도 고정  (0) 2023.09.02
TextMeshPro 폰트 생성  (0) 2023.07.31
클리커 게임 단위 구하기  (0) 2023.03.15
곡사체 관련 공식  (0) 2021.03.20
could not create asset from file could not be read  (0) 2021.01.06
반응형

최근 애플 앱 심사 거절 이유로 회원탈퇴를 하는 경우 revoke token 처리를 해야한다고 한다.

 

클라단에서 처리를 못하기에 서버단에서 처리하기로 결정하고 서버에서 처리한다.

 

https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens

 

    //client_secret 생성
    public static string GenerateAppStoreJwtToken(string teamId, string keyId, string clientId, string p8key)
    {
        var aud = "https://appleid.apple.com";
        string iss = teamId;
        string sub = clientId;
        string kid = keyId;

        IList<Claim> claims = new List<Claim> {
            new Claim ("sub", sub)
        };

        try
        {
            var cngKey = CngKey.Import(Convert.FromBase64String(p8key), CngKeyBlobFormat.Pkcs8PrivateBlob);

            var signingCred = new SigningCredentials(new ECDsaSecurityKey(new ECDsaCng(cngKey)),
                                                    SecurityAlgorithms.EcdsaSha256);

            var token = new JwtSecurityToken(iss,
                                            aud,
                                            claims,
                                            DateTime.Now,
                                            DateTime.Now.AddDays(180),
                                            signingCred);

            token.Header.Add("kid", kid);
            token.Header.Remove("typ");

            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();

            return tokenHandler.WriteToken(token);
        }
        catch(Exception ex)
        {
            //로그 출력
            return null;
        }
    }
    private async Task<bool> SendAppleRevokeToken(string token)
    {

        var clientSecret = GenerateAppStoreJwtToken(AppleTeamId,
                                                    AppleTeamId,
                                                    AppleClientId,
                                                    ApplePrivateKey);
                                                    
        if(string.IsNullOrEmpty(clientSecret) == true)
        {
            Log.Error($"failed to generate token!");
            return false;
        }
        var parameters = new Dictionary<string, string>
        {
            { "client_id", AppleClientId },
            { "client_secret", clientSecret },
            { "token", token },
            { "token_type_hint", "access_token" }
        };
        //post send
        var response = await HttpRequester.Get(@"https://appleid.apple.com/auth/revoke", parameters);

        return string.IsNullOrEmpty(response) == false;
    }
반응형

'개발관련 > ETC..' 카테고리의 다른 글

aws ec2 프리티어 메모리 부족  (0) 2023.09.22
Linux에 서비스 등록  (0) 2023.01.10
AWS CodeBuild, CodeDeploy  (0) 2019.07.25
객체 지향 설계의 원칙>SOLID  (0) 2019.05.08
javascript> Drag & Drop 파일 읽기  (0) 2018.12.05
반응형

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequestrewindextensions.enablebuffering?view=aspnetcore-2.2 

 

HttpRequestRewindExtensions.EnableBuffering Method (Microsoft.AspNetCore.Http)

Ensure the requestBody can be read multiple times. Normally buffers request bodies in memory; writes requests larger than 30K bytes to disk.

docs.microsoft.com

 

더 큰 요청에 대한 임시 파일은 ASPNETCORE_TEMP환경 변수(있는 경우)에 명명된 위치에 기록됩니다. 해당 환경 변수가 정의되지 않은 경우 이러한 파일은 현재 사용자의 임시 폴더에 기록됩니다. 파일은 연결된 요청이 끝나면 자동으로 삭제됩니다.

 

해당 경로에 폴더가 생성이 안되는 경우 패킷을 받지 못하는 이슈가 발생한다.

반응형

'개발관련 > C#' 카테고리의 다른 글

System.IO.Pipelines  (0) 2023.02.08
Thread Synchronization spinlock vs lock performance  (0) 2022.12.20
소켓 비정상 종료 처리 TcpKeepAlive  (0) 2018.09.13
C++/CLI를 통한 C++ 클래스 마샬링  (0) 2018.08.11
sql compact 4.0 설정  (2) 2018.07.30

+ Recent posts