반응형

 

 

 

You can flag a thread that you want to give special attention by marking it with an icon in the Threads, Parallel Stacks (thread view), Parallel Watch, and GPU Threads windows. This icon can help you and others distinguish flagged threads from other threads.

Flagged threads also receive special treatment in the Thread list on the Debug Location toolbar and in the other multithreaded debugging windows. You can show all threads or only flagged threads in the Thread list or in the other windows.

To flag or unflag a thread

  • In the Threads or Parallel Watch window, find the thread you are interested in and click the flag icon to select or clear the flag.
  • In the Parallel Stacks window, right-click on a thread or group of threads and select Flag / <thread> or Unflag / <thread>.

To unflag all threads

  • In the Threads window, right-click any thread and then click Unflag All Threads.
  • In the Parallel Watch window, select all flagged threads, then right-click and select Unflag.

To display only flagged threads

  • Choose the Show Flagged Threads Only button in one of the multithreaded debugging windows.

To flag Just My Code

  1. On the toolbar at the top of the Threads window, click the flag icon.
  2. In the drop-down list, click Flag Just My Code.

To flag threads that are associated with selected modules

  1. On the toolbar of the Threads window, click the flag icon.
  2. In the drop-down list, click Flag Custom Module Selection.
  3. In the Select Modules dialog box, select the modules that you want.
  4. (Optional) In the Search box, type a string to search for specific modules.
  5. Click OK.

 

 

ref : https://learn.microsoft.com/en-us/visualstudio/debugger/how-to-flag-and-unflag-threads?view=vs-2022 

 

Flag and Unflag Threads - Visual Studio (Windows)

Learn to flag or unflag threads in Visual Studio. Flag or unflag a thread, several threads, or all threads. Flag just your code or ones associated with a module.

learn.microsoft.com

 

반응형
반응형

샘플러 상태 사용

연결된 텍스처와 샘플러

대부분의 경우 텍스처를 셰이더에서 샘플링할 때 텍스처 샘플링 상태는 텍스처 설정에서 나옵니다. 기본적으로 텍스처와 샘플러는 함께 결합되어 있습니다. 다음은 DX9 스타일 셰이더 구문을 사용할 때의 기본 동작입니다.

sampler2D _MainTex;
// ...
half4 color = tex2D(_MainTex, uv);

sampler2D, sampler3D, samplerCUBE HLSL 키워드를 사용하여 텍스처와 샘플러를 선언합니다.

대부분의 경우는 이 옵션이 적합하며, 구형 그래픽스 API(예: OpenGL ES)는 이 옵션만 지원합니다.

분리된 텍스처와 샘플러

많은 그래픽스 API와 GPU에서는 텍스처보다 적은 샘플러를 사용할 수 있으며, 연결된 텍스처+샘플러 구문으로 인해 더 복잡한 셰이더를 작성하지 못할 수도 있습니다. 예를 들어, Direct3D 11은 한 셰이더에 최대 128개의 텍스처와 16개의 샘플러를 지원합니다.

Unity는 DX11 스타일 HLSL 구문으로 텍스처와 샘플러를 선언할 수 있으며, 특수한 명명 규칙으로 일치시킵니다. “sampler”+TextureName 형식의 이름을 가진 샘플러는 샘플링 상태를 해당 텍스처에서 가져옵니다.

위 섹션의 셰이더 스니핏은 DX11-style HLSL 구문으로 재작성해도 같은 동작을 합니다.

Texture2D _MainTex;
SamplerState sampler_MainTex; // "sampler" + "_MainTex"
// ...
half4 color = _MainTex.Sample(sampler_MainTex, uv);

그러나 이렇게 하면 한 개 이상의 텍스처를 샘플링하면서 샘플러를 다른 텍스처에서 “재사용”하게 셰이더를 작성할 수 있습니다. 아래 예제에서는 3개의 텍스처에 하나의 샘플러만을 사용합니다.

Texture2D _MainTex;
Texture2D _SecondTex;
Texture2D _ThirdTex;
SamplerState sampler_MainTex; // "sampler" + "_MainTex"
// ...
half4 color = _MainTex.Sample(sampler_MainTex, uv);
color += _SecondTex.Sample(sampler_MainTex, uv);
color += _ThirdTex.Sample(sampler_MainTex, uv);

그러나 DX11-style HLSL 구문은 일부 구형 플랫폼(예: OpenGL ES 2.0)에서는 동작하지 않는다는 점에 유의하십시오. 자세한 내용은 Unity에서 HLSL 사용를 참조하십시오. #pragma target 3.5(셰이더 컴파일 타겟 참조)을 지정해 구형 플랫폼이 해당 셰이더를 건너뛰도록 할 수 있습니다.

Unity는 이 “분리된 샘플러” 접근 방법을 사용한 선언과 텍스처 샘플링에 도움이 될만한 여러 셰이더 매크로를 제공하고 있으니 빌트인 매크로를 참조하십시오. 위의 예제는 이 매크로를 사용해 이렇게 재작성할 수 있습니다.

UNITY_DECLARE_TEX2D(_MainTex);
UNITY_DECLARE_TEX2D_NOSAMPLER(_SecondTex);
UNITY_DECLARE_TEX2D_NOSAMPLER(_ThirdTex);
// ...
half4 color = UNITY_SAMPLE_TEX2D(_MainTex, uv);
color += UNITY_SAMPLE_TEX2D_SAMPLER(_SecondTex, _MainTex, uv);
color += UNITY_SAMPLE_TEX2D_SAMPLER(_ThirdTex, _MainTex, uv);

위는 Unity가 지원하는 모든 플랫폼에서 컴파일할 수 있지만 DX9 같은 구형 플랫폼에서는 3개의 샘플러를 사용하도록 폴백합니다.

인라인 샘플러 상태

“sampler”+TextureName 이름의 HLSL SamplerState 오브젝트 외에도 Unity는 샘플러 이름의 다른 패턴도 인식합니다. 간단하게 하드코드된 샘플링 상태를 셰이더에 바로 선언할 때 유용합니다. 예를 들면, 다음과 같습니다.

Texture2D _MainTex;
SamplerState my_point_clamp_sampler;
// ...
half4 color = _MainTex.Sample(my_point_clamp_sampler, uv);

“my_point_clamp_sampler” 이름은 포인트(가장 가까운) 텍스처 필터링과 클램프 텍스처 랩 모드를 사용하는 샘플러로 인식됩니다.

샘플러 이름은 “inline” 샘플러 상태로 인식됩니다(대소문자 구분).

  • “Point”, “Linear” 또는 “Trilinear”(필수)는 텍스처 필터링 모드를 설정합니다.
  • “Clamp”, “Repeat”, “Mirror” 또는 “MirrorOnce”(필수)는 텍스처 랩 모드를 설정합니다.
    • 랩 모드는 축(UVW)에 지정할 수 있습니다. 예를 들어, “ClampU_RepeatV”
  • “Compare”(선택)는 뎁스 비교용 샘플러를 설정합니다. HLSL SamplerComparisonState 타입과 SampleCmp/SampleCmpLevelZero 함수를 사용합니다.
  • “AnisoX”(X는 2/4/8 또는 16일 수 있음. 예: Ansio8)를 추가하여 이방성 필터링을 요청할 수 있습니다.

다음은 각각 sampler_linear_repeat 샘플링 텍스처와 sampler_point_repeat SamplerState의 예제로, 이름이 필터링 모드를 조절하는 방식을 보여줍니다.

다음은 각각 SmpClampPoint, SmpRepeatPoint, SmpMirrorPoint, SmpMirrorOncePoint, Smp_ClampU_RepeatV_Point SamplerState의 예제로, 이름이 랩 모드를 조절하는 방식을 보여줍니다. 이 마지막 예제에서는 가로(U)와 세로(V) 축에 대해 다른 랩 모드가 설정되었습니다. 모든 경우에 텍스처 좌표는 –2.0 - +2.0입니다.

분리된 텍스처+샘플러 구문과 마찬가지로 인라인 샘플러 상태가 모든 플랫폼에서 지원되지는 않습니다. 현재는 Direct3D 11/12 및 Metal에서 구현됩니다.

참고로, “MirrorOnce” 텍스처 랩 모드는 대부분의 모바일 GPU/API에서 지원되지 않고, 이런 경우 Mirror 모드로 폴백합니다.

“AnisoX” 필터링 모드는 플랫폼 기능과 일부 API에서 최선의 수단입니다. 실제 값은 지원되는 최대 이방성 수준에 기반하여 고정됩니다(이방성 필터링이 지원되지 않는 경우의 비활성화 포함).

 

https://docs.unity3d.com/kr/current/Manual/SL-SamplerStates.html

반응형
반응형

 

 

선 개념 : 원 순열의 반대말은 직순열이다

위 A, B, C 는 보기에는 달라 보이지만 돌려 보면 모두다 같다

즉 원순열 기준으로는 모두다 같다고 본다 (돌렸을때 같으면 같다고 보는것이 원순열, 특정 기준 없이)

주의 : 원순열에선 보기엔 A 가 위에 있을지라도 맨 위에 있다 옆에 있다 그런 개념이 없다, 특정 기준 없이는

직순열과 원순열의 차이는 자리마자 고유번호가 있다인데 이것은 특정 원소를 기준으로 자리를 매길 수 있다는 말입니다

아래 것은 왼쪽이 첫번째 것이라고 특정 기준을 잡은것임으로 직순열이다

하지만 원순열은 고유번호가 없다, 돌려도 같기 때문

그래서 원순열을 풀때는 기준을 잡고 직순열로 바꿔서 푼다

즉 고유번호 엇는 것을 고유번호 있게 만드는 것이 첫번째 과정이다

=> 원순열에 누군가 한명이 앉으면 그때부턴 기준이 생기게 됨으로 원순열이 아닌 직순열이 된다

첫번재 앉은 사람 왼쪽 또는 오른쪽에 누구를 앉힐 지 정할 수 있는 기준이 되는사람이 앉았음으로

문제 1)

이런 볼품 없는 원탁의자가 있다(그림은 저모양 일지라도 의지는 모두다 같고 구분 할수 없다 가정한다)

이때 첫번째 사람이 앉는 경우의 수는?

답 = 1가지 , 3가지가 아닌데 왜냐면 원순열에서는 돌려도 같기 때문에 처음엔 어떤 기준이 없어서 첫번째 앉는 사람이 어디에 앉든 모두다 같은 경우가 됨으로, 즉 어디에 앉든 다 똑같다( 기준이 애초에 없었음으로 1명이 앉기 전까진)

문제 2)

문제 1 에서 모두다 앉는 경우의 수는?

답 1 * 2! 인데

1명이 앉고 나머지 2명이 앉는 경우의 수임으로 직순열(Permutation) 로 풀면 된다

문제 3)

6명이 원탁에 앉는 경우의 수는?

답 = 1 * 5!

n 명이 원탁에 둘러 앉는 경우의 수는?

$1\ \cdot \ \left(n-1\right)!$1 · (n1)!

문제)

A, B 를 포함한 6명이 있는데

A, B 가 이웃하여 앉는 경우의 수를 구하시오

이땐 순열 top 5중

A, B 를 주머니에 넣고 하나라 생각한 후 풀면 됩니다

그래서 우선 총 개수는 5개라 생각하면

1 * 4!

그리고 주머니에 있는 것 2! 을 곱하면

답 = 1 * 4! * 2!

문제 )

A, B 가 마주보며 앉는 경우의 수는?

원으로 생각하면 어려우니깐 일단 기준을 잡아야 한다

즉 A 를 먼저 앉힌다음 생각해보면

A 를 앉히는 경우의 수 1 가지 , 앉히면 직순열이 됨

그런데 그리고 B 가 앉는 경우의 수 1 가지 왜냐면 A 의 반대편아 앉아야 함으로

여기까진 1 * 1 이 된다 그런다음

그림 처럼 4자리에 나머지 4명이 앉으면 됨으로

4!

A 가 처음 앉을때 원을 돌아가면서 앉을때도 고려해야 하는것 아닌가? 라고 생각 할수 있지만

원순열에서 A 가 앉음과 동시에 직순열이 되었음으로 이건 이미 고려 대상에선 끝

답 = 1 * 1 * 4!

반응형

'수학 (Mathematics) > 확률과통계' 카테고리의 다른 글

조합 Top5 (1)  (0) 2023.01.06
조합 (Combination)  (0) 2023.01.06
순열 Top5 간단 정리  (0) 2023.01.06
조합(combination)  (0) 2012.11.02
순열 permutation  (0) 2012.11.02
반응형

 

조합 Top 5

1. 포함되도록

2. 포함되지 않도록

3. 적어도 1명이 포함되도록

4. 악수하는 방법의 수

5. 조 나누기

출처 입력

 

 

 

 

[포함되도록]

 

문제) 갑,을 을 포함한 학생 총 10명이 있다

이때 학생 대표단 3명을 뽑는데 갑과 을이 모두 포함되도록 뽑는 경우의 수는?

 

 

답 = 1 * 8C1

 

1은 갑과 을은 이미 뽑아 놓았음으로

 

Permutation 과 헷갈리면 안되는건 사람을 뽑는 것은 줄을 세우는 것이아니기 때문에

갑과을에 대한 줄을 세우는 경우의 수는 고려할 필요가 없습니다

그래서 나머지 8 명 중에서 한명만 뽑으면 됨으로 8C1

 

 

 

[포함되지 않도록]

 

문제) 문제 갑과 을이포함 되지 않도록 3명을 뽑는 경우의 수는?

 

 

답 = 8C3

갑과 을을 배제하고 3명을 뽑으면 됨으로

 

 

 

 

[적어도 1명이 포함되도록]

 

갑과 을 중 적어도 1명이 10명 중 대표 3명에 포함되도록 하는 경우의 수는?

 

 

갑과 을 중 한명을 뽑아서

나머지 8명중 2명을 뽑는 경우의 수

 

(case 1) 2C1 * 8C2 = 56

 

 

갑과을 두명을 뽑아서 나머지 8명중 1명을 뽑는 경우의 수

(case 2) 2C2 * 8C1

 

2C2 * 8C1 = 1 * 8C1 = 8

 

 

 

답 = 56 + 8 = 64

 

적어도 한명이라고 하면 1명인 케이스와 요건을 충족하는 인원수 까지

카운팅 하면서 각각 맞는 경우들을 계산하여 더한다는 개념

=> 즉 분류해서 더한다(위 문장을 외우는 것이 아닌 분류하고 합치는 개념이 중요)

 

 

적어도 1명이 포함되도록 의 케이스는 여사건으로 풀어도 가능하다

 

10C3 - 8C3 전체 중에 3명 뽑는것에서 8명중에서 3명 뽑는 경우의 수

 

120 - 56 = 64

 

 

 

 

 

[악수하는 방법의 수]

 

5명이 서로 악수하는 경우의 수는?

 

악수할때는 2명이 필요하니

5명중에서 2명씩만 뽑으면 된다 그래서

 

답 = 5C2

 

 

 

 

[조 나누기]

이건 다음에..

 

 

조합 계산기 : https://ko.numberempire.com/combinatorialcalculator.php

이미지 썸네일 삭제
조합 계산기

조합 계산기

ko.numberempire.com

 

 

반응형

'수학 (Mathematics) > 확률과통계' 카테고리의 다른 글

원 순열  (0) 2023.01.06
조합 (Combination)  (0) 2023.01.06
순열 Top5 간단 정리  (0) 2023.01.06
조합(combination)  (0) 2012.11.02
순열 permutation  (0) 2012.11.02
반응형

 

순열 : N 명중에서 R 명을 뽑는 수, 하지만 줄을 세운다

 

조합 : N 명중에서 R 명을 뽑는 수, 하지만 줄을 세우진 않는다 = 순서 상관 없다 = 즉 뽑는 Action 만 따진다

= 선택하는 경우의 수로 말하기도 함 ex) 10명중에서 5명을 선택하는 경우의 수

 

 

이렇게 되는데

 

n명중에 r명을 뽑아 줄을 세운다음 줄 세운것을 취소하면 n명 중에서 r 명을 뽑기만 하는것이 된다

 

 

 

다른 말로

 

순열 nPr 이란 얘기는 n명중에서 r명을 뽑고(여기까지가 nCr ) 다음 그 이후 줄까지 세운 것을 말한다

 

 

 

ex) 10 명중에서 3명을 줄을 세우는 방법의 수는?

 

 

10C3 = 10P3 / 3! = 120

 

 

콤비네이션은 Permutation 으로 풀어서 푸는것이 좋다

 

 

 

 

ex) 서로다른 5권의 책이 있는데 그 중 2권을 책을 가지고 가고 싶을때?

 

5C2 = 5P2 / 2! = 10

 

 

 

외우편 편합 조합들

 

 

문제 )

100명이 있는데 도시락이 만원이다

 

100명의 도시락을 사려 했더니 98만원 밖에 없다

 

이때 98명을 뽑는 경우의 수는?

 

 

100C98 = 100P98 / 98!

 

그런데 계산하기 너무 많다 이럴때

 

 

단) n은 r 보다 크거나 같다는 조건이 있습니다

 

 

nCr = nC(n-r) 과 같다는 조합의 성질이 있다 (계산해보면 같다는 것을 알수 있습니다)

 

즉 100C98 = 100C2

 

