티스토리 뷰


이전에 했던 서버 만들기를 이어서 하겠다. 

서버에 사용자가 접속하고 접속을 해제할 때, 사용자의 정보를 List Box에 나타내주려고 한다. 

먼저 사용자가 접속시 사용자의 정보를 나타내도록 하겠다.







이제 데이터를 수신하도록 처리해야 한다. 데이터를 저장하거나 보내는 경우는 자동으로 일어나기 때문에 문제가 없지만, 데이터를 읽어 들이는 경우 (클라이언트가 서버로 데이터 전송) 그 파일 데이터의 크기를 모르기 때문에 데이터가 더는 전송되지 않을 때까지 읽어 나가야 한다. 따라서 효율적으로 하기 위해 '패킷(또는 프레임)'이라는 방식을 사용한다.

패킷은 네트워크상에서 한 번에 전송하는 정보의 단위를 뜻한다. 데이터는 Header와 Body로 나누어지는데, 헤더를 통해 바디의 정보를 알 수 있도록 정보를 쪼개서 보내준다. 헤더 파일에 바디 내용을 명시해주면 헤더 파일을 먼저 읽은 뒤, 바디의 정보를 얻어서 읽어나가는 데 유리하다. 보통 헤더 프레임, 바디 프레임을 다르게 구분한다.

현재 클라이언트 서버 방식은 헤더 안에 바디의 정보를 유추할 수 있도록 넣어주며(message ID) 실제 바디 데이터를 분석하지 않고 message ID만 가지고 파악할 수 있게 된다.


패킷이 필요한 또 다른 이유는 우리가 서버를 만들게 되면 여러 군데에서 접속을 시도하게 된다.

대표적으로 1. 웹 서버 검색봇 2. 해킹툴 3. 잘못된 아이피나 찔러보기 공격 등 이 있다.

서버는 접속을 시도하는 모든 대상의 정보를 읽으려고 한다. 클라이언트는 서버에 접속할 때, 기본적으로 웹 서버 프로토콜로 들어온다. 그런데 위에서 언급한 대상들은 웹 서버 프로토콜이 아닌, 다른 프로토콜을 사용하여 서버에 접속하려 하기 때문에 서버가 죽을 수 있다. 따라서 서버 입장에서는 받아들일 대상의 기준을 마련하여 방어해야한다. 클라이언트의 데이터가 들어오면 무조건 1 바이트는 들어오게 되어있다. 이 1바이트를 읽어보고 원하는 내용이면 진행하고 아니면 자르거나 격리 시킬 수 있다. 

1 바이트에 서버가 클라이언트을 식별할 수 있는 고유한 값을 넣는다. 여기서는 우선 0x21로 하도록 하겠다.

만약에 상용화할 생각이 있다면 이것에 대한 고민이 더 필요한데 만약 지금과 같이 간단한 숫자로 할 경우 재수없으면 프로그램이 죽을 수 있다. 

헤더 사이즈는 사용자가 정의할 수 있다. 우선 우리는 4바이트로 하도록 하겠다. 첫 1바이트는 유효성 체크를 위한 바이트 두번째는 뒤에 오는 바디가 어떤 정보를 가지고 있는지 message ID 마지막 2바이트는 바디사이즈를 넣어서 보낸다.



여기서 또 다른 문제점이 발생할 수 있는데, A(서버)와 B(클라이언트)가 통신을 하는데 A와 B의 속도격차가 큰 경우이다. 

이 외에도 여러가지 문제들이 발생될 수 있기 때문에 흐름제어가 필요하다. 네트워크 통신을 하는데, 수신하는 쪽은 수신버퍼가 있고 송신하는 쪽은 송신버퍼가 존재한다. 송신하는 쪽은 그냥 보내면 되기 때문에 신경쓰지 않아되는 반면에 수신버퍼에는 상대편이 송신한 데이터가 들어오는 순간 FD_READ 메세지가 발생한다. 만약 데이터가 1바이트씩 들어온다고 해서 계속 메세지가 발생하는 것은 비효율적이기 때문에 FD_READ가 발생하는 세가지 조건이 있다.


(1) 수신버퍼가 비어있는 상황에서 data가 수신되는 경우 //FD_READ발생, 최초 한 번


(2) recv함수로 data를 읽었는데 수신버퍼에 data가 남아있는 경우 한 번 더 발생시킨다.

