본문 바로가기

- Programming/- C#

★ 4. c# 네트워크 개발 p3

반응형

==================================================================

모든 출처는

- 유니티 개발자를 위한 C#으로 온라인 게임 서버 만들기 - 저자 이석현, 출판사 한빛미디어

그리고 URL :

http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Lecture_series&no=63

==================================================================


C#으로 게임 서버 만들기 - 3. tcp 패킷 수신 처리


- 2장. TCP에서의 메시지 처리 방법 -

2-1. 메시지 경계 처리


tcp는 메시지의 경계가 없는 프로토콜입니다. 연속된 바이트들을 보내고 받을 뿐이죠.

경계가 없다는 것은 전송과 수신이 1:1로 이루어지지 않는다는 뜻입니다.

보내는 곳에서는 한번에 보내도 받는 곳에서는 두번에 걸쳐 받을 수 있습니다.

클라이언트가 "Hello" "World"라는 문자열을 두번에 걸쳐 서버에 보냈을 경우 서버에서 수신할 때는 아래의 모든 상황이 다 발생할 수 있습니다.

Hello
World
- 두번에 걸쳐서 받는 경우.

He
llo
World
- He를 먼저 받고 그 다음에 llo, World를 받아 총 세번에 걸쳐 받는 경우

HelloWorld
- 한번에 "Hello" "World"전체를 받는 경우.

소켓에서는 우리가 보낸 데이터가 어떤 의미를 갖는지 알 수 없습니다. 그것은 어플리케이션 내의 구현 영역인 것이죠. 단지 바이트화가 되어있는 데이터들을 네트워크 선로를 따라 목적지까지 보낼 뿐입니다. 이 바이트들이 무엇을 의미하는지 소켓에서는 전혀 신경쓰지 않습니다.
그렇다면 He + llo 이런 식으로 데이터가 잘려서 올 경우, 또는 HelloWorld처럼 붙어서 올 경우에는 어떻게 "Hello" "World"라는 문자열로 이쁘게 만들어 낼 수 있을까요?

소켓 버퍼로부터 데이터를 수신할 때 몇개의 부가정보를 파라미터로 받을 수 있다는 것을 기억하시는지요? 이전 강좌에서 봤던 ReceiveAsync 메소드의 콜백 처리 부분을 다시 한번 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void process_receive(SocketAsyncEventArgs e)
{
    // check if the remote host closed the connection
    CUserToken token = e.UserToken as CUserToken;
    if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
    {
        // 이후의 작업은 CUserToekn에 맡긴다.
        token.on_receive(e.Buffer, e.Offset, e.BytesTransferred);
 
        // 다음 메시지 수신을 위해서 다시 ReceiveAsync 메소드를 호출한다.
        bool pending = token.socket.ReceiveAsync(e);
        if (!pending)
        {
            process_receive(e);
        }
    }
    else
    {
        Console.WriteLine(string.Format("error {0}, transferred {1}", e.SocketError, e.BytesTransferred));
        close_clientsocket(token);
    }
}
cs

token.on_receive(e.Buffer, e.Offset, e.BytesTransferred);
이 코드를 보면 CUserToken.on_receive 메소드를 호출하면서 세 개의 파라미터를 전달하는 것을 알 수 있습니다. 각각 의미하는 내용은 아래와 같습니다.

- e.Buffer
소켓으로부터  수신된 데이터가 들어 있는 바이트 배열.

- e.Offset
데이터의 시작 위치를 나타내는 정수 값.

- e.BytesTransferred
수신된 데이터의 바이트 수

콜백 메소드가 호출될 때마다 새로운 데이터가 소켓 버퍼를 통해 들어오게 됩니다.
우리가 할 일은 이 버퍼에 들어있는 데이터를 어플리케이션 버퍼로 복사하여 패킷이라는 의미 있는 메시지로 만드는 것입니다.
그러기 위해서는 데이터의 시작 위치를 가리키는 Offset과 수신된 데이터의 크기를 나타내는 BytesTransferred값을 이용해야합니다. 이 정보들을 사용해서 하나의 데이터가 잘려서 오거나 여러 개의 데이터가 붙어서 올 경우에 대한 로직을 만들면 됩니다.

