티스토리 뷰

1부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 1 (헤더파일)

http://andrew0409.tistory.com/97


2부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 2 (서버cpp파일)

http://andrew0409.tistory.com/99


3부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 3 (클라이언트cpp파일)

http://andrew0409.tistory.com/100


소켓 기본 마지막 포스팅. 오늘은 지금까지 배운 CServerSocket과 CClientSocket을 이용해서 직접 서버에 연결하고 데이터를 전달해보는 예제 소스코드를 보고 연습해보겠다.


1
2
3
4
5
6
7
8
9
10
11
12
void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
    if (mServer.Create(26000== false)
    {
        MessageBox(L"서버가 구동 중입니다.", L"에러", MB_ICONERROR);
        return;
    }
 
    MessageBox(L"클라이언트를 기다리고 있습니다.", L"알림", MB_ICONINFORMATION);
 
    AfxBeginThread(ServerProc, this);
}
cs


우선 키 다운 메세지를 통해서 키보드로부터 어떤값이든 입력을 받으면 서버가 구동되도록 간단하게 코딩했다.

서버소켓이 생성되고 클라이언트를 기다리고있다는 메세지를 띄워주고 나면 쓰레드를 통하여 서버를 실행한다. 쓰레드를 사용하는 이유는.. 쓰레드를 사용하지 않으면 해당 단계에서 서버는 listen->accept를 하면서 connect가 들어올때까지 멈춰서 다른 조작을 할 수 없다. 그것은 서버가 Blocking 소켓이기 때문인데 (반대는 Non Blocking 소켓) 이에 대해서는 아마 다음이나 그다음 포스팅쯤에서 다뤄보는게 어떨까 하고 계획하고 있다.


간략하게만 소개를 하겠다.

블로킹, 논블로킹을 소켓모드라고 칭하는데, 소켓은 소켓 함수 호출시 동작 방식에 따라 블로킹 혹은 논블로킹 소켓으로 구분 할 수 있다.


⊙ 블로킹 소켓

소켓 함수 호출시 조건이 만족되지 않으면 함수는 리턴하지 않고 해당 스레드는 대기 상태가 된다.

소켓 함수는 리턴하지 않으므로 멀티스레드를 사용하여 다른 작업을 하지 않는 한 애플리케이션이 더는 진행할 수 없다. socket() 함수는 기본적으로 블로킹 소켓이다.


⊙ 넌블로킹 소켓

소켓 함수 호출시 조건이 만족되지 않더라도 함수가 리턴하므로 해당 스레드는 계속 진행할 수 있다.

넌블로킹 소켓의 장점이라면 소켓 함수 호출시 블록되지 않으므로 다른 작업을 진행할 수 있기 때문에 멀티스레드를 사용하지 않고도 여러개의 입출력을 처리할 수 있다는 점이지만, 해당 함수를 호출할 때마다 WSAEWOULDBLOCK등 오류코드를 확인해서 처리하고, 다시 해당 함수를 호출해야 하므로 프로그램 구조가 복잡해지며, 블로킹 소켓을 사용한 경우보다 CPU 사용률이 높다.


우리가 만들었던 서버소켓은 socket() 함수를 통하여 생성되었기 때문에 블로킹 소켓이다. 따라서 ServerProc에서 accept를 진행하면 Connect가 들어올때까지 프로그램은 멈춰있게 된다. 이제 서버 프로시저를 살펴보겠다.


1
2
3
4
5
6
7
8
9
10
11
12
UINT ServerProc(void *p)
{
    CChildView* view = (CChildView*)p;
    
    while (true)
    {
        SOCKET client = view->mServer.Accept();
 
        AfxBeginThread(ClientProc, new PARAM(view, client));
    }
    return 0;
}
cs

짧다 왜냐하면 서버 프로시저에서 클라이언트 프로시저를 호출할 것이기 때문이다.
매개변수로 ChildView의 포인터를 받는다.

매개변수를 전달하는 방법에 대해서 궁금하다면 이전에 스레드에 대해서 다룬 포스팅 http://andrew0409.tistory.com/93 을 참조하면 되겠다.

while문으로 계속적으로 반복하며, 새로 client라는 소켓을 선언하고 뷰클래스에 선언해 놓았던 mServer의 Accept함수를 사용하여 반환값으로 초기화 한다. 이제 이 부분에서 아까 설명한 블로킹이 동작하게 되고 Connect가 들어올때까지 사용이 제한되지만 상관없다. 왜냐하면 별도의 스레드에서 처리했기 때문에 다른 기능에는 영향을 주지 않는다.

Accept를 성공적으로 수행했다면 블로킹이 풀리고 다음 라인으로 넘어가게 될텐데 해당 라인에서 뷰클래스와 방금 초기화한 client 소켓을 전달인자로 ClientProc를 호출해서 새로운 스레드를 생성한다.

//추가
PARAM 구조체에 대한 정보를 실수로 누락해서 첨부합니다.
1
2
3
4
5
6
7
struct PARAM
{
    CChildView *view;
    SOCKET        sock;
 
    PARAM(CChildView *v, SOCKET s) : view(v), sock(s) {}
};
cs


이제 오늘의 하이라이트라고도 할 수 있는 클라이언트 프로시저.. ^^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
UINT ClientProc(void *p)
{
    PARAM param = *(PARAM*)p;
    delete (PARAM*)p;
 
    std::vector<CPoint> *current = nullptr;
    std::vector<std::vector<CPoint> * > path;
 
    CClientDC dc(param.view);
    CClientSocket client;
    client.Attach(param.sock);
 
    char buf[256];
    int recvd = 0;
 
    while (true)
    {
        int ret = client.Recv(buf + recvd, sizeof(buf) - recvd);
 
        if (ret == SOCKET_ERROR || ret == 0)
            break;
 
        recvd += ret;
 
        int pos = 0;
        while (true)
        {
            if (recvd - pos < 12// 데이터 크기
            {
                memcpy(buf, buf + pos, recvd - pos);
                recvd -= pos;
                break;
            }
 
            int *= (int*)(buf + pos);
            TRACE(L"[%d] %d, %d\n", p[0], p[1], p[2]);
 
            CPoint point{ p[1], p[2] };
 
            if (p[0== CChildView::DRAW_START)
            {
                current = new std::vector < CPoint >;
                current->push_back(point);
            }
            else if (p[0== CChildView::DRAW_END)
            {
                path.push_back(current);;
                current = nullptr;
            }
            else if (p[0== CChildView::DRAW_MOVE)
            {
                if (current == nullptr)
                    break;
 
                current->push_back(point);
                dc.Polyline(&(*current)[0], current->size());
            }
            pos += 12;
        }
    }
    return 0;
}
cs

서버 프로시저에서 던져준 변수를 PARAM 구조체로 받는다.
값으로 저장하며 전달받은 메모리는 즉시 해제해주는것이 추후에 해제할 필요가 없어서 편하다.

클라이언트의 소스코드를 설명하기에 앞서서, 이번 예제는 클라이언트에서 LButtonDown, MouseMove, LButtonUp 메세지를 이용해 마우스 입력으로 선을 그리면 서버에 있는 dc에도 똑같이 그려지는 예제이다. 한마디로 마우스로 입력받는 화이트보드라고 할 수 있겠다.

시작 전에 상단에 #include <vector> 을 입력해서 벡터를 프로젝트에 포함하길 바란다.

CPoint의 벡터인 current의 포인터를 nullptr(널포인터)로 초기화하고
또 current가 차곡차곡 쌓일수 있도록 CPoint의 벡터의 포인터의 벡터('ㅋㅋ') path를 선언해준다.

아까 클라이언트 프로시저에서 전달인자로 던져준 view를 이용하여 dc를 받는다.
클라이언트 소켓을 만들고 Attach 함수를 통하여 생성한 클라이언트소켓 client에 전달받은 sock을 붙이도록 한다.

//추가
Attach 함수에 대한 부분이 누락되어 아래와 같이 추가합니다.
1
2
3
4
5
6
7
void CClientSocket::Attach(SOCKET sock)
{
    if (IsValid() == true)
        Close();
 
    mSock = sock;
}
cs


그다음 데이터를 받아올 buf와 recv함수의 반환값을 저장할 ret 변수를 선언한다.

이번 예제에서 클라이언트단에서 서버단에게 데이터를 12바이트씩 전달하게 된다는점을 참고해서 다음 코드부터 살펴보도록 하자.

1
int ret = client.Recv(buf + recvd, sizeof(buf) - recvd);
cs

클라이언트 단으로부터 데이터를 전달받는데 뭘 이렇게 더하고 빼는지 처음엔 어려울 수 있다. 침착하게 살펴보자. 우선 buf는 char*의 자료형이므로 buf에다 int형을 더한다는것은 주소를 옮겨준다는 것을 알고 있을 것이다. 이를테면 buf가 100번째 방의 주소인데 5를 더하면 105번째방을 가리키는 주소가 되는 것이다.

우리는 클라이언트단으로부터 12바이트를 받아야 하는데 만약 내가 12바이트에 못미치는 데이터를 전달받았다 만약 그 크기가 10이라고 해보자. 그럼 buf에는 10개의 char가 들어있고 선언해 놓은 ret 변수는 10이 되어 있을 것이다. 그럼 이어서 buf의 11번째 방부터 12-10 즉 2바이트만큼의 데이터만 전달받는다는 코드이다. 헷갈릴 수도 있는 개념이기 때문에 침착하게 살펴보는것이 좋다. 소켓에서는 항상 이런식으로 데이터를 전달받으니 이것이 기본코드라고 할 수 있을것이다.

그 다음 전달받은값이 없거나, 에러라면 종료.
그리고 recvd 에 ret를 더하는 라인은 별로 어렵지 않다.

int형 pos를 선언하여 0으로 초기화하고, while문을 시작하고.. 수월하다.

이 다음 다시한번 어려운 부분이 나오는데..

1
2
3
4
5
6
if (recvd - pos < 12// 데이터 크기
{
    memcpy(buf, buf + pos, recvd - pos);
    recvd -= pos;
    break;
}
cs

처음 프로그램을 실행해서 데이터를 전달받았을때는 ret가 10, recvd도 10, pos는 0이지만 recvd가 12 미만이기 때문에 해당 조건문에 걸리게 되는데 pos의 값이 0이기 때문에 변하는 것 없이 break를 한다. 다시 위로 올라가서 10바이트를 받으면 ret가 10이되고 이를 recvd에 더해주므로 recvd는 20이 된다. 이제 조건문을 건너뛰어서 데이터 처리를 하게 된다.

1
int *= (int*)(buf + pos);
cs

buf를 int포인터인 p로 형변환 하여 받았다. 왜냐하면 클라이언트단에서 전달하는 데이터가 사실은 숫자이기 때문이다. 12바이트라고 한것은 숫자3개, 헤더하나와 CPoint가 될 x좌표, y좌표이다.

그래서 p는 p[0]과 p[1],p[2] 이렇게 세개의 숫자로 이루어 지는데 구성은 위에 써놓은 바와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CPoint point{ p[1], p[2] };
 
            if (p[0== CChildView::DRAW_START)
            {
                current = new std::vector < CPoint >;
                current->push_back(point);
            }
            else if (p[0== CChildView::DRAW_END)
            {
                path.push_back(current);;
                current = nullptr;
            }
            else if (p[0== CChildView::DRAW_MOVE)
            {
                if (current == nullptr)
                    break;
 
                current->push_back(point);
                dc.Polyline(&(*current)[0], current->size());
            }
cs

p[1]과 p[2]를 이용하여 CPoint형 point를 초기화 한다.

앞서 p[0]이 헤더라고 했는데, 헤더라고 함은 프로젝트에 include시키는 헤더파일을 뜻함이 아니라 데이터를 구분하기 위하여 임의로 매기는 상수값이라고 할 수 있다. 나는 LButtonDown과 MouseMove, LButtonUp을 구분하기 위하여 DRAW_START, DRAW_MOVE, DRAW_END 세개의 상수를 선언해서 사용했다.

조건문 안에 있는 코드는 일반적인 MFC 코드이므로 이 글을 보는 여러분이 한번 해석해보길 바란다.

아무튼 이렇게 데이터를 처리하고 나면 pos에 12를 더해서 저장하고 반복문으로 인해 if(recvd-pos < 12) 라인으로 돌아가게 되는데 recvd는 그대로 20, pos는 12이므로 20에서 12를 빼면 8이라는 결과값이 12미만이므로 다시 조건문 블록으로 들어오게 된다.

아까와는 상황이 조금 다르다 pos가 0이 아니기 때문이다.

1
2
3
4
5
6
if (recvd - pos < 12// 데이터 크기
{
    memcpy(buf, buf + pos, recvd - pos);
    recvd -= pos;
    break;
}
cs

pos가 12라는건 앞서 있던 buf배열중 20까지의 값중 앞에있는 12바이트를 처리를 했다는 것이다. 그래서 앞에있는 12바이트를 제거하고 남은 값들을 0번방으로 당겨오게된다. 이 조건문 블록이 이번 포스팅의 핵심코드중 하나인데 (하나는 client.Recv 하는 부분) 설명하려고 너무 길게 돌아온것 같다.
아무튼 12개의 데이터를 제거하고 남은 데이터를 0번방으로 당겨온 뒤에 recv도 데이터의 크기에 맞게 12개를 제거한다.

스레드도 두개나 사용하고 반복문도 계속 도는데 코드안에서 더했다 뺐다 하는 부분이 잦아서 처음에 한번보고는 이해가 안될 가능성이 높다. 침착하게 천천히 읽어보길 바라고 헤더부분을 처리하는 조건문을 제외한 부분, client.Recv 하는데 buf에다 recvd를 더하고 빼는 부분이나, recvd - pos 부분은 다른 프로젝트를 하더라도 기본이 될 구문이기 때문에 여건이 된다면 암기하는 것이 좋다.

이것으로 소켓 기본 클래스의 소스코드 분석 4부작을 마친다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함