Win socket API2

2021. 11. 3. 23:29게임서버/win socket 프로그래밍

320x100

안녕하세요 대학생 개발자 입니다.

오늘도 역시 winsocket api에 대해서 정리를 해볼까 하는데요...

마지막으로 남겼을때가 listen, accept, bind, socket함수 정도 했었는데요

오늘은... 일단 에코서버의 남은 부분인 recv, send에 대해서 정리를 해볼까 합니다.

 

recv에 대해서 먼저 설명을 해볼까 합니다...

일단 먼저 recv라고 하면 recieve의 줄인말이겠죠 . 받다.

도대체 받는다는게 무엇을 의미하는지 궁금합니다.

 

우리는 recv를 호출해야지 과연 상대방이 보낸 데이터를 우리의 TCP가 받아들이는 걸까요?

음... 우선 L7과 그 밑을 얘기를 해야겠는데요 먼저 TCP와 소켓은 커널입니다. 

 

커널이라고 하면 우리가 접근할 수 없는 부분인데요... 왜 접근을 못하게 만들었느냐... 거기는 os및 다양한 윈도우즈에서 사용하는 핸들들이 많이 모여있기때문에 잘못건드리면 os자체가 펑하는 경우도 있어서 거기는 절대적으로 접근을 못하게 막고있습니다.

네 그래서 우리의 윈도우즈는 하나의 프로세스를(32비트 기준) 4기가 바이트의 공간을 주고 널포인터 초기화 0~4096 어플리케이션 에서 사용하는거 ~2기가 커널에서 사용하는거 ~4기가 이렇게 두고 사용합니다. 자기가 정해진 구역 외에는 건드리지 말라고 하는거죠

 

반대로 L7영역은 이제 우리가 다루는 어플리케이션 영역입니다. 즉 우리가 제로베이스부터 엔드포인트까지 모든걸 다 만들어 줘야한다는거죠 아까 약 2기가정도되는 공간 그 공간이 우리 어플리케이션에서 사용할 수 있는 공간입니다. 생각보다 적나요..? 64비트로 가게되면 이게 갑자기 어마어마하게 늘어날거에요 사용가능한 메모리만 16테라인가 그러니까요 ㅋㅋㅋ... 요샌 거의 대부분의 게임이 64비트를 기반으로 제작되니까요.. 아니면 너무 메모리가 적죠

 

그래서 쓸데없는 소리를 하면서 다른길로 빠질뻔 했는데 그래서 recv를 호출하면 우리의 TCP가 받을까 하고 말씀드린것... 음... 저기 위에 있는 내용을 이해하셨다면 아마 답이 나왔을 겁니다.

 

답은 역시 아니다 입니다.

그 이유는 커널이니까에요 말그대로 우리가 직접 뭔가를 할 수 없는 곳에 있는 녀석들입니다. 걔내들은 알아서 TCP와 소켓이 알아서 해줄 부분이에요 우리는 그저 L4까지 무사히 도착한 데이터를 L7으로 끌어올리는 API인 recv를 사용하는것 뿐이니까요.

우린 그저 recv의 형태에 맞춰서 인자들만 제공해주면 L4의 메모리를 L7으로 끌어올려주는 기능을 얻을 수 있는거에요 즉 다른 클라이언트나 혹은 서버에서 보낸 메시지를 받을 수 있다는 겁니다.

 

일단 함수의 원형부터 한번 본다면

int recv(SOCKET s, char* buffer, int len, int flag); 이렇게 있는데요

뭐 먼저 소켓이나 버퍼 len은 대충 예상이 됩니다. 하지만 flag 이건 뭐죠????

얜... 뭐 간단합니다 OOB data같은 녀석들을 처리할때 쓰는 플레그인데요 send에도 마찬가지로 똑같이 있습니다. OOB의 경우에는 좀 다르게 처리해야되기 때문에(우선순위가 없이 가장 먼저 가야하는 녀석들이라서) 다르게 처리하기 위해서 받는겁니다. 그래서 옵션이 없으면 일반적으로 0을 집어넣습니다.

 

리턴의 경우에는 3개의 리턴을 하는데요

일반적인 성공의 경우에는 받은 byte가 나오게 됩니다.

0이나오는 케이스와 SOCKET_ERROR 이 나오는 케이스가 있는데

0은 상대가 closesocket을 했을때 즉 FIN시그널이 도착했을때 0바이트가 수신됩니다.