좀 더 단순하게 풀기 위해 소켓이라는 개념을 잠시 내려 놓고 알고리즘 측면에서 접근해 봅시다.
제가 만든 "Hello" "World"만들기 문제입니다.

1) "HelloWorld"라는 문자열이 있는데 이것을 "Hello", "World"로 분리 시키기.
2) "He", "llo", "World"를 "Hello", "World"로 만들기.
3) "Hel", "loWor", "ld"를 "Hello", "World"로 만들기.

이제 각각의 경우에 대해서 로직을 세워보겠습니다.
1번- "Hello"는 다섯 글자 라는 것을 알 수 있으니 문자열의 첫 다섯 글자만 잘라서 하나를 만들고, 그 다음 다섯 글자를 잘라서 또 하나를 만들면 "Hello", "World"로 분리됩니다.

2번- "He"는 아직 다섯 글자에 못미치니 다음 데이터를 붙여 봅니다.
"He" + "llo" = "Hello" 다섯 글자 하나가 완성되었네요.
그 다음 "World"도 다섯 글자이니 또 하나 완성.

3번- "Hel"은 아직 다섯 글자에 못미치니 다음 데이터를 붙여 봅니다.
"Hel" + "loWor" = "HelloWor" 다섯 글자가 넘어서 버리는군요. 이럴땐 다섯 글자 까지만 잘라서 하나를 만듭니다. 잘려진 나머지 데이터 "Wor"에 대해서 다시 다섯 글자를 만들어 봅시다.
뒤에 "ld"를 붙이면 "World"가 완성됩니다.

이 문제에서 봤던 문자열을 소켓에서 넘어온 데이터라고 가정하면 됩니다.
물론 글자 크기에 맞게 자르고 붙이고 남은 데이터는 잠시 보관해 놓는 등의 작업들이 귀찮기는 하겠지만 tcp에서 패킷을 주고 받으려면 반드시 필요한 부분입니다.

2-2-1. 패킷 설계

앞에서 설명한 문제에서는 문자열이라고 말했는데 이제는 바이트라는 개념으로 바꿔서 생각하겠습니다. 사람이 보기에는 문자, 숫자이지만 컴퓨터로 표현되는 것은 그냥 바이트의 나열일 뿐입니다.
그리고 "Hello"가 다섯 글자라고 말했지만 이제부터는 5바이트라고 생각하도록 합시다.
(유니코드 utf-8로 인코딩 했을 때 5바이트가 됩니다.)

여기서 잠시 의문을 가져봅니다.
"Hello", "World"는 각각 다섯 글자. utf-8로 인코딩 할 경우 5바이트라는 것을 우리는 눈으로 봐서 알 수 있습니다. 그래서 로직을 세울 때도 5바이트만큼 잘라서 하나의 데이터를 완성하였죠.
그런데 만약 "Hello"(5바이트) "World!!"(7바이트) 이렇게 데이터의 크기가 모두 다를 경우에는 어떻게 처리해야 할까요?

1) 패킷의 크기를 모두 고정시킨다.
2) 데이터 앞에 크기를 나타내는 값을 넣어서 같이 전송한다.
3) 데이터의 끝을 나타내는 특수 문자를 삽입한다.

tcp를 설명한 책에 보면 이렇게 세 가지 예시가 나오더군요.
1번은 너무 제한적입니다. 게임에서 주고 받는 패킷의 크기가 모두 같을 수는 없죠. 최대 크기로 만든다면 낭비도 심합니다.
3번은 너무 효율이 안좋을 것 같습니다.
바이트를 하나하나 비교해가며 데이터의 끝인지 아닌지 체크해야 하기 때문에 성능이 많이 떨어지겠네요. 또 어떤 문자를 써야하는지도 애매합니다.
2번이 기가 막힌 아이디어 같습니다. 데이터 앞에 크기를 써 넣고 그 크기만큼만 뽑아서 하나의 패킷을 완성합니다. 다음 데이터도 마찬가지로 계속 반복하면 되겠죠. 그렇다면 2번 방법으로 로직을 세워보겠습니다.