100*99/2 = 4950

 

 

 

[특징]

 

0! = 1

nP0 = 1 (약속임 이건 그냥 정의)

nC0 = 1 , n 명중에 한명도 안뽑은 것도 개수를 쳐서 1이라 함

 

nP1 =n

nC1 = n

 

 

그래서 아래와 같은 특성이 생기게 됩니다

이 성질을 잘 생각하면서 보면 4C2 를 기준으로 대칭이 된다는 것을 알 수 있습니다

 

 

 

 

 

 

 

반응형

'수학 (Mathematics) > 확률과통계' 카테고리의 다른 글

원 순열  (0) 2023.01.06
조합 Top5 (1)  (0) 2023.01.06
순열 Top5 간단 정리  (0) 2023.01.06
조합(combination)  (0) 2012.11.02
순열 permutation  (0) 2012.11.02
반응형

 

이웃하도록 = 주머니

이웃하지 않도록 = 칸막이

남녀 교대로 = 남여 중 어떤 한쪽에서 한명이 적거나 또는 둘이 같거나, 2개 이상 적은 케이스는 수학에서 없다

~사이에 ~가 오도록 = Box치고 Box 계산 이후 Box도 줄을 서야 함으로 주머니 처럼 계산

적어도 한쪽 끝에 ~가 오도록 = 여사건

 

반응형

'수학 (Mathematics) > 확률과통계' 카테고리의 다른 글

원 순열  (0) 2023.01.06
조합 Top5 (1)  (0) 2023.01.06
조합 (Combination)  (0) 2023.01.06
조합(combination)  (0) 2012.11.02
순열 permutation  (0) 2012.11.02
반응형

 

command 패턴으로 서버에서 처리할 명령을 바로 처리하지 않고 우선 처리할 명령을 Queue 에 먾어 넣어 모아두었다가

이 방식은 서버가 처리 할수 있을때 처리 하는 방식으로 직접적으로 명령을 다수의 스레드가 한번에 처리 할때 lock 을 걸어야 하는 상황에서의 부하를 줄일 수 있다


 

설명

이전까지는 클라이언트 세션을 연결되자마자 바로 GameRoom의 List에 넣어주고 있었지만, 이번에는 JobQueue 개념을 이용해 Queue에 일단 GameRoom의 List에 넣어주는 작업을 '예약'해주고 있다.


코드

ServerCore

JobQueue.cs

public interface IJobQueue
    {
        void Push(Action job);
    }

    public class JobQueue : IJobQueue
    {
        Queue<Action> _jobQueue = new Queue<Action>();  
        object _lock = new object();
        bool _flush = false; // 큐에 쌓인걸 '자신이' 실행할건지. 누군가 하고 있으면 자신은 하지 않는다.

        public void Push(Action job)
        {
            bool flush = false;

            lock(_lock)
            {
                _jobQueue.Enqueue(job);
                if (_flush == false)
                    flush = _flush = true;

            }

            if (flush)
                Flush();
        }

        void Flush()
        {
            while(true)
            {
                // 하나씩 꺼내는 와중에도 다른 애가 Push해서 JobQueue에다 넣을 수 있기 때문에 Pop을 할 때 lock을 잡아줘야 한다.
                Action action = Pop();
                if (action == null)
                    return;

                action.Invoke();
            }
        }

        Action Pop()
        {
            lock(_lock)
            {
                if(_jobQueue.Count == 0)
                {
                    _flush = false;
                    return null;
                }
                return _jobQueue.Dequeue(); 
            }
        }
    }
public class Listener
	{
		Socket _listenSocket;
		Func<Session> _sessionFactory;

		public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 10)
		{
			_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			_sessionFactory += sessionFactory;

			// 문지기 교육
			_listenSocket.Bind(endPoint);

			// 영업 시작
			// backlog : 최대 대기수
			_listenSocket.Listen(backlog);

			for (int i = 0; i < register; i++)
			{
				SocketAsyncEventArgs args = new SocketAsyncEventArgs();
				args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
				RegisterAccept(args);
			}
		}
        
        ...

Server

GameRoom.cs

 class GameRoom : IJobQueue
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        JobQueue _jobQueue = new JobQueue();

        public void Push(Action job)
        {
            _jobQueue.Push(job);
        }

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            foreach (ClientSession s in _sessions)
                s.Send(segment);
            
        }

        public void Enter(ClientSession session)
        {
            _sessions.Add(session);
            session.Room = this;
        }

        public void Leave(ClientSession session)
        {
            _sessions.Remove(session);
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Push(() => Program.Room.Enter(this));
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Push(() => Room.Leave(this));
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		GameRoom room = clientSession.Room;

		// 할 일을 바로 해주는게 아니라 Push
		room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
	}
}
반응형

'서버(Server) > Server' 카테고리의 다른 글

동기화 처리 : 탈것  (0) 2023.03.18
TCP : 3 way handshake(연결), 4 way handshake(종료)  (0) 2023.03.06
채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
PacketSession  (0) 2022.12.29
반응형

설명

Server의 Main에서 Listener를 Init할때 ClientSession 객체를 바로 생성해주는 것이 아닌, SessionManager를 통해 Generate해서 모든 세션을 관리하는 방식을 채택했다.
세션마다 고유 번호를 할당하고 id를 키값으로 Dictionary에 ClientSession을 넣어서 생성, 찾기, 삭제를 할 때마다 lock을 걸어 동시 접근을 차단하는 식으로 구현했다.

서버에는 하나의 GameRoom이 존재하며, 하나의 GameRoom에는 List타입의 _sessions가 존재한다. 그리고 GameRoom에서 어떤 하나의 클라이언트가 메시지를 서버에게 전송하면 서버는 입장 중인 모든 세션 객체에 그 메시지를 Broadcast한다.

하나의 ClientSession이 서버와의 접속이 끊어질 경우, OnDisconnected함수가 호출되어 SessionManager.Remove와 해당 세션이 참여 중인 GameRoom이 있을 경우 GameRoom.Leave() 함수 호출을 통해 퇴장하도록 했다.

10명의 유저가 한 공간에 존재한다고 할때, 한 명의 유저가 메시지를 전송하면, 그 공간 안에 10명의 유저에게 뿌려줘야 한다. 그렇다면 10명의 유저가 동시에 메시지를 하나씩 전송한다면, 10*10=100번의 패킷을 전송해야한다는 말이다. 시간복잡도로 말하자면 O(n^2)가 된다. 그래서 n을 100, 1000으로 늘리면 늘릴수록 서버에 부담이 갈 수 있다.

코드를 그대로 실행해보면 Broadcast 부분에서 수많은 작업자 스레드가 lock에서 대기하고 있는 모습을 볼 수 있다. 하나의 쓰레드가 foreach문을 다 돌 때까지 다른 쓰레드가 대기를 할 수 밖에 없는 상황인데, 최악인 것은 쓰레드가 처리를 못하고 시간을 끌면 작업자 쓰레드를 새로 만들어버리는 악순환이 발생한다는 것이다.

따라서 모든 로직을 lock을 잡아 실행하는 것이 아니라 GameRoom에 Queue를 하나 만들고, 쓰레드들이 일감을 queue에 넣어두고 대기하지 말고 각자 할 일을 하러 가게끔 만들면 된다. 그런 큐를 JobQueue라고 한다.

 

 

 

코드

PDL.xml

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name="C_Chat">
	  <string name="chat"/>
  </packet>
  <packet name="S_Chat">
	<int name="playerId"/>
	<string name="chat"/>
  </packet>
</PDL>

Server

GameRoom.cs

 class GameRoom
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        object _lock = new object();

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            lock(_lock)
            {
                foreach (ClientSession s in _sessions)
                    s.Send(segment);
            }
        }

        public void Enter(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Add(session);
                session.Room = this;
            }
        }

        public void Leave(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Remove(session);
            }
        }
    }

SessionManager.cs

 class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        // 세션마다 고유 번호
        int _sessionId = 0;
        Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession> ();
        object _lock = new object ();   

        public ClientSession Generate()
        {
            lock(_lock)
            {
                int sessionId = ++_sessionId;

                ClientSession session = new ClientSession ();
                session.SessionId = sessionId;
                _sessions.Add(sessionId, session);

                Console.WriteLine($"Connected : {sessionId}");

                return session;
            }
        }

        public ClientSession Find(int id)
        {
            lock(_lock)
            {
                ClientSession session = null;
                _sessions.TryGetValue (id, out session);
                return session;
            }
        }

        public void Remove(ClientSession session)
        {
            lock(_lock)
            {
                _sessions.Remove(session.SessionId);
            }
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Enter(this);
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Leave(this);
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		clientSession.Room.Broadcast(clientSession, chatPacket.chat);
	}
}

DummyClient

Program.cs

class Program 
	{
		static void Main(string[] args)
		{
			// DNS (Domain Name System)
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			IPAddress ipAddr = ipHost.AddressList[0];
			IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

			Connector connector = new Connector();

			connector.Connect(endPoint, () => { return SessionManager.Inst.Generate(); }, 10); // 클라 숫자 10개 접속

			while (true)
			{
				try
				{
					SessionManager.Inst.SendForEach();
				}
				catch (Exception e)
				{
					Console.WriteLine(e.ToString());
				}

				Thread.Sleep(250);
			}
		}
	}

SessionManager.cs

  class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        List<ServerSession> _sessions = new List<ServerSession>();
        object _lock = new object();
        public void SendForEach()
        {
            lock (_lock)
            {
                foreach (ServerSession session in _sessions)
                {
                    C_Chat chatPacket = new C_Chat();
                    chatPacket.chat = $"Hello Server!!!";
                    ArraySegment<byte> segment = chatPacket.Write();

                    session.Send(segment);
                }
            }
        }

        public ServerSession Generate()
        {
            lock(_lock)
            {
                ServerSession session = new ServerSession();
                _sessions.Add(session);
                return session;
            }
        }
    }

PacketHandler.cs

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
		S_Chat chatPacket = packet as S_Chat;
		ServerSession serverSession = session as ServerSession;
		
        Console.WriteLine(chatPacket.chat);
	}
}

 

 

ref : https://velog.io/@fere1032/%EC%B1%84%ED%8C%85%ED%85%8C%EC%8A%A4%ED%8A%B8-1

반응형
반응형

아래는 프로토버프에서 구조화된 구조체를 만들기 위한 프로토버프의 문법으로 만든 예시이다

 

위 처럼 패킷을 프로토 버프 문법으로 작성한다음 

protobuf 에서 제공하는 컴파일로 컴파일 하게 되면 직렬화하여 넣을 수 있는 구조가 만들어지고

다음 처럼 C++ 에서 사용 하여 데이터를 넣을 수 있다

 

add_buffs() 는 데이터를 내무적으로 추가하고 포인터를 얻어와 data 포인터를 통해서 데이터를 쓰는 방식이다

 

 

sendbuffer 에 패킷 데이터를 밀어 넣기

->Buffer());  //위 코드 중 짤린 부분

 

S_TEST 패킷을 만들어 데이터를 넣은 모습이다

 

 

실제 패킷 보내기처리

 

 

 

 

다음은 역질렬화방법이다

헤더 만큼은 건너 뛰고 S_TEST::pkt 에 데이터를 담아온다

 

 

 

 

 

 

 

 

 


 

프로토토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한, 직렬화 데이타 구조 (Serialized Data Structure)이다. C++,C#, Go, Java, Python, Object C, Javascript, Ruby 등 다양한 언어를 지원하며 특히 직렬화 속도가 빠르고 직렬화된 파일의 크기도 작아서 Apache Avro 파일 포맷과 함께 많이 사용된다.

(직렬화란 데이타를 파일로 저장하거나 또는 네트워크로 전송하기 위하여 바이너리 스트림 형태로 저장하는 행위이다.)

 

특히 GRPC 라는 네트워크 프로토콜의 경우 HTTP 2.0 을 기반으로 하면서, 메세지를 이 프로토콜 버퍼를 이용하여 직렬화하기 때문에, 프로토콜 버퍼를 이해해놓으면 GRPC를 습득하는 것이 상대적으로 쉽다.

 

프로토콜 버퍼는 하나의 파일에 최대 64M까지 지원할 수 있으며, 재미있는 기능중 하나는 JSON 파일을 프로토콜 버퍼 파일 포맷으로 전환이 가능하고, 반대로 프로토콜 버퍼 파일도 JSON으로 전환이 가능하다.

설치 및 구성

프로토콜 버퍼 개발툴킷은 크게 두가지 부분이 있다. 데이타 포맷 파일을 컴파일 해주는 protoc 와 각 프로그래밍 언어에서 프로토콜 버퍼를 사용하게 해주는 라이브러리 SDK가 있다.

 

protoc 컴파일러와, 각 프로그래밍 언어별 SDK는 https://github.com/google/protobuf/releases  에서 다운 받으면 된다.

 

protoc 는 C++ 소스 코드를 직접 다운 받아서 컴파일하여 설치할 수 도 있고, 아니면 OS 별로 미리 컴파일된 바이너리를 다운받아서 설치할 수 도 있다.  

 

각 프로그래밍 언어용 프로토콜 버퍼 SDK는 맞는 버전을 다운 받아서 사용하면 된다. 파이썬 버전 설치 방법은  https://github.com/google/protobuf/tree/master/python 를 참고한다.

이 글에서는 파이썬 SDK 버전을 기준으로 설명하도록 한다.

구조 및 사용 방법

프로토콜 버퍼를 사용하기 위해서는 저장하기 위한 데이타형을 proto file 이라는 형태로 정의한다. 프로토콜 버퍼는 하나의 프로그래밍 언어가 아니라 여러 프로그래밍 언어를 지원하기 때문에, 특정 언어에 종속성이 없는 형태로 데이타 타입을 정의하게 되는데, 이 파일을 proto file이라고 한다.

이렇게 정의된 데이타 타입을 프로그래밍 언어에서 사용하려면, 해당 언어에 맞는 형태의 데이타 클래스로 생성을 해야 하는데, protoc 컴파일러로 proto file을 컴파일하면, 각 언어에 맞는 형태의 데이타 클래스 파일을 생성해준다.

 

다음은 생성된 데이타 파일을 프로그래밍 언어에서 불러서, 데이타 클래스를 사용하면 된다.

 

 

 

 

 

 

 

 

 

 

ref : https://bcho.tistory.com/1182

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버 JobQueue 방식으로 부하 줄이기(command 패턴)  (0) 2023.01.06
채팅서버  (0) 2023.01.06
PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
반응형
  • 배열(예를들면 네트워크에서 받은 데이터를 저장한 버퍼 byte[] buffer)의 특정 위치에서 특정 크기만큼 참고하고 싶을 때 보통 새로 배열을 만든 후 복사해야 원하는 데이터만을 참조할 수 있다.
  • 그러나 ArraySegment를 사용하면 새로 배열을 만들지 않으면서 버퍼의 특정 데이터를 참조할 수 있다.
  • ArraySegment를 사용하여 1차원 배열의 랩퍼로 배열 내의 요소를 범위(시작 위치와 길이)를 지정하여 구별한다.
int[] ary1 = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// 배열을 나눈다. ary1의 범위만큼의 참조를 만든다.
ArraySegment<int> arySeg = new ArraySegment<int>(ary1, 2, 5);

// 나누어진 범위의 요소를 출력한다.
for (int i = arySeg.Offset; i < arySeg.Offset + arySeg.Count; i++)
{
	Console.WriteLine("{0}:{1}", i, arySeg.Array[i]);
}

아래와 같이 표시된다

2:2
3:3
4:4
5:5
6:6

 

ref : https://jacking75.github.io/csharp_ArraySegment/

반응형

'프로그래밍(Programming) > C#' 카테고리의 다른 글

Linq (1)  (0) 2023.04.08
async/await & 커피와 베이컨  (0) 2023.04.07
ThreadLocal<T> 와 AsyncLocal<T> 의 차이점  (0) 2022.12.29
Common Memory Leaks In C#  (0) 2022.12.04
C# 에서 Dispose  (0) 2022.12.04
반응형

픽킹이란 2D 스크린 좌표를 3D 좌표로 변경한 후 동일한 공간 내에서 교차 판정 하는 것이다.

 

여기서 중요한 것은 2D 좌표를 3D 좌표로 변경한다는 것이고, 동일한 공간! 내에서 교차판정한다는 것이다.

동일한 공간이 중요하다.

지형의 좌표가 존재하는 공간이 월드 공간이기 때문에 반드시 월드 공간으로 바꿔줘야한다.

 

과정을 간단히 말하자면,

1. 윈도우 좌표를 3D 좌표로 변경시킨다.

2. 폴리곤과 충돌체크를 한다. (IntersectTriangle)

이다.

 

픽킹이 처음에 어렵게 느껴지는 이유는 이 IntersectTriangle 함수가 프로그램을 위해 최적화된 함수로 몇몇 과정들이 축소되어있기 때문이다.

 

 

( Picking에 관한 코드는 Dx SampleBrowser에도 존재 하니 그걸 참고하세용 ~ 저도 그걸 참고해서 했어용>,<! )

 

 

먼저 2D 좌표에서 3D 공간 Ray를 뽑아내는 것을 보겠다.

 

 

<설명>

 

1) 윈도우 좌표를 알아낸 후

 

2) 공식을 통해 3D 좌표로 변경시킨다.

 