혹은 shutdown(SD_SEND)를 했거나요 즉 상대가 더이상 나와 연결을 하지 않겠다는 의사를 보였을때 0바이트를 수신하게 됩니다. (RST의 경우는 아님)

 

그리고 다른 하나의 경우입니다.

SOCKET_ERROR이 리턴되었을때 인데요 어... SOCKET_ERROR은 그냥 ~0입니다. 모든 비트가 1로된 즉 -1입니다.

음... 이건 케이스가 많아서 구체적으로 뭐다라고 말씀드릴순 없지만 주로 네트워크 문제거나 혹은 RST신호가 도착했을 때 이렇게 됩니다. 강제종료의 케이스에도 마찬가지입니다. 강제종료를 했을 때는 이제 L7에서는 할 수 있는게 없기 때문에 os에서 남아있는 연결에 대해서 RST시그널을 쏴버립니다.

 

 

음... 그럼 여기까지 정리를 해봤는데요 한가지 궁금한게 있습니다.

강제종료는 비정상 종료인가 아닌가입니다.

그리고 비정상 종료와 정상종료를 나눠야 할 필요성이 있는가에 대한 걱정입니다.

 

우리는 비정상 종료가 들어왔을때 그에대한 방어책을 매우 철저하게 설계를 해서 코딩을 할겁니다.

그 튼튼한 방패를 두고 굳이 정상종료의 케이스를 하나 더 만들 필요가있을까요??

 

네... 맞습니다 제가 하고싶은 말은 이겁니다.

굳이 잘만든 비정상 종료케이스를 왜 정상종료 케이스때는 안쓰냐 이겁니다. 그렇게되면 종료에 대한 코드를 1번만 구현해도 될뿐... 복잡해지지도 않을텐데 말이죠.

그럼 제가 쓴 이글을 보신분들은 두번 구현하는 행동은 안하겠죠... 허허헣

 

그럼 recv에 대한 조금더 자세한 내용을 알아보겠습니다.

tcp에는 기본적으로 네이글 알고리즘이 켜져있습니다. 네이글 알고리즘에 대해서는 사전에 정리를 했기때문에 스킵하도록 하구요.

비동기 소켓의 경우에는 recv를 호출하면 2가지의 케이스가 있을텐데요

첫번째로 블로킹에 걸리는 경우와

두번째로 블로킹에 걸리지 않는 경우입니다.

 

블로킹에 걸리는 경우는 TCP 수신 버퍼에 데이터가 없을 경우에 블로킹에 걸려있겠죠...

근데 블로킹에 걸려있는 상태에서 TCP 수신버퍼에 데이터가 빠바바바박 들어오고 있다면 어떻게 될까요?

여러가지 가정을 해볼 수 있는데요... 음... 

이럴때는 데이터가 쭉들어올때까지 내버려 둡니다. 그러면 언제 리턴을 하는가?

답은 이겁니다 L7에서 send했던 단위로 PSH시그널이 붙게되는데 그 PSH시그널이 들어오는 순간 리턴을 하게됩니다.

혹은 내가 넣었던 len만큼의 크기가 꽉 차게 되면 그때 리턴을 하게 되구요.

혹은 내가 recv를 호출하는 순간에 이미 데이터가 있었거나요 ㅋㅋㅋ...

 

아마 이정도 까지는 충분히 다들 예상을 하셨을거라 생각합니다.

하지만 여기서 한스텝 더 들어가본다면 과연 블로킹에 걸린 상태로 데이터가 들어올때 어디로 바로 들어오느냐 입니다.

TCP 수신 버퍼를 거쳐서 들어오는가 아니면 바로 내가 제공한 버퍼에 먼저 밀어넣는가 입니다.

 

음... 저도 확실하게 TCP수신버퍼를 안거친다고는 못말하겠습니다 하지만 패킷을 캡쳐해서 어크를 보면... 윈도우 사이즈가 줄어들지 않습니다.

이걸보고 그냥 저는 추측상 TCP수신버퍼를 거치지 않고 바로 내가 제공한 메모리로 펌핑된다고 추측하고 있습니다.

 

그럼 대충 정리를 해보자면 이렇게 되겠죠

recv가 리턴하는 경우는

1. recv를 호출하는순간 이미 수신버퍼에 데이터가 있을경우