앞에서 나왔던 "Hello" "World"문제를 약간 바꿔서 "Hello" "World!!"라고 바꿔보겠습니다. 그리고 각각의 데이터 앞에 데이터의 크기를 나타내는 값을 넣어보겠습니다.

"5Hello", "7World!!"

소켓으로부터 데이터가 넘어오면 제일 처음 해야 할 일은 데이터 크기가 몇 바이트인지 알아내는 것입니다. 첫 부분을 보면 5라고 되어있습니다. 5바이트 만큼만 앞의 로직대로 처리하여 하나의 패킷을 만듭니다. 그 다음에는 7바이트이므로 "World!!"를 만들어서 또 하나의 패킷을 완성합니다.

근데 너무 간단해 보여서 허전합니다.
우리가 한가지 놓친 것이 있습니다. 데이터 크기가 맨 앞에 붙어 있다는 것은 알았는데 이 크기를 나타내는 값은 도대체 몇 바이트인 것일까요?
이제 간단하지만 아주 쓸모 있는 패킷 헤더를 설계해 볼 시간입니다.
헤더에는 이 데이터가 몇 바이트인지 나타내주는 값이 들어가게 됩니다.

[헤더][데이터][헤더][데이터]...

바로 이런 모습이 되도록 패킷을 만들어 볼 것입니다. [헤더]에는 데이터의 크기가 들어가는데 이 헤더의 바이트는 2바이트로 고정하겠습니다.
데이터를 읽어올 때 처음 2바이트는 무조건 헤더라고 생각하면 됩니다. 그리고 헤더에 나타난 데이터 크기 만큼 읽어와서 하나의 패킷을 만들어주면 됩니다.

그렇다면 "Hello"는 5바이트라는 것을 하나의 패킷으로 만들어 봅시다.

1
2
short size = 5;
string data = "Hello";
cs

헤더를 2바이트로 고정한다고 말씀 드렸기 때문에 2바이트 자료형인 short를 사용했습니다.
이것의 의미는 우리가 보낼 데이터의 크기가 short자료형의 최대 값을 넘지 않는다는 뜻입니다.
short자료형이 표현할 수 있는 최대 값은 37,767이기 때문에 게임에서 쓰기에는 충분할 것 같아서 이렇게 결정했습니다. 더 큰 데이터를 보내고 싶으시면 short대신 int(4byte)등을 사용하면 되겠습니다. 아니면 unsigned short를 써서 같은 2바이트 범위이지만 더 큰 수치를 표현해도 됩니다.
데이터 크기는 -값이 올 수 없으므로 unsigned를 써도 괜찮은 방법이겠네요.
하지만 저는 그냥 short를 쓰도록 하겠습니다. 어차피 32,767만으로도 충분하기 때문이죠.
그리고 unsigned 자료형이 없는 언어와의 호환성도 맞출 수 있습니다(자바가 그렇다네요).

1
2
3
4
5
6
7
8
9
10
short size = 5;
string data = "Hello";
 
byte[] header = BitConverter.GetBytes(size);
byte[] body = Encoding.UTF8.GetBytes(data);
 
byte[] packet = new byte[1024];
 
Array.Copy(header, 0, packet, 0, header.Length);
Array.Copy(body, 0, packet, header.Length, body.Length);
cs

헤더와 데이터를 하나의 패킷으로 만드는 코드입니다.
BitConverter.GetBytes 메소드는 데이터를 바이트 배열로 변환해주는 역할을 수행합니다.
short 자료형으로 되어 있는 헤더를 바이트로 변환하여 byte header[] 배열에 채워 넣습니다. 다음으로 문자열 데이터도 바이트로 변환하여 넣는데 인코딩 방식은 utf-8을 사용했습니다.
그리고 byte[] packet = new byte[1024]; 라는 패킷 버퍼를 만들어서
"[헤더][데이터]"의 구조로 패킷을 구성합니다.
Array.Copy 메소드는 원본 배열 데이터를 다른 곳으로 복사할 때 사용하는 메소드입니다.

여기까지 하면 하나의 패킷이 완성된 것입니다.
header, body, packet의 내용을 디버거로 확인해보면 다음과 같이 채워져 있는 것을 볼 수 있습니다.

header
[0]    5
[1]    0