3) m_vOrig 는 카메라의 위치이고, m_vDir은 Ray의 방향이다. 

m_vDir에 3D 벡터 Ray를 설정한다.

 

4) 카메라 공간 좌표를 월드 공간 좌표로 변경하기 위해 뷰행렬의 역행렬을 곱한다.

여기서 주의해야 할 점은 m_vOrig는 D3DXVec3TransformCoord이고, m_vDir은 D3DXVec3TransformNormal라는 점이다.

왜냐하면 Coord는 위치를 변환하는 것이고, Normal은 벡터를 변환하는 것이다.

 

5) m_vDir 을 정규화하면 Ray가 완성된다.

 

 

이제 폴리곤과 충돌체크를 하는 IntersectTriangle 함수를 알아보겠다.

앞서 말했듯 이 함수는 몇몇 식이 축약됬기 때문에 이해하기가 어렵다.

 

BOOL IntersectTriangle

(D3DXVECTOR3& v0,D3DXVECTOR3& v1,D3DXVECTOR3& v2, FLOAT* t,FLOAT* u,FLOAT* v);

 

m_vOrig는 월드 공간 상의 카메라의 원점이고, m_vDir은 Ray의 방향이다.

v0, v1, v2  삼각형 정점의 위치이다. 

t vOrig에서 v0까지의 거리이다.

u v0에서 v1의 비율이고, v v0에서 v2의 비율이다

 

 

 

점 p(벡터 p)가 v0,v1,v2내에 있는지 판단하는 방법은 벡터로 한다. 

만약, 삼각형을 △OAB라고 할 때, 벡터 P가 삼각형 내에 있을 조건

 

1) u ≥ 0

2) v ≥ 0

3) u + v ≤ 1 

 

이다. 때문에 u와 v를 구해 점 p가 폴리곤 내에 있는지 판단한다. 

 

 

 

 

위 그림을 보면

 

v = |Ob|/|OB| 이다. (왜냐하면, |OB|= |Ob| * v 니까.)

 

nA라는 A의 법선벡터를 구한다.

 

 

 

v = |Ob|/|OB|

 

이 식의 분모와 분자에 nAcosΘ 를 곱한다.

 

v = |Ob| nAcosΘ /|OB| nAcosΘ 

 

이것은 내적을 풀어쓴 것으로 

 

v = Ob · nA / OB · nA 

라고 할 수 있다.

 

그런데 nA에 투영했을 때, OP와 Ob의 정사영이 같으므로 

 

v = OP · nA / OB · nA

 

라고 할 수 있다. 이와 마찬가지로 

 

u = OP · nB / OA · nB

 

라고 할 수 있다.

 

 

N을  v0와 v2의 평면의 법선벡터라고 하자.

여기서 (vOrig – v0) · N = (P – v0) · N 의 정사영이 같음을 볼 수 있다.

따라서 N = vDir × (v2 – v0) 이다. 

 

 

t는 거리라고 했다.

 

tvec = vOrig – v0

e1 = v1 – v0

e2 = v2 – v0

 

라고 하자.

 

u = OP · nB / OA · nB

 

에서 OP는 거리이므로 tvec으로 고칠 수 있다. 그리고 nB는 위에서 법선벡터 N을 구했으므로 vDir x e2 라고 할 수 있다.

 

따라서, 

 

u = tvec · (vDir x e2) / e1 · (vDir x e2) 
v = tvec · (vDir x e1) / e2 · (vDir x e1) 
 
이렇게 구한 u와 v를 통해 p가 삼각형 내에 있는지 판단하는 것이다.
 
IntersectTriangle은 이 식을 풀어서 쓴 것이다.
 
마지막으로 거리 t 를 구하기 위해서 분모를 fDet로 놓는다.
 
fDet = e1 · (vDir x e2) = vDir · (e1 x e2)
 
[ fDet 가 0이면, Ray가 같은 평면이고, 0보다 작으면 뒤에서 쏜 Ray, 0보다 크면 앞에서 쏜 Ray이다. ]
[평면의 방정식이 0보다 크면 Ray가 앞에 있는 것이다.]
 

 

 
위의 식에서
 
|P - vOrig | = t * |vDir |
 
이다. 따라서
 
t = |P - vOrig |/ |vDir |
 
분모, 분자에 |N|cosΘ 를 곱하면
 
t = |P - vOrig | |N|cosΘ  / |vDir | |N|cosΘ 
 
이고, 이것은 내적을 풀어쓴 것이므로 정리하면
 
t = (P - vOrig) · N / vDir · N 
 
이다. 근데 (P - vOrig)과 –tvec의 법선벡터 N에 대한 정사영이 같으므로
 
t = -tvec · N / vDir · N 
 
이다.
 

 

 

ref : https://showmiso.tistory.com/140

반응형
반응형

멀티 스레드 환경에서 각 스레드별로 하나의 공통 데이터에 접근하여 처리 될때 스레드 마다 원자성을 보장하면서
데이터를 다뤄야 하는 상황이 발생할 수 있습니다.

이때 제공 되는것이 Thread Local Storage(TLS) 변수라는 것이 있습니다.

쉽게 말해 tls변수는 스레드별 고유한 데이터를 저장(?)할 수 있는 공간이라고 이해 하면 됩니다. 또한 이 tls변수는(이하 거론되는 ThreadLocal<T> AsyncLocal<T> 동일) 자체적으로 데이터 원자성이 보장되기에 lock없이 접근하여 사용이 가능합니다.

닷넷 C#에서는 이것을 System.ThreadStaticAttribute 어트리뷰트 클래스로 제공 하고 있으며
System.Threading.ThreadLocal<T> 그리고 System.Threading.AsyncLocal<T> 클래스로 제공하고 있고, C++에서는 __declspec(thread)로 사용 가능합니다.

System.Threading.ThreadLocal<T> 같은 경우 System.ThreadStaticAttribute 보다 변수 초기 방법을 제공하고 있어 [ThreadStatic] 어트리뷰트 보다 좀 더 최신 기능을 제공합니다.

이번 포스트는 System.Threading.ThreadLocal<T>  System.Threading.AsyncLocal<T> 가 서로 어떤 부분에 차이점이 있는지에 대한 내용 입니다.

자 그럼 어떻게 다른지 살펴보겠습니다.

ThreadLocal<T> AsyncLocal<T> 차이점

위 tls역할을 하는 두 클래스의 큰 차이점은 스레드풀을 사용하는 스레드 환경에서 차이점이 있습니다.

우선 System.Threading.ThreadLocal<T> 은 해당 스레드가 스레드풀에 반환되어도 해당 데이터는 항상 유지 되어 집니다.

private static ThreadLocal<int> _tl = new ThreadLocal<int>();

private static async Task WorkAsync(int i)
{
  if(_tl.Value == 0)
  {
    _tl.Value = i;
    Console.WriteLine($"[새로운 값] Thread Id : {Environment.CurrentManagedThreadId} - tl Value : {_tl.Value}");
  }
  else
  {
    Console.WriteLine($"[기존 값] Thread Id : {Environment.CurrentManagedThreadId} - tl Value : {_tl.Value}");
  }
  Console.WriteLine("-----------------------------------------");
}

위 처럼 System.Threading.ThreadLocal<T> 을 사용하는 메서드를 스레드로 호출해 봅니다. 그리고 차이점을 확실히 보기 위해 스레드풀의 스레드 개수 제한을 4개로 설정해 보았습니다.

// ThreadPool의 개수를 4개로 임의 제한
ThreadPool.SetMinThreads(4, 4);
ThreadPool.SetMaxThreads(4, 4);

for (int i = 1; i < 11; i++)
{
  await Task.Run(() => WorkAsync(i));
}

스레드를 사용하여 WorkAsync() 메서드를 10번 호출하였고 파라메터로 tls값을 동시에 넘겨 주었습니다.

결과는 어떻게 나올까요?

위 처럼 처음 스레드가 사용되어서 tls값이 설정 되고 해당 스레드가 반환된 이후 다시 같은 스레드가 사용 되었을때
tls값이 처음 설정 된 값으로 유지 되고 있는 걸 확인 할 수 있습니다.

반면,

AsyncLocal<T> 는 어떻게 처리 되는지 위 코드에서 System.Threading.ThreadLocal<T> 부분만 바꿔서 실행 시켜 보겠습니다.

결과를 보니 위 결과와는 다르게 사용된 스레드가 스레드풀에 반환될때는 값이 초기화가 되는 걸 볼 수 있습니다.

 

ref :https://blog.arong.info/c%23/2022/01/14/C-ThreadLocal-T-%EC%99%80-AsyncLocal-T-%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90.html

반응형

'프로그래밍(Programming) > C#' 카테고리의 다른 글

async/await & 커피와 베이컨  (0) 2023.04.07
C# - ArraySegment  (0) 2023.01.04
Common Memory Leaks In C#  (0) 2022.12.04
C# 에서 Dispose  (0) 2022.12.04
ReaderWriterLockSlim  (0) 2022.11.27
반응형

 

결과 화면인데 패킷중 첫번째를 size로 받아와 size 바이트 만큼 받아 왔으면 그다음 id 그리고 데이터를 순차적으로 읽어온다, 여기서 패킷은 TCP 로 전송 될경우 데이터 한 덩어리중 일부가 지연으로 나중에 올경우를 대비해 

 

패킷 중 사이즈가 2바이트라면 2바이트를 먼저 받고 해당 사이즈를 먼저 추출한다음 전체 바이트 만큼 모두 도착 할때까지 수신을 반복한다, 그리고 모두다 데이터를 수신 받았다면 이후부터 패킷의 내용들을 까서 데이터로 받는 처리를 하게 된다

 

TCP 는 데이터의 순서가 보장 됨으로 나중에 왔다 하더라고 모두 전송된 데이터의 비트들은 정상적으로 정렬되어 있다

 

 

public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq:
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

 

 

Thread Local Storage(TLS) 

TLS : 스레드 빌딩 블록으로 스레드 마다 고유한 메모리 영역에 할당되게 된다

public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

 

 

 


코드

ServerCore

Session.cs

namespace ServerCore
{
	public abstract class PacketSession : Session
	{
		public static readonly int HeaderSize = 2;

		// size = size 포함한 전체 패킷 크기
		// [size(2)][packetId(2)][ ... ][size(2)][packetId(2)][ ... ]
		public sealed override int OnRecv(ArraySegment<byte> buffer) // 오버라이드 다시 불가
		{
			int processLen = 0;

			while (true)
			{
				// 최소한 헤더는 파싱할 수 있는지 확인
				if (buffer.Count < HeaderSize)
					break;

				// 패킷이 완전체로 도착했는지 확인
				ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ushort
				if (buffer.Count < dataSize)
					break;

				// 여기까지 왔으면 패킷 조립 가능. new 사용했다고 힙에다 할당해주는게 아니라 스택 복사
				OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
				
				processLen += dataSize;
				// [size(2)][packetId(2)][ ... ] 다음 부분으로 위치 변경
				buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
			}

			return processLen;
		}

		public abstract void OnRecvPacket(ArraySegment<byte> buffer);
	}

	public abstract class Session
	{
		Socket _socket;
		int _disconnected = 0;

		RecvBuffer _recvBuffer = new RecvBuffer(1024);

		object _lock = new object();
		Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
		List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
		SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
		SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

		public abstract void OnConnected(EndPoint endPoint);
		public abstract int  OnRecv(ArraySegment<byte> buffer); // 얼마만큼 데이터를 처리했는지 리턴
		public abstract void OnSend(int numOfBytes);
		public abstract void OnDisconnected(EndPoint endPoint);

		public void Start(Socket socket)
		{
			_socket = socket;

			_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
			_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

			RegisterRecv();
		}

		public void Send(ArraySegment<byte> sendBuff)
		{
			lock (_lock)
			{
				_sendQueue.Enqueue(sendBuff);
				if (_pendingList.Count == 0)
					RegisterSend();
			}
		}

		public void Disconnect()
		{
			if (Interlocked.Exchange(ref _disconnected, 1) == 1)
				return;

			OnDisconnected(_socket.RemoteEndPoint);
			_socket.Shutdown(SocketShutdown.Both);
			_socket.Close();
		}

		#region 네트워크 통신

		void RegisterSend()
		{
			while (_sendQueue.Count > 0)
			{
				ArraySegment<byte> buff = _sendQueue.Dequeue();
				_pendingList.Add(buff);
			}
			_sendArgs.BufferList = _pendingList;

			bool pending = _socket.SendAsync(_sendArgs);
			if (pending == false)
				OnSendCompleted(null, _sendArgs);
		}

		void OnSendCompleted(object sender, SocketAsyncEventArgs args)
		{
			lock (_lock)
			{
				if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
				{
					try
					{
						_sendArgs.BufferList = null;
						_pendingList.Clear();

						OnSend(_sendArgs.BytesTransferred);

						if (_sendQueue.Count > 0)
							RegisterSend();
					}
					catch (Exception e)
					{
						Console.WriteLine($"OnSendCompleted Failed {e}");
					}
				}
				else
				{
					Disconnect();
				}
			}
		}

		void RegisterRecv()
		{
			_recvBuffer.Clean(); // 커서가 너무 뒤로 가있는 상태 방지
			// 유효한 범위 설정. 다음으로 버퍼를 받을 공간 Set.
			ArraySegment<byte> segment = _recvBuffer.WriteSegment;
			_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // count=freesize, 이만큼 받을 수 있다.

			bool pending = _socket.ReceiveAsync(_recvArgs);
			if (pending == false)
				OnRecvCompleted(null, _recvArgs);
		}

		void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
		{
			if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
			{
				try
				{
					// Write 커서 이동
					if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
					{
						Disconnect();
						return;
					}

					// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다
					int processLen = OnRecv(_recvBuffer.ReadSegment);
					if (processLen < 0 || _recvBuffer.DataSize < processLen)
					{
						Disconnect();
						return;
					}

					// Read 커서 이동
					if (_recvBuffer.OnRead(processLen) == false)
					{
						Disconnect();
						return;
					}

					RegisterRecv();
				}
				catch (Exception e)
				{
					Console.WriteLine($"OnRecvCompleted Failed {e}");
				}
			}
			else
			{
				Disconnect();
			}
		}

		#endregion
	}
}

SendBuffer

namespace ServerCore
{
	public class SendBufferHelper
	{
		// 전역이지만 내 쓰레드에서만 사용할 수 있음.
		public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; }); // 처음 만들어질때 null만 리턴하도록

		public static int ChunkSize { get; set; } = 4096 * 100;

		public static ArraySegment<byte> Open(int reserveSize)
		{
			if (CurrentBuffer.Value == null) // SendBuffer 한번도 사용 안한 상태
				CurrentBuffer.Value = new SendBuffer(ChunkSize);

			if (CurrentBuffer.Value.FreeSize < reserveSize)
				CurrentBuffer.Value = new SendBuffer(ChunkSize); // 기존 청크 없앤 후 새롭게 할당

			return CurrentBuffer.Value.Open(reserveSize);
		}

		public static ArraySegment<byte> Close(int usedSize)
		{
			return CurrentBuffer.Value.Close(usedSize);
		}
	}

	public class SendBuffer
	{
		// RecvBuffer처럼 Clean이 없는 이유는 내가 사용이 끝난 부분 이전 부분을 다른 세션에서 Session클래스의 _sendQueue에 넣어논 상태일 수 있기 때문에, 즉 누군가 앞부분을 참조 중인 상태일 수 있기 때문에 재위치 시킬 수 없다.
		// 패킷에 가변인자 들어가면 크기 예측하는게 까다롭기 때문에, 크게 할당 받아놓고 자르는 방식
		// [][][][][][][][][u][] : ChunkSize = 4096 * 100
		byte[] _buffer;
		int _usedSize = 0;

		public int FreeSize { get { return _buffer.Length - _usedSize; } }

		public SendBuffer(int chunkSize)
		{
			_buffer = new byte[chunkSize];
		}

		public ArraySegment<byte> Open(int reserveSize) // 요구할 예약 공간
		{
			if (reserveSize > FreeSize)
				return null;

			// 예약공간이기 때문에 usedSize 이동 x
			return new ArraySegment<byte>(_buffer, _usedSize, reserveSize); // usedSize 위치 포함해서 reserveSize만큼
		}

		public ArraySegment<byte> Close(int usedSize) // 예약 사이즈는 3이라도 실제로 2개 사용되었다면 2만큼 범위 리턴
		{
			ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
			_usedSize += usedSize;
			return segment;
		}
	}
}

Server

ClientSession.cs

namespace Server
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ClientSession : PacketSession
	{
		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			Thread.Sleep(5000);
			Disconnect();
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq: // required
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

		// TEMP
		public void Handle_PlayerInfoOk(ArraySegment<byte> buffer)
		{

		}
        
        ...
}

DummyClient

ServerSession.cs

