책정리/윈도우 네트워크 프로그래밍

4장 TCP 서버/클라이언트 구조

GONII 2015. 4. 13. 16:24
  • TCP 서버/클라이언트 구조

    PC에서 사용하는 대표적인 웹 클라이언트인 인터넷 익스플로러는 사용자가 입력한 주소를 참조하여 접속 대기 중인 웹 서버에 접속한 후, HTTP를 이용하여 요청 메시지를 보낸다. 웹 서버는 이 데이터를 분석한 후 HTTP를 이용하여 응답 메시지를 다내 보낸다. 익스플로러는 웹 서버가 보낸 데이터를 받아 화면에 표시한다. HTTP는 TCP에 기반한 프로토콜이므로 웹 서버/클라이언트는 대표적인 TCP 서버/클라이언트 애플리케이션이라고 할 수 있다.

    TCP 서버/클라이언트 동작 방식은 다음과 같다

    • 서버는 먼저 싱행하여 클라이너트가 접속하기를 기다린다(listen)
    • 클라이언트가 서버에 접속(connet)하여 데이터를 보낸다(send)
    • 서버는 클라이언트 접속을 수용하고(accept), 클라이언트가 보낸 데이터를 받아서(recv)처리한다.
    • 서버는 처리한 데이터를 클라이언트에 보낸다(send)
    • 클라이언트는 서버가 보낸 데이터를 받아서(recv) 자신의 목적에 맞게 사용한다.

      동작 원리

      TCP서버에 TCP클라이언트가 접속하여 통신을 수행하는 과정은 다음과 같다

      • 서버는 소켓을 생성한 후 클라이언트가 접속하기를 기다린다. 이때 서버가 사용하는 소켓은 특정 포트 번호와 결합되어(bind) 있어서 이 포트 번호로 접속하는 클라이언트만 수용할 수 있다

      • 클라이언트가 접속한다. 이때 TCP프로토콜 수준에서 연결 설정을 위한 패킷 교환이 이루어진다

      • TCP 프로토콜 수준의 연결 절차가 끝나면, 서버는 접속한 클라이언트와 통신할 수 있는 새로운 소켓을 생성한다. 서버가 클라이언트와 데이터를 주고 받을 때는 이 소켓을 사용한다. 기존의 소켓은 새로운 클라이언트 접속을 수용하는 용도로 계속 사용한다.

      • 두 클라이언트가 접속한 후의 상태이면, 서버측에서는 총 세 개의 소켓이 존재하며, 이 중 두 소켓이 실제 클라이언트와 통신하는 용도로 사용된다.

      서버측 소켓과 클라이언트측 소켓이 1대1로 대응하며, 한 클라이언트가 두 개 이상의 소켓을 사용하여 서버에 접속하는 것도 가능하다

      [실습] 간단한 TCP 서버/클라이언트

      서버와 클라이언트의 동작은 다음과 같다

    • 서버 : 클라이언트가 보낸 데이터를 받아(recv), 이를 문자열로 간주하여 무조건 화면에 출력한다(printf). 그리고 받은 데이터를 변경 없이 다시 클라이언트로 보낸다(send)
    • 클라이언트 : 사용자가 키보드로 입력한 문자열을(fgets) 서버에 보낸다(send). 서버가 받은 데이터를 그대로 되돌려 보내면, 클라이언트는 이를 받아(recv) 화면에 출력한다(printf)

    • 예제 TCPServer

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(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", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if(listen_sock == INVALID_SOCKET)

{

err_quit("socket()");

}

// bind()

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(listen_sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR )

{

err_quit("bind()");

}

// listen()

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

{

err_quit("listen()");

}

   

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE+1];

   

while(1)

{

// aceept()

addrlen = sizeof(clientaddr);

client_sock = accept(listen_sock, (SOCKADDR*)&clientaddr, &addrlen);

if( client_sock == INVALID_SOCKET )

{

err_display("accept()");

continue;

}

printf("\n[TCP서버]클라이언트 접속: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

   

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

retval = recv(client_sock, buf, BUFSIZE, 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

break;

}

else if( retval == 0 )

{

break;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[TCP/%s:%d] %s\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port), buf);

   

// 데이터 보내기

retval = send(client_sock, buf, retval, 0);

if(retval == SOCKET_ERROR)

{

err_display("sned()");

break;

}

}

   

// closesocket()

closesocket(client_sock);

printf("[TCP서버] 클라이언트 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

}

   

// closesocket()

closesocket(listen_sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

  • 예제 TCPClient

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(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", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

// 사용자 정의 데이터 수신 함수

int recvn(SOCKET s, char* buf, int len, int flags)

{

int rcv;

char* ptr = buf;

int left = len;

   

while( left > 0 )

{

rcv = recv(s, ptr, left, flags);

if( rcv == SOCKET_ERROR )

{

return SOCKET_ERROR;

}

else if( rcv == 0 )

{

break;

}

left -= rcv;

ptr += rcv;

}

   

return (len - left);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

if(sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// connet()

SOCKADDR_IN serveraddr;

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

//

retval = connect(sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR)

{

err_quit("connet()");

}

   

// 데이터 통신에 사용할 변수

char buf[BUFSIZE+1];

int len;

   

// 서버와 데이터 통신

while(1)

{

// 데이터 입력

ZeroMemory(buf, sizeof(buf));

printf("\n[보낸데이터]");

if(fgets(buf, BUFSIZE+1, stdin) == NULL )

{

break;

}

// '\n' 문자 제거

len = strlen(buf);

if( buf[len-1] == '\n')

{

buf[len-1] = '\0';

}

if( strlen(buf) == 0 )

{

break;

}

   

// 데이터 보내기

retval = send(sock, buf, strlen(buf), 0);

if( retval == SOCKET_ERROR)

{

err_display("send()");

break;

}

printf("[TCP클라이언트]%d바이트 보냄\n", strlen(buf));

   

// 데이터 받기

retval = recvn(sock, buf, retval, 0);

if( retval == SOCKET_ERROR)

{

err_display("recv()");

}

else if( retval == 0 )

{

break;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[TCP클라이언트]%d바이트 받음\n", strlen(buf));

printf("[받은데이터]:%s\n",buf);

}

   

// closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

return 0;

}

  • TCP 서버/클라이언트 분석

    애플리케이션 관점에서 소켓은 운영체제의 TCP/IP구현에서 제공하는 데이터 구조체를 참조하기 위한 매개체가 된다. TCP/IP를 이용하여 애플리케이션이 통신을 수행하기 위해서는 다음과 같은 요소가 결정되어야 한다.

    • 프로토콜

      소켓을 생성할 때 결정한다

    • 지역(local) IP주소와 지역 포트 번호

      서버 또는 클라이언트 자신의 주소를 의미한다

    • 원격(remote) IP주소와 원격 포트 번호

      서버 또는 클라이언트가 통신하는 상대의 주소를 의미한다

      서버 함수

      일반적으로 TCP서버는 다음과 같은 순서로 소켓 함수를 호출한다

      • socket( )함수를 이용하여 소켓을 생성
      • bind( )함수를 이용하여 지역 IP주소와 지역 포트 번호를 결정
      • listen( )함수를 이용하여 TCP상태를 LISTENING으로 변경
      • accept( )함수를 이용하여 자신에 접속한 클라이언트와 통신할 수 있는 새로운 소켓을 생성(이때 원격 IP주소와 원격 포트 번호가 결정)
      • send( ), recv( )등의 데이터 전송 함수를 이용하여 클라이언트와 통신을 수행한 후, closesocket( )함수를 이용하여 소켓을 닫음
      • 새로운 클라이언트 접속이 들어 올때마다 4~5과정을 반복

    • bind( )함수

      bind함수는 서버의 지역 IP주소와 지역 포트 번호를 결정하는 역할을 한다

      int bind(

      SOCKET s,

      const struct sockaddr* name,

      int namelen

      ); // 성공:0, 실패:SOCKET_ERROR

      • s

        클라이언트 접속을 숭요할 목적으로 만든 소켓으로 지역 IP주소와 지역 포트 번호가 아직 결정되지 않은 상태

      • name

        소켓 구조 구조체(TCP/IP의 경우 SOCKADDR_IN타입) 변수를 지역 IP주소와 지역 포트 번호로 초기화한 후, 이 변수의 주소값을 여기에 대입함

      • namelen

        소켓 주소 구조체 변수의 길이(바이트 단위)를 대입

      TCPServer 예제에서 bind( )함수를 사용한 부분은 다음과 같다

// bind()

// 선언

SOCKADDR_IN serveraddr;

// 0으로 초기화

ZeroMemory(&serveraddr, sizeof(serveraddr));

// 인터넷 주소 체계 AF_INET 사용

serveraddr.sin_family = AF_INET;

// 포트 번호, htons함수로 바이트 정렬로 변경한 값을 대입

serveraddr.sin_port = htons(9000);

// 서버의 지역 IP주소를 설정

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

// bind( )함수 호출

retval = bind(listen_sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR )

{

err_quit("bind()");

}

  • listen( )함수

    listen( )함수는 소켓과 결합된 TCP포트 상태를 LISTENING으로 바꾸는 역할을 한다. 이는 클라이언트 접속을 받아들일 수 있는 상태가 됨을 의미함

    int listen(

    SOCKET s,

    int backlog

    ); // 성공:0, 실패:SOCKET_ERROR

    • s

      클라이언트 접속을 수용할 목적으로 만든 소켓으로 bind( )함수에 의해 지역 IP주소와 지역 포트 번호가 설정된 상태다

    • backlog

      서버가 당장 처리하지 않더라도 접속 가능한 클라이언트의 개수다. 클라이언트의 접속 정보는 연결큐(connection queue)에 저장되며, backlog는 이 연결 큐의 길이를 나타낸다고 볼 수 있다. 하부 프로토콜에서 지원 가능한 최대값을 사용하려면 SOMAXCONN값을 대입한다

    TCPServer 예제에서 listen( )함수를 사용한 부분은 다음과 같다

retval = listen(listen_sock, SOMAXCONN);

if( retval == SOCKET_ERROR )

{

err_quit("listen()");

}

  • accept( )함수

    accept( )함수는 서버에 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성하여 리턴하는 역할을 한다. 또한 접속한 클라이언트의 IP주소와 포트번호(서버입장에서는 원격 IP주소와 원격 포트번호, 클라이언트 입장에서는 지역 IP주소와 지역 포트번호)를 알려준다.

    SOCKET accept(

    SOCKET s,

    struct sockaddr* addr,

    int* addrlen

    ); // 성공: 새로운 소켓, 실패: INVALID_SOCKET

    • s

      클라이언트 접속을 수용할 목적으로 만든 소켓

    • addr

      소켓 주소 구조체 변수를 정의한 후, 이 변수의 주소값을 대입한다. accept()함수는 addr이 가리키는 메모리 영역을 클라이언트의 IP주소와 포트 번호로 채워 넣는다.

    • addrlen

      정수형 변수를 addr이 가리키는 메모리 영역의 크기로 초기화한 후, 이 변수의 주소값을 여기에 대입한다. accept()함수가 리턴하면, 정수형 변수는 addrlen은 함수가 초기화한 메모리 크기값(바이트 단위)을 가진다.

    접속한 클라이언트가 없을 경우 accept()함수는 서버를 대기 상태(wait state)로 만든다. 클라이언트가 접속하면 서버는 깨어나고 accept()함수가 리턴하게 된다.

    TCPServer에서 accept()함수를 사용한 부분은 다음과같다

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE+1];

   

while(1)

{

// aceept()

addrlen = sizeof(clientaddr);

client_sock = accept(listen_sock, (SOCKADDR*)&clientaddr, &addrlen);

if( client_sock == INVALID_SOCKET )

{

err_display("accept()");

continue;

}

printf("\n[TCP서버]클라이언트 접속: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

   

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

retval = recv(client_sock, buf, BUFSIZE, 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

break;

}

else if( retval == 0 )

{

break;

}

// 받은 데이터 출력

buf[retval] = '\0';

printf("[TCP/%s:%d] %s\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port), buf);

   

// 데이터 보내기

retval = send(client_sock, buf, retval, 0);

if(retval == SOCKET_ERROR)

{

err_display("sned()");

break;

}

}

   

// closesocket()

closesocket(client_sock);

printf("[TCP서버] 클라이언트 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

}

클라이언트 함수

일반적으로 TCP클라이언트는 다음과 같은 순서로 소켓 함수를 호출한다

  • socket( )함수를 이용하여 소켓을 생성한다
  • connet( )함수를 이용하여 서버에 접속한다
  • send( ), recv( )등의 데이터 전송 함수를 이용하여 서버와 통신을 수행한 후, closesocket( )함수를 이용하여 소켓을 닫는다

  • connet( )함수

    connet( )함수는 클라이언트가 서버에 접속하여 TCP 프로토콜 수준의 연결이 이루어지도록 한다.

    int connect(

    SOCKET s,

    const struct sockaddr* name,

    int namelen

    ); // 성공:0, 실패:SOCKET_ERROR

    • s

      서버와 통신을 하기 위해 만든 소켓

    • name

      소켓 주소 구조체 변수를 서버 주소(원격IP 주소와 원격 포트 변호)로 초기화한 후, 이 변수의 주소값을 여기에 대입

    • namelen

      소켓 주소 구조체 변수의 길이(바이트 단위)를 대입

    클라이언트는 서버와 달리 bind( )함수를 호출하지 않는다. bind( )함수를 호출하지 않은 상태에서 connect( )함수를 호출하면 운영체제는 자동으로 지역 IP 주소와 지역 포트 번호를 설정한다. 이때 자동으로 할당되는 포트 번호는 운영체제에 따라 다를 수 있으며, 윈도우의 경우 1024~5000 범위 중 하나가 할당된다.

    TCPClient 예제에서 connect( )함수를 사용한 부분은 다음과 같다

// connet()

SOCKADDR_IN serveraddr;

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

//

retval = connect(sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if( retval == SOCKET_ERROR)

{

err_quit("connet()");

}

데이터 전송 함수

데이터 전송 함수는 크게 데이터를 보내는 함수와 데이터를 받는 함수로 구분할 수 있다. 가장 기본이 되는 함수는 send( )와 recv( )며, 그 밖에 WSA*( )형태의 확장 함수가 존재한다

데이터 전송 함수를 다루기 전에 소켓 데이터 구조체를 살펴보자

각각 자신과 상대방의 IP주소와 포트 번호 외에 데이터 송수신 버퍼가 있음을 알 수 있다. 송신 버퍼(send buffer)는 데이터를 전송하기 전에 임시로 저장해두는 여역이고, 수신 버퍼(receive buffer)는 받은 데이터를 애플리케이션이 처리하기 전까지 임시로 저장해두는 영역이다. 송신 버퍼와 수신 버퍼를 통틀어서 소켓 버퍼(socket buffer)라 부른다. send( )와 recv( )함수는 소켓을 통해(간접적으로) 소켓 버퍼를 접근할 수 있도록 만든 함수라고 볼 수 있다.

데이터 전송 함수를 사용할 때는 하부 프로토콜의 특성을 잘 알고 있어야 한다. 이 절에서 다루는 TCP 프로토콜은 애플리케이션이 보낸 데이터의 경계를 구분하지 않는다는 특징이 있다. 예르르 들면, 클라이언트가 100, 200, 300바이트 데이터를 차례로 보낼 경우 서버가 100, 200, 300 바이트 데이터의 경계를 구분하지 못하고, 350, 250바이트 데이터를 읽을 수 있다. 따라서 TCP 서버/클라이언트를 작성할 때는 데이터 경계 구분을 위한 상호 약속이 필요하며, 이를 애플리케이션 수준에서 처리해야 한다.

  • send( )함수

    send( )함수는 애플리케이션 데이터를 송신 버퍼에 복사함으로써 궁극적으로 하부 프로토콜(ex) TCP/IP)에 의해 데이터가 전송되도록 한다. send( )함수는 데이터 복사가 성공적으로 이루어지면 곧바로 리턴하므로 send( )함수가 성공했다고 실제 데이터 전송이 완료된 것은 아니다.

    int send(

    SOCKET s,

    const char* buf,

    int len,

    int flags

    ); // 성공:보낸 바이트수, 실패:SOCKET_ERROR

    • s

      통신할 대상과 연결된(connected) 소켓

    • buf

      보낼 데이터를 담고 있는 애플리케이션 버퍼의 주소

    • len

      보낼 데이터 크기(바이트 단위)

    • flags

      ssend( )함수의 동작을 바꾸는 옵션, 대부분 0을 사용하며, MSG_DONTROUTE, MSG_OOB를 사용할때도 있다

    send( )함수는 첫번째 인자로 사용한 소켓의 특성에 따라 두 종류의 성공적인 리턴을 할 수 있다.

    • 블로킹(blocking) 소켓

      send( )함수를 호출할 때, 송신 버퍼의 여유 공간이 send( )함수의 세 번째 인자인 len보다 작을 경우 해당 프로세스는 대기 상태(wait state)가 된다. 송신 버퍼에 충분한 공간이 생기면 프로세스는 깨어나고, len크기만큼 데이터 복사가 이루어진 후 send( )함수가 리턴한다.

    • 넌블로킹(Nonblocking) 소켓

      ioctlsocket( )함수를 이용하면 블로킹 소켓을 넌블로킹 소켓으로 바꿀 수 있다. 넌블로킹 소켓에 대해 send( )함수를 호출하면, 송신 버퍼의 여유 공간만큼 데이터를 복사한 후 실제 복사한 데이터 바이트 수를 리턴한다. 이 경우 send( )함수의 리턴값은 최소1, 최대 len이 된다.

  • recv( )함수

    recv( )함수는 수신 버퍼에 도착한 데이터를 애플리케이션 버퍼로 복사하는 역할을 한다

    int recv(

    SOCKET s,

    char* buf,

    int len,

    int flags

    ); // 성공:받은 바이트 수 도는0(연결 종료시), 실패:SOCKET_ERROR

    • s

      통신할 대상과 연결된(connected) 소켓

    • buf

      받은 데이터를 저장할 애플리케이션 버퍼의 주소

    • len

      수신 버퍼로부터 복사할 최대 데이터 크기(바이트 단위), 이 값은 buf크기보다 크면 안된다

    • flags

      recv( )함수의 동작을 바꾸는 옵션, 대부분 0을 사용하며, 드물게 MSG_PEEK과 MSB_OOB를 사용하는 경우도 있다. recv( )함수는 수신 버퍼의 데이터를 애플리케이션 버퍼로 복사한 후 해당 데이터를 수신 버퍼에서 삭제한다.

    recv( )함수는 두 종류의 성공적인 리턴을 할 수 있다.

    • 수신 버퍼에 데이터가 도달한 경우

      len보다 크지 않은 범위 내에서 가능한 많은 데이터를 애플리케이션 버퍼로 복사한다. 이 경우 복사한 바이트 수가 리턴되며, 최대 리턴값은 len이 된다

    • 접속이 정상 종료된 경우

      상대 애플리케이션이 closesocket( )함수를 사용하여 접속을 종료하면, TCP프로토콜 수준에서 접속 종료를 위한 패킷 교환 절차가 이루어진다. 이 경우 recv( )함수는 0을 리턴한다. 리턴값이 0인 경우를 정상 종료(normal close = graceful close)라 부른다

    자신이 받을 데이터의 크기를 미리 알고 있다면, 이 크기만큼 받을 때까지 recv( )함수를 여러 번 호출해야 한다.

    예제에서는 사용자 정의 함수인 recvn( )을 정의해서 처리하고 있다.

int recvn(SOCKET s, char* buf, int len, int flags)

{

int rcv;

char* ptr = buf;

int left = len;

   

while( left > 0 )

{

rcv = recv(s, ptr, left, flags);

if( rcv == SOCKET_ERROR )

{

return SOCKET_ERROR;

}

else if( rcv == 0 )

{

break;

}

left -= rcv;

ptr += rcv;

}

   

return (len - left);

}

recvn( )함수 인자는 recv( )함수와 동일하다. 포인터 변수 ptr이 애플리케이션 버퍼의 시작 주소를 가리키도록 한다. 데이터를 읽을 때마다 ptr변수는 증가한다. left 변수는 아직 읽지 않은 데이터의 크기를 나타낸다.

TCPClient예제에서 send( ), recv( )함수를 사용한 부분은 다음과 같다

char buf[BUFSIZE+1];

int len;

   

// 서버와 데이터 통신

while(1)

{

// 데이터 입력

ZeroMemory(buf, sizeof(buf));

printf("\n[보낸데이터]");

if(fgets(buf, BUFSIZE+1, stdin) == NULL )

{

break;

}

// '\n' 문자 제거

len = strlen(buf);

if( buf[len-1] == '\n')

{

buf[len-1] = '\0';

}

if( strlen(buf) == 0 )

{

break;

}

   

// 데이터 보내기

retval = send(sock, buf, strlen(buf), 0);

if( retval == SOCKET_ERROR)

{

err_display("send()");

break;

}

printf("[TCP클라이언트]%d바이트 보냄\n", strlen(buf));

   

// 데이터 받기

retval = recvn(sock, buf, retval, 0);

if( retval == SOCKET_ERROR)

{

err_display("recv()");

}

else if( retval == 0 )

{

break;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[TCP클라이언트]%d바이트 받음\n", strlen(buf));

printf("[받은데이터]:%s\n",buf);

}

  • 애플리케이션 프로토콜과 메시지 설계

    애플리케이션 프로토콜

    애플리케이션 프로토콜은 애플리케이션 수준(application-level)에서 주고받는 데이터의 형식과 의미 그리고 처리 방식 등을 정의한 프로토콜이다. 일단 애플리케이션 프로토콜이 결정되면 소켓 프로그래밍을 이용하여 데이터를 주고받도록 작성하면 되는 것이다.

    애플리케이션 프로토콜의 기본은 주고받을 메시지 형식을 정하는 것이다.

    두 프로그램이 주고 받아야 할 요소는 다음과 같다

    • 선의 시작과 끝점
    • 두께와 색상

    주고 받을 데이터를 구조체로 표현하면 다음과 같다

    struct drawMessage1

    {

    int x1, y1; // 시작점

    int x2, y2; // 끝점

    int width; // 두께

    int color; // 색상

    };

    추가로 더 필요한 데이터가 있을 경우 필드를 추가하면 된다

    메시지 설계

    통신 양단이 주고 받을 데이터 요소를 구조체로 정의하는 것만으로는 아직 충분하지 않다. 메시지를 설계할 때 고려해야 할 사항을 알아보도록 하자

    • 경계 구분

      TCP같이 메시지 경계를 구분하지 않는 프로토콜을 사용할 경우, 애플리케이션 수준에서 이를 처리해야 한다. 다음과 같이 세 가지 방법을 생각해볼 수 있다.

      [송신자]

      • 항상 고정 길이 데이터를 보낸다
      • 경계 구분을 위해 특별한 표시(EOR, End of Record)를 삽입한다
      • 보낼 데이터 길이를 고정 길이 데이터로 보낸 후, 가변 길이 데이터를 이어서 보낸다

      [수신자]

      • 항상 고정 길이 데이터를 받는다
      • EOR이 나올때까지 데이터를 읽은 후 처리한다
      • 고정 길이 데이터를 읽어 뒤따라올 데이터의 길이를 알아낸다. 이 길이만큼 가변 길이 데이터를 읽어 처리한다.

      1번은 송신자와 수신자가 처리하기에 가장 간편하지만 미래에 사용할 가장 긴 데이터를 감안하여 고정 길이를 정해야 한다는 문제가 있다. 이렇게 하면 길이가 짧은 데이터를 주고 받을 때는 낭비하는 부분이 생긴다.

      2번은 송신자와 수신자 쪽에서 데이터를 처리하기 쉽지 않다는 문제가 있다. 첫째, 송신자 쪽에서는 데이터 중간에 EOR과 똑같은 패턴이 있을 경우를 특별하게 처리해주어야 한다. 둘째 수신자 쪽에서는 데이터를 한 바이트씩 읽어서 처리해야 하므로 효율성이 떨어지게 된다. 또한 데이터에 속한 EOR인지, 경계를 나타내는 EOR인지 구분하는 작업도 해야 한다.

      3번은 구현하기도 쉽고 효율성도 높아 일반적으로 많이 사용한다. 이 경우 두 번의 recvn( )함수 호출로 데이터를 읽을 수 있다.

    • 바이트 정렬

      서로 다른 바이트 정렬 방식을 사용하는 시스템 사이에 데이터를 교환할 때는 바이트 정렬 방식을 통일해야 한다. 특별한 전제가 없다면 빅 엔디안 방식으로 통일하는 것이 좋다.

    • 멤버 정렬

      멤버 정렬(member alignment)이란 구조체(공용체, 클래스 포함)멤버의 시작 주소에 대한 제약 사항을 의미한다.

struct myMessage

{

int a; // 4바이트

char b; // 1바이트

int c; // 4바이트

char d; // 1바이트

};

myMessage msg;

//

send(sock, char*)&msg, sizeof(msg), 0);

위 메시지를 정의하여 보내면 sizeof(msg)는 10(4+1+4+1)이 아닌 16이 되게 된다.

양쪽 프로그램이 동일한 구조체 멤버 정렬 방식을 사용한다면 이와 같이 메세지를 전송해도 문제가 되지 않는다. 특별한 이유로 인해 정확히 10바이트를 보내려고 한다면 다음과 같이 #pragma pack컴파일러 명령을 사용하면 된다.

#pragma pack(1) // 멤버 정렬 방식 전환: 1바이트 경계

struct myMessage

{

int a; // 4바이트

char b; // 1바이트

int c; // 4바이트

char d; // 1바이트

};

#pragma pack(); // 디폴트 멤버 정렬 방식으로 복귀

myMessage msg;

//

send(sock, char*)&msg, sizeof(msg), 0);

[실습] 파일 전송 프로그램

서버는 파일을 받기만 하고, 클라이언트는 보내기만 한다고 가정한다. 클라이언트가 서버에 보낸 메시지는 다음과 같이 정의하였다. 실제 파일 데이터를 전송하면, 서버는 자신이 받을 크기를 미리 알고 있으므로 전송 완료 여부를 알 수 있다.

  • 예제 FileSender

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 4096

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(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", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

if( argc < 2 )

{

fprintf(stderr, "Usage:%s[FileName]\n", argv[0]);

return -1;

}

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);

if( sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// connect()

SOCKADDR_IN serveraddr;

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

retval = connect(sock, (SOCKADDR*)&serveraddr,

sizeof(serveraddr));

if( retval == SOCKET_ERROR)

{

err_quit("connect()");

}

   

// 파일 열기

FILE* fp = fopen(argv[1],"rb");

if(fp == NULL)

{

perror("파일 입출력 오류");

return -1;

}

   

// 파일 이름 보내기

char filename[256];

ZeroMemory(filename, 256);

sprintf(filename, argv[1]);

retval = send(sock, filename, 256, 0);

if(retval == SOCKET_ERROR)

{

err_quit("send()");

}

   

// 파일 크기 얻기

fseek(fp, 0, SEEK_END);

int totalbyte = ftell(fp);

   

// 파일 크기 보내기

retval = send(sock, (char*)&totalbyte,

sizeof(totalbyte), 0);

if(retval == SOCKET_ERROR)

{

err_quit("send()");

}

   

// 파일 데이터 전송에 사용할 변수

char buf[BUFSIZE];

int numread;

int numtotal = 0;

   

// 파일 데이터 보내기

rewind(fp); // 파일 포인터를 제일 앞으로 이동

while(1)

{

numread = fread(buf, 1, BUFSIZE, fp);

if( numread > 0 )

{

retval = send(sock, buf, numread, 0);

if(retval == SOCKET_ERROR)

{

err_display("send()");

break;

}

numtotal += numread;

}

else if( numread == 0 && numtotal == totalbyte )

{

printf("파일 전송 완료! : %d바이트\n", numtotal);

break;

}

else

{

printf("파일 입출력 오류");

break;

}

}

//

fclose(fp);

   

// closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

  • 예제 FileReceiver

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

   

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 4096

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

   

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 오류 함수

void err_display(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", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

// 사용자 정의 데이터 수신 함수

int recvn(SOCKET s, char* buf, int len ,int flags)

{

int received;

char* ptr = buf;

int left = len;

   

while(left > 0)

{

received = recv(s, ptr, left, flags);

if(received == SOCKET_ERROR)

{

return SOCKET_ERROR;

}

else if(received == 0)

{

break;

}

left -= received;

ptr += received;

}

   

return (len-left);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if( listen_sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// bind()

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(listen_sock, (SOCKADDR*)&serveraddr, sizeof(serveraddr));

if(retval == SOCKET_ERROR)

{

err_quit("bind()");

}

   

// listen()

retval = listen(listen_sock, SOMAXCONN);

if(retval == SOCKET_ERROR)

{

err_quit("listen()");

}

   

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE];

   

while(1)

{

// accept()

addrlen = sizeof(clientaddr);

client_sock = accept(listen_sock, (SOCKADDR*)&clientaddr, &addrlen);

if(client_sock == INVALID_SOCKET)

{

err_display("accept()");

continue;

}

printf("\nFileSender접속:IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

   

// 파일 이름 받기

char filename[256];

ZeroMemory(filename, 256);

retval = recvn(client_sock, filename, 256, 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

closesocket(client_sock);

continue;

}

   

// 파일 크기 받기

int totalbyte;

retval = recvn(client_sock, (char*)&totalbyte, sizeof(totalbyte), 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

closesocket(client_sock);

continue;

}

   

printf("-> 받을 파일 크기: %d\n", totalbyte);

   

// 파일 열기

FILE* fp = fopen(filename, "wb");

if(fp == NULL)

{

perror("파일 입출력 오류");

closesocket(client_sock);

continue;

}

   

// 파일 데이터 받기

int numtotal = 0;

while(1)

{

retval = recvn(client_sock, buf, BUFSIZE, 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

break;

}

else if(retval == 0)

{

break;

}

else

{

fwrite(buf, 1, retval, fp);

if( ferror(fp) )

{

perror("파일 입출력 오류");

break;

}

numtotal += retval;

}

}

fclose(fp);

   

// 전송 결과 출력

if(numtotal == totalbyte)

{

printf("-> 파일 전송 완료\n");

}

else

{

printf("-> 파일 전송 실패\n");

}

   

// closesocket

closesocket(client_sock);

printf("FileSender 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

}

   

// closesocket()

closesocket(listen_sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

반응형