body
[0]    72
[1]    101
[2]    108
[3]    108
[4]    111

packet
[0]    5
[1]    0
[2]    72
[3]    101
[4]    108
[5]    108
[6]    111
[7]    0
[8]    0
[9]    0
.
.
[1023]    0

header는 short 자료형이므로 2바이트를 차지하며 내용은 5라는 정수 값입니다.
body는 string을 바이트로 변환한 데이터이며 "Hello"에 해당하는 유니코드 문자열 코드값이 들어가 있습니다. utf-8로 인코딩 하였으므로 크기는 5바이트만큼 차지합니다.
packet은 header와 body를 합쳐 놓은 것이기 때문에 [header][data]가 차례로 들어가게 됩니다. 뒤에 0으로 채워진 데이터는 비어 있는 공간입니다.
실제로 원격지에 전송할 때는 이 빈 공간을 제외한 "헤더 + 데이터"의 크기 만큼만 소켓 버퍼로 전송해야 합니다.

2-2-2. 패킷 수신

tcp 게임 서버 구현시 패킷 처리하는 방법에 대해 이론적으로 알아봤습니다.
이제 본격적으로 패킷 수신 코드를 작성해보겠습니다.

지금까지 단편적인 소스코드를 통해서 설명드렸기 때문에 서버 라이브러리의 전체적인 모습은
잘 그려지지 않을겁니다. 먼저 작은 부분들을 이해한 뒤 이런 코드 조각들이 어떻게 조화를
이루게 되는지 설명 드리도록 하겠습니다.
전체적인 설계는 프로그래머에 따라서 달라질 수 있기 때문에 처음부터 전체 모습을 보여드리면 혼란이 올 듯 하여 소주제 위주로 설명해 나가는 방식을 선택했습니다.

