Window Socket API 기본 정리1

2021. 11. 1. 19:08게임서버/win socket 프로그래밍

320x100

안녕하십니까 여러분 대학생 개발자입니다.

 

OSI레이어부터 시작해서 L2 L3 L4까지 차근차근 하나씩 단계를 밟아가면서 정리했습니다.

이제 L5 ~ L7이라고 불릴 수 있는 어플리케이션 레이어에서의 네트워크를 정리하려고 합니다.

 

msdn을 조금씩 참고해서 적긴하는데... msdn에 있는 문서나 사진을 올리진 않습니다...
제 머릿속에 있는 내용을 최대한 끄집어 내려고 하기 때문이니까... 이해 부탁드립니다. 

 

음... 순서도 그렇고 내용도 그렇고 뒤죽박죽일겁니다...

하지만 이 블로깅에 목적은 역시 제가 나중에 다시 보고 공부하기 위해서 남기는 목적이다보니...

이런 배려는 없다는점 유의하시길 바랍니다 ㅋㅋㅋㅋㅋ

추가로 여기에 올라오는것중 많은 부분이 생략이 되어있을 겁니다.

그건... 다른 블로그나 다른 사이트 혹은 책을 뒤져보시면 금방 나오는 내용들이라 생략했던 것입니다.

그부분도 유의하시길 바랍니다.

그럼 시작해보겠습니다.

네 여러분이 알고계신 일반적인 서버는 이렇게 생기지 않았습니다...

지금의 케이스는 이해를 돕기위해 아주 기본적이고도 단편적인 부분만 컷해서 온거니까요(심지어 windows가 아니고 linux의 api입니다...)

먼저 TCP에서 하나의 연결 단위는 소켓 이라고 했습니다.

그 소켓은 실제 win socket에서 SOCKET 이라는 타입으로 정의가 되어있고 그냥 하나의 정수 값을 타입정의 한겁니다.

소켓을 생성하는 함수는 socket이구요.

이때 그 소켓이 미리 어떤 프로토콜을 가질지 알려줘서 커널에서 준비를 할 수있게 인자로 전달을 합니다..

뭐 지금의 경우에는 TCP echo server의 모습이니까.

SOCK_STREAM, IPPROTO_TCP라고 치고 하겠습니다.(이런 열거 타입이나... 상수 디파인 된 케이스들은 정리 없이 넘어갈겁니다).

그럼 소켓을 생성했으니 서버에선 뭘 해야 될까요...

아까 적어뒀던 아이피와 포트를 지정해야됩니다.

물론 사진에는 그 과정이 생략되어있지만 이 과정을 바인딩 한다고 합니다.

bind인데요 이 때 bind에는 특별한 구조체가 들어가게 됩니다.

sockaddr 타입의 포인터와 그 사이즈를 요구하는데요

sockaddr에 대해서는 간단하게 정리하고 넘어가겠습니다.

말 그대로 L4의 주소체계와 L3의 주소체계에 대해서 적어달라는 의미입니다.

실제 sockaddr은 address family(주소체계) 와 14바이트의 데이터 파트로 나눠 져있습니다.

예전에는 이정도면 충분했는데 지금은 14바이트가 충분하지 않은 케이스도 존재하기 때문에 형변환된 포인터가 들어올때는 뒤의 사이즈라는 인자를 보고 그만큼 읽는 식으로 하고있습니다.

째뜬 우리가 쓰게될 ipv4의 구조체는 sockaddr_in 인데요 sockaddr + inet의 줄임이라고 생각하심 될 것 같아요.

네 일단 in_addr은 잘못되어있긴 하지만 그부분을 넘어가서 저기 sockaddr_in 부분을 보면 제일 처음 주소 체계를 적고 그다음으로 L4의 주소인 포트를 적습니다.

포트의 경우에는 0~65535이기 때문에 unsigned short로 기입된 것을 확인 할 수있구요.

 

그리고 in_addr인데요

이렇게 생겼습니다.

1바이트 4개를 담은 4바이트와, 2바이트 2개를 담은 4바이트와 마지막으로 ulong을 유니온으로 묶어버렸습니다.

이렇게되면 이 구조체는 사이즈는 가장 큰 객체인(다 똑같긴하지만...) 녀석으로 되게 됩니다.

