본문 바로가기
전공 수업/컴퓨터 통신(Computer Communication)

[7주 차] - TCP 서버 - 클라이언트의 동작, 여러 가지 함수

by TwoJun 2022. 10. 11.

    과목명 : 컴퓨터 통신(Computer communication)

수업일자 : 2022년 10월 10일 (월)

 

 

 

 

1. TCP 서버(Server) - 클라이언트(Client) 구조

1-1. TCP 서버 - 클라이언트의 개요, 동작

(1) 개요

- 대표적인 웹 클라이언트인 웹 사이트는 사용자가 입력한 주소를 참조하여 접속 대기 중인 웹 서버에 접속한 후 HTTP 프로토콜을 이용해 요청 메시지를 보내게 되고 웹 서버는 웹 브라우저로부터 받은 메시지를 해석하여 HTTP 프로토콜을 이용해 요청에 대한 응답 메시지를 다시 보내게 됩니다.

 

- 이를 통해 웹 브라우저는 서버로부터 받은 데이터를 처리해 화면에 노출시킬 수 있습니다. HTTP(HyperText Transfer Protocol) 프로토콜은 TCP 프로토콜에 기반한 프로토콜이므로 웹 서버 - 클라이언트 구조는 대표적인 TCP 서버 - 클라이언트 응용 프로그램이라고 할 수 있습니다.

 

 

 

(2) 동작

TCP 서버 - 클라이언트의 동작 구조

(1) listen

- 서버는 항상 실행되고 있으며 클라이언트가 접속하기를 기다리고 있습니다

(2) connect, send

- 클라이언트는 서버에 접속하여 데이터를 보냅니다.

(3) accept, recv

- 서버는 클라이언트의 접속을 수용하고 클라이언트가 보낸 데이터를 받아서 처리합니다.

(4) send

-  서버는 처리한 데이터를 클라이언트에게 보냅니다.

(5) recv

- 클라이언트는 서버가 보낸 데이터를 받아 처리합니다.

(6) closesocket

- 데이터를 모두 주고받으면 접속을 종료합니다.

 

 

 

 

 

1-2. TCP 서버 - 클라이언트의 동작 원리 

- 위의 동작 과정을 좀 더 자세히 작성해 보겠습니다

 

(1) 서버는 소켓을 생성하고 클라이언트의 접속을 기다리고 있습니다. 서버가 사용하는 소켓은 특정 포트 번호와 연결되어 있어서 이 포트 번호로 접속하는 클라이언트만 수용할 수 있는 상태입니다.

클라이언트의 접속을 기다리고 있는 서버

 

 

 

(2) 클라이언트가 서버에 접속하게 되고, TCP 프로토콜 수준에서 연결 설정을 위한 패킷 교환이 발생합니다.

(TCP의 경우 3-way handshaking 방식으로 패킷을 주고받습니다.)

클라이언트 접속 발생

 

 

 

(3) TCP 프로토콜 수준의 연결 절차가 끝나면 서버는 클라이언트와 통신할 수 있는 새로운 소켓을 생성합니다. 이 소켓을 이용해 서버는 클라이언트와 데이터를 주고받으며 기존에 생성된 소켓은 다른 클라이언트의 접속을 수용하는 용도로 사용하게 됩니다.

클라이언트와 통신 중인 서버(기존 소켓은 다른 클라이언트의 접속을 기다리고 있다.)

 

 

 

(4) 이후 다른 클라이언트까지 서버와 통신하는 상황을 표현한 것이며 총 3개의 소켓이 존재하고 두 개의 소켓은 클라이언트와 통신, 나머지 한 개는 계속해서 다른 클라이언트의 접속을 기다리고 있습니다.

두 개의 클라이언트와 통신 중이며 마찬가지로 기존 소켓은 다른 클라이언트의 접속을 기다린다.

 

 

 

 

 

 

2. TCP 서버(Server) - 클라이언트(Client) 분석

- 응용 프로그램의 관점에서 소켓은 운영체제의 TCP/IP 구현에서 제공하는 데이터를 참조하기 위한 매개체입니다.

 