namespace DummyClient
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ServerSession : Session
	{
		// unsafe : 포인터 조작. 속도가 빠른 장점
		static unsafe void ToBytes(byte[] array, int offset, ulong value) 
		{
			fixed (byte* ptr = &array[offset])
				*(ulong*)ptr = value;
		}

		static unsafe void ToBytes<T>(byte[] array, int offset, T value) where T : unmanaged
		{
			fixed (byte* ptr = &array[offset])
				*(T*)ptr = value;
		}

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			
			// 패킷의 크기는 아래에서 정해짐
			PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };


			// 보낸다
			for (int i = 0; i < 5; i++)
			{
				ArraySegment<byte> s = SendBufferHelper.Open(4096);
				//byte[] size = BitConverter.GetBytes(packet.size);
				//byte[] packetId = BitConverter.GetBytes(packet.packetId);
				//byte[] playerId = BitConverter.GetBytes(packet.playerId);

				ushort size = 0; // int로 만들면 TryWriteBytes의 버전 중 int 버전으로 넘겨줌. ToUInt16
				bool success = true;
			
				// destination(Span<byte>)의 공간보다 value의 크기가 크다면 실패
				// offset+size부터 offset-size만큼의 범위
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.packetId);
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.playerId);
				size += 8;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), size);

				ArraySegment<byte> sendBuff = SendBufferHelper.Close(size); // 실질적으로 보내줄 버퍼.

				if (success)
					Send(sendBuff);
			}
		}
        
        ...
}

 

ref : https://velog.io/@fere1032/SendBuffer-PacketSession

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
반응형

TCP와 UDP 의 차이

중요한 차이점은, TCP는 전송순서가 보장 된다는 것과 받는 측에서 전체 메세지중 일부만 받을 수 있다면 일부만 보내고 나머지 일부는 나중에 보낼 수 있다는 TCP 의 성질이 있다

UDP 는 전송 일부를 보내지 않고 전체다 보내거나 보내지 않거나 하는 특징이 있다

 

릴라이어블 UDP : 

UDP를 사용하는데 TCP와 유사하게 분실이 일어날때 TCP 처럼 처리 하거나 전송순서를 보장해주는 기능이 있는 UDP를 개조한 방식을 말한다

 

UDP 사용 => 캐릭터 위치 : 포지션 위치는 계속 보내주기 때문에 중간에 1,2개 정도 유실 된다 해도 새로운 위치로 바로 갱신되는것이 일반 적임으로

 

MMORPG 는 TCP 를 주료 사용하는데 대체적으로 속도가 엄청 중요한 요소는 아니기 때문

반응에 민감한 FPS 경우에는 UDP를 많이 사용한다

반응형
반응형

_pendingList.Count == 0 이면 보내기를 시작한다는 얘기는 현재 send 를 할때 보낼 데이터중 큐에 있는 것을 모두 뽑아와 한번에 보내는 한번의 send 처리가 이뤄지는 단계라는 것이다

이것으 ArraySegment 를 통해 버퍼의 범위를 지정하여 리스트 혀앹로 담아서

_sendArgs.BufferList = 에 할당해 주면 되고 보낼때는 이 전과 동일하게 SendAsync 를 호출해주면 한번에 보내지게 된다

 

그래서 sendAsync 를 할때 여러 리스트로 구성된 데이터를 한번에 보내게 되고 보내는 처리가 완료되면

OnSendCompleted 가 callback 으로 호출 되게 된다

 

이때 callback 은 별도의 소켓 스레드에 의해서 호출 된 것임으로 sendAsync 를 호출한 후와 OnSendCompleted  이 함수가 호출되기 전 이 사이에서 다른 소켓에 의해 _sendQueue 에 데이터가 채워질수 있기 때문에

 

OnSendCompleted  에서는 _sendQueue 에 데이터가 채워진게 있다면 다시 send 하는 처리 => 여기선 RegisterSend() 를 호출하여 send 하는 처리를 다시 반복시켜준다 : 데이터를 보내야 할 것이 있었던것이기 때문에 보내줘야 함

 

 

 

 

 

 

고려해 봐야 할 점

  • 생각해 봐야 할것은 만약 유저가 엄청 많이 몰려있어서 많은 데이터를 다른 유저간에 서로 보내줘야 할때
    이렇게 요청이 올때마다 보내는 것 보단 일정 패킷을 모아서 다른 유저들에게 한번에 보내주는 것이 더 효과 적일 수도 있다
  • 또는 어떤 유저가 악의적으로 요청(데이터)을 계속 보내서 DDOs 서버에서 이것을 감당하기 힘든 상태가 되는 경우가 발생할 수 있음으로 비정상적으로 많은 데이터를 어떤 클라에서 보내고 있다면 해당 연결을 Disconnect 처리해서 이것을 막아야한다

 

 

 

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
반응형

구문

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }

TagName1 Value1을, TagName2 Value2를 지정합니다. 태그를 원하는 수만큼 지정할 수 있습니다.

세부 정보

태그는 기본적으로 키-값 페어입니다. 서브셰이더 내에서 태그는 서브셰이더의 다른 파라미터 및 렌더링 순서를 결정하기 위해 사용합니다. Unity가 인식하는 다음 태그는 반드시 패스가 아닌 서브셰이더 섹션 내에 위치해야 합니다.

Unity가 인식하는 빌트인 태그 외에도 Material.GetTag 함수를 사용하여 자신의 태그를 사용하고 쿼리할 수 있습니다.

렌더링 순서 - Queue 태그

Queue 태그를 사용하여 오브젝트를 드로우 처리할 순서를 결정할 수 있습니다. 셰이더는 자신의 오브젝트가 어떤 렌더 대기열에 속하는지를 결정하며 이 방식으로 모든 불투명 오브젝트가 그려진 후에 모든 투명 셰이더가 그려지도록 할 수 있습니다.

사전 정의된 렌더 대기열은 4개가 있으며 사전 정의된 대기열 사이에 추가로 대기열을 넣을 수 있습니다. 사전 정의된 대기열은 다음과 같습니다.

  • Background- 이 렌더 대기열은 다른 대기열보다 먼저 렌더링됩니다. 보통 배경에 배치해야 할 항목에 사용합니다.
  • Geometry (디폴트) - 대부분 오브젝트에 사용합니다. 불투명 지오메트리가 이 대기열을 사용합니다.
  • AlphaTest - 알파 테스트된 지오메트리가 이 대기열을 사용합니다. 알파 테스트된 오브젝트는 모든 솔리드 오브젝트를 드로우한 후에 렌더링하는 것이 훨씬 효율적이므로 이 대기열은 Geometry 대기열과는 별도로 존재합니다.
  • Transparent - 이 렌더 대기열은 Geometry  AlphaTest 후에 뒤에서 앞의 순서로 렌더링됩니다. 알파 블렌드된 모든 항목(즉, 뎁스 버퍼에 기록하지 않는 셰이더)은 여기 속해야 합니다(유리, 파티클 효과 등).
  • Overlay - 이 렌더 대기열은 오버레이 효과용입니다. 마지막에 렌더링되는 모든 항목이 속해야 합니다(예: 렌즈 플레어 등).
Shader "Transparent Queue Example"
{
     SubShader
     {
        Tags { "Queue" = "Transparent" }
        Pass
        {
            // rest of the shader body...
        }
    }
}

투명 대기열에서 렌더링하는 방법을 보여주는 예제

특수 용도를 위해 대기열 사이에 대기열을 둘 수 있습니다. 내부적으로 각 대기열은 정수 인덱스로 대표됩니다. 예를 들어, Background는 1000, Geometry는 2000, AlphaTest는 2450, Transparent는 3000, Overlay는 4000입니다. 셰이더가 대기열을 다음과 같이 사용한다면

Tags { "Queue" = "Geometry+1" }

렌더 대기열 인덱스가 2001이 되므로(Geometry+1), 이 오브젝트는 모든 불투명 오브젝트가 렌더링된 후에 렌더링되고 투명 오브젝트보다는 먼저 렌더링됩니다. 이 방법은 일부 오브젝트가 항상 다른 오브젝트 세트 사이에 그려지도록 하고 싶을 때 유용합니다. 예를 들어, 투명한 물은 불투명한 오브젝트보다는 나중에 그려야 하지만 투명한 오브젝트보다 먼저 그려야 합니다.

2500 이하의 대기열(“Geometry+500”)은 “불투명”으로 간주되며 최상의 성능을 위해 오브젝트의 드로우 순서를 최적화합니다. 더 높은 인덱스의 렌더링 대기열은 “투명 오브젝트”로 간주되며 거리에 따라 오브젝트를 정렬합니다. 즉, 가장 먼 오브젝트부터 렌더링하여 가장 가까운 오브젝트를 맨 마지막에 렌더링합니다. 스카이박스는 모든 불투명 오브젝트와 투명 오브젝트 사이에 그려집니다.

 

 

 

ref : https://docs.unity3d.com/kr/530/Manual/SL-SubShaderTags.html

반응형
반응형

send 함수를 멀티스레드로 보내는데 우선 queue 에 담아 놓고

현재 보낼 수 있으면 큐에 있는 내용을 보내고 보낼수 없는 상황이라면 큐에 보낼 내용을 담아 놓기만한다

 

보낼수 있는 상황과 보낼수 없는 상황은

우선 한번 SendAsync 를 할때 동시에 할수는 없고 한번에 하나만 보낼수 있음으로 SendAsync 가 완료 될때까지 다른 스레드가 SendAsync 를 하지 못하게 데이터만 큐에 쌓아 놓게 한뒤 SendAsync 처리가 완료되는 OnSendCompleted 가 호출 되면 SendAsync  처리를 마무리 한다

 

그런데 OnSendCompleted 가 호출 될때 SendAsync 의 한번의 루틴이 끝나기 전에 데이터가 쌓인게 있다면

다시 한번 RegisterSend 를 호출하여 기존에 쌓여 있는것을 다시 send 해주는데

이렇게 하는 이유는 SocketAsyncEventArg 를 여러개 생성하지 않고 재사용하기 위해 이런 처리를 하는건데

이유는 좀금이라도 send 할때의 비용을 아끼기 위해서이다 (유저가 많아질 수록 이 부하도 점점 쌓이기 때문)

 

 

session 코드

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false;
        object _lock = new object();

        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

        }

        
        public void Send(byte[] sendBuff)
        {
            lock(_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pending == false)
                {
                    RegisterSend();
                }
            }
           
            
        }


        void RegisterSend()
        {
            _pending = true;
            byte[] buff = _sendQueue.Dequeue();
            
            _sendArgs.SetBuffer(buff, 0, buff.Length);
            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
            
        }

        //다른 소켓에 의해 OnSendCompleted 호출 될 수 있음으로 lock 을 걸어준다
        private void OnSendCompleted(object value, SocketAsyncEventArgs args)
        {

            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();     //send를 보내는 처리 과정 사이에 다른 스레드에 의해서 send 가 보내진게 있다면 다시 send 를 보내도록 한다
                        }
                        else
                        {
                            _pending = false;
                        }
                        
                    }
                    catch (System.Exception ex)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {ex}");
                    }
                }
                else
                {
                    DisConnect();
                }
            }

        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }


        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {

            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                DisConnect();
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

서버 시작 부분

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리슨 부분 ( 이전에 있던 Listen 과 유사하다)

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            //Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가
            //대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            //SocketAsyncEventArgs 가 하나일때
            //AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
            //하나하고 그다음 하나 step by step
            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

클라이언트 더미 코드 또한 동일하다 : 좀 더 다양하게 서버로 접속하게 처리해야 하지만 우선 send 에 만 비동기이면서 멀티스레드로 보낼수 있는 구조로 만드는 것에 초점을 맞춘다

 

반응형
반응형

 

서버코어 부분인데 이 부분에서 listen과 receive 를 하기 위한 초기호및 보내기 데이터 설정 등을 한다

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

리슨을 담당하는 코드

 

Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가 대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

세션코드로 클라로부터 들어온 데이터를 비동기로 받는 로직이다

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;
        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }

        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

 

 

서버와 클라간의 테스트를 위한 더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

bool pending = _listenSocket.AcceptAsync(args);

SocketAsyncEventArgs 가 하나일때(즉 n 번 선언해 놓고 Register 를 n 번 한게 아닌 n = 1 일때)
AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
하나하고 그다음 하나 step by step

 

 

클라 접속이 이뤄지더라도 AcceptAsync를 명시적으로 호출해줘야 OnAcceptCompleted가 실행이 된다
즉, AcceptAsync는 일종의 입장 허락의 개념인데 
그 전에 접속을 희망한 클라들은 대기열(큐)에 입장 대기를 하고 있게 되고
OnAcceptCompleted를 호출해서 입장 관련 처리를 다 끝낸 다음,
마지막에 RegisterAccept를 다시 호출해서 다음 입장을 받아주기 때문에
동시에 여러 쓰레드가 OnAcceptCompleted를 실행할 수 없기도 하다



 

Accepting connections asynchronously gives you the ability to send and receive data within a separate execution thread. Before calling the AcceptAsync method, you must call the Listen method to listen for and queue incoming connection requests.

 

 

 

결과 화면 : 더미 클라에서 send 를 연달아 다섯번정도하기 때문에 서버에서 비동기 receive 로 5번이 처리 되는것 것을 볼 수 있다, 콘솔 창에서 보면 알 수 있듯이 receive 가 동시에 멀티 스레드 처럼 5번 처리 되는것이 아니고 한번에 한번만 처리 되는 것이라 볼 수 있는데 현재 Recv 코드는 오직 1번에 1개의 쓰레드만 접근 할 수 있다

 

다시 말해 최초에 RegisterRecv 를 1개만 걸어 놨기 때문에 Register->Completed  .. 과정도 오로지 한번에 1개만 일어난다

 

 

 

ref : https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.acceptasync?view=netframework-4.7.2&f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Net.Sockets.Socket.AcceptAsync)%3Bk(TargetFrameworkMoniker-.NETFramework%2CVersion%253Dv4.7.2)%3Bk(DevLang-csharp)%26rd%3Dtrue 

 

Socket.AcceptAsync Method (System.Net.Sockets)

Accepts an incoming connection.

learn.microsoft.com

 

반응형
반응형

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                //클라이언트로부터 받아오는 처리
                byte[] recvBuff = new byte[1024];
                int recvBytpes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                Console.WriteLine($"[From Client] {recvData}");


                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                clientSocket.Send(sendBuff);

                clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                clientSocket.Close();   //클라와 서버간의 연결 끊기
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리스터 코드 : 비동기 accept

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
                    int bytesSent = socket.Send(sendBuff);

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

AcceptAsync : 비동기 방식으로 클라의 접속을 말한다

AcceptAsync 로 처리하면 서버에서 클라의 접속을 받아 Accept() 함수 처럼 accept 하게 될때 해당 라인에서 무한 대기 하지 않고 클라와 통신하기 위한 전용 socket 을 만들어 리턴하고 서버에선 바로 다음 로직을 처리 할 수 있게 된다

 

bool pending = _listenSocket.AcceptAsync(args);

 

 

결과화면

반응형

'서버(Server) > Server' 카테고리의 다른 글

멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
기본적인 소켓 프로그래밍  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
반응형

 

C# 으로 서버와 클라이언트 기본적인 연결 및 string 데이터 전송 코드이다

 

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {
        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                listenSocket.Bind(endPoint);        //소켓에 ip 와 포트 할당


                //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
                listenSocket.Listen(10);


                while (true)
                {
                    Console.WriteLine("Listening .... ");

                    //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
                    //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
                    Socket clientSocket = listenSocket.Accept();


                    //클라이언트로부터 받아오는 처리
                    byte[] recvBuff = new byte[1024];
                    int recvBytpes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                    Console.WriteLine($"[From Client] {recvData}");


                    //클라이언트로 보내는 처리
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                    clientSocket.Send(sendBuff);

                    clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                    clientSocket.Close();   //클라와 서버간의 연결 끊기

                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
            socket.Connect(endPoint);
            Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


            //서버로 보낸다
            byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
            int bytesSent = socket.Send(sendBuff);

            //서버에서 받는다
            byte[] recvBuff = new byte[1024];
            int recvBytes = socket.Receive(recvBuff);
            string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
            Console.WriteLine($"received : {recvData}");

            //서버와 통신을 위해 생성했던 소켓 종료 처리
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            

        }
    }
}

 

결과 창

 

서버에서는 클라에서 보낸 데이터를 보여주고

클라에서는 서버로부터 받은 데이터를 뿌려주고 종료한다

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
반응형

소켓 통신은

Bind 로 ip 와 port 를 설정한 뒤에 

Listen 으로 한번에 몇개의 접속을 받을 수 있을지를 설정하면서 대기하게 하고

Accept 하면서 대기 하고 있다가 클라로부터 접속이 오면 클라와 전용으로 통신할 소켓을 하나 만들어 통신하게 하는 구조다

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
기본적인 소켓 프로그래밍  (0) 2022.12.23
통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [1] catch & all  (0) 2018.08.19
반응형

 

 

어플리케이션 : 유저인터페이스,  HTTP, FPT, DNS 

  • 4단계 : 트랜스포트 TCP/UDP 전송확인/오류해결
  • 3계층 : 네트워크 단계, 라우터 : IP, ipv4, ip6, 라우터 로 경로 설정
  • 2계층 : 데이터 링크 계층 으로  , 스위치, mac address 네트워크 카드에 박혀 있는 주소

 

7 계층 : 도메인 인데 이 도메인을  나중에 IP 로 바꿔야 함

 

 

 

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

기본적인 소켓 프로그래밍  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [1] catch & all  (0) 2018.08.19
Promise [2]  (0) 2018.08.16
반응형

C#은 상당히 좋은 언어다. 가장 많이 알려진 C#의 특징 중 하나는 메모리 관리에 부담이 없다는 점이다.

