삽질의 현장/- 네트워크 프로그래밍

#009_Window_Network_클라이언트 서버 통신 (TCP 서버 - 코드)

shovelman 2015. 10. 4. 00:35


안녕하세요 삽잡이입니다.


이번시간에는 TCP 서버에 대한 실제 코드를 보면서 공부하는 시간을 가지도록 하겠습니다.


우선 통신을 위해 서버를 만들어야겠지요...

그 중 가장 우선이 통신을 하기 위한 객체를 생성해야합니다. 소켓이지요....

정확히는 소켓의 핸들이겠지만요...



첫번째 인자는 주소 체계를 설정하는 인자입니다. 

주소 체계란, 주소 지정 방법을 가리킵니다. 

프로토콜에 따라 주소를 지정하는 방법이 다르기 때문에 설정을 해줘야하는 것입니다.


통신을 하기 위해서 여러가지 채널들을 제공합니다.

예를 들어 군사망, 범용망, 그 외 특수한 망 등등을 말입니다. 

이를 구분짓는 정의 값을 첫번째 인자로 결정하는 것입니다.


통신영역을 설정하는 함수로써, 인터넷 영역을 사용할지 , IPv4 방식을 사용할지

즉, 어떻게 통신을 할지에 대해서 설정을 합니다.

AF_INET 는 주소 체계중 하나로써 IPv4 인터넷 프로토콜을 사용하겠다는 뜻입니다.


두번째 인자는 데이터를 전송하는데 있어서 프로토콜의 유형을 정하는 인자입니다.

즉, 프로토콜의 특성을 나타내는 값이라는 것입니다.

SOCK_STREAM의 경우 TCP/IP 기반을 사용하겠다는 의미이며,

SOCK_DGRAM의 경우 UDP 프로토콜을 사용하겠다는 의미입니다.


마지막 인자는 생략을 해도 됩니다.

이유는 주소 체계와 소켓 타입만으로도 프로토콜을 결정할 수 있기 때문입니다.


아래의 예제 코드와 같이 사용할 수 있습니다.



결론적으로, 소켓 하나를 생성하고 TCP 통신을 할 것이라는 설정을 한 것입니다. 

즉, TCP 통신을 위한 소켓을 하나 생성하게 된 것이지요...



다음으로 bind 작업을 수행해야하는데 우선 SOCKADDR_IN 구조체를 사용합니다.

SOCKADDR_IN 구조체는 '소켓 주소 구조체'라고 부릅니다. 16바이트로 이루어져있지요...

네트워크 프로그램에서 필요한 주소 정보를 담고 있습니다.


내부를 한번 까보도록 하겠습니다.



해당 구조체는 간단하게 서버의 정보를 포함합니다.

접속하고자 하는 서버를 어떻게 동작시킬 것인지 채우는것입니다.


첫번째 인자에는 프로토콜의 체계를 써넣고,

두번째 인자는 포트번호를 위한 인자입니다.

TCP 통신에서 도착지의 PC까지는 IP 계층의 데이터까지만 까보면 되지만,

어떤 프로세스에게 데이터를 줄지에 대해서는 포트번호를 보고 결정하게 됩니다.

세번째 인자는 주소를 집어넣습니다.

마지막 인자는 미래를 위해 만들어 놓은 인자인데 미래란 없습니다... 생략하셔도 됩니다.


포트번호에 대해 이야기가 나왔으니 조금 더 알아보도록 하겠습니다.

포트번호는 0번부터 시작되는데 1023번까지는 잘 알려져있는 포트로 이미 예약된 번호입니다.

대표적으로 웹 서버는 80번 포트, ftp는 21번 포트가 있습니다.

어찌됬건 TCP 프로토콜을 사용하는 놈들은 IP와 함께 포트번호가 있어야되는 사실을 기억하시길 바랍니다.


 


포트번호를 설정할 때에는 

htons 함수를 사용하여 네트워크 표준을 빅엔디안 형식으로 변환해주는 작업을 수행합니다.

그리고 주소를 설정할 때에는 INADDR_ANY 를 사용했는데

이는 '어떤 컴퓨터든지 서버로 사용하겠다.'라는 뜻을 가지고 있습니다.


예를들어 서버로 만든 프로그램을 A PC, B PC, C PC등등 

어떤 PC 던지 서버로 동작시키면 해당 PC의 IP가 서버가 되는 것입니다.

만약 해당 인자에 IP를 지정하게 되면, 다른 PC에서는 서버로 동작시키는데 실패하게 됩니다.

결론은, '어떤 IP에서든지 서버로 동작시키겠다.'라는 뜻으로 이해하시면 됩니다.