- 아래 그림은 TCP 서버 - 클라이언트가 소켓을 이용해 통신할 때 운영체제 레벨에서 관리해 주는 데이터 구조체를 나타내고 있습니다.

 

- 소켓 함수의 호출이 정상적으로 이루어지면 내부적으로 아래와 같은 데이터들이 생성되며 해당 데이터들은 Descriptor라고 부르기도 하며, Descriptor는 필요한 정보가 저장되어 운영체제 레벨에서 관리됩니다.

 

- 서버뿐만이 아닌 클라이언트도 소켓이라는 함수를 호출합니다.

소켓 데이터 구조체

 

 

2-1. 소켓 통신을 위해 결정해야 할 요소 

(1) 프로토콜(Protocol)

- 통신 규약, 소켓을 생성할 때 결정해야 합니다.

 

(2) 지역 IP 주소와 Port 번호 

- 서버 또는 클라이언트 자신의 주소입니다.

 

(3) 원격 IP 주소와 원격 Port 번호

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

 

 

 

 

 

2-2. TCP 서버 함수

(1) TCP 서버의 소켓 함수 호출 순서 및 동작

- 일반적으로 TCP 서버는 아래 그림과 같은 순서로 소켓 함수를 호출하게 됩니다.

윈속(Winsock) 응용 프로그램의 공통 구조(서버)

(a) socket() 함수로 소켓을 생성하며 사용할 프로토콜을 결정합니다.

(b) bind() 함수로 지역 IP 주소와 포트 번호를 결정합니다.

(c) listen() 함수로 TCP를 LISTENING 상태로 변경합니다.(클라이언트의 접속을 기다리고 있는 상태)

(d) accept() 함수로 자신에게 접속한 클라이언트와 통신할 수 있는 새로운 소켓을 생성하며 원격 IP 주소와 원격 포트 번호가 결정됩니다. 

(e) send(), recv() 등의 데이터 전송 함수로 클라이언트와 통신을 수행한 후 통신이 종료되었다면 closesocket() 함수로 소켓을 닫게 됩니다.

(f) 새로운 클라이언트의 접속에 대해 (d)~(e) 사이클을 반복합니다.

 

 

 

 

 

(2-2. a) bind() 함수 

- 소켓의 지역 IP 주소와 지역 포트 번호를 결정합니다.

int bind(
    SOCKET s,
    const struct sockaddr *name,
    int namelen
);

(1) s

- 클라이언트의 접속을 수용할 목적으로 만든 소켓입니다.

 

(2) name

- 소켓 구조체를 지역 IP 주소와 지역 포트 번호로 초기화하여 전달합니다.

 

(3) namelen

- 소켓 구조체의 길이(바이트 단위)입니다.

 

 

 

 

(2-2. b) listen() 함수

- 소켓의 TCP 포트 상태를 LISTENING으로 변경합니다.

int listen(
    SOCKET s,
    int backlog    // 몇 명까지 클라이언트가 접속 가능한지
);

(1) s

- 클라이언트의 접속을 수용할 목적으로 만든 소켓입니다.(bind() 함수로 지역 IP 주소, 지역 포트 번호를 설정한 상태)

 

(2) backlog

- 접속 가능한 클라이언트의 수를 나타냅니다.

 

 

 

 

(2-2. c) accept() 함수

- 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성해서 리턴합니다.

- 접속한 클라이언트의 IP 주소와 포트 번호를 알려줍니다.

SOCKET accept( 
    SOCKET s,
    struct sockaddr *addr,      // 클라이언트의 주소
    int *addrlen
);

 (1) s

- 클라이언트의 접속을 수용할 목적으로 만든 소켓입니다.(listen() 함수로 포트 상태를 LISTENING으로 변경한 상태)

 

(2) addr

- 소켓 주소 구조체를 전달하면 접속한 클라이언트의 주소 정보로 채워집니다.

 

(3) addrlen 

- 정수형 변수를 addr이 가리키는 소켓 주소 구조체의 크기로 초기화한 후 전달합니다.

 