이 경우에서는 4가 되겠죠.

그럼 우리는 4바이트를 한꺼번에 채워 넣어도 되고 1바이트씩 따로 넣어도 됩니다만.

여기서 주의할 점이 있죠.

우리가 생각하는 1바이트씩 넣겠다는...

바이트 오더링의 함정에 빠지면 안됩니다.

만약 우리가 네트워크에 뭔가를 보내고 싶다면 데이터는 상관없으나 헤더는 바이트 오더링을 해줘야 한다는 것입니다. 그래서127.0.0.1이 들어간다면 실제로는 1.0.0.127이 들어가게 될겁니다.

바이트 오더링에 따라서 물론 포트도 반대로 되겠죠...

0x1234를 넣는다면 포트는 34 12 가 들어갈거에요.

 

그럼 우리가 직접 저렇게 바이트 오더링을 해줘야 하는가...

그건또 아닙니다.

그걸 해주는 API들이 존재합니다.

포트의 경우에는 htons, ntohs, 아이피의 경우에는 htonl, ntohl 이런 함수들이 존재해요.
Winsocket에서는 WSAStringToAddress, WSAAddressToString 이런 함수를 지원하는데 결국 내용은 똑같습니다.

아무꺼나 쓰시면 됩니다.

 

저런 함수들을 통해서 우리는 대부분의 cpu가 채택하는 리틀 엔디안 바이트 오더에서 네트워크에서 채택하는 빅 엔디안 바이트오더로 바꿔서 넣을 수가 있는 겁니다.(왜 이럴꺼면 다르게 만들어놨지...)

 

네 그럼 이까지 했다면 이제 아이피, 포트를 모두 채워넣었을 텐데요.

아이피와 포트가 기입이 되고나면 해야 하는 과정이 위에서 정리했던 bind입니다.

 

bind를 하게되면 TCP에서는 내가 갖다준 아이피와 포트값을 소켓에 물려놓은채로 가지고 있고 다른 프로세스에서 사용이 불가능하게 만들어 준답니다.

물론 제대로 했으면 상관없지만 가끔 바인드에서도 문제가 생기는 경우가 있는데요.

IP나 포트가 유효하지 않거나, SOCKET이 유효하지 않거나, 혹은 다른 프로세스가 이미 포트를 사용중일때 입니다.

그래서 서버입장에서는 사용하는 포트인지 아닌지 잘 판단해서 쓰셔야되요.

 

네 그럼 서버에서는 이런 준비가 끝났으니 listen이라는 함수를 호출해서 내가 지정한 문을 열어두고는 기다리고 있는 상태가 되는겁니다.

 

그렇게 되면 이제 클라이언트에서 진입을 하게 됩니다.

클라이언트에서는 서버의 아이피와 포트를 입력후 바로 connect라고 하는데...

그러면 서버는 어떻게 클라이언트의 아이피 포트를 알게 되는걸까요... 라고 하신다면 바보입니다. 예전에 말씀드렸듯 동적 포트를 사용해서 1씩 증가시켜나가면서 사용하고 있습니다.

bind없이 connect를 호출하시면 자동으로 ip와 동적포트를 통해서 binding해버리고 connect작업을 실행합니다.

 

그럼 클라이언트는 어떻게 진입을 한다는 의미일까요...

그냥 문이 열려있으니 그냥 바로 데이터를 보내고 받고 할수있는 상황이 된걸까요?

그건 아닙니다.

사실 listen에 들어가는 소켓인 리슨소켓은 따지고보면 비서 정도로 보셔도 될 것 같아요.

아주 강력한 비서죠.

사장님이 accept라고 하기 전까지는 연결만 성사시켜놓고 기다리고 있는 상태입니다.

이 기다리는 줄을보고 backlog라고 하기도하고 listen queue라고 하기도 합니다.

뭐 ... 현업에서는 backlog라는 말을 제일 많이 쓴다고 합니다.

 

backlog의 사이즈를 직접 지정할 수 있는데요 (대기실의 크기라고 보시면 되겠네요).

일반적으로 SOMAXCONN을 넣습니다.

뭐 backlog사이즈에 관한 내용은 제가 다른 글에서 다루고 있기때문에 구체적으로는 하지 않고 스킵하겠습니다.