So Cool~ C# 메모리
C/C++를 사용하면서 포인터 때문에 괴로워 해본 적이 있는가? 그렇다면 C#에 관심을 가져보는 것이 좋다. C#은 다음과 같은 특징들을 제공하기 때문이다.

- 메모리 해제에 신경 쓰지 않아도 된다.
- 이미 삭제된 메모리에 접근하는 실수를 방지해준다.
- 잘못된 캐스팅으로 엉뚱한 메모리에 접근하지 않게 한다.
- 배열 크기보다 큰 메모리에 접근하지 못한다.
- 메모리 단편화에 대해 신경 쓰지 않아도 된다.

편한 C#, 마구잡이로 사용하면 낭패
골치 아픈 메모리 관리를 신경 쓰지 않아도 된다는 점은 사용자들에게 무척 편리하게 다가온다. 하지만 C#에서도 메모리를 다루기 위해서는 세심한 주의가 필요하다. 마음 놓고 개발하다 당황했던 과거 필자의 경험을 살펴보도록 하자. 

개 발 초창기, 게임 플레이 중에 주기적으로 랙이 발생했다. 로직을 확인해 봤지만 특별히 로딩 중이거나 초기화된 부분이 없어 의아했다. 유니티 엔진에서 제공하는 프로파일러로 한 프레임에 걸리는 시간을 측정해봤다. 측정 결과, System.GC.Collect() 호출에 오랜 시간이 걸린다는 점이 발견됐다. 플레이 중에 프레임마다 소모되는 시간을 그래프로 보여주는 <그림 1>을 보면 System.GC.Collect() 호출 시 그래프가 크게 튀어 오른 모습이 확인된다. C#에서 사용하지 않는 메모리를 정리하면서 가비지 컬렉션(Garbage collection) 랙이 발생한 것이다.

 


<그림 1> 프로파일러 이미지

이때는 가비지 컬렉션이 동작하는 횟수를 줄여서 랙 발생을 줄이면 된다. 가비지 발생을 줄이면 가비지 컬렉션이 호출되는 시간을 늦출 수 있어 동작 횟수가 줄어든다. 가비지란 프로그램이 실행되면서 어디에서든 더 이상 참조되지 않는 메모리를 의미하므로 가능한 한 메모리를 할당했다 금방 버려지는 상황을 만들지 않는 것이 좋다. 몇 가지 사례들을 살펴보자.

‘+’ operator를 통한 문자열 조합 
C#은 문자열 조합이 쉽다. <리스트 1>에 보이는 것처럼 ‘+’로 연결하면 간단히 문자열 조합이 이뤄진다. 모든 객체가 ToString()을 지원하기 때문에 문자열끼리만 조합되는 게 아니라 int, float 등의 값도 알아서 문자열로 변환·조합된다.

<리스트 1> ‘+’로 연결한 문자열 조합
class Names
{
    public string[] name = new string[100];
    public void Print()
    {
        for (int index = 0; index < name.Length; index++)
        {
            string output = "[" + index + "]" + name;
            Console.WriteLine(output);
        }
    }
}

문제는 <리스트 1>에서 가비지가 많이 발생한다는 점이다. ‘+’ 연산자로 두 값을 연결할 때마다 새로운 string 인스턴스가 생성된다. 연이어 ‘+’ 연산자가 나오기 때문에 다시금 새로운 string 인스턴스가 생성되고, 이전에 만들어진 string 인스턴스는 가비지가 된다. string 조합을 위해 ‘+’ operator 호출이 많아질수록 많은 가비지가 만들어지는 것이다.

그래서 문자열을 조합하는 동안 새로운 객체를 생성하지 않는 System.Text.StringBuilder 객체를 소개한다. ‘+’ operator가 아닌 Append() 메소드를 통해 문자열을 추가하며, string 객체를 만들어내는 게 아니라 이미 잡아놓은 메모리 공간에 문자열만 복사해 뒀다가 한번에 ToString()으로 string 객체를 생성해낸다.

<리스트 2> System.Text.StringBuilder 객체 사용
class NewNames
{
    public string[] name = new string[100];
    private StringBuilder sb = new StringBuilder();

    public void Print()
    {
        sb.Clear();     // sb.Length = 0;
        for (int index = 0; index < name.Length; index++)
        {
            sb.Append("[");
            sb.Append(index);
            sb.Append("] ");
            sb.Append(name);
            sb.AppendLine();
        }
        Console.WriteLine(sb.ToString());
    }
}

과다한 Append() 메소드 호출이 필요해 ‘+’ 코드보다 깔끔하지 못하다고 생각된다면 AppendFormat()을 사용하는 것도 좋다.

 <리스트 3> AppendFormat() 활용
class NewNames
{
    public string[] name = new string[100];
    private StringBuilder sb = new StringBuilder();

    public void Print()
    {
        sb.Clear();     // sb.Length = 0;
        for (int index = 0; index < name.Length; index++)
        {
            sb.AppendFormat("[{0}] {1}", index, name.ToString());
        }
        Console.WriteLine(sb.ToString());
    }
}

string처럼 Immutable pattern을 사용한 객체들의 값에 접근할 때는 기존 메모리를 수정하지 않고 새로운 메모리를 만들어 반환하거나 입력받으므로 사용 시 주의가 필요하다.

메소드 안에서 생성한 객체
C#은 C++과 달리 클래스를 인스턴싱하려면 반드시 new를 해줘야 한다. 이때 heap에서 메모리 할당이 일어난다. 

<리스트 4>와 같이 메소드 안에서 new로 생성된 인스턴스는 메소드를 빠져나오면 더 이상 사용하지 않게 돼 가비지로 처리된다. 이런 패턴의 메소드가 자주 호출될수록 가비지도 많이 발생한다.

<리스트 4> new로 생성된 인스턴스
public class MyVector
{
    public float x, y;
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    public static void PrintVectorLength(float x, float y)
    {
        MyVector v = new MyVector(x, y);
        Console.WriteLine("Vector=({0},{1}), lenght={2}", x, y, v.Length());
    }
}

Vector 클래스를 구조체로 바꿔보면, new 연산자로 인스턴스를 만들어도 heap 영역에 메모리가 할당되지 않는다. 구조체 역시 Value type이기 때문에 stack 영역에 메모리가 할당되며, 메소드를 빠져나갈 경우 자동으로 삭제된다. 물론 heap 영역에 생성된 메모리가 아니기 때문에 가비지 컬렉션의 대상이 되지도 않는다. 

<리스트 5> Vector 클래스를 구조체로 변환
public struct MyVector
{
    public float x, y;
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    public static void PrintVectorLength(float x, float y)
    {
        MyVector v = new MyVector(x, y);
        Console.WriteLine("Vector=({0},{1}), lenght={2}", x, y, v.Length());
    }
}

구조체로 바꿀 수 없다면, <리스트 6>처럼 멤버변수 사용을 권장한다.

<리스트 6> 멤버변수 사용
public class MyVector
{
    public float x, y;
    public MyVector() { x = .0f; y = .0f; }
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    private static MyVector m_cachedVector = new MyVector();
    public static void PrintVectorLength(float x, float y)
    {
        m_cachedVector.x = x;
        m_cachedVector.y = y;

        Console.WriteLine("Vector=({0},{1}), lenght={2}",
x, y, m_cachedVector.Length());
    }
}

속도 저하가 큰 Boxing
Boxing이란 Value type 객체를 Reference type 객체로 포장하는 과정을 뜻한다. C#의 모든 객체는 object로부터 상속되는데, 심지어 상속받지 못하는 int, float 등의 Value type 조차도 object로부터 상속된 것처럼 사용할 수 있다. 하지만 가비지 컬렉션에 의한 부하 못지않게 boxing으로 인한 부하도 크다. 무심코 만든 코드에서 boxing 과정이 일어나는 경우가 있으니 잘 이해하고 사용해야 한다.

<리스트 7>을 보면 리스트에 서로 다른 type의 값을 추가했지만, loop 안에서 추가 값을 object type으로 받아 하나의 코드로 처리할 수 있음을 알 수 있다.

<리스트 7> 서로 다른 type 값 추가
class MyClass
{
    public override string ToString() { return "다섯"; }

    static public void Sample()
    {
        ArrayList list = new ArrayList();
        list.Add(1);
        list.Add(1.5f);
        list.Add(‘3’);
        list.Add("four");
        list.Add(new MyClass());

        foreach (object item in list)
            Console.WriteLine(item.ToString());
    }
}

 


<그림 2> <리스트 7>의 실행 결과

매력적인 C#이지만 Value type의 값을 Reference type인 object로 바꿔주는 과정에는 많은 시간이 걸리며 변환 시에는 System.Object 내부에 랩핑하고 관리되는 heap에 저장된다. 즉 새로운 객체가 만들어지는 셈이다. MSDN에서 발췌한 <그림 3>을 참조하길 바란다.

 

 

 

https://msdn.microsoft.com/ko-kr/library/yz2be5wk.aspx

http://www.gamedevforever.com/322


<그림 3> boxing과 unboxing의 비교

따라서 한 번에 다양한 type을 처리하는 경우가 아니라면 collection에 사용된 값의 type을 명시해주는 Generic collection 사용을 권한다. Generic은 C++의 template와 비슷하다. 그래서 Generic collection들은 C++의 STL container들과 비슷하게 생겼다. <리스트 8>을 참고하자.