자... 그래서 아무튼... bind 작업을 합니다.

아까 생성한 소켓과 아까 설정한 구조체의 주소, 마지막으로 사이즈를 명시해줘야합니다.

사이즈는 반드시 알려줘야합니다.

왜냐하면, 프로토콜에 따라서 소켓 주소 구조체의 크기가 달라질 수 있기 때문입니다.


두번째 인자를 보시면 주소를 보낼 때 자료형을 명시합니다.

그런데 우리가 아까 확인한 구조체는 SOCKADDR_IN 형식인데, _IN이 사라졌습니다.

사실, 이전에는 SOCKADDR를 사용했었습니다. 우선 SOCKADDR를 까보겠습니다.


 

정보가 family만 있지 다른 정보들은 없습니다.

즉, 사용하기 불편하기에 만들었다고 생각하시면 됩니다.


하지만, 소켓 라이브러리는 SOCKADDR을 사용하기 때문에 

주소를 넘길 때에는 SOCKADDR로 형변환을 해줘야하는 것입니다.

즉, 어떤 값을 넣든지 주소만 보내면 형변환을 통해 접근하여 사용할 수 있다는 것입니다.


그리고 원형을 보면 소문자로 되어있는데 왜 우리는 대문자로 사용하느냐?

윈도우즈에서 사용하는 방식이 대문자로 쓰여있기 때문입니다.

Windows에서는 CHAR, LONG 다 이렇게 쓰지 않습니까...

Unix/ Linux 계열에서는 다 소문자를 씁니다....

이는 역사적 문제인데 표준화 하기 이전부터 이렇게 사용했기 때문에 뭐... 넘어갑시다.


지금까지 여러분은 소켓을 생성하고 bind를 통해서 소켓에 속성을 부여한 것을 보고 계십니다.


자... 다음으로 listen 함수는 접속 대기 큐의 크기를 설정해줍니다.


 

두번째 인자인 SOMAXCONN은 접속 대기 큐의 최대 개수를 만들라는 예약값입니다.

보통 동시 최대 접속 개수는 일반적인 OS에서는 64대입니다.

오해하실 수 있는데, 동시 접속할 수 있는 대수가 64대라는 것이지 클라이언트가 64대라는 것이 아닙니다.

접속 대기 큐에 최대 64대까지 클라이언트를 수용하는데,

accept 함수에서 클라이언트의 정보를 받지 않게되면 계속해서 클라이언트들이 쌓일것입니다.

동시 접속이라는 말은 클라이언트의 일을 처리하던 하지 않던 대기하고 있을 때를 말합니다.

즉, 클라이언트가 처리되지 않고 동시 접속할 수 있는 대수를 의미한다는 것입니다.


여기까지 성공을 했다면 클라이언트의 접속은 성공하게 됩니다.

클라이언트는 접속 대기 큐까지 들어가는 데 성공을 했다고 판단하기 때문입니다.

아직 서버와 통신하는 상태를 말하는 것은 아니니 오해하지 마시길 바랍니다.


어찌됬건, 다음으로 accept() 함수가 실행됩니다. accept함수는 blocking 함수입니다. 


 


따라서 , 클라이언트의 접속 요청이 들어올 때까지...

즉, 접속 대기 큐에 클라이언트가 들어올 때까지 blocking 상태에 놓이는 것입니다.


  


첫번째 인자로 대기 소켓을, 두번째 인자를 클라이언트의 정보를,

세번째 인자로 주소 구조체 형식의 크기를 넘깁니다.

accept 함수를 통해 반환되는 값은 

클라이언트와 통신할 수 있는 클라이언트와 연결되어있는 '통신 소켓'의 핸들입니다.


다시 말씀드리지만 accept()함수는 클라이언트가 접속할 때까지 기다립니다.

클라이언트가 붙게되면 return 하는 것이구요...


아무튼... 통신 소켓이 제대로 얻어지게 된다면, 정보를 주고 받을 준비가 된 것입니다.

어떻게 주고 받는다고 했나요?? 바로, recv, send 함수를 통해 주고 받는다고 했었지요...


우선, recv 함수에 대해서 알아봅시다...



첫번째 인자로 통신 소켓의 핸들을 주고, 두번째 인자로 데이터를 저장할 버퍼의 포인터, 

세번째 인자로 수신할 데이터의 최대 크기를 줍니다.


'첫번째로 받은 인자의 소켓놈에게 buf로 데이터를 받겠다... 최대 버퍼 사이즈 만큼을...' 이 되겠군요...


마지막 인자는 Out Of Bend라고.... 비상 통로를 말하는데요...