1
2
3
4
5
6
7
private void process_receive(SocketAsyncEventArgs e)  
{  
    ...
    // 이후의 작업은 CUserToken에 맡긴다.
    token.on_receive(e.Buffer, e.Offset, e.BytesTransferred);
    ...
cs

다시 process_receive 메소드로 돌아와봅시다. token.on_receive 메소드로 넘기는 세 개의 파라미터에 대해서는 위에서 이미 설명드렸습니다.
이제 저 파라미터들을 사용하여 바이트 덩어리를 패킷이라는 의미 있는 데이터로 만드는 방법에 대해 알아보겠습니다.

1
2
3
4
public void on_receive(byte[] buffer, int offset, int transfered)
{
    this.message_resolver.on_receive(buffer, offset, transfered, on_message);
}
cs

CUserToken.on_receive 메소드의 내용입니다.
메소드 안에서 message_resolver의 on_receive를 또 호출해주고 있습니다.
이것은 설계하는 사람에 따라 다를 수 있는 부분인데요, CUserToken 클래스에서 직접 데이터를 해석하지 않고 message_resolver를 따로 둔 이유는 다음과 같습니다.

추후 확장성을 고려하여 다른 resolver를 만들 때 CUserToken의 코드 수정을 최소화 하기 위해서

강좌에서는 tcp 메시지 경계 처리라는 목표에 집중하기 위해서 "[헤더][데이터]" 구조로 이루어진 방식을 채택했지만 추후에 다른 방식으로 구성된 메시지를 사용하게 될 경우도 생길 수 있습니다.
따라서 데이터 해석은 message_resolver가 처리하고 CUserToken 클래스는 유저 객체 본연의 임무만 수행하도록 한 것입니다.

다음은 message_resolver의 on_receive 메소드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/// <summary>
/// 소켓 버퍼로부터 데이터를 수신할 때 마다 호출
/// 데이터가 남아 있을 때까지 계속 패킷을 만들어 callback을 호출
/// 하나의 패킷을 완성하지 못했다면 버퍼에 보관해 놓은 뒤 다음 수신을 기다림
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="transffered"></param>
public void on_receive(byte[] buffer, int offset, int transffered, CompletedMessageCallback callback)
{
    // 이번 receive로 읽어오게 될 바이트 수
    this.remain_bytes = transffered;
 
    // 원본 버퍼의 포지션 값.
    // 패킷이 여러 개 뭉쳐 올 경우 원본 버퍼의 포지션은 계속 앞으로 가야 하는데 그 처리를 위한 변수
    int src_position = offset;
 
    // 남은 데이터가 있다면 계속 반복
    while (this.remain_bytes > 0)
    {
        bool completed = false;
 
        // 헤더만큼 못 읽은 경우 헤더를 먼저 읽음
        if (this.current_position < Defines.HEADERSIZE)
        {
            // 목표 지점 설정 (헤더 위치까지 도달하도록 설정)
            this.position_to_read = Defines.HEADERSIZE;
 
            completed = read_until(buffer, ref src_position, offset, transffered);
            if (!completed)
            {
                // 아직 다 못읽었으므로 다음 receive를 기다림
                return;
            }
 
            // 헤더 하나를 온전히 읽어왔으므로 메시지 사이즈를 구함
            this.message_size = get_body_size();
 
            // 다음 목표 지점 (헤더 + 메시지 사이즈)
            this.position_to_read = this.message_size + Defines.HEADERSIZE;
        }
 
        // 메시지를 읽음
        completed = read_until(buffer, ref src_position, offset, transffered);
 
        if (completed)
        {
            // 패킷 하나를 완성
            callback(new Const<byte[]>(this.message_buffer));
 
            clear_buffer();
        }
    }
}
 
 
/// <summary>
/// 목표 지점으로 설정된 위치까지의 바이트를 원본 버퍼로 복사
/// 데이터가 모자랄 경우 현재 남은 바이트 까지만 복사
/// </summary>
/// <param name="buffer"></param>
/// <param name="src_position"></param>
/// <param name="offset"></param>
/// <param name="transffered"></param>
/// <returns>다 읽었으면 true, 데이터가 모자라서 못 읽었으면 false 리턴</returns>
bool read_until(byte[] buffer, ref int src_position, int offset, int transffered)
{
    if (this.current_position >= offset + transffered)
    {
        // 들어온 데이터 만큼 다 읽은 상태이므로 더이상 읽을 데이터가 없음.
        return false;
    }
 
    // 읽어와야 할 바이트.
    // 데이터가 분리되어 올 경우 이전에 읽어 놓은 값을 빼줘서 부족한 만큼 읽어올 수 있도록 계산해줌
    int copy_size = this.position_to_read - this.current_position;
 
    // 남은 데이터가 더 적다면 가능한 만큼만 복사
    if (this.remain_bytes < copy_size)
    {
        copy_size = this.remain_bytes;
    }
 
    // 버퍼에 복사.
    Array.Copy(buffer, src_position, this.message_buffer, this.current_position, copy_size);
 
    // 원본 버퍼 포지션 이동
    src_position += copy_size;
 
    // 타겟 버퍼 포지션도 이동
    this.current_position += copy_size;
 
    // 남은 바이트 수
    this.remain_bytes -= copy_size;
 
    // 목표 지점에 도달 못했으면 false
    if (this.current_position < this.position_to_read)
    {
        return false;
    }
 
    return true;
}
cs

코드가 좀 길군요. 주석에도 설명을 달았지만 이해하기 쉽게 다시 설명하겠습니다.
이 클래스에서 가장 중요한 메소드인 on_receive와 read_until 메소드입니다.
on_receive는 소켓 버퍼로부터 데이터 수신이 발생할 때마다 호출되게끔 구조가 잡혀 있습니다.
read_until은 목표 지점으로 설정된 데이터를 소켓 버퍼로부터 유저 객체의 패킷 버퍼로 복사하는 역할을 합니다. 위에서 이론적으로 알아봤던 메시지 경계 처리 부분이 이 메소드를 통해 구현됩니다.
그리 좋은 알고리즘은 아니지만 강좌에 사용하는데 무리 없으리라 판단하여 코드를 오픈합니다.

로직은 아래와 같은 흐름으로 세웠습니다.

on_receive
1) 최초로 헤더를 읽는다.
2) 헤더를 다 못읽었다면 수신된 바이트 만큼만 복사한 뒤 다음 수신을 기다린다.
3) 헤더를 다 읽었다면 헤더로부터 데이터 사이즈를 구한 뒤 데이터를 읽는다.
4) 수신된 데이터가 사이즈보다 적게 들어왔다면 수신된 만큼만 복사한 뒤 다음 수신을 기다린다.
5) 데이터를 모두 읽었다면 하나의 패킷이 완성된 것으로 보고 1)부터 다시 반복한다.