<리스트 8> Generic collection
class Example
{
    static public void BadCase()
    {
        ArrayList list = new ArrayList();
        int evenSum = 0;
        int oddSum = 0;

        for (int i = 0; i < 1000000; i++)
            list.Add(i);

        foreach (object item in list)
        {
            if (item is int)
            {
                int num = (int)item;
                if(num % 2 ==0) evenSum += num;
                else oddSum += num;
            }
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }

    static public void GoodCase()
    {
        List<int> list = new List<int>();
        int evenSum = 0;
        int oddSum = 0;

        for (int i = 0; i < 1000000; i++)
            list.Add(i);

        foreach (int num in list)
        {
            if (num % 2 == 0) evenSum += num;
            else oddSum += num;
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }
}

메모리가 계속 늘어나는 또 다른 문제의 발생!
이 글을 시작하며 C#에서 사용자는 메모리 해제에 신경 쓸 필요가 없다고 했지만 의도하지 않게 메모리가 늘어나기도 한다. C#에는 delete 같은 메모리 해제 명령이 없기에 메모리 릭(memory leak) 현상이 발생하면 당혹스러울 수 있다. 여기서 C# 메모리의 특징을 다시 한 번 떠올려보자.

시스템에서 더 이상 참조가 없는 메모리를 알아서 해제하는 것을 우리는 가비지 컬렉션이라 부른다. 가비지는 더 이상 참조가 없는 메모리다. C# 애플리케이션이 메모리가 해제되지 않고 계속 증가되고 있다면 어디선가 의도하지 않는 참조가 일어나고 있다고 보면 된다. 그렇다면 어디에서 의도하지 않은 참조가 일어나는 것일까? 예를 통해 확인해 보자.

 


<그림 4> #1 - 케릭터 매니저에서 케릭터를 생성한다

 


<그림 5> #2 - 누군가 디버깅을 위해 '캐릭터 위치 표시' 객체를 만들고 캐릭터 매니저에 접근해 등록된 캐릭터를 모두 참조한다

 


<그림 6> #3 - 캐릭터 매니저에서 필요없는 캐릭터를 삭제한다

 


<그림 7> #4 - 캐릭터 매니저에서 삭제됏지만 '캐릭터 위치 표시' 객체에서는 여전히 참조 중이다. 가비지가 아니기 때문에 메모리에 계속 남아있으며, 구현에 따라서는 의도하지 않게 화면에 남을 수도 있다.

WeakReference로 의도하지 않은 참조를 없애보자
System.WeakReference 는 가비지 컬렉션에 의한 객체 회수를 허용하면서 객체를 참조한다. 인스턴스를 참조하려면 Weak Reference.Target으로 접근해야 하는데 원본 인스턴스가 가비지 컬렉터에 의해 회수되면 WeakReference.Target은 null이 반환된다.

<리스트 9> WeakReference.Target
public class Sample
{
    private class Fruit
    {
        public Fruit(string name) { this.Name = name; }
        public string Name { private set; get; }
    }

    public static void TestWeakRef()
    {
        Fruit apple = new Fruit("Apple");
        Fruit orange = new Fruit("Orange");
           
        Fruit fruit1 = apple;   // strong reference
        WeakReference fruit2 = new WeakReference(orange);
        Fruit target;
           
        target = fruit2.Target as Fruit;
        Console.WriteLine(" (1) Fruit1 = \"{0}\", Fruit2 = \"{1}\"",
            fruit1.Name, target == null ? "" : target.Name);

        apple = null;
        orange = null;

        System.GC.Collect(0, GCCollectionMode.Forced);
        System.GC.WaitForFullGCComplete();

        // fruit1과 fruit2의 값을 바꾼 적은 없지만, fruit2의 결과가 달라진다.
        target = fruit2.Target as Fruit;
        Console.WriteLine(" (2) Fruit1 = \"{0}\", Fruit2 = \"{1}\"",
            fruit1==null ? "" : fruit1.Name,
            target == null ? "" : target.Name);
    }
}

<리스트 9>의 실행으로 <그림 8>을 확인할 수 있다. Fruit2가 참조하고 있던 orange 인스턴스는 가비지 컬렉터에 의해 회수돼 null이 됐다.

 


<그림 8> <리스트 9>의 실행 결과

‘캐릭터 매니저’처럼 객체의 생성·삭제를 직접 관여하는 모듈이 아닌 곳에서는 가능한 WeakRefernce를 사용하는 것이 좋다. ‘객체 위치 표시 객체’처럼 인스턴스를 참조하는 모듈에서 WeakReference를 사용하면, 의도하지 않은 참조로 메모리가 해제되지 않는 실수를 방지할 수 있다. 주의할 점은 Weak Reference.Target 값을 보관하면 안 된다는 것이다. 만약 그대로 보관하고 있으면 강한 참조(strong reference)가 일어나 이를 인식한 가비지 컬렉터는 회수를 실행하지 않게 된다.

C/C++처럼 원하는 시점에 객체를 삭제하고 싶다면
C#에서 는 할당된 메모리를 임의로 해제할 수 없다. 컬렉션에 보관된 인스턴스를 제거하거나 인스턴스를 담고 있던 변수에 null을 넣어 더 이상 참조하지 않는 방법이 있지만 실제 인스턴스가 삭제되는 시점은 가비지 컬렉션 동작 이후가 되므로, 언제가 될 지 정확히 알 수 없다. 의도한 시점에 맞춰 정확히 삭제할 수 없다는 점이 그렇게 중요하지는 않다. 하지만 캐릭터 매니저에서 캐릭터를 제거했는데도 여전히 캐릭터 인스턴스가 남아서 화면에 한동안 계속 나타나는 경우가 발생할 수 있다.

Dispose pattern 소개C#에서는 관리되지 않는 메모리(리소스)를 해제하는 용도로 System.IDisposable이라는 인터페이스를 제공한다. IDisposable 인터페이스를 상속받은 클래스라면 용도에 맞게 Dispose()를 구현해줘야 하는데 이는 FileStream 관련 객체들에서 많이 볼 수 있다.

리소스를 강제로 해제시키려면 직접 Release(), Delete(), Destroy(), Close() 등의 메소드를 만들어 사용하면 되는데 굳이 IDisposable을 사용할 필요가 있을까? 서로 다른 type의 객체여도 IDisposable 인터페이스를 상속받고 있다면, 하나의 코드로 다양한 type의 메모리를 정리할 수 있기 때문에 IDisposable을 사용할 필요가 있다. 또한 Dispose() 메소드만 보고도 “아, 이 클래스는 사용이 끝나면 Dispose()를 호출해서 메모리를 정리해야 하는구나” 라고 금방 알 수 있다.

캐릭터 객체에서 IDisposable 인터페이스를 구현해보자. 업데이트 목록에서도 제외시키고 렌더링 정보도 다 지우자. 캐릭터의 Dipsose()를 호출한 이후에 캐릭터는 어떠한 동작도 하지 못하게 된다. 물론 Dispose()를 호출한다고 캐릭터가 가비지 컬렉터에 의해 메모리 해제되는 것은 아니다.

WeakReference과 IDisosalbe의 조합원하는 시점에 메모리를 해제하려면 앞서 설명한 Weak Reference와 IDisposable을 개별적으로 사용하는 것으로는 부족하다. 둘을 함께 사용하는 것이 좋다. <리스트 10>을 보자.

<리스트 10> Disposable 인터페이스를 상속받아 구현된 캐릭터 클래스
namespace MyApp
{
    public class SampleChar : IDisposable
    {
        private IRenderObject m_Render = Renderer.CreateRenderObject();

        public void Dispose()
        {
            SampleCharManager.Remove(this);
            m_Render = null;
        }

        public bool isRemoved { get { return m_Render == null; } }
        public void Render()
        {
            if (m_Render == null) return;
            // 렌더링
        }

        public void Update() { }
    }
}

예제로 만들어 본 캐릭터 클래스는 Disposable 인터페이스를 상속받아 구현된다. Dispose 후에는 더 이상 업데이트가 되지 않도록 SampleCharManager에서 제거되며, 렌더링 객체를 null로 만들어 화면에 그려지지 않도록 했다.

IRenderObject 인터페이스는 <리스트 11>과 같이 구현된다.

<리스트 11> IRenderObject 인터페이스
namespace MyApp
{
    public interface IRenderObject
    {
        void Render();
    }

    public static class Renderer
    {
        public static IRenderObject CreateRenderObject()
{
return new DumyRenderObject(); // IRenderObject를 상속받은 더미 객체
}
    }
}

<리스트 12>의 캐릭터 매니저 클래스는 등록된 캐릭터들을 일괄적으로 업데이트시키고 렌더링한다.

<리스트 12> 등록 캐릭터 일괄 업데이트 및 렌더링
namespace MyApp 
{
    static class SampleCharManager
    {
        private static List<SampleChar> m_list = new List<SampleChar>();

        public static void Update()
        {
            foreach (SampleChar obj in m_list) 
                obj.Update();
        }

        public static void Render()
        {
            foreach (SampleChar obj in m_list)
                obj.Render();
        }

        public static void Add(SampleChar obj)
        {
            m_list.Add(obj); 
        }

        public static void Remove(SampleChar obj)
        {
            m_list.Remove(obj);
        }
    }
}

<리스트 13>의 디버깅을 위한 ‘캐릭터 위치 표시 객체’는 WeakReference를 통해 SampleChar 객체를 참조하도록 구현돼 있고, SampleCharManager에서 캐릭터를 삭제하더라도 안전하게 가비지가 회수된다. 업데이트 시 DisplayCharInfo는 삭제된 캐릭터를 스스로 판단해 목록에서 제거한다.

<리스트 13> 디버깅을 위한 캐릭터 위치 표시 객체
namespace MyDebug
{
    static class DisplayCharInfo
    {
        private static List<WeakReference> m_list = new List<WeakReference>();
        private static Queue<WeakReference> m_removeQueue =
new Queue<WeakReference>();

        public static void Update()
        {
            foreach (WeakReference item in m_list)
            {
                MyApp.SampleChar obj = (item.Target != null) ?
 item.Target as MyApp.SampleChar : null;

                if (obj == null || obj.isRemoved)
                {
                    m_removeQueue.Enqueue(item);
                }
                else 
                { 
                    /* 캐릭터 정보 표시 */ 
                }
            }

            while(m_removeQueue.Count > 0)
            {
                WeakReference item = m_removeQueue.Dequeue();
                m_list.Remove(item);
            }
        }

        public static void Add(MyApp.SampleChar obj)
        {
            m_list.Add(new WeakReference(obj));
        }
    }
}

C#에서 메모리를 관리하는 데 도움되길 바라며, 지금까지 설명한 내용을 요약하면 다음과 같다.

- string 조합이 많다면, StringBuilder 활용
- Immutable 객체의 값 접근 시 매번 메모리가 생성될 수 있으므로 주의
- 매번 호출되는 메소드 안에서 반복해서 일회성 인스턴스가 생성되지 않도록 주의
- Boxing / unboxing이 가능한 일어나지 않도록 주의
- WeakReference를 사용해서 의도하지 않은 참조 줄이기
- IDisposable 인터페이스를 사용해 사용자가 원하는 시점에 객체 삭제하기

Value type과 Reference type 비교

Value type은 stack 영역에 할당되며 값이 통째로 복사된다.

 

유니티 3D엔진에서의 메모리 관리
유니티 3D엔진으로 개발하면서 주의할 내용을 알아보자. 유니티 3D엔진은 크게 모노 메모리와 엔진에서 관리하는 메모리로 나뉜다. 둘 다 메모리가 부족하면 내부에 관리하는 heap 영역을 늘려 메모리를 할당한다. 이렇게 한 번 늘어난 heap은 줄어들지 않는 특징을 가진다. 물론 늘어난 heap 안에서 메모리가 재사용되므로, 무턱대고 늘어나진 않는다. 하지만 가비지를 너무 많이 생성시키면 GC.Collect()로 인한 성능저하와 더불어 최대 메모리가 늘어날 수도 있으니 주의해야 한다. 가능한 가비지가 덜 생성되도록 코드를 구현하는 게 좋다. 메모리는 한 번에 잡는 것이 좋고, caching이나 memory pool을 사용하는 것도 도움이 된다.
<리스트 14> Value typepublic static class Sample
{
    public static void TestValueType()
    {
        int a = 100;
        int b = a;
       
        a = 200;
        Console.WriteLine(" a={0}, b={1}", a, b);
    }
}

<리스트 14>를 실행하면 <그림 9>와 같은 결과를 확인할 수 있다. a와 b는 서로 다른 메모리 공간을 가지고 있다.





<그림 9> <리스트 14>의 실행 결과

Reference Type은 heap 영역에 할당되며, C/C++의 포인터나 레퍼런스처럼 new로 생성한 인스턴스를 참조한다.
<리스트 15> Reference type
public class MyInt
{
    public int Value { get; set; }
    public MyInt(int val) { this.Value = val; }

    public static void TestReferenceType()
    {
        MyInt a = new MyInt(100);
        MyInt b = a;

        a.Value = 200;
        Console.WriteLine(" a={0}, b={1}", a.Value, b.Value);
    }
}

<리스트 15>의 실행 결과로 <그림 10>을 확인할 수 있다. a와 b는 같은 메모리를 참조한다.





<그림 10> <리스트 15>의 실행 결과

 

 

*** 출처: 마이크로소프트웨어

*** 링크: 효과적인 C#메모리 기법

ref : https://semoa.tistory.com/988

반응형
반응형

Introduction

We all know that the .NET framework is an object-oriented, memory-managed, and type-safe framework. C# is the main development language in the .Net framework that has the same qualities. When we talk about memory management, we offer to refer to the garbage collector (GC) which will reclaim any unused objects and hence release them from memory. This potentially means we do not need to worry about memory leaks. However, there might be situations where we might create memory leaks which would be difficult for the GC to collect or run out of memory. Let us look at some of these situations and how to avoid them. So, let us begin.

Memory Leaks in C#
 

Implementing the IDisposable pattern

Always remember to implement the Dispose method on a class that implements the IDisposable interface. Otherwise, a memory leak can be caused. The best way to do this is to use the “using” statement which will call the disposal method for every condition. If you cannot use the “using” statement, remember to do this manually and also suppress the Finalize method as this would not be required.

Very Long Running Threads

If you have implemented a very long-running or infinite running thread that is not doing anything and it holds on to objects, you can cause a memory leak as these objects will never be collected. The fix for this is to be very careful with long-running threads and not hold on to objects not needed.

Over Caching

It has been seen that in some applications there is too much caching. This might help to speed up your application in the beginning but if you continue to do this you will eventually run out of memory. The best solution is to only cache objects very frequently used and not add unnecessary objects to the cache. Also, keeping the size of the cache objects to a reasonable one is important.

Using Static Objects

The use of static objects can also cause memory leaks. The reason for this is that static objects and everything they reference are not collected by the Garbage collector. Hence, use them to a bare minimum.

Using Unmanaged Objects

Whenever we are working with objects that are not collected by the garbage collector, we need to be extra careful to dispose of them after use. Otherwise, these will cause a memory leak as they are not cleaned up by the GC.

Summary

In this article, we looked at some potential situations that could lead to memory leaks or we can say inefficient use of the memory in our C# code, and how this can be avoided. There might be other similar situations as well. Hence, it is always a good idea to ensure memory is not being held for longer than required. Happy coding!

 

 

ref : https://www.c-sharpcorner.com/article/common-memory-leaks-in-c-sharp/

반응형

'프로그래밍(Programming) > C#' 카테고리의 다른 글

C# - ArraySegment  (0) 2023.01.04
ThreadLocal<T> 와 AsyncLocal<T> 의 차이점  (0) 2022.12.29
C# 에서 Dispose  (0) 2022.12.04
ReaderWriterLockSlim  (0) 2022.11.27
SpinLock  (0) 2022.11.27
반응형

소스 코드를 완벽하게 파악하고 있는 Accelerate Solutions 팀은 Unity 엔진을 최대한 활용할 수 있도록 수많은 고객을 지원합니다. 팀은 크리에이터 프로젝트를 심도 있게 분석하여 속도, 안정성, 효율성 등을 향상시키기 위해 최적화할 부분을 파악합니다. Unity에서 가장 경력이 많은 소프트웨어 엔지니어로 이루어진 이 팀과 함께 모바일 게임 최적화에 관한 전문적인 지식을 공유하는 자리를 마련했습니다. 

모바일 게임 최적화에 관한 인사이트를 공유하기 시작하면서, 원래 계획한 하나의 블로그 포스팅에 담기에는 너무나 방대한 정보가 있다는 사실을 알게 되었습니다. 따라서 이 방대한 지식을 한 권의 전자책(여기에서 다운로드 가능)과 75가지 이상의 실용적인 팁을 담은 블로그 포스팅 시리즈를 통해 제공하기로 했습니다.

이번 시리즈의 첫 게시물에서는 프로파일링, 메모리, 코드 아키텍처를 통해 게임 성능을 개선하는 방법을 자세히 들여다 봅니다. 이후 몇 주 이내에 두 개의 포스팅이 추가로 업로드될 예정입니다. 첫 번째는 UI 물리에 대해, 두 번째는 오디오와 에셋, 프로젝트 설정, 그래픽스에 대해 다룹니다.

시리즈 전체 내용을 지금 바로 확인하고 싶다면 무료로 전자책을 다운로드하세요.

그럼 시작하겠습니다.

프로파일링

모바일 성능 데이터의 프로파일링과 수집 및 활용 과정은 진정한 모바일 성능 최적화가 시작되는 지점입니다.

개발 초기부터 타겟 기기에서 자주 프로파일링 실행하기

Unity 프로파일러는 사용 시 애플리케이션에 대한 필수 성능 정보를 제공합니다. 출시가 멀지 않은 시점이 아닌 개발 초기에 프로젝트를 프로파일링하세요. 여러 오류나 성능 문제를 발생 즉시 조사하세요. 프로젝트의 '성능 시그니처'를 개발하면서 새로운 문제를 보다 쉽게 발견할 수 있습니다.

에디터에서 프로파일링을 수행하면 다양한 시스템의 상대적인 게임 성능에 관한 정보를 얻을 수 있는 반면, 각 기기를 대상으로 프로파일링하면 보다 정확한 인사이트를 확보할 수 있습니다. 가능하면 타겟 기기에서 개발 빌드를 프로파일링하세요. 지원할 기기 중 최고 사양의 기기와 최저 사양의 기기 모두를 프로파일링하고 모두 최적화해야 합니다.

Unity 프로파일러와 더불어 다음과 같은 iOS 및 Android의 네이티브 툴을 사용하면 각 엔진에서 추가로 성능 테스트를 수행할 수 있습니다.

특정 하드웨어는 Arm Mobile Studio, Intel VTune, 및 Snapdragon Profiler 등 추가 프로파일링 툴을 활용할 수 있습니다. 자세한 내용은 Profiling Applications Made with Unity 학습 자료를 참고하세요.

올바른 영역 최적화하기

게임 성능을 저하시키는 요인을 추정하거나 가정하지 않도록 합니다. Unity 프로파일러 및 플랫폼별 툴을 사용하여 성능 저하의 정확한 원인을 찾으세요. 

물론 여기에서 설명하는 최적화가 모두 애플리케이션에 적용되지는 않습니다. 다른 프로젝트에는 적합했던 최적화라도 현재 프로젝트에는 적용되지 않을 수 있습니다. 실제 병목 지점을 파악하고 실질적으로 최적화가 필요한 부분에 집중하세요.

Unity 프로파일러의 작동 방식 이해하기

Unity 프로파일러는 런타임 시 성능 저하 또는 중단의 원인을 감지하고 특정 프레임 또는 시점에 발생하는 상황을 보다 정확하게 이해하는 데 도움이 될 수 있습니다. CPU 및 메모리 트랙을 기본적으로 활성화하세요. 물리를 많이 사용하는 게임이나 음악에 기반한 게임플레이를 개발 중이라면 필요에 따라 렌더러, 오디오, 물리와 같은 보조 프로파일러 모듈을 모니터링할 수 있습니다.

 
Unity 프로파일러를 사용하여 애플리케이션의 성능 및 리소스 할당 테스트확장

Development Build  Autoconnect Profiler를 선택하여 기기에 애플리케이션을 빌드하거나 수동으로 연결하여 앱 시작 시간을 단축하세요.

 
확장

프로파일링을 실행할 타겟 플랫폼을 선택하세요. 녹화 버튼은 애플리케이션 재생을 몇 초 동안 추적합니다(기본 300 프레임). 더 오래 캡처해야 한다면 Unity > Preferences > Analysis > Profiler > Frame Count로 이동하여 이 값을 2000까지 늘릴 수 있습니다. 이렇게 하면 Unity 에디터가 더 많은 CPU 작업을 수행하고 더 많은 메모리를 사용하지만, 상황에 따라 유용할 수 있습니다.

이는 계측 기반 프로파일러로 ProfileMarkers에 명시적으로 래핑된 코드 타이밍을 프로파일링합니다(예: MonoBehaviour의 Start나 Update 메서드 또는 특정 API 호출). 또한 Deep Profiling 설정을 사용하면 Unity는 스크립트 코드에 있는 모든 함수 호출의 시작과 끝을 프로파일링하여 정확히 애플리케이션의 어느 부분에서 성능 저하가 발생하는지 제시합니다.

 
타임라인 뷰를 사용하여 CPU 바운드 또는 GPU 바운드 여부 판단확장

게임을 프로파일링할 때는 성능 불안정과 평균 프레임의 비용을 모두 살펴보는 것이 좋습니다. 각 프레임에서 발생하는 높은 비용의 작업을 파악하고 최적화하면 타겟 프레임 속도 이하에서 실행되는 애플리케이션에 더 유용할 수 있습니다. 스파이크 현상을 살펴볼 때는 물리, AI, 애니메이션 등의 고비용 작업을 먼저 검토한 다음 가비지 컬렉션을 살펴봅니다.

창을 클릭하여 특정 프레임을 분석하세요. 그리고 나서 타임라인 또는 계층 구조 뷰를 사용합니다. 

  • 타임라인: 특정 프레임의 타이밍을 시각적으로 요약하여 제시합니다. 이를 통해 각 활동이 다양한 스레드 전반에서 서로 어떤 관계를 맺고 있는지 시각화할 수 있습니다. 이 옵션을 사용하여 CPU 바운드 또는 GPU 바운드 여부를 판단하세요.
  • 계층 구조: 그룹화된 ProfileMarkers의 계층 구조를 제시합니다. 이를 통해 밀리초 단위(Time ms  Self ms)의 시간 비용을 기준으로 샘플을 정렬할 수 있고, 함수에 대한 호출 수와 프레임의 관리되는 힙 메모리(GC Alloc)의 양도 알 수 있습니다.
 
계층 구조 뷰를 통해 시간 비용에 따라 ProfileMarkers 정렬확장

여기에서 Unity 프로파일러의 전체 개요를 읽어보세요. 프로파일링이 생소하다면 Unity 프로파일링 소개 영상을 시청해 보세요.

프로젝트에서 최적화를 수행하기 전에 먼저 Profiler .data 파일을 저장하세요. 변경 사항을 구현하고 수정 이전  이후에 저장된 .data 파일을 비교해 보세요. 반복적인 프로파일링, 최적화, 비교를 통해 성능을 향상할 수 있습니다. 이후에도 이 과정을 반복합니다.

Profile Analyzer 사용하기

이 툴을 사용하면 Profiler 데이터의 여러 프레임을 집계한 다음 관심 있는 프레임을 찾을 수 있습니다. 프로젝트를 변경하면 Profiler에 어떤 영향을 주는지 확인하고 싶으신가요? Compare 뷰를 사용하면 두 데이터 세트를 로드해서 차이점을 확인할 수 있으므로 변경 사항을 테스트하고 결과를 개선할 수 있습니다. Profile Analyzer는 Unity의 패키지 관리자를 통해 사용할 수 있습니다.

 
기존 Profiler를 보완하는 Profile Analyzer로 프레임과 마커 데이터 확인확장

프레임당 정해진 시간 예산으로 작업하기 

각 프레임은 목표로 하는 초당 프레임 수(fps)를 기반으로 시간 예산을 가집니다. 30fps로 실행되는 애플리케이션의 경우 프레임당 약 33.33ms(1000ms/30fps)를 허용하는 것이 이상적입니다. 마찬가지로 60fps의 경우 목표 시간 예산은 프레임당 16.66ms입니다.

컷씬이나 로딩 시퀀스 같은 짧은 시간 동안 기기는 예산을 초과할 수도 있지만, 긴 시간 동안 초과할 수는 없습니다.

기기 온도 고려하기

모바일 기기에서는 기기가 과열되거나 OS가 CPU 및 GPU를 서멀 스로틀링할 수 있으므로 최대 시간을 지속적으로 사용하지 않는 것이 좋습니다. 프레임 간 쿨다운을 위해 가용 시간의 약 65%만 사용할 것을 권장합니다. 일반적인 프레임 예산은 30fps에서는 프레임당 약 22ms, 60fps에서는 11ms입니다. 

대부분의 모바일 기기에는 데스크톱과 달리 냉각 장치가 없습니다. 물리적인 열기가 성능에 직접적인 영향을 미칠 수 있습니다.

기기가 가열되면 장기적인 문제로 이어지지 않는다 해도 Profiler에서 성능 저하를 인식하고 이를 보고할 수 있습니다. 프로파일링 과열을 방지하기 위해 짧은 간격으로 프로파일링하세요. 이렇게 하면 기기가 냉각되고 실제 사용 조건에서 시뮬레이션됩니다. 일반적으로 다시 프로파일링하기 전에 10~15분 간 기기를 냉각하는 것이 바람직합니다.

GPU 바운드 또는 CPU 바운드 여부 판단하기

Profiler를 통해 CPU가 프레임 예산을 할당된 양보다 오래 사용 중이거나 GPU에 문제가 있는지 파악할 수 있습니다. 다음과 같이 Gfx가 접두어인 마커를 방출하는 방식을 사용합니다.

  • Gfx.WaitForCommands 마커가 표시되면 렌더 스레드가 준비되었으나 메인 스레드에 병목 현상이 일어날 가능성을 의미합니다.
  • Gfx.WaitForPresent가 빈번하게 발생하면 메인 스레드는 준비되었으나 GPU가 프레임을 표시하기를 기다리는 중이었음을 의미합니다.
메모리

Unity는 사용자 생성 코드 및 스크립트에 자동 메모리 관리를 사용합니다. 값이 입력된 로컬 변수처럼 작은 데이터는 스택에 할당됩니다. 더 큰 데이터와 장기 스토리지는 관리되는 힙에 할당됩니다.

가비지 컬렉터는 사용되지 않은 힙 메모리를 주기적으로 파악하여 할당을 해제합니다. 이 작업은 자동으로 실행되지만, 힙의 모든 개체를 검사하는 과정에서 게임이 끊기거나 느리게 실행될 수 있습니다.

메모리 사용량을 최적화하려면 힙 메모리의 할당 및 할당 해제 시점뿐 아니라 가비지 컬렉션의 영향을 최소화하는 방법을 알아야 합니다. 자세한 내용은 관리되는 힙의 이해를 참조하세요.

 
Memory Profiler에서 스냅샷 캡처, 검토 및 비교확장

Memory Profiler 사용하기 

Memory Profiler는 별도 애드온(패키지 관리자에서 실험 패키지 또는 프리뷰 패키지로 제공)으로 관리되는 힙 메모리의 스냅샷을 캡처하여 단편화 및 메모리 누출 같은 문제를 식별할 수 있습니다.

메모리에 보관된 네이티브 오브젝트에 대한 변수를 추적하려면 Tree Map 뷰를 클릭하세요. 여기에서는 과도하게 큰 텍스처 또는 중복 에셋과 같은 일반적인 메모리 사용량 문제를 확인할 수 있습니다.

Unity의 Memory Profiler를 활용하여 메모리 사용량을 개선하는 방법을 알아보세요. 공식 Memory Profiler 기술 자료도 확인할 수 있습니다.

가비지 컬렉션의 영향 줄이기

Unity는 Boehm-Demers-Weiser 가비지 컬렉터를 사용하며, 이 컬렉터는 프로그램 코드의 실행을 중단하고 작업이 완료될 때만 일반 실행을 재개합니다. 

힙의 불필요한 할당은 GC(가비지 컬렉션) 스파이크를 유발할 수 있으므로 유의해야 합니다. 

  • 문자열: C#에서 문자열은 값 유형이 아닌 참조 유형입니다. 불필요한 문자열의 생성 또는 조작을 줄이세요. JSON, XML과 같은 문자열 기반 데이터 파일은 구문 분석하지 않는 것이 좋습니다. 데이터를 ScriptableObjects 또는 MessagePack이나 Protobuf 같은 형식으로 대신 저장하세요. 런타임에서 문자열을 빌드해야 하는 경우 StringBuilder 클래스를 사용합니다.
  • Unity 함수 호출: 일부 함수는 힙 할당을 생성합니다. 참조를 루프 도중에 할당하지 말고 배열에 저장하세요. 아울러 가비지 생성을 방지하는 함수들을 활용하세요. 예를 들어 문자열을 GameObject.tag와 직접 비교하는 대신 GameObject.CompareTag를 사용하는 방법이 있습니다(새로운 문자열 반환은 가비지를 생성합니다).
  • 박싱: 참조 유형 변수 대신 값 유형 변수를 전달하지 않도록 합니다. 이렇게 하면 임시 오브젝트가 생성되며 동반되는 잠재적 가비지가 값 유형을 암묵적으로 타입 오브젝트로 변환합니다(예: int i = 123, object o = i). 대신 전달하려는 값 유형에 구체적인 오버라이드를 제공해 보세요. 이러한 오버라이드에는 제네릭이 사용될 수도 있습니다.
  • 코루틴: yield는 가비지를 생성하지 않지만 새로운 WaitForSeconds 오브젝트를 만들면 가비지가 생성됩니다. yield 라인에서 생성하는 대신 WaitForSeconds 오브젝트를 캐시하고 재사용하세요.
  • LINQ 및 정규식: 두 가지 모두 박싱에서 가비지를 생성합니다. 성능이 문제라면 LINQ와 정규식을 사용하지 마세요. 새로운 배열을 만드는 대신 for 루프와 리스트를 사용하세요.

가능한 경우 가비지 컬렉션 측정하기

가비지 컬렉션 멈춤 현상이 게임의 특정 지점에 영향을 주지 않는다면 System.GC.Collect로 가비지 컬렉션을 트리거할 수 있습니다.

자동 메모리 관리의 이해에서 이 방식을 활용하는 방법을 참고하세요.

점진적 가비지 컬렉터를 활용하여 GC 워크로드 분할하기 

점진적 가비지 컬렉션을 사용하면 프로그램 실행 중에 한 번 길게 중단되는 것이 아니라 훨씬 짧은 중단이 여러 프레임에 걸쳐 여러 번 나타납니다. 가비지 컬렉션이 성능에 영향을 미친다면 이 옵션을 사용하여 GC 스파이크를 줄일 수 있는지 확인해 보세요. Profile Analyzer를 사용하여 이 방식이 애플리케이션에 어떤 이점으로 작용하는지 확인하세요.

 
점진적 가비지 컬렉터를 사용하여 GC 스파이크 줄이기확장
프로그래밍 및 코드 아키텍처

Unity PlayerLoop에는 게임 엔진의 코어와 상호작용하기 위한 함수가 포함되어 있습니다. 이 구조는 초기화와 프레임별 업데이트를 처리하는 다양한 시스템을 포함합니다. 모든 스크립트가 이 PlayerLoop를 활용하여 게임플레이를 생성하게 됩니다.

프로파일링 시에는 PlayerLoop 아래에 프로젝트의 사용자 코드가 표시됩니다(EditorLoop 아래에는 에디터 컴포넌트).

 
전체 엔진 실행의 맥락에서 커스텀 스크립트, 설정, 그래픽스를 제시하는 Profiler확장
 
확장

PlayerLoop 및 스크립트의 수명 주기를 파악하세요.

다음과 같은 유용한 팁으로 스크립트를 최적화할 수 있습니다.

Unity PlayerLoop 이해하기 

Unity 프레임 루프의 실행 순서를 이해해야 합니다. 모든 Unity 스크립트는 사전에 정해진 순서대로 여러 이벤트 함수를 실행합니다. Awake, Start, Update 및 스크립트의 수명 주기를 생성하는 다른 함수들 사이의 차이점을 이해해야 합니다. 

이벤트 함수의 구체적인 실행 순서는 스크립트 수명 주기 플로우 차트를 참고하세요.

매 프레임에 실행되는 코드 최소화하기 

코드를 반드시 모든 프레임에 실행해야 하는지 확인하세요. 불필요한 로직을 Update, LateUpdate, FixedUpdate에서 제외하세요. 이러한 이벤트 함수에는 프레임마다 업데이트해야 하는 코드를 편리하게 배치할 수 있으며, 같은 빈도로 업데이트할 필요가 없는 로직은 추출됩니다. 가능하다면 상황이 바뀌는 경우에만 로직을 실행하세요.

반드시 Update를 사용해야 한다면 n개 프레임마다 코드를 실행하는 방안을 검토해 보세요. 이는 여러 프레임에 대규모 워크로드를 분산하는 일반적인 기법인 타임 슬라이싱의 적용 방식 중 하나이기도 합니다. 이 예에서는 3개 프레임마다 한 번씩 ExampleExpensiveFunction을 실행합니다.

 

Start/Awake에서 대규모 로직 사용 방지 

첫 번째 씬을 로드하면 다음과 같은 함수가 각 오브젝트에 대해 호출됩니다.

  • Awake
  • OnEnable
  • Start

애플리케이션이 첫 번째 프레임을 렌더링하기 전까지는 이러한 함수에서 고비용 로직의 사용을 피하세요. 그렇게 하지 않으면 필요 이상으로 로딩 시간이 길어질 수 있습니다.

첫 번째 씬 로드에 대한 자세한 내용은 이벤트 함수의 실행 순서를 참조하세요.

빈 Unity 이벤트 방지 

빈 MonoBehaviours도 리소스를 필요로 하므로 비어 있는 Update 또는 LateUpdate 메서드는 제거해야 합니다.

테스트에 이러한 메서드를 사용하고 있다면 다음과 같은 프리 프로세서 지시문을 사용하세요.

 

빌드에 불필요한 오버헤드가 유입되는 일 없이 에디터 내에서 Update를 자유롭게 사용하여 테스트할 수 있습니다.

Debug Log 구문 제거하기 

Log 구문, 특히 Update, LateUpdate 또는 FixedUpdate에 있는 Log 구문은 성능을 낮출 수 있습니다. 빌드를 만들기 전에 Log 구문을 비활성화하세요.

이 작업을 보다 쉽게 하려면 프리 프로세서 지시문과 함께 조건부 속성을 만드는 것이 좋습니다. 예를 들어 다음과 같은 커스텀 클래스를 만듭니다.

 
 
커스텀 프리 프로세서 지시문을 추가하여 스크립트 분리확장

커스텀 클래스로 로그 메시지를 생성합니다. Player Settings에서 ENABLE_LOG 프리 프로세서를 비활성화하면 모든 Log 구문이 동시에 사라집니다.

문자열 파라미터 대신 해시 값 사용하기 

Unity는 내부적으로 애니메이터나 머티리얼, 셰이더 프로퍼티를 식별할 때 문자열 이름을 사용하지 않습니다. 빠른 처리를 위해 모든 프로퍼티 이름이 프로퍼티 ID에 해시되어 있으며, 해당 ID가 실제로 프로퍼티를 식별하는 데 사용됩니다.

애니메이터, 머티리얼 또는 셰이더에서 Set 또는 Get 메서드를 사용하는 경우에는 문자열 값 메서드 대신 정수 값 메서드를 사용하세요. 문자열 메서드 역시 문자열 해싱을 수행한 다음 해시된 ID를 정수 값 메서드로 전달하기 때문입니다.

애니메이터 프로퍼티 이름에는 Animator.StringToHash를, 머티리얼 및 셰이더 프로퍼티 이름에는 Shader.PropertyToID를 사용하세요.

올바른 데이터 구조 선택

한 번 선택한 데이터 구조는 프레임당 수천 번씩 반복되므로 효율성에 영향을 줍니다. 컬렉션에 리스트, 배열 또는 딕셔너리 중 무엇을 사용해야 할지 고민하고 계신가요? 올바른 자료 구조를 선택하기 위한 일반적인 가이드인 C#의 데이터 구조 MSDN 가이드를 참고하세요.

런타임 시 컴포넌트 추가 방지 

런타임에 AddComponent를 호출하면 비용이 발생합니다. Unity는 런타임에 컴포넌트가 추가될 때마다 중복 또는 기타 필요한 컴포넌트를 확인해야 합니다. 

이미 설정된 원하는 컴포넌트로 프리팹을 인스턴트화하는 것이 일반적으로 더 효과적입니다.

게임 오브젝트 및 컴포넌트 캐시하기 

GameObject.Find, GameObject.GetComponent, Camera.main(2020.2 이전 버전)은 비용이 많이 들 수 있으므로 Update 메서드에서는 호출하지 않는 것이 가장 좋습니다. 대신 Start에서 호출하고 결과를 캐시하세요.

다음 예는 반복적인 GetComponent 호출의 비효율성을 보여줍니다.

 

함수의 결과가 캐시되므로 GetComponent는 한 번만 호출하세요. Update에서 GetComponent를 추가로 호출하지 않아도 캐시된 결과를 재사용할 수 있습니다.

 

오브젝트 풀 사용하기 

Instantiate 함수  Destroy 함수는 가비지와 가비지 컬렉션 스파이크를 야기하며, 일반적으로 속도가 느린 프로세스입니다. 게임 오브젝트를 계속 인스턴트화하고 삭제하기보다는(예: 총알 발사) 재사용 및 재활용할 수 있는 사전에 할당된 오브젝트 을 사용해 보세요.

 
재사용 가능한 PlayerLaser 인스턴스 20개를 생성하는 ObjectPool의 예확장

CPU 스파이크가 적을 때 게임 내 한 지점(예: 메뉴 화면)에서 재사용 가능한 인스턴스를 생성합니다. 컬렉션으로 이러한 오브젝트 '풀'을 추적합니다. 게임플레이 중에는 필요할 때 다음으로 사용 가능한 인스턴스를 사용 설정하고, 오브젝트를 삭제하는 대신 사용 중지한 다음 풀로 돌려 보내면 됩니다.

 
비활성 및 발사 준비 상태의 PlayerLaser 오브젝트 풀확장

이렇게 하면 프로젝트에서 관리되는 할당의 수가 줄어들기 때문에 가비지 컬렉션 문제를 방지할 수 있습니다.

Unity에서 간단한 오브젝트 풀링 시스템을 만드는 방법은 여기에서 알아보세요.

스크립터블 오브젝트 사용하기 

변하지 않는 값 또는 설정은 MonoBehaviour가 아닌 스크립터블 오브젝트에 저장하세요. 스크립터블 오브젝트는 한 번만 설정하면 되는 프로젝트 내부의 에셋으로 게임 오브젝트에 직접 연결할 수 없습니다.

스크립터블 오브젝트에서 필드를 생성하여 값 또는 설정을 저장한 다음 MonoBehaviours에서 스크립터블 오브젝트를 참조하세요.

 
다양한 게임 오브젝트의 설정을 보관하는 Inventory 스크립터블 오브젝트확장

스크립터블 오브젝트의 필드를 사용하면 MonoBehaviour로 오브젝트를 인스턴스화할 때마다 데이터의 불필요한 중복을 방지할 수 있습니다.

스크립터블 오브젝트 소개 튜토리얼에서 스크립터블 오브젝트의 장점을 알아보세요. 관련 기술 자료는 여기에서도 찾을 수 있습니다.

모바일 성능 팁 전체 목록 다운로드

다음 블로그 포스팅에서는 그래픽스 및 GPU 최적화에 대해 자세히 알아보겠습니다. 팀에서 제공하는 유용한 팁 목록을 모두 보고 싶다면 여기에서 전자책으로 참고하실 수 있습니다.

 

 

ref : https://blog.unity.com/kr/technology/optimize-your-mobile-game-performance-tips-on-profiling-memory-and-code-architecture

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

렌더링 순서 - Queue 태그  (0) 2022.12.24
효과적인 C# 메모리 관리 기법  (0) 2022.12.04
C#은 stackless 코루틴  (0) 2022.12.04
Unity vertex / fragment Shader01 -빨강 쉐이더  (0) 2022.12.02
Keyword Boolean  (0) 2022.11.27
반응형

C#은 stackless한 코루틴을 지원합니다

  • stackless 코루틴은 stackful 코루틴처럼 사기군같은 컨텍스트 스위칭 없이 자체 상태머신을 이용한 멀쩡해보이는 방법으로 구현한다.

Stackful Coroutines vs Stackless Coroutines

Stackful Coroutines

먼저 스택풀 코루틴(Stackful Coroutine)은 함수 호출시에 사용되는 분리된 스택을 가지고 있다. 코루틴이 어떻게 동작하는지 정확하게 이해하기 위해서 로우레벨 관점에서 간단히 함수 프레임과 함수 호출에 대해서 살펴볼 것이다. 하지만 먼저 스택풀 코루틴의 특성에 대해서 알아보자.

  • 스택풀 코루틴은 자신만의 스택을 가지고 있다.
  • 스택풀 코루틴의 수명은 호출한 코드로부터 독립적이다.

스택이란 것은 메모리 관점에서 볼 때 연속된 메모리 블럭이며, 스택은 지역변수 및 함수 인자등을 저장하기 위해 필요하다. 하지만 더 중요한 것은 각 함수 호출 후에 추가적인 정보가 스택에 배치되어 호출된 함수가 피호출자(callee)에게 반환하고 프로세서 레지스터를 복원하는 방법을 알려주게 된다.

몇가지 레지스터들은 특정 목적을 가지고 있으며 함수 호출시 스택에 저장된다. 이러한 레지스터는 다음과 같다.

  • SP – Stack Pointer
  • LR – Link Register
  • PC – Program Counter

스택포인터 (Stack Pointer)는 현재 함수 호출에 속하는 스택의 시작주소를 가지고 있는 레지스터다. 이 값 덕분에 스택에 저장된 인수와 지역 변수를 쉽게 참조할 수 있다.

링크 레지스터(Link Register) 함수 호출에서 매우 중요하다. 현재 함수 실행이 끝난 후 실행할 코드가있는 반환 주소(수신자 주소)를 저장한다. 함수가 호출되면 PC가 LR에 저장되고, 함수가 반환되면 LR을 사용하여 PC가 복원된다.

프로그램 카운터(Program counter)는 현재 실행중인 명령어의 주소다.

함수가 호출 될 때마다 링크 레지스터가 저장되므로 함수가 완료된 후 반환 할 위치를 알 수 있다.

함수의 호출과 반환에서 PC 및 LR 레지스터의 동작

스택풀 코루틴이 실행되면 호출 된 함수는 이전에 할당 된 스택을 사용하여 인수와 지역 변수를 저장한다. 함수 호출은 스택풀 코루틴에 대한 모든 정보를 스택에 저장하기 때문에 코루틴에서 호출되는 모든 함수들의 실행을 중단 할 수 있다.

Stackless Coroutines

스택리스 코루틴(Stackless Coroutine)은 스택풀 코루틴과 비교했을 때 약간의 차이점이 있다. 그러나 주요 특징은 여전히 시작(started)되고, 스스로 중단(suspended) 된 후에 다시 재개(resumed)할 수 있다는 점이다.

스택리스 코루틴의 특징은 다음과 같다.

  • 코루틴은 호출자(caller)와 강하게 연결되어 있다. 코루틴에 대한 호출은 실행을 코루틴으로 전송하고 코루틴에서 양보(yield)하는 것은 호출자에게 돌아온다.
  • 스택풀 코루틴은 스택의 수명만큼 유지 되지만, 스택리스 코루틴은 객체의 수명만큼 유지된다.

그러나 스택리스 코루틴의 경우 전체 스택을 할당할 필요가 없기 때문에 훨씬적은 메모리를 요구하지만, 그 때문에 몇가지 제약사항이 생긴다.

스택이 없다면 메모리에서 스택을 할당하지 못하면 어떻게 동작하는거지? 스택에 저장될 모든 데이터는 어디로 가는걸까?라는 의문이 생길 수 있다.  정답은 바로 호출자의 스택을 사용하는 것이다.

스택리스 코루틴은 최상위 레벨(top-level) 함수에서만 스스로를 중단(suspend) 할수 있다. 일반적인 함수의 경우, 데이터가 호출 수신자 스택에 할당되기 때문에 코루틴에서 호출 된 모든 함수는 코루틴이 중단되기 전에 완료되어야 한다. 코루틴이 상태를 유지하는 데 필요한 모든 데이터는 Heap 메모리에 동적으로 할당된다. 이것은 일반적으로 미리 할당 된 전체 스택보다 크기가 훨씬 작은 두 개의 지역 변수와 인수를 사용한다.

코루틴 중단 및 재개에서 기억해야 할 데이터가 훨씬 적지만 코루틴은 최상위 레벨 함수에서만 일시 중단하고 자신을 반환 할 수 있다. 모든 함수 및 코루틴 호출은 동일한 방식으로 발생하지만 코루틴의 경우 일부 추가 데이터를 호출에서 보존해야 중단 지점으로 이동하고 지역 변수의 상태를 복원하는 방법을 알 수 있다. 게다가 함수 프레임과 코루틴 프레임 사이에는 차이가 없다.

코루틴은 다른 코루틴을 호출 할 수도 있다. 스택리스 코루틴의 경우, 하나에 대한 각 호출이 새로운 코루틴 데이터를 위한 새로운 공간을 할당하게 된다. (코루틴에 대한 여러 호출은 다양한 동적 메모리 할당을 유발할 수 있음).

코루틴을 사용하기 위해서는 코루틴을 지원하는 언어가 필요한 이유는 컴파일러가 코루틴의 상태를 설명하는 변수를 결정하고 중단 지점으로의 점프를 위한 보일러 플레이트 코드를 생성해야 하기 때문이다.

 

 

ref : https://pjc0247.tistory.com/100

ref : https://www.charlezz.com/?p=44635 

반응형
반응형

C#은 가비지 컬렉터(GC)가 메모리를 자동으로 관리한다. 필요 없는 클래스의 인스턴스를 메모리에서 바로 지우는 게 아니라, 조건이 될 때까지 기다렸다가 지우기 때문에 클래스를 지웠다고 해도 그게 실제로 바로 삭제되는 것은 아니다. 일반적인 메모리라면 GC에 맡겨도 상관이 없지만, 관리되지 않는(Unmanaged, Native) 리소스는 즉각 해제해야 하는 경우가 생기는데, 그럴 때 필요한 것이 Dispose이다.

 

그래서, C++의 경우 소멸자에 각종 변수의 메모리를 해제하는 것으로 간단하게 구현이 될 만한 내용이지만, C#에서는 바로 삭제가 필요한 리소스를 해제하기 위해서 Dispose 함수가 필요하다.

 

소멸자와 Dispose의 차이 : stackoverflow.com/questions/339063/what-is-the-difference-between-using-idisposable-vs-a-destructor-in-c?newreg=eff4d2b40e844d5581272cc65983c418

 

What is the difference between using IDisposable vs a destructor in C#?

When would I implement IDispose on a class as opposed to a destructor? I read this article, but I'm still missing the point. My assumption is that if I implement IDispose on an object, I can

stackoverflow.com

그런데, Dispose를 호출하는 걸 깜박하면 어떻게 될까?

그럴 경우에는 GC에서 해당 인스턴스를 제거할 때, (소멸자에서) Dispose를 호출해준다.

큰 문제가 없으면 그냥 냅 둬도 상관없다는 얘기인데, 다만 다음과 같은 부작용이 생길 수 있다고 한다.

 

1.GC가 해당 인스턴스를 언제 지울 지 알 수 없다.

 즉, Dispose가 언제 호출될지 알 수 없다. 애플리케이션이 종료될 때까지 호출이 안될 수도 있다. 중요한 리소스를 사용하거나, 메모리를 많이 먹는 클래스라면 위험할 수도 있겠다.

 

2.GC가 해당 인스터스를 지울 때 Dispose를 호출하게 되면 그 순간 부하가 걸린다.

 즉, 랜덤하게 랙을 유발할 수도 있다. 성능에 예민한 앱이라면 주의가 필요하다.

 

3.GC가 해당 인스턴스를 지울 때 Dispose에서 예외가 발생하면 어플 자체가 크래시 할 수도 있다.

 수동으로 Dispose를 할 때에는 try/catch로 보호할 수 있지만, 자동제거일 경우 예외를 잡을 수 없기 때문에 매우 위험할 수도 있다.

 

참고 링크 : stackoverflow.com/questions/32397248/what-happens-if-i-dont-call-dispose

 

 

매번 Dispose를 호출하기 귀찮을 때는 using 블록을 사용하면 된다. using을 사용하면 자동으로 Dispose를 해준다.

using과 try/catch를 같이 사용해야 할 때에는 using을 try 안쪽에 넣으면 된다.

 

참고 링크 : theeye.pe.kr/archives/2736

 

Unity C# – IDisposable 알아보기 | 아이군의 블로그

이번에는 IDisposable 인터페이스에 대해서 알아보겠습니다. C#은 가비지콜랙터(Garbage Collector)를 가지고 있습니다. 이 GC는 기본적으로 관리되는 모든 객체들의 참조 링크를 관리하며 더이상 참조되

theeye.pe.kr

 

Dispose 기능이 필요한 커스텀 클래스를 만들어야 할 때에는 IDisposal을 상속 받아서 만들면 된다.

 

참고 링크  : medium.com/dotnetdev/idisposable-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-4fa0fcf0e67a

 

IDisposable 패턴의 올바른 구현 방법

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을

medium.com

참고 링크 2 : codeless.tistory.com/entry/C-Disposable-%ED%8C%A8%ED%84%B4

 

C# Disposable 패턴

public class MyResourceHog : IDisposable { private bool alreadyDisposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool isDisposing)..

codeless.tistory.com

 

 

ref : https://chomdoo.tistory.com/15

반응형

'프로그래밍(Programming) > C#' 카테고리의 다른 글

ThreadLocal<T> 와 AsyncLocal<T> 의 차이점  (0) 2022.12.29
Common Memory Leaks In C#  (0) 2022.12.04
ReaderWriterLockSlim  (0) 2022.11.27
SpinLock  (0) 2022.11.27
c# AutoResetEvent, ManualResetEvent (커널모드)  (0) 2022.11.27
반응형

금일은 요즘 유니티를 사용하면서 문제점에 부디친 URP 때문에
유니티 쉐이더를 다시 들여다 보려고 합니다.(역시 기본을 아는 것이 가장 중요한 것 같습니다.)

기존의 유니티 쉐이더는 3가지 방식으로 작성될 수 있었습니다.

ShaderLab, Surface Shader 그리고
전통적인 방식인 vertex, fragment 쉐이더 방식으로 작성되어오고 있습니다.

작성법도 편하고 빛계산도 비교적 수월한 Surface 쉐이더를 많이 사용하고 있는 것 같은데요 최근의 URP 파이프라인으로 넘어오면서는 전통적인 HLSL
(vertex, fragment )방식으로 쉐이더가 작성되고 있습니다. (물론 쉐이더그래프를 지원하며 더 직관적인것 같긴 하지만…. 그 원리를 모른다면 매번 문제에 직면 하는 것 같습니다.)
그래서 이전의 vertex, fragment 쉐이더방식을 처음부터 차근차근 되짚어 보려고 합니다.

그 중에서 모든 쉐이더 공부의 첫 시작은 빨갱이 쉐이더인 것 같아서
버텍스쉐이더와 프래그먼트쉐이더를 사용해서 빨강 쉐이더를 만들어 보려고 합니다. (URP이전의 쉐이더 입니다.)

정말 간결하고 아름답습니다.
왜 아름다운지 그 구조부터 들여다 보겠습니다. 유니티 쉐이더의 구조는

좌측 그림처럼 Shader 안에 Properties(속성) 안에 SubShader (여러개가 존재할수 있습니다.) 그리고 마지막에 fallback 이라는 구조로 구성되어 있습니다.(참조 https://jinhomang.tistory.com/43)

그런데 왜 서브쉐이더라는 것이 존재하며 여러개 존재할 수 있을까요?
바로 하드웨어의 성능 때문이라고 합니다.
하드웨어 마다 다양한 성능을 가지고 있기때문에 거기에 적합한 쉐이더들을 각각의 서브쉐이더에 작성을 해둔다고 합니다.

유니티가 렌더링을 할때 서브쉐이더를 들여다 보면서 가장 위의 서브쉐이더(가장 최상품질)부터 찾아본후 그에 적합한 서브쉐이더를 적용하게 되며
마지막까지 적합한 스펙이 없으때 fallBack 이라는 부분에 적혀 있는 쉐이더를 사용하게 된다고 합니다.

https://jinhomang.tistory.com/43 흑기사님의 글이 정말 주옥같습니다.!

그리고 서브쉐이더 안에는 여러개의 pass가 존재하는데요 오브젝트의 렌더링되는 단위라고 합니다. 즉 한번만 그리는 것이 아니라 pass를 통해 여러번 단계로 그릴수 있다는 것을 의미합니다.

자 이제 빨강쉐이더로 돌아가 보겠습니다.

Shader “CustomRout/CustomShader”{
// 쉐이더의 이름과 경로를 정해주게 됩니다. 쉐이더 선택창에 CustomRout 안에 CustomShader로 보여집니다.
Properties{
_Color(“MainColor”, Color) = (1,1,1,1)
}
// 속성을 적용할수 있는 부분이며 메터리얼 창에 나타나게 됩니다. 메터리얼 창에 MainColoe 로 보여지며 디폴트값은 1,1,1,1 즉 화이트로 보여지게 됩니다.

//서브쉐이더가 시작됩니다.
SubShader
{
Tags { “RenderType”=”Opaque” } // 렌더타입을 적어줍니다. 이후에 알아봅니다.

LOD 100

// 패스가 시작됩니다.
Pass
{

// 실직적인 쉐이더 프로그래밍은 CGPROGRAM ~ ENDCG 안에서 이루어 집니다.
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 버텍스 함수는 vert라는 함수가 사용될것이다
// 프래그먼트 함수는 frag 라는 함수가 사용될것이다, 즉 vert 와 frag는 사용자가 이름을 바꾸어줄 수도 있다는 뜻입니다.

struct vertexInput
{
float4 vertex : POSITION;
};

//버텍스 입력을 받는 구조체입니다.
float4 형식의 vertex 가 POSITION 값을 가지고 있습니다.

struct vertexOutput
{
float4 pos : SV_POSITION;
};

//버텍스 출력을 하는 구조체입니다. float4 형식의 pos 가 SV_ POSITION 값을 가지고 있습니다. SV_POSITION 은 버텍스의 위치를 출력해준다라고 생각하시면 됩니다.

float4 _Color;

//프라퍼티에 적용하였던 값이며 꼭 한번은 선언을 해주어야 작동하게 됩니다.

vertexOutput vert (vertexInput v)

{
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);

// 버텍스함수입니다.

버텍스 인풋구조체 v의 값을 인자로 가집니다.
o의 오브젝트 공간의 위치를 v의 버텍스위치에서 클립공간으로 바꾸어줍니다.

오브젝트 공간의 한 점을 동질적인 좌표에 있는 카메라의 클립 공간으로 변환합니다. mul(UNITY_MATRIX_MVP, float4(pos, 1.0))과 같으며, 이를 대신하여 사용해야 합니다.
로컬 공간(오브젝트 공간) 의 버텍스를 클립공간으로 변환합니다. 기본적으로 공간변환은 로컬 — 월드 — 뷰 — 프러스텀인데, 이걸 모델-뷰-프러스텀이라고 부르면서 MVP라고 부릅니다. 그런데 프러스텀은 니어와 파로 잘라지죠? 그러면 절두체 모양이 됩니다. 일종의 찌그러진 큐브죠. 이 큐브 영역을 0~ 1사이로 변환한 것이 클립 포지션이라 합니다. 즉 최종 영역까지 한 번에 변환하는 함수입니다.

출처: https://chulin28ho.tistory.com/539 [대충 살아가는 게임개발자]

return o; // o구조체를 반환홥니다.
}

//프래그먼트 함수입니다.

fixed4 frag (vertexOutput i) : SV_Target

{

float4 col = float4(1.0,0.0,0.0,1.0);

return col;

//레드컬러를 반환합니다.

}

ENDCG

}

}

}

이상으로 기본적인 짧은 unlit 빨강 쉐이더를 알아보았습니다.
(참고 unlit 은 unlighting 즉 빛이 적용되지않는다! 라는 뜻입니다.

 

 

ref : https://medium.com/@p5js/unity-vertex-fragment-shader01-%EB%B9%A8%EA%B0%95-%EC%89%90%EC%9D%B4%EB%8D%94-a3fdebbc0c8f

반응형
반응형

 

 

ReaderWriterLock 은 EnterWriterLock 호출 되기 전까진 EnterReadLock과 ExitReadLock  구간을 lock 없이 읽을 수 있는데 EnterWriterLock  이 호출 되면 이때 lock 이 점유되어 놓아지기 전까지 EnterReadLock ~ ExitReadLock   구간을 다른 스레드가 접근하지 못하게 된다

 

아이템 업데이트를 서버에서 잠깐 처리해야 할때 흔히자읂은 업데이트에서 사용 될 수도 있다

 

 

 

반응형
반응형

SpinLock 사용 예시

 

static SpinLock _lock

 

Enter 에서 현재 lock 을 제대로 잡았는지에 대한 상태를 lockTaken 변수를 통해서 알 수 있게 된다

 

Enter(Boolean) 메서드 호출에서 예외가 발생하는 경우에도 안정적인 방식으로 잠금을 얻으며 잠금을 얻었는지 확인하기 위해 lockTaken을 안정적으로 검사할 수 있습니다.
Exit() 잠금을 해제합니다.
Exit(Boolean) 잠금을 해제합니다.

 

 

//      SpinLock.IsThreadOwnerTrackingEnabled
    static void SpinLockSample2()
    {
        // Instantiate a SpinLock
        SpinLock sl = new SpinLock();

        // These MRESs help to sequence the two jobs below
        ManualResetEventSlim mre1 = new ManualResetEventSlim(false);
        ManualResetEventSlim mre2 = new ManualResetEventSlim(false);
        bool lockTaken = false;

        Task taskA = Task.Factory.StartNew(() =>
        {
            try
            {
                sl.Enter(ref lockTaken);
                Console.WriteLine("Task A: entered SpinLock");
                mre1.Set(); // Signal Task B to commence with its logic

                // Wait for Task B to complete its logic
                // (Normally, you would not want to perform such a potentially
                // heavyweight operation while holding a SpinLock, but we do it
                // here to more effectively show off SpinLock properties in
                // taskB.)
                mre2.Wait();
            }
            finally
            {
                if (lockTaken) sl.Exit();
            }
        });

 

 

 

ref : https://learn.microsoft.com/ko-kr/dotnet/api/system.threading.spinlock?view=net-7.0

반응형

+ Recent posts