-> 60바이트가 들어와있는 상황에서 50바이틀 읽어가면 (2)가 없으면 (1)에 의해  FD_READ가 발생하지 않기 때문에 필요한 조건


(3) WSAAsyncSelect 함수를 이용하여 비동기 설정시에 수신버퍼에 데이터가 있으면 발생한다

-> 내 PC가 느려서 accept 되자마자 상대방이 데이터를 막 보내면 내가 비동기를 걸기전에 데이터가 들어오게 되는데, 그때 이미 수신버퍼에 찼기 때문에 그 다음에 들어온 데이터를 (1)에 의해 FD_READ를 발생시키지 않게됩니다. 따라서 필요한 조건입니다.


여기서 문제점이 또 발생할 수 있다. 결과적으로 우리는 데이터를 4번 끊어읽게 되는데, 한번 끊어 읽을 때마다 (2)조건에 의해 FD_READ메세지가 계속 발생하게 된다. 자세히 말하자면 우선 송신된 데이터가 수신버퍼에 찼을때 25002번 메세지가 발생한다. 그때  recv를 통해 1바이트를 읽는다. 

그러면 수신버퍼에 데이터가 남아있기 때문에 FD_READ메세지를 다시 발생시키고, 두번째 1바이트를 읽으면 또 수신버퍼에 데이터가 있기 때문에 FD_READ가 발생한다. 마지막으로 bodysize를 있으면 FD_READ가 발생하고 그다음에 body를 읽고나면 메세지 큐에 FD_READ가 세개 남아있게 된다. 만약 메세지는 있는데, 수신버퍼가 비어있다면 읽으려고 하지만 데이터가 없기 때문에 프로그램이 응답없음 상태가 되었다가 정상이 되었다가 응답없음이 반복되는 상황을 볼 수 있게 된다.

그렇다면 3번의 횟수가 정해져 있기 때문에 변수를 이용해 카운트를 세서 쓸 수 있다고 생각 할 수 있지만 컴퓨터 

양쪽이 속도가 다르면 3번이라고 횟수를 예측해서 사용할 수 없다. 보내는쪽이 느려서 10바이트를 보내는데 읽는 쪽이 상대적으로 빨라 족족 읽어서 버퍼가 계속 빈다면 정해진 횟수대로 발생하지 않을 것이다.


따라서 문제를 해결하기 위해서는 현상을 보고 막으려고 하기 보다는 원인을 찾아야 한다. 이런 상황이 발생되는 것은 우리가 비동기를 쓰기 때문인데, 즉 FD_READ를 쓰고난 뒤 비동기를 끊어주면 된다. 작업이 끝난 후에 비동기를 다시 써주면 된다. 데이터가 남아있을 때 비동기를 끊어도 다시 비동기 할 때 데이터가 남아있기 때문에 문제가 발생하지 않는다.



else {//FD_READ 여기서 시작

WSAAsyncSelect(h_socket, m_hWnd, 25002, FD_CLOSE);

//소켓 매니저에게 재등록 , FD_READ에 관해서 상관하지 않겠어. 

//FD_READ에 관한 이벤트가 일어나지 않게 된다.


unsigned char key, msg_id;

unsigned short body_size;

recv(h_socket, (char *)&key, 1, 0);

if (key == 0x21) {//정상 사용자

recv(h_socket, (char *)&msg_id, 1, 0);

recv(h_socket, (char *)&body_size, 2, 0);

if (body_size > 0) {

char * p_body_data = new char[body_size];

int total_size = 0, read_size = 0, retry = 0 ;

while (total_size < body_size ) {

read_size = recv(h_socket, p_body_data + total_size, (body_size - total_size), 0);

if(read_size == SOCKET_ERROR) break;

   //중간에 연결이 끊어지는 경우 socket error 발생

   total_size += read_size;

if (read_size == 0 || total_size != body_size) {

//시스템의 속도 차이가 너무 나서 수신 버퍼에 데이터가 없다.

//한번에 읽지 못한거 역시 시스템의 속도 차이가 나기 때문에 CPU 점유율을 낮춰준다.

Sleep(50);

retry++;

if (retry > 30) break;

}

}

AddEventString((wchar_t *)p_body_data);

delete[] p_body_data;

}

}

else {


}

WSAAsyncSelect(h_socket, m_hWnd, 25002, FD_CLOSE | FD_READ);

}




댓글