티스토리 뷰

  • 문제 상황 )
    • 수신 버퍼에 데이터가 없는데, **read**
    • 송신 버퍼가 꽉 찼는데, **write**
  • Select 모델(selec함수가 핵심)
    • 소켓 함수 호출이 성공할 시점을 미리 알 수 있다.
  • 블로킹 소켓 : 조건이 만족되지 않아서 블로킹되는 상황 예방
  • 논 블로킹 소켓 : 조건이 만족되지 않아서 불필요하게 반복 체크하는 상황을 예방

실행 순서

  1. 읽기[ ] 쓰기[ ] 예외(00B)[ ] 관찰 대상 등록
    단) OutOfBrand는 send() 마지막 인자 MSG_OOB로 보내는 특별한 데이터 받는 쪽에서도 recv 00B 세팅을 해야 읽을 수 있음.
  2. select(readSet, writeSet, exceptSet); -> 관찰 시작
  3. 적어도 하나의 소켓이 준비되면 리턴 -> 낙오자는 알아서 제거됨
  4. 남은 소켓 체크해서 진행

  • 사용 함수
  1. fd_set set
  2. FD_ZERO : 비운다 ex) FD_ZERO(set);
  3. FD_SET : 소켓 s를 넣는다.( 최대 크기 64개 ex) FD_SET(s, &set)
  4. FD_CLR : 소켓 s를 제거 ex) FD_CLR(s, &set);
  5. FD_ISSET : 소켓 s가 set안에 들어있으면 0이 아닌 값을 리턴한다.

#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
#include<stdio.h>
#include<Winsock2.h>
#include<stdlib.h>

#define PORT	9000
#define BUFSIZE 512

//소켓 정보 저장을 위한 구조체와 변수
struct SOCKETINFO {
    SOCKET sock;
    char buf[BUFSIZE + 1];
    int recvBytes;
    int sendBytes;
};

int nTotalSockets = 0;
SOCKETINFO* SocketInfoArray[FD_SETSIZE];

//소켓 관리 함수
BOOL AddSocketInfo(SOCKET sock);
void RemoveSocketInfo(int nIndex);

//오류 출력 함수
void err_quit(const char* msg);

int main()
{
    ...

    if (bind(listen_s, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR) err_quit("Bind");
    if (listen(listen_s, SOMAXCONN) == SOCKET_ERROR) err_quit("listen");

    //비동기 소켓으로 전환 하는 과정
    u_long on = 1;
    retVal = ioctlsocket(listen_s, FIONBIO, &on);
    if (retVal == SOCKET_ERROR) err_quit("ioctlsocket()");

    //데이터 통신에 사용할 변수
    FD_SET rset, wset;
    SOCKET clientsock;
    SOCKADDR_IN clientaddr;
    int addrlen, i;

    while (1) {
        //소켓 초기화
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        FD_SET(listen_s, &rset);
        for (int i = 0; i < nTotalSockets; i++) {
            if (SocketInfoArray[i]->recvBytes > SocketInfoArray[i]->sendBytes)
                FD_SET(SocketInfoArray[i]->sock, &wset);
            else
                FD_SET(SocketInfoArray[i]->sock, &rset);
        }

        //select()
        retVal = select(0, &rset, &wset, nullptr, nullptr);
        if (retVal == SOCKET_ERROR) err_quit("select()");

        //소켓 셋 검사(1) : 클라이언트 접속 수용
        if (FD_ISSET(listen_s, &rset)) {
            addrlen = sizeof(clientaddr);
            clientsock = accept(listen_s, (SOCKADDR*)&clientaddr, &addrlen);
            if (clientsock == INVALID_SOCKET) {
                err_quit("accept()");
            }
            else {
                printf("[TCP서버] 클라이언트 접속 IP : %s %d\r\n",
                    inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
                AddSocketInfo(clientsock);
            }
        }

        //소켓 셋 검사(2) : 데이터 통신
        for (i = 0; i < nTotalSockets; i++) {
            SOCKETINFO* ptr = SocketInfoArray[i];
            if (FD_ISSET(ptr->sock, &rset)) {
                //데이터 받기
                retVal = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
                if (retVal == SOCKET_ERROR) {
                    err_quit("Recv()");
                    RemoveSocketInfo(i);
                    continue;
                }
                else if (retVal == 1) {
                    RemoveSocketInfo(i);
                    continue;
                }
                ptr->recvBytes = retVal;
                addrlen = sizeof(clientaddr);
                getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);
                ptr->buf[retVal] = '\0';
                printf("[TCP/%s : %d] %s\r\n", inet_ntoa(clientaddr.sin_addr),
                    ntohs(clientaddr.sin_port), ptr->buf);
            }
            if (FD_ISSET(ptr->sock, &wset)) {
                //데이터 전달
                retVal = send(ptr->sock, ptr->buf + ptr->sendBytes, ptr->recvBytes - ptr->sendBytes, 0);
                if (retVal == SOCKET_ERROR) {
                    err_quit("send()");
                    RemoveSocketInfo(i);
                    continue;
                }
                ptr->sendBytes += retVal;
                if (ptr->recvBytes == ptr->sendBytes) {
                    ptr->recvBytes = ptr->sendBytes = 0;
                }
            }
        }
    }

    closesocket(listen_s);
    WSACleanup();

    return 0;
}

//소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock) {
    if (nTotalSockets >= FD_SETSIZE) {
        printf("[오류] 소켓 정보를 추가할 수 없습니다.\n");
        return FALSE;
    }

    SOCKETINFO* ptr = new SOCKETINFO;
    if (ptr == NULL) {
        printf("[오류] 메모리가 부족합니다.\n");
        return FALSE;
    }

    ptr->sock = sock;
    ptr->recvBytes = 0;
    ptr->sendBytes = 0;
    SocketInfoArray[nTotalSockets++] = ptr;

    return TRUE;
}

//소켓 정보 삭제
void RemoveSocketInfo(int nIndex) {
    SOCKETINFO* ptr = SocketInfoArray[nIndex];

    //클라 정보 얻기
    SOCKADDR_IN clientaddr;
    int addrlen = sizeof(clientaddr);
    getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);

    printf("[TCP 서버] 클라이언트 종료 : IP  주소 = %s, 포트번호 = %d\r\n",
        inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    closesocket(ptr->sock);
    delete ptr;
    if (nIndex != (nTotalSockets - 1))
        SocketInfoArray[nIndex] = SocketInfoArray[nTotalSockets - 1];

    --nTotalSockets;
}
void err_quit(const char* msg) {
    LPVOID lpMsgBuf;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL, WSAGetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpMsgBuf, 0, NULL);
    printf("[%s] %s\n", msg, (char*)lpMsgBuf);
    LocalFree(lpMsgBuf);
    exit(1);
}

아래는 WSAAsyncSelect를 활용하여 만들어진 Select 모델입니다.

  • 순서 방식
    1. WSAAsyncSelect() 함수를 호출하여, 소켓 이벤트를 알려줄 윈도우 메시지와 관심 있는 네트워크 이벤트를 등록한다.
    2. 등록한 네트워크 이벤트가 발생하면 윈도우 메시지가 발생하여 윈도우 프로시저가 호출된다.
    3. 윈도우 프로시저에서는 받은 메시지의 종류에 따라 적절한 소켓 함수를 호출하여 처리한다.
#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")
#include<stdio.h>
#include<Winsock2.h>
#include<stdlib.h>

#define PORT	9000
#define BUFSIZE 512
#define WM_SOCKET (WM_USER+1)

//소켓 정보 저장을 위한 구조체와 변수
struct SOCKETINFO {
    SOCKET sock;
    char buf[BUFSIZE + 1];
    int recvBytes;
    int sendBytes;
    BOOL recvdelayed;
    SOCKETINFO* next;
};

SOCKETINFO* SocketInfoArray;

//윈도우 메시지 처리 함수
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
void ProcessSocketMessage(HWND, UINT, WPARAM, LPARAM);

//소켓 관리 함수
BOOL AddSocketInfo(SOCKET sock);
SOCKETINFO* GetSocketInfo(SOCKET sock);
void RemoveSocketInfo(SOCKET sock);

//오류 출력 함수
void err_quit(const char* msg);
void err_display(int errcode);
int main()
{
    WNDCLASS wndclass;
    int retVal;

    //윈도우 초기화
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = NULL;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = "MyWndClass";
    if (!RegisterClass(&wndclass)) return 1;

    //윈도우 생성
    HWND hWnd = CreateWindow("MyWndClass", "TCP 서버", WS_OVERLAPPEDWINDOW, 0, 0, 600, 200, NULL, NULL, NULL, NULL);
    if (hWnd == NULL) return 1;
    ShowWindow(hWnd, SW_SHOWNORMAL);
    UpdateWindow(hWnd);

    WSADATA wsaData;
    SOCKET listen_s;
    SOCKADDR_IN sock_addr;


    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        err_quit("WSAStartup");

    //소켓 생성
    listen_s = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_s == INVALID_SOCKET)
        err_quit("socket");

    ZeroMemory(&sock_addr, sizeof(struct sockaddr_in));
    sock_addr.sin_family = PF_INET;
    sock_addr.sin_port = htons(PORT);
    sock_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    if (bind(listen_s, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in)) == SOCKET_ERROR) err_quit("Bind");
    if (listen(listen_s, SOMAXCONN) == SOCKET_ERROR) err_quit("listen");

    //WSAAsyncSelect()
    retVal = WSAAsyncSelect(listen_s, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
    if (retVal == SOCKET_ERROR) err_quit("WSAAsyncSelect()");

    //메시지 루프
    MSG msg;
    while (GetMessage(&msg, 0, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    WSACleanup();
    return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParm, LPARAM lParm) {
    switch (uMsg)
    {
    case WM_SOCKET:
        ProcessSocketMessage(hWnd, uMsg, wParm, lParm);
        return 0;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hWnd, uMsg, wParm, lParm);
}

void ProcessSocketMessage(HWND hWnd, UINT uMsg, WPARAM wParm, LPARAM lParm) {

    //데이터 통신에 사용할 변수
    SOCKETINFO* ptr;
    SOCKET clientsock;
    SOCKADDR_IN clientaddr;
    int addrlen, retVal;

    //오류 발생 여부
    if (WSAGETSELECTERROR(lParm)) {
        err_display(WSAGETSELECTERROR(lParm));
        RemoveSocketInfo(wParm);
        return;
    }

    //메시지 처리
    switch (WSAGETSELECTEVENT(lParm)) {
    case FD_ACCEPT:
        addrlen = sizeof(clientaddr);
        clientsock = accept(wParm, (SOCKADDR*)&clientaddr, &addrlen);
        if (clientsock == INVALID_SOCKET) {
            err_quit("accept()");
        }
        printf("[TCP서버] 클라이언트 접속 IP : %s %d\r\n",
            inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        AddSocketInfo(clientsock);
        retVal = WSAAsyncSelect(clientsock, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
        if (retVal == SOCKET_ERROR) {
            err_quit("WSAAsyncSelecT()");
            return;
        }
        break;
    case FD_READ:
        ptr = GetSocketInfo(wParm);
        if (ptr->recvBytes > 0) {
            ptr->recvdelayed = TRUE;
            return;
        }
        //데이터 받기
        retVal = recv(ptr->sock, ptr->buf, BUFSIZE, 0);
        if (retVal == SOCKET_ERROR) {
            err_quit("recv()");
            RemoveSocketInfo(wParm);
            return;
        }
        ptr->recvBytes = retVal;

        //받은 데이터 출력
        addrlen = sizeof(clientaddr);
        getpeername(ptr->sock, (SOCKADDR*)&clientaddr, &addrlen);
        ptr->buf[retVal] = '\0';
        printf("[TCP/%s : %d] %s\r\n", inet_ntoa(clientaddr.sin_addr),
            ntohs(clientaddr.sin_port), ptr->buf);
    case FD_WRITE:
        ptr = GetSocketInfo(wParm);
        if (ptr->recvBytes <= ptr->sendBytes)
            return;
        //데이터 보내기
        retVal = send(ptr->sock, ptr->buf + ptr->sendBytes, ptr->recvBytes - ptr->sendBytes, 0);
        if (retVal == SOCKET_ERROR) {
            err_quit("send()");
            RemoveSocketInfo(wParm);
            return;
        }
        ptr->sendBytes += retVal;
        //받은 데이터 전부 보냈는지 체크
        if (ptr->recvBytes == ptr->sendBytes) {
            ptr->recvBytes = ptr->sendBytes = 0;
            if (ptr->recvdelayed) {
                ptr->recvdelayed = FALSE;
                PostMessage(hWnd, uMsg, wParm, lParm);
            }
        }
        break;
    case FD_CLOSE:
        RemoveSocketInfo(wParm);
        break;
    }
}

//소켓 정보 추가
BOOL AddSocketInfo(SOCKET sock) {

    SOCKETINFO* ptr = new SOCKETINFO;
    if (ptr == NULL) {
        printf("[오류] 메모리가 부족합니다.\n");
        return FALSE;
    }

    ptr->sock = sock;
    ptr->recvBytes = 0;
    ptr->sendBytes = 0;
    ptr->recvdelayed = FALSE;
    ptr->next = SocketInfoArray;
    SocketInfoArray = ptr;

    return TRUE;
}

SOCKETINFO* GetSocketInfo(SOCKET sock) {
    SOCKETINFO* ptr = SocketInfoArray;

    while (ptr) {
        if (ptr->sock == sock)
            return ptr;
        ptr = ptr->next;
    }
    return NULL;
}
//소켓 정보 삭제
void RemoveSocketInfo(SOCKET sock) {

    //클라 정보 얻기
    SOCKADDR_IN clientaddr;
    int addrlen = sizeof(clientaddr);
    getpeername(sock, (SOCKADDR*)&clientaddr, &addrlen);

    printf("[TCP 서버] 클라이언트 종료 : IP  주소 = %s, 포트번호 = %d\r\n",
        inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    SOCKETINFO* curr = SocketInfoArray;
    SOCKETINFO* prev = NULL;
    while (curr) {
        if (curr->sock == sock) {
            if (prev)
                prev->next = curr->next;
            else
                SocketInfoArray = curr->next;
            closesocket(curr->sock);
            delete curr;
            return;
        }
        prev = curr;
        curr = curr->next;
    }
}
void err_quit(const char* msg) {
    LPVOID lpMsgBuf;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL, WSAGetLastError(),
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpMsgBuf, 0, NULL);
    printf("[%s] %s\n", msg, (char*)lpMsgBuf);
    LocalFree(lpMsgBuf);
    exit(1);
}
void err_display(int errcode) {
    LPVOID lpMsgBuf;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        NULL, errcode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpMsgBuf, 0, NULL);
    printf("[ 오류 ] %s\n", (char*)lpMsgBuf);
    LocalFree(lpMsgBuf);
    exit(1);
}
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함