2. recv로 블로킹 걸려있는동안 데이터가 오는데 PSH시그널이 있을경우

3. 입력한 len만큼 다 차버린경우

 +로 2,3의 경우에는 recv가 리턴되지 않아도 오는족족 바로 Application Level의 buffer로 바로 집어넣어 버린다. 가되겠군요.

 

 

그럼 다음으로 send에 대해서 얘기를 해보겠습니다.

 

int send(SOCKET s, char* buffer, int len, int flag) recv와 동일합니다. 스킵하겠습니다.

 

하지만 다른점이 하나있어요 len의 의미가 달라지는데요

recv의 len은 최대 len만큼 받아라의 의미가 있어요 maximum의 의미가 있는거죠 하지만

send의 lend은 너가 len만큼 채우기 전까지는 리턴하지 말아라 라는 minimum의 의미가 내제되어있습니다.

 

send의 리턴값도 동일하게 내가 쓴 바이트수 만큼이 리턴값일거에요. 아마 성공의 케이스에는 len이 나오겠죠 그럼

 

send는 블로킹 함수임에도 불구하고 거의 블로킹이 걸릴 확률이 매우 낮아요 이유가... 송신버퍼가 매우 클 뿐더러 상대의 윈도우 사이즈를 다 채워야 내 송신버퍼를 채우기 시작하니까요. 그 전까지는 밑빠진 독에 물 채우기 식으로 조금 차면 빠져버리고 조금 차면 빠져버리고 하거든요.

 

하지만 우리는 게임서버를 만들 사람들이고 게임서버에는 당연히 이런 상황이 간간히 발생할 수 있어요. 그러면 그럴땐 어떻게 해야될지에 대한 얘기를 해봐야되는데요.

 

당연히 정석의 상황이면 상대의 Window size가 0이면서 동시에 내 수신 버퍼가 가득찼다고한다면 L7에서 우리가 추가로 버퍼를 할당받아서 거기에 넣어두고 상대의 window size가 0이 아니게되면 그때 송신버퍼를 우선 처리하고 그다음 L7에 있는 버퍼를 처리해야될거에요.

 

하지만 만약 우리가 네트워크 엔진이나 라이브러리를 만든다고 한다면... 당연히 어느정도 보장은 해줘야될겁니다. 하지만 우리는 게임서버를 만드는 입장이고 저런 클라이언트는 아주 가끔 나올뿐더러... 저 한명 때문에 다른 플레이어가 사용해야될 버퍼를 뺏을순 없습니다. 그런 입장에서 생각을 해본다면.

사실... 우리의 메세지라는것은 작게는 수바이트 크게는 수백바이트의 작은 구조체에 불과할겁니다.

이걸가지고 64kb... 옵션으로 윈도우 스케일이 붙게되었다면 크게는 몇 메가바이트에 해당하는 크기가 붙을텐데... 평균 수십바이트에 불과한 메세지로 메가바이트단위를 채우려고 한다면... 사실상 이 클라이언트는 이미 오래전부터 recv를 한건도 처리하지 못했다는 말이 되고 이 클라이언트의 루틴이 돌고있는지 여부 자체를 의심해봐야 될겁니다.

 

사양이 안좋다면... 처리를 못하고 있는 중일 것이고... 사양이 좋다고 한다면 어떠한 버그가 생겨서 게임 내부에서 무한 루프가 돌고있다는 의미가 될것이에요... 당연히 이런경우 저 클라이언트는 게임이 먹통이되어서 움직이지도 않을거에요 한프레임에 한번 실행되는 recv마저 처리를 못하고있으니까요... 이런경우에는 게임서버 차원에서 클라이언트를 배려해서 빨리 끊어줘야됩니다... 그 클라이언트는 얼마나 답답하겠어요 게임이 돌생각을 안하니까요...

 

분명히 확실하게 해야되는 것은 우리는 게임서버를 만들기 때문에 한명의 아쉬운 현상도 안되게 잡는것도 중요하지만... 그 한명으로 인해서 리스크가 너무 커진다면... 당연히 다른 플레이어들의 쾌적한 플레이를 위해서 과감하게 잘라낼줄도 알아야한다는겁니다...

 

자... 오늘은 여기까지 정리를 해보겠습니다.

오늘도 긴 글 읽어주신 여러분들께 정말 감사드립니다.

안녕히계세요 여러분

 

 

 

 

 

 

 

320x100