구현해야 할 규칙은 명확합니다.
byte[] 배열에서 n 만큼 복사해오는 것입니다.
여기에는 1바이트가 들어 있을 수도 있고, 100바이트가 들어 있을 수도 있습니다.
그것을 나타내주는 파라미터가 Offset, ByteTransferred 값이며 이 값을 통해 정확한 양만큼 데이터를 가져오면 되는 것입니다.

직접 구현해보시면 이해가 더 빠를겁니다.
저는 일단 로직을 종이에 그려본 뒤 그 내용대로 코딩을 진행합니다.
코딩하면서 이상한 느낌이 들면 설계를 다시 살핍니다. 분명히 모든 상황을 다 대비해 놓은 것 같은데 한 두군데씩 빠진 부분이 생기더군요.
잘못된 부분이 있으면 설계를 조금 수정해서 다시 코딩에 들어갑니다.
이렇게 90%정도 완성되었다고 생각했을 때 더이상 머리가 안돌아가게 됩니다.
이 때부터 각종 상황을 만들어 놓은 뒤 브레이크 포인트를 걸고 디버깅을 하며 코드를 완성해 나갑니다. 최초 설계 했을 때 구멍난 부분이 디버깅을 해보면서 모두 다 드러나는군요. 실력이 부족해서인지 아직까지 한방에 설계하고 한방 코딩으로 끝나는 경우는 경험하지 못했습니다.

잘못된 데이터가 들어가 있는 경우, 데이터가 아예 없는 경우 등 오류 처리까지 해줘야
완벽하게 작동될 것입니다.
하지만 이 강좌에서는 이런 오류 처리까지는 구현하지 않겠습니다.

message_resolver의 전체 소스코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
class Defines
{
    public static readonly short HEADERSIZE = 2;
}
 
/// <summary>
/// [header][body] 구조를 갖는 데이터를 파싱하는 클래스.
/// - header : 데이터 사이즈. defines.HEADERSIZE에 정의된 타입만큼의 크기를 가짐
///                2바이트일 경우 Int16, 4바이트는 Int32로 처리하면 됨
///                본문의 크기가 Int16.Max 값을 넘지 않는다면 2바이트로 처리하는 것이 좋을 것 같음
///    - body : 메시지 본문.
/// </summary>
class CMessageResolver
{
    public delegate void CompletedMessageCallback(Const<byte[]> buffer);
 
    // 메시지 사이즈
    int message_size;
 
    // 진행중인 버퍼.
    byte[] message_buffer = new byte[1024];
 
    // 현재 진행중인 버퍼의 인덱스를 가리키는 변수
    // 패킷 하나를 완성한 뒤에 0으로 초기화 시켜야 함
    int current_position;
 
    // 읽어와야할 목표 위치
    int position_to_read;
 
    // 남은 사이즈
    int remain_bytes;
 
    public CMessageResolver()
    {
        this.message_size = 0;
        this.current_position = 0;
        this.position_to_read = 0;
        this.remain_bytes = 0;
    }
 
    /// <summary>
    /// 목표 지점으로 설정된 위치까지의 바이트를 원본 버퍼로 복사
    /// 데이터가 모자랄 경우 현재 남은 바이트 까지만 복사
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="src_position"></param>
    /// <param name="offset"></param>
    /// <param name="transffered"></param>
    /// <returns>다 읽었으면 true, 데이터가 모자라서 못 읽었으면 false 리턴</returns>
    bool read_until(byte[] buffer, ref int src_position, int offset, int transffered)
    {
        if (this.current_position >= offset + transffered)
        {
            // 들어온 데이터 만큼 다 읽은 상태이므로 더이상 읽을 데이터가 없음.
            return false;
        }
 
        // 읽어와야 할 바이트.
        // 데이터가 분리되어 올 경우 이전에 읽어 놓은 값을 빼줘서 부족한 만큼 읽어올 수 있도록 계산해줌
        int copy_size = this.position_to_read - this.current_position;
 
        // 남은 데이터가 더 적다면 가능한 만큼만 복사
        if (this.remain_bytes < copy_size)
        {
            copy_size = this.remain_bytes;
        }
 
        // 버퍼에 복사.
        Array.Copy(buffer, src_position, this.message_buffer, this.current_position, copy_size);
 
        // 원본 버퍼 포지션 이동
        src_position += copy_size;
 
        // 타겟 버퍼 포지션도 이동
        this.current_position += copy_size;
 
        // 남은 바이트 수
        this.remain_bytes -= copy_size;
 
        // 목표 지점에 도달 못했으면 false
        if (this.current_position < this.position_to_read)
        {
            return false;
        }
 
        return true;
    }
 