긴급 호출 메시지로 보낼지를 뜻합니다. 지금은 안쓰긴 하는데....

긴급 메시지라고 해서 데이터를 보내면 버퍼에 쌓일터인데,

버퍼에 만약 쌓여져 있다면 긴급 메시지가 나중에 오더라도 먼저 올라가게 되는 것입니다.


왜 안사용하느냐 하면,

네트워크란 나의 데이터를 던지고 던지고 던지고 하는데,

먼저 보낸 데이터보다 나중에 보낸 데이터가 먼저 도착할 수도 있습니다.

순차적으로 데이터가 먼저 올 수 있다는 것을 보장도 못할 뿐더러,

버퍼에 쌓이지 않고 바로바로 데이터를 읽어들인다면 역할이 쓸모가 없다 이겁니다...


아무튼... Out of Bend 메시지가 도착했는데 만약 어플리케이션에서 

읽지 않은 메시지들이 쌓여있다면 우선적으로 먼저 Out of Bend 메시지가 읽어진다는 것입니다.

그런데... 이 기능을 사용하기 위해서는 또 그냥 recv 함수로 읽지 못하고

몇가지 작업들을 추가적으로 해줘야합니다... 불편하지요... 결론은 안쓴다구요...


어찌됬건 recv 함수는 데이터를 가져오는 함수임을 알립니다.

가져올 데이터가 없다면 blocking 상태에 빠지지요...

누군가 데이터를 보내면 다시 깨어날 것이구요...


 

recv의 return값은 정말로 중요합니다.

왜냐, return 타입이 만약 0 혹은 -1이 되면 종료를 뜻하기 때문입니다.


우선, 0은 클라이언트의 종료를 말합니다.

'서로간에 어떤 사건이 일어나면 그 사실을 서로 알지 못한다.'

이런 말을 파이프를 배웠을때 언급한 적이 있습니다... (아닐수도...)


서로 알지 못하다가 I/O작업을 할 때 알 수 있게 된다 이겁니다...

즉, 통신하는 두 놈들중 하나가 이미 종료를 했더라도 서로 모르고 있다가 

recv 함수를 호출 할 때에 그 사실을 알 수 있다는 것입니다.

recv함수를 호출하면 통신하는 놈의 데이터를 받으려고 할 터인데 

통신하던 놈이 없으니 0을 반환하고 종료하게 됩니다.


-1이 반환된다는 것은...

뭔가 나와 접속되어있던 놈이 정상 종료가 아닌 어떠한 무언가의 문제가 있었을 때 반환됩니다.

정상 종료는 closesocket함수를 통해 종료됩니다.

즉, closesocket함수로 종료가 된 것이 아니라 다른 무언가에 의해 문제가 발생했을 때

-1을 반환한다는 것입니다.



만약 순차적 처리를 하는 서버에 클라이언트들이 붙는다고 생각해보겠습니다.

즉, 이 서버는 한 클라이언트와 연결이 되면 해당 클라이언트와의 연결이 끊어질 때까지

다른 클라이언트들과 통신을 하지 못하는 것입니다.


자... 대기 큐에 있는 클라이언트는 자신이 접속에 성공했다고 생각을 합니다.

그리고 데이터를 아무리 날려봤자 서버에게 전달되지 않겠지요....

왜냐, 예시에서 언급한 함수는 순차적 처리를 하는 함수니까요...

그런데 통신 중이던 클라이언트가 종료되고 

대기큐에서 신나게 데이터를 날리던 클라이언트가 연결이 됬다고 가정해봅시다.

신나게 두들기던 데이터는 서버에서 한꺼번에 가져옵니다. 데이터를 나눠서 쐈을지라도 말입니다...


TCP는 데이터의 경계가 없다고 하는데 맞습니다... 경계가 존재하지 않습니다.

그래서 stream이라고 부르지요... TCP는 stream 방식을 사용한다는 것입니다.

stream은 '연결되어있는', '끊김이 없는'이라는 뜻을 가지고 있는데요,

TCP에서는 굉장히 중요한 개념입니다. 

데이터를 두번써도 한번에 가져올 수도 있고,

한번에 쏴도 여러번에 나눠서 가져올 수 도 있다는 것입니다.


나중에 자세하게 들여다볼 기회가 있으리라 확신을 하니... 그 때 다시 들여다 보도록 하겠습니다.


아직 클라이언트 입장에서 접근하지는 않았지만,

다음 시간에 알아보도록 하겠습니다.


이번 시간에는 TCP 서버를 만들고 데이터를 받고 뿌리는 과정을 살펴본 것입니다.


이상 삽잡이였습니다!