그러면 우리는 리슨 상태가 되어있는 하나의 포트를 얻게 되는겁니다.

그 포트를 통해서 다른 클라이언트들이 치고 들어오겠죠.

그러면 우리는 accept를 호출해서 하나의 클라이언트에 대한 소켓을 얻어내게 됩니다.

이미 이전에 연결과정은 끝나있구요.

비서가 준비성이 참 빠르죠... ㅋㅋㅋㅋ

 

가끔 인터넷을 보면 accept를 함으로써 연결이 성사된다고 잘못 적혀있는 글들도 있는데 이미 listen을 했고 포트가 열려있는 상태에서 올바른 포트로 진입을 했다면 이미 연결은 성사되어있는겁니다.

그상황에서 accept는 그 중 한명을 사장실로 불러서 면접보고 취직시키는 과정이라고 생각하시면됩니다.

물론 연결도 두개의 백로그가 있습니다.

SYN backlog가 있고 Accept queue라는 것이 있구요...

SYN backlog의 경우에는 진짜 상대에게 연결을 하자는 초기 연락만 온것이구요.

사장 이야기를 쭉 이어서 해본다면 이제 막 도착해서 서류를 작성해야되는 사람이구요...

accept queue는 3way handshake가 끝난 상태로 뭐 서류작성등 필요한 과정을 거친후 사장실에 들어갈 준비가 다 된 사람을 의미합니다

위의 그림과 같이 처음 SYN을 받았을때는 그 클라이언트의 정보는 SYN backlog에 있고 실제 accept가 된다면 accept queue로 옮겨지는 것입니다.

네 다른그림도 한번 가져와봤는데요...

나머지는 맞지만 하나 여기서 걸리는 부분이 있어서 이걸 꼭 정리를 해야 겠더라구요

이 그림을 보면 두가지로 해석이 될 수 있어요 1은 accept가 연결을 성사시키는 과정을 한다와

2번째는 백로그에 들어와있고 이미 연결이 끝난걸 accept한다 이죠

1번은 틀렸습니다...

분명히 정리하고 넘어갑니다.

accept는 이미 연결이 끝난 상태인 클라이언트를 큐에서 뽑아서 소켓을 반환하는 역할을 하는 함수입니다.

이런 그림을 보고 헷갈려 하는 사람이 분명히 존재하기 때문에 정리를 하고 넘어갑니다...

네 그리고 서버가 accept를 하지 않는다면 실제 연결의 갯수는 backlog queue의 갯수만큼이 될 것입니다.

만약 백로그 큐가 다 차버린다면 connect를 했을때 rst가 도착해 버립니다...

좀 너무하네요.

난 너랑 친구가 되고싶어 했는데 그냥 읽고 ㅗ하나 날린거니까요ㅠㅠㅠ.

 

방금위의 케이스를 적용해본다면 connect의 경우에는 에러의 케이스가 2개 존재합니다.

첫번째는 한참 기다리다가 꺼지는 case 두번째는 바로 에러가 튀어나오는 케이스... 이렇게입니다

1번의 경우에는 destination unreachable일 확률이 매우 높습니다.

listen이 아니거나 혹은 방화벽에 의한 전달이 차단이 된 케이스겠죠.

2번의 경우에는 백로그가 가득 차있는 경우입니다.

백로그가 가득차면 바로 RST시그널이 도착하기 때문에 바로 종료가 되는거죠.

 

소켓과 TCP와 같이 데이터가 들어오면 바로 해야될 행동이 있는 녀석들은 안타깝게도 전부 Non Paged pool에 들어가버려요 그래서 작업관리자를 connect만 주구장창하거나 listen해놓고 accept를 하지 않으면 ... 논페이지드풀이.... 어마어마하게 늘어나는걸 확인 하실 수 있을겁니다...

accept가 리턴을 하는 경우는 역시... 백로그에 데이터가 있을 경우겠죠?

리턴 값은 연결의 단위인 소켓이 리턴이 됩니다.

그럼 여기서 끊어야 겠군요...

저 오늘 매우 바빠요 흑흑

오늘도 긴 글 읽어주셔서 정말 감사드립니다.

다음엔 recv send 등을 가지고 오도록 하겠습니다 이상입니다.

그럼 안녕히계세요.

320x100