    /// <summary>
    /// 소켓 버퍼로부터 데이터를 수신할 때 마다 호출
    /// 데이터가 남아 있을 때까지 계속 패킷을 만들어 callback을 호출
    /// 하나의 패킷을 완성하지 못했다면 버퍼에 보관해 놓은 뒤 다음 수신을 기다림
    /// </summary>
    /// <param name="buffer"></param>
    /// <param name="offset"></param>
    /// <param name="transffered"></param>
    /// <param name="callback"></param>
    public void on_receive(byte[] buffer, int offset, int transffered, CompletedMessageCallback callback)
    {
        // 이번 receive로 읽어오게 될 바이트 수
        this.remain_bytes = transffered;
 
        // 원본 버퍼의 포지션 값.
        // 패킷이 여러 개 뭉쳐 올 경우 원본 버퍼의 포지션은 계속 앞으로 가야 하는데 그 처리를 위한 변수
        int src_position = offset;
 
        // 남은 데이터가 있다면 계속 반복
        while (this.remain_bytes > 0)
        {
            bool completed = false;
 
            // 헤더만큼 못 읽은 경우 헤더를 먼저 읽음
            if (this.current_position < Defines.HEADERSIZE)
            {
                // 목표 지점 설정 (헤더 위치까지 도달하도록 설정)
                this.position_to_read = Defines.HEADERSIZE;
 
                completed = read_until(buffer, ref src_position, offset, transffered);
                if (!completed)
                {
                    // 아직 다 못읽었으므로 다음 receive를 기다림
                    return;
                }
 
                // 헤더 하나를 온전히 읽어왔으므로 메시지 사이즈를 구함
                this.message_size = get_body_size();
 
                // 다음 목표 지점 (헤더 + 메시지 사이즈)
                this.position_to_read = this.message_size + Defines.HEADERSIZE;
            }
 
            // 메시지를 읽음
            completed = read_until(buffer, ref src_position, offset, transffered);
 
            if (completed)
            {
                // 패킷 하나를 완성
                callback(new Const<byte[]>(this.message_buffer));
 
                clear_buffer();
            }
        }
    }
 
    int get_body_size()
    {
        // 헤더 타입의 바이트만큼을 읽어와 메시지 사이즈를 리턴
 
        Type type = Defines.HEADERSIZE.GetType();
        if (type.Equals(typeof(Int16)))
        {
            return BitConverter.ToInt16(this.message_buffer, 0);
        }
 
        return BitConverter.ToInt32(this.message_buffer, 0);
    }
 
    void clear_buffer()
    {
        Array.Clear(this.message_buffer, 0this.message_buffer.Length);
 
        this.current_position = 0;
        this.message_size = 0;
    }
}
cs

이번 강좌에서는 tcp에서 데이터 경계를 구분하여 메시지를 수신하는
로직을 구현해 봤습니다. 다음 강좌에서는 데이터 전송 부분에 대해 알아보겠습니다.


반응형

'- Programming > - C#' 카테고리의 다른 글

★ 6. c# 네트워크 개발 p5  (0) 2017.02.10
★ 5. c# 네트워크 개발 p4  (0) 2017.02.09
★ 3. c# 박싱과 언박싱  (0) 2017.02.08
★ 2. c# 네트워크 개발 p2  (0) 2017.02.08
★ 1. c# 네트워크 개발 p1  (2) 2017.02.06