본문 바로가기
개발/윈도우

[Window] TCP 서버 / 클라이언트 구현

by p_human 2020. 4. 1.

일단 윈속 프로그램을 작성하기전에 라이브러리를 추가시켜줘야 합니다.

저만 따라오시면 다할 수 있습니다. 천천히 과정을 다 찍어서 보여드릴게요.

 

라이브러리 추가 순서

  1. 프로젝트 우클릭
  2. 속성 클릭
  3. 추가 종속성의 아래 화살표 클릭
  4. 편집 클릭
  5. "ws2_32.lib" 추가후 확인

위의 과정을 프로젝트마다 해줘야 합니다.

이게 귀찮다면 #pragma comment(lib, "ws2_32.lib")만 헤더파일 밑에 넣어주면 됩니다.

 


윈도우 기반 TCP 서버/클라이언트 프로그램

아래는 윈도우에서 실행가능한 서버/클라이언트 코드입니다. 두 프로젝트에 ws2_32.lib를 추가해줘야 합니다.

// C++ 서버프로그램
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <cstdio>
#include <cstdlib>
#include <ws2tcpip.h>
void ErrorHandling(const char* _Message);
int main(int argc, const char* argv[])
{
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // 윈속 프로그래밍을 하기 위해서 없어서는 안될
    // 중요한 함수이다.
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");

    // 리눅스와 마찬가지로 소켓을 생성할 때는 별 다른점이 없다.
    // 반환되는 값이나 에러가 다르다는 점??
    SOCKET hServSock = socket(PF_INET, SOCK_STREAM, 0);
    if (hServSock == INVALID_SOCKET)
        ErrorHandling("socket() error!");

    // 리눅스에서와 똑같다.
    SOCKADDR_IN servAddr;
    ZeroMemory(&servAddr, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));

    // 여기도 마찬가지...
    if (bind(hServSock, (const SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        ErrorHandling("bind() error!");

    if (listen(hServSock, 5) == SOCKET_ERROR)
        ErrorHandling("listen() error!");

    SOCKADDR_IN clntAddr;
    ZeroMemory(&clntAddr, sizeof(clntAddr));
    int szClntAddr = sizeof(clntAddr);
    SOCKET hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
    if (hClntSock == INVALID_SOCKET)
        ErrorHandling("accept() error!");
	
    char message[] = "Hello World!";
    send(hClntSock, message, sizeof(message), 0);
    closesocket(hClntSock);
    closesocket(hServSock);
    // 이 함수는 윈속 라이브러리를 더이상 사용하지 않겠다는 의미에서 호출하면 된다.
    WSACleanup();
    return 0;
}

void ErrorHandling(const char* _Message)
{
    fputs(_Message, stderr);
    fputc('\n', stderr);
    exit(1);
}

// C++ 클라이언트 프로그램
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <cstdio>
#include <cstdlib>
#include <ws2tcpip.h>
void ErrorHandling(const char* _Message);

int main(int argc, const char* argv[])
{
    if (argc != 3)
    {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");

    SOCKET hSocket = socket(PF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
        ErrorHandling("socket() error!");

    SOCKADDR_IN servAddr;
    ZeroMemory(&servAddr, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    // 다른 부분은 몰라도 여기는 꼭 봐야한다.
    // inet_pton함수는 성공 시 1을 리턴하고, 실패 시 -1을 리턴한다.
    // 첫 번째 인자에는 주소체계를 지정
    // 두 번째 인자에는 실제 연결할 상대의 주소
    // 세 번째 인자에는 저장될 변수의 주소값
    // 이 함수는 inet_addr과 같은 역할을 하는 함수이므로 인자값만
    // 유의 하면된다.
    if(inet_pton(AF_INET, argv[1], &servAddr.sin_addr.S_un.S_addr)== -1)
        ErrorHandling("failed init address error!");

    servAddr.sin_port = htons(atoi(argv[2]));

    if (connect(hSocket, (const SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        ErrorHandling("connect() error!");

    char message[30];
    int strLen;
    strLen = recv(hSocket, message, sizeof(message) - 1, 0);
    if (strLen == -1)
        ErrorHandling("recv() error!");


    printf("Message from server: %s \n", message);

    closesocket(hSocket);
    WSACleanup();
    return 0;
}

void ErrorHandling(const char* _Message)
{
    fputs(_Message, stderr);
    fputc('\n', stderr);
    exit(1);
}

윈속 기본 함수

리눅스에서 사용하는 함수와 윈도우에서 사용하는 함수는 반환값과 매개변수가 다르기 때문에 조금 설명이 필요한 부분이므로 윈속 서버프로그램에서 기본적으로 사용하는 함수에 대해서 설명을 해보겠습니다.

 

일단 기본적으로 윈속 프로그램을 사용하기 위해서는 외부 라이브러리를 추가해야 한다고 위에서 언급을 해서 외부 라이브러리를 추가했을 것입니다.

추가를 했다면 아래와 같이 윈속 프로그래밍을 할 때에 절대로 빠지지 말아야 할 WSAStartup 함수를 호출해줘야 합니다.


#include <ws2tcpip.h>

// 성공시 0, 실패 시 에러코드 값 반환
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

첫 번째 인자에는 사용할 윈속의 버전정보 전달

두 번째 인자에는 WSADATA라는 구조체 변수의 주소 값 전달


원래 이 함수를 포함하고 있는 헤더파일은 winsock2이지만, ws2tcpip헤더파일을 포함한 이유는 이전 버전과의 호환성 때문입니다. 

그럼 함수에 대한 설명을 계속해보겠습니다. 첫 번째 인자에는 윈속의 버전정보를 전달한다고 설명이 되어있습니다.

어떤 형식으로 전달을 하게 될까요? 만약에 사용할 윈속 버전이 1.2라면 1이 주 버전이고, 2가 부 버전이므로, 0x0201를 전달해야 한다고 합니다. 상위 8비트에는 부 버전 정보, 하위 8비트에는 주 버전 정보를 넣게 됩니다. 위와 같이 윈속 버전을 2.2를 사용한다면 매개변수에 0x0202를 전달 해야겠네요. 그렇지만 매번 이렇게 전달하면 실수를 할 수 있고, 보기에 헷갈릴 수도 있기 때문에 매크로 함수 MAKEWORD를 사용해서 버전 정보를 전달합니다.

 

  • MAKEWORD(2,2)
  • MAKEWORD(1,2)

위와 같은 형태로 버전 정보를 전달하면 됩니다.

 

다음으로 두 번째 매개변수에 대해서 알아보겠습니다. 두 번째 매개변수는 LPWSADATA가 필요하다고 합니다. 그럼 WSADATA 구조체의 주소값을 넘겨주면 됩니다. 이 함수가 실행이 되면 WSADATA에는 초기화된 라이브러리에 대한 정보가 들어간다고 합니다. 그래서 우리가 윈속에 대한 여러가지 함수를 사용할 수 있게 되나 봅니다. 윈속 프로그래밍을 하는 프로그래머가 이 함수를 사용하지 않고 자기가 스스로 소켓을 만들어서 사용하지 않는 이상 거의 대부분 이 함수를 호출해서 사용합니다.


아래의 코드는 첫 번째 매개변수와 두 번째 매개변수를 전달해서 완전한 WSAStartup 함수를 호출한 모습입니다.

그리고 WSACleanup는 윈속 라이브러리의 해제를 해주는 함수이기 때문에 꼭 호출해줘야 합니다.

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <cstdio>
#include <ws2tcpip.h>
void ErrorHandling(const char* _Message);
int main(int argc, const char* argv[])
{
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error!");
    WSACleanup(void);
    return 0;
}
void ErrorHandling(const char* _Message){
    fputs(_Message, stderr);
    fputc('\n', stderr);
    ExitProcess(EXIT_FAILURE);
}

윈속 프로그램 시 사용 함수

이제 리눅스에 서버/클라이언트 프로그램에서 사용했었던 함수와 윈도우에서 사용하는 함수를 비교해보면 알겠지만, 이반환형과 매개변수를 제외하고는 똑같습니다. 물론 기능도 똑같고요. 이러한 점이 리눅스와 윈도우에서의 프로그래밍을 수월하게 해줍니다.

#include <ws2tcpip.h>

// 성공 시 소켓 핸들 반환, 실패 시 INVALID_SOCKET 반환
SOCKET socket(int af, int type, int protocol);

// 성공 시 0, 실패 시 SOCKET_ERROR 반환
int bind(SOCKET s, const struct sockaddr * name, int namelen);

// 성공 시 0, 실패 시 SOCKET_ERROR 반환
int listen(SOCKET s, int backlog);

// 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환
SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen);

// 성공 시 0, 실패 시 SOCKET_ERROR 반환
int connect(SOCKET s, const struct sockaddr * name, int namelen);

// 성공 시 0, 실패 시 SOCKET_ERROR 반환
int closesocket(SOCKET s);

위의 나열한 함수들 중에 눈여겨볼 함수는 socket입니다. 이 socket함수는 반환 형이 SOCKET입니다. 그말은 즉슨 리눅스와 비슷하게 반환되는 소켓에 대한 번호를 지정해주는 것으로 생각할 수 있습니다. 실제로도 그렇고요.

책에서 나온 정확한 표현이 있어서 인용하겠습니다.

정수로 표현되는 소켓의 핸들 값 저장을 위해서 typedef 선언으로 정의된 새로운 자료형의 이름

쉽게 생각해서 소켓의 핸들 값인것입니다. 그럼 윈도우에서도 리눅스에서처럼 저 SOCKET을 이용해서 클라이언트의 요청을 받아들이거나, 데이터를 송수신할 때 사용할 수 있게 되겠죠?

 

이제 마지막으로 윈도우 기반의 입출력 함수에 대해서 알아보고 마치겠습니다.

윈도우 기반 입출력 함수

윈도우는 리눅스와는 다른 방식으로 소켓을 취급하기 때문에 윈도우에서 소켓 입출력을 할 때 파일 입출력 함수를 사용하면 안됩니다. 아래는 윈도우의 소켓 기반의 데이터 입출력 함수들이다.

#include<ws2tcpip.h>

// 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환
int send(SOCKET s, const char * buf, int len, int flags);

// 성공 시 수신한 바이트 수(단 EOF 전송 시 0), 실패 시 SOCKET_ERROR 반환
int recv(SOCKET s, const char * buf, int len, int flags);

위의 두 함수와 기존의 리눅스에서 사용하던 read, write 함수와 비교했을 때 차이점은 마지막에 int형을 필요로 하는 매개변수가 추가된 것 말고는 차이가 없음을 쉽게 알 수 있습니다. 지금은 아무런 옵션을 사용하지 않으니 마지막 인자값은 0으로 전달하면 됩니다.

 

이걸로 윈도우를 기반으로 한 TCP 서버/클라이언트 프로그램에 대해서 알아보았습니다. 내일은 음.... 뭘하면 좋을까요?

지금 리눅스의 진도에 맞춰서 같이 올리겠습니다.

이 글은 본의 아니게 좀 많이 길어졌습니다. 간단하게 함수만 소개하고, 코드 작성한 거 보여주면 짧게 될 줄 알았는데...

어쨌든 여기 끝까지 보신 분들 정말 감사합니다.

'개발 > 윈도우' 카테고리의 다른 글

[Window]에코 서버/클라이언트 프로그램  (0) 2020.04.02