예전 포스팅에서 직렬화 클래스를 만들면서 패킷이 직렬화 클래스를 상속하는 구조로 사용했는데
이게 과연 옳은 것일까!? 의문을 가지게 됐다.
먼저, 이런 의문을 가지게 된 이유는 새로이 iocpServer를 다루던 중 직렬화 기능을 사용하려고 클래스를
작성했다가, 패킷에 shift operator를 이용해 데이터를 직렬화하듯 집어넣을 때 문제를 발견했기 때문이다.
이전 채팅 프로그램을 작성하면서는 정확히 패킷이라는 개념이 존재하지 않았다. 직렬화 한 버퍼를 이리저리
돌려가며 네트워크 통신을 한 것이지 패킷 클래스를 따로 작성한 적이 없었다. 그렇기 때문에 직렬화 기능과 패킷 간의
관계와 구조를 제대로 파악하지 못했다. 기존에 생성해둔 직렬화 클래스는 굉장히 패킷 종류를 따지지 않고, 그저 데이터를 차례차례 bit convert 하여 버퍼에 쌓아두고, 읽거나 쓰는 역할을 했다.
이런 경우 패킷이 직렬화 클래스를 가지고 있다면 모든 패킷 종류에 대해서 적용될 수 있는 직렬화를 제공해야한다. 이것은 불가능하다. 정확히는 멤버 함수들이 굉장히 늘어나 불편해질 것이다. 이것을 해결하기 위해서는 직렬화 클래스를 포함하는 패킷 클래스를 패킷 종류에 따라 상속하게 하여 사용하는 것이다. 즉, 이 문제는 하나의 패킷 클래스로 모든 패킷에 대해 처리하려고 하니 발생한 문제라고 할 수 있다.
하지만, 또 다른 문제가 발생했는데.. 패킷에 shift operator를 이용해 데이터들을 직렬화하면서 집어넣으면서 발생하는 문제였다. 이는 나중에 다루어 보기로 하고 이전에 사용하던 직렬화 프로그램과 몇 가지 다르게 프로그램을 작성했었다.
첫 째는 패킷 헤더에는 "size"라는 녀석이 들어가야 한다. 이는 예전 프로그램에서 생각하지 않았던 부분이기도 하다.
그렇기 때문에 이번에 추가해주면서 문제가 발생했다..
두번 째는 앞에서 언급한 패킷 내부에 직렬화 멤버 변수를 포함하는 것이다.
size 변수가 패킷 헤더에 들어가는 이유에 대해 조사한 결과, tcp의 특성과 관련이 있었는데, tcp를 사용한다면,
내가 보내려는 데이터가 여러 패킷으로 쪼개져서 상대방에게 전송이 될 수 있다는 특성과 관련이 있었다.
예를 들어, 이런 버퍼를 하나 생각해보자.
[H][E][L][L][O][][][][][]
해당 버퍼의 데이터를 네트워크로 전송할 때, 상대방이 한 번에 HELLO라는 텍스트를 받는다는 보장이 없다는 것이다.
부분적으로 쪼개져서 도착할 수 있기 때문에, 이런 경우 패킷을 제대로 처리할 수 없어서 완성된 패킷이 올 때까지 기다리는 과정이 필요하다. 더 현실적인 예를 들자면 패킷을 다음과 같이 정의해보겠다.
[PacketID][Contents]
여기서, PacketID는 '이런 패킷은 이렇게 처리하자' 같은 프로토콜로 생각할 수 있다. 만약 PacketID는 제대로 왔지만, Contents 부분이 전부 도착하지 않는다면 어떻게 할 것인가? 이와 관련해서 PacketSize가 필요하다는 것을 알 수 있다. 정확히는, 전부 도착했는지 안 했는지를 어떻게 판단할 수 있는가? PacketSize가 없다면.. 이 패킷이 완전체 패킷인지 아닌지 알 수 없다. 또, 가변 길이를 가지는 멤버들이 패킷에 들어올 때도 문제다. string이나 vector 등등.. 고정된 크기가 아닐 경우에는 미리 약속한(클라-서버 간) 크기로 비교조차 할 수 없기 때문이다. 따라서, 만약 로직이 패킷이 도착할 때마다 패킷 id에 따른 함수를 실행하는 로직이라면.. 다 오지도 않는 패킷에 대한 처리로 인해서 제대로 동작이 되지 않을 것이다.
ex) HELLO! 라는 String을 클라에서 서버로 5번 날린다고 하자. 서버는 receive 할 때마다 해당 string을 print 찍어준다.
그때, 이런 식의 출력 화면이 나올 수도 있다.
[from client] HELLO
[from client] HEL
[from client] LOHELLO
[from client] HELLOHELLO
위의 예시에서 내가 보내고자하는 패킷이 뭉쳐서 오거나 잘려서 오는 것을 볼 수 있는데.. 그때마다 패킷의 크기를 알고 있다면 그만큼을 잘라서 처리할 수 있으니 크기를 알고 있어야 하는 것은 당연해 보인다.
예전 메시지 프로그램에서는 size를 따로 산정하지 않았었는데 어떻게 된건지가 의문이다..
추측하기에는 패킷 사이즈 자체가 굉장히 작았기(수신버퍼크기보다) 때문에 분할이 일어나지 않았던 것 같다. 이 때문에 size는 필요 없다고 생각했는데 위와 같은 경우가 발생할 수 있기 때문에 미리 조치를 취해 놓아야 한다. 또한, 꼭 지정한
사이즈보다 작지 않더라도 패킷 분할이 일어나는 경우도 있다고 한다. (정확히 이유는 모르겠다..)
서버는 당해보지 않고는 깨닫기 힘들다는 게 이런 걸까? 당장 일어나지 않는 일을 예방하는 것은 너무 힘든 것 같다 ㅜ
(+추가)
delay를 살짝 주면서 send를 보내면 뭉치는 일은 없다! 이전 프로그램은 delay를 넣어줬다.
아무튼.. size를 포함한다는 가정하에 두 번째 문제와 합쳐져 문제가 발생했다.
size는 언제 측정되어야 하는가?
앞에서 문제 삼은 것은 shift operator를 이용해 패킷 자체가 직렬화하며 데이터를 넣고 있다는 점이다. 가장 먼저 넣어져야 하는 것은 패킷 헤더의 size다. 그런데 이상하다.. contents를 모르는데 size를 어떻게 측정할 수 있는 것인가? 처음엔 이것저것 생각해보며 맨 앞의 2byte(size = unsigned short type)는 비워두었다가 마지막에 채워 넣자 생각했는데 너무 복잡하고 더러운 코드가 생성될 것 같아서 그만두었다. ( 이게 맞네요.. 마지막에 채워 넣을 때 size를 앞부분에 집어넣어야 합니다.)
결론부터 말하자면, size는 마지막에 측정되어야 하며, 그게 가능하려면 Packet은 shift operator를 이용해 데이터를 하나하나 집어넣는 게 아니라, 패킷에 들어갈 데이터를 멤버로 미리 받아놓고, 내부적으로 직렬화 과정을 거쳐서 처리해야 한다는 것이다.
결국, 이 모순된 아이디어는 (패킷 == 직렬화) 이런 식으로 사용하려다 보니 발생한 것이다. shift operator를 패킷에 적용시켜 바로 직렬 화하는 것이 잘못된 것이다.
해결방법은 앞에서 이미 말했지만, 상속을 통해 패킷 종류별로 패킷 클래스를 만드는 것. 이렇게 하면 패킷마다 채워 넣어야 하는 데이터 값을 따로 설정할 수 있어 외부에서 데이터를 채운 후 Serialize() 같은 함수를 이용해 미리 산정된 크기를 앞에 넣고 패킷 id, contents들을 차례로 넣을 수 있다.
'게임 서버 프로그래밍 > 윈도우 소켓 프로그래밍' 카테고리의 다른 글
Serializer 제작 (0) | 2020.09.15 |
---|---|
client , server 연습 .. (0) | 2020.02.22 |
getaddrinfo() (0) | 2020.02.22 |