- 만약 접속한 클라이언트가 존재하지 않을 경우 accept() 함수는 서버의 상태를 대기 상태(Wait state)로 만듭니다.

 

 

 

- TCP 서버에서 accept() 함수를 사용한 예시는 다음과 같습니다.

// accept() 함수의 리턴값을 저장할 소켓을 선언
SOCKET client_sock;

// accept() 함수의 두 번째 인자, accept() 함수가 리턴 시, 접속한 클라이언트의 IP 주소, 포트 번호가 저장된다.
SOCKADDR_IN clientaddr;

// accept() 함수의 세 번째 인자
int addrlen;

// 서버는 클라이언트의 요청을 처리해야 하므로 무한 루프를 설정한다.
while (1) {
    // accept()
    
    // accept() 함수의 세 번째 인자로 전달할 변수 addrlen을 소켓 주소 구조체 변수의 크기로 초기화한다.
    addrlen = sizeof(clientaddr);
    
    // accept() 함수를 호출하고 오류를 처리한다.
    // 이전에 사용한 소켓 함수와 달리 오류가 발생하면 err_display() 함수로 오류 내용을 출력하고 무한 루프를 탈출
    client_sock = accept(listen_sock, (SOCKADDR *)*clientaddr, &addrlen);
    if (client_sock == INVALID_SOCKET) {
        err_display("accept()");
        break;
    }
    
    
    // 접속한 클라이언트 정보 출력 : inet_ntoa, inet_ntohs() 함수 사용
    printf("\n[TCP 서버] 클라이언트 접속: IP 주소 = %s, 포트 번호 = %d\n", inet_ntoa(clientaddr.sin_addr, ntohs(clientaddr.sin_port));
    
    
    // 클라이언트와 데이터 통신
    while (1) {
        ...
    }
    
    
    // closesocket()
    closesocket(client_sock);
    printf("\n[TCP 서버] 클라이언트 종료: IP 주소 = %s, 포트 번호 = %d\n", inet_ntoa(clientaddr.sin_addr, ntohs(clientaddr.sin_port));
    }

 

 

 

 

 

 

2-3. TCP 클라이언트 함수

- 일반적으로 TCP 클라이언트 함수는 아래 그림과 같이 소켓 함수를 호출합니다.

TCP 클라이언트의 함수

- socket() 함수로 소켓을 생성하고 사용할 프로토콜을 결정합니다.

- connect() 함수로 서버에 접속하고, 이때 원격 IP 주소, 원격 포트 번호는 물론 지역 IP 주소, 지역 포트 번호까지 모두 결정됩니다.

- send(), recv() 등의 데이터 전송 함수를 통해 서버와 통신한 후 closesocket() 함수로 소켓을 닫습니다.

 

 

 

 

(2-3. a) connect() 함수

- TCP 프로토콜의 수준에서 서버와 논리적 연결을 설정합니다.

int connect(
    SOCKET s,     // 서버의 소켓 주소 
    const struct sockaddr *name,
    int namelen
)

(1) s

- 서버와 통신할 목적으로 만든 소켓입니다.

 

(2) name

- 소켓 주소 구조체를 서버 주소로 초기화하여 전달합니다.

 

(3) namelen

- 소켓 주소 구조체의 길이(바이트 단위)입니다.

 

 

 

 

 

 

2-4. TCP 데이터 전송 함수

(1) 전송 함수의 특징

- 크게 데이터를 보내는 함수와 받는 함수로 구분할 수 있습니다.

 

- 송신 버퍼(Send buffer) : 상대방 측으로 데이터를 보낼 때 상대방이 받을 데이터에 대한 내용이 저장됩니다.

(데이터를 전송하기 전에 임시로 저장해 두는 영역)

 

- 수신 버퍼(Receive buffer) : 상대방이 자신에게 보낸 데이터들이 저장되어 있습니다.

(수신받은 데이터를 응용 프로그램이 처리하기 전까지 저장해 두는 영역)

 

- 두 버퍼 모두 운영체제 레벨에서 관리되며 운영체제의 자원의 상황, 여러 가지 경우에 따라 버퍼의 동작이 조금씩 달라질 수 있습니다.

 

- TCP 프로토콜은 데이터의 경계를 구분하지 못한다는 특성상, 데이터 경계 구분을 위한 상호 약속이 필요하며 이를 응용 프로그램의 수준에서 처리될 수 있도록 해 주어야 합니다.

소켓 데이터 구조체

 

 

 

 

(2-4. a) send() 함수

- 응용 프로그램의 데이터를 운영체제의 송신 버퍼에 복사함으로써 데이터를 전송합니다.

- send() 함수의 리턴값은 실제로 송신한 데이터의 바이트 수입니다.

int send(
    SOCKET s,
    const char *buf,
    int len,
    int flags     // 동기, 비동기 방식 결정
);

(1) s

- 통신할 대상과 연결된 소켓입니다.

 

(2) buf

- 보낼 데이터를 담고 있는 응용 프로그램의 버퍼 주소입니다.

 

(3) len

- 보낼 데이터의 크기(바이트 단위)입니다.

 

(4) flags

- send() 함수의 동작을 변경하는 옵션입니다.

 

 

 

 

(2-4. b) recv() 함수

- 운영체제의 수신 버퍼에 도착한 데이터를 응용 프로그램 버퍼에 복사합니다.

 

- recv() 함수의 리턴값은 응용 프로그램의 버퍼로 읽어들인 데이터의 바이트 수입니다.

int recv( 
    SOCKET s,
    char *buf,
    int len,
    int flags     // 동기, 비동기 방식 결정
);

(1) s

- 통신할 대상과 연결된 소켓입니다.

 

(2) buf

- 받은 데이터를 저장할 응용 프로그램의 버퍼 주소입니다.

 

(3) len

- 운영체제의 수신 버퍼로부터 복사할 최대 데이터 크기입니다.

- 이 값은 buf가 가리키는 응용 프로그램의 버퍼보다 크지 않아야 합니다.

 

(4) flags

- recv() 함수의 동작을 변경하는 옵션입니다.

 

 

 

- recv() 함수는 다음과 같은 두 가지의 리턴값을 가집니다.

(a) 수신 버퍼에 데이터가 도달한 경우

- recv() 함수의 세 번째 인자인 len보다 크지 않은 범위에서 가능하면 많은 데이터를 응용 프로그램 버퍼에 복사한 후 실제 복사한 바이트 수를 리턴합니다.

 

(b) 접속이 정상 종료된 경우

- 상대 응용 프로그램이 closesocket() 함수를 호출해 접속을 종료하면 TCP 프로토콜 수준에서 접속 종료를 위한 패킷 교환 절차가 일어납니다. 이 경우 recv() 함수는 0을 리턴합니다.

 

 

 

 

 

 

(2-4. c) 사용자 정의 함수 recvn() 함수

- recv() 함수 사용 시, 세 번째 인자인 len으로 지정한 크기보다 적은 데이터가 응용 프로그램의 버퍼에 복사될 수 있습니다. 따라서 자신이 받을 데이터의 크기를 미리 알고 있다면 그만큼 받을 때까지 recv() 함수를 여러 번 호출해야 하므로 recvn() 함수를 정의해 편리하게 처리할 수 있습니다.

 

recvn() 함수의 동작 원리

 

- recvn() 함수의 사용 예제 코드입니다.

int recvn(SOCKET s, char *buf, int len, int flags)
{
    int received;
    char *ptr = buf;        // 버퍼의 시작 주소를 가리킨다. 데이터를 읽을 때마다 ptr 증가
    int left = len;         // 읽지 않은 데이터의 크기이며 데이터를 읽을 때마다 left 감소
    
    // 읽지 않은 데이터가 존재하면 계속 루프를 돌게 된다.
    while (left > 0) {       
    
        // recv() 함수를 호출하여 오류 발생 시 곧바로 리턴한다.
        received = recv(s, ptr, left, flags);
        if(received == SOCKET_ERROR)
            return SOCKET_ERROR;
        
        // recv() 함수의 리턴 값이 0이면 정상 종료, 상대가 데이터를 보내지 않은 것이므로 루프를 탈출한다.
        else if (received == 0)
            break;
            
        // recv() 함수가 성공했으므로 left, ptr 변수를 갱신
        left -= received;
        ptr += received;
        }
        
        // 읽은 바이트 수를 리턴한다.
        return (len - left);
        }

 

 

 

 

 

(2) 데이터 전송 함수의 예 - TCP 클라이언트

- TCP 클라이언트에서 send(), recv() 함수를 사용한 예제 코드는 아래와 같습니다.

// 데이터 통신에 사용될 변수 
char buf[BUFSIZE + 1];      // 보낼 데이터 또는 받은 데이터를 저장할 버퍼 선언
int len;      // 사용자가 입력한 문자열 데이터의 길이를 계산할 때 사용할 변수

// 서버와 데이터 통신
while (1) {
    // 데이터 입력
 
    // fgets() 함수를 이용해 사용자로부터 문자열을 입력받음.
    printf("\n[보낼 데이터] ");
    if(fgets(buf, BUFSIZE + 1, stdin) == NULL) 
        break;
        
        
    // '\n' 문자 제거
    
    // '\n' 문자를 제거한다. 데이터 출력 시 줄바꿈 여부 혹은 줄바꿈 방식을 서버가 결정하도록 하기 위함임.
    len = strlen(buf);
    if (buf[len -1] == '\n')
        buf[len -1] = '\0';
        
    // '\n' 문자를 제거한 후 문자열 길이가 0이면, 사용자가 Enter키를 입력한 경우이다.
    // 이 경우 루프를 탈출하고 closesocket() 함수를 호출해 접속을 종료한다.
    if (strlen(buf) == 0) 
        break;
        
        
    // 데이터 송신
    
    // send() 함수를 호출하고 오류를 처리, send() 함수의 리턴값은 strlen(buf) 값과 동일
    retval = send(sock, buf, strlen(buf), 0);
    if (retval == SOCKET_ERROR) {
        err_display("send()");
        break;
    }
    printf("[TCP 클라이언트 %d바이트를 보냈습니다.\n", retval);
    
    
    // 데이터 수신
    
    // recvn() 함수를 호출하고 오류를 처리한다.
    // 서버로부터 받을 데이터의 크기를 미리 알고 있으므로 사용자 정의 함수 recvn()를 사용
    retval = recvn(sock, buf, retval, 0);
    if (retval == SOCKET_ERROR) {
        err_display("recv()");
        break;
    }
    else if (retval == 0)
        break;
        
        
        
    // 받은 데이터 출력
    
    // 받은 데이터의 끝에 '\0'을 추가하여 출력
    buf[retval] = '\0';
    printf("[TCP 클라이언트 %d바이트를 받았습니다.\n", retval);
    printf("[받은 데이터] %s\n", buf);
    }

 

 

 

(3) 데이터 전송 함수의 예 - TCP 서버

- TCP 서버에서 send(), recv() 함수를 사용한 예제 코드는 아래와 같습니다.

// 수신받은 데이터를 저장할 응용 프로그램의 버퍼
char buf[BUFSIZE + 1];

while (1) {

    // 클라이언트와 데이터 통신
    
    // recv() 함수의 리턴값이 0(정상 종료) 또는 SOCKET_ERROR(오류 발생)가 될 때까지 루프를 반복하며 데이터를 수신한다.
    while (1) {
        // 데이터 수신
        
        // recv() 함수를 호출하고 오류를 처리한다.
        // 클라이언트로부터 받을 데이터 크기를 알 수 없으므로 recvn() 함수는 사용할 수 없음.
        retval = recv(client_sock, buf, BUFSIZE, 0);
        if (retval == SOCKET_ERROR) {
            err_display("recv()");
            break;
        }
        else if (retval == 0)
            break;
            
        
        // 수신받은 데이터 출력
        
        // 받은 데이터 끝에 '\0'을 추가하여 출력
        buf[retval] = '\0';
        printf("[TCP /%s:%d] %s\n", inet_ntoa(clientaddr.sin_addr, ntohs(clientaddr.sin_port), buf);
        
        
        // 데이터 송신
        
        // send() 함수를 호출하여 오류를 처리
        retval = send(client_sock, buf, retval, 0);
        if (retval == SOCKET_ERROR) {
            err_display("send()");
            break;
        }
    }  // 안쪽 while 루프의 종료 
} // 바깥쪽 while 루프의 종료

 

 

 

 

3. TCP 서버 - 클라이언트 Echo 동작 실습

- TCP 프로토콜로 서버와 클라이언트의 Echo 동작을 확인해 보겠습니다.

 

(1) 서버

- 클라이언트가 보낸 입력 데이터를 받아서 화면에 출력하고 해당 데이터를 클라이언트 측으로 전송

 

(2) 클라이언트

- 사용자가 입력한 데이터를 서버로 전송, 서버로부터 입력했던 데이터를 그대로 돌려받아 화면에 출력

 

 

3-1. TCP 서버 소스코드

// TCPServer.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>

#define SERVERPORT 9000
#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, (char *)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_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVERPORT);
    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) {
        // accept()
        addrlen = sizeof(clientaddr);
        client_sock = accept(listen_sock, (SOCKADDR *)&clientaddr, &addrlen);
        if (client_sock == INVALID_SOCKET) {
            err_display("accept()");
            break;
        }

        // 접속한 클라이언트 정보 출력
        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("send()");
                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;
}

 

 

 

 

- 컴파일 후 빌드하여 실행 시 클라이언트의 연결을 기다리고 있는 서버 콘솔 화면이 노출됩니다.

클라이언트의 연결을 기다리고 있는 서버

 

 

 

 

 

 

- 클라이언트에서 접속을 시도하여 서버 화면에서 아래와 같이 노출됩니다.

 

 

 

 

 

- 클라이언트에서 "Hello Client!"를 서버 측으로 보내면 서버 콘솔에서 클라이언트가 입력한 데이터가 노출되고 서버는 이 데이터를 클라이언트 측으로 송신합니다.

 

 

 

 

- 이후에 클라이언트에서 어떠한 데이터도 입력하지 않고 엔터를 누르는 경우 클라이언트의 접속을 종료하게 되는 코드에 따라 클라이언트의 접속이 종료되고, 서버도 클라이언트와의 연결이 종료되었다는 문구가 출력됩니다.

 

- 클라이언트는 종료되어도 서버는 계속 실행 중인 것을 알 수 있습니다.

 

 

 

 

 

3-2.  TCP 클라이언트 소스코드

// TCPClient.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#pragma comment(lib, "ws2_32")
#include <winsock2.h>
#include <stdlib.h>
#include <stdio.h>

#define SERVERIP   "127.0.0.1"
#define SERVERPORT 9000
#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, (char *)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 sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET) err_quit("socket()");

    // connect()
    SOCKADDR_IN serveraddr;
    ZeroMemory(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVERIP);
    serveraddr.sin_port = htons(SERVERPORT);
    retval = connect(sock, (SOCKADDR *)&serveraddr, sizeof(serveraddr));
    if (retval == SOCKET_ERROR) err_quit("connect()");

    // 데이터 통신에 사용할 변수
    char buf[BUFSIZE + 1];
    int len;

    // 서버와 데이터 통신
    while (1) {
        // 데이터 입력
        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", retval);

        // 데이터 받기
        retval = recvn(sock, buf, retval, 0);
        if (retval == SOCKET_ERROR) {
            err_display("recv()");
            break;
        }
        else if (retval == 0)
            break;

        // 받은 데이터 출력
        buf[retval] = '\0';
        printf("[TCP 클라이언트] %d바이트를 받았습니다.\n", retval);
        printf("[받은 데이터] %s\n", buf);
    }

    // closesocket()
    closesocket(sock);

    // 윈속 종료
    WSACleanup();
    return 0;
}

 

 

 

 

 

- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.

- 내용 중에서 오타 또는 잘못된 내용이 있을 시 지적해 주시기 바랍니다.

댓글