Network/소켓통신(Linux)

TCP 서버 - 클라이언트 구조

busy맨 2023. 9. 2. 13:59

1. TCP 서버 - 클라이언트 개념

▷핵심 동작 과정

TCP 서버-클라이언트 핵심 동작

  1.  서버는 먼저 실행하여 클라이언트가 접속하기를 기다린다(listen).
  2. 클라이언트는 서버에 접속(connect)하여 데이터를 보낸다(send).
  3. 서버는 클라이언트 접속을 수용하고(accept), 클라이언트가 보낸 데이터를 받아서 처리(recv)한다.
  4. 서버는 처리한 데이터를 클라이언트에 보낸다(send).
  5. 클라이언트는 서버가 보낸 데이터를 받아서 처리한다(recv).
  6. 데이터를 주고받는 과정을 모두 마치면 접속을 끊는다(close).
    • 윈도우에서는 closesocket, 리눅스에서는 close를 사용

TCP 서버 클라이언트의 대표적인 예는, 웹 서버와 웹 브라우저의 동작 과정이다.

 

▷TCP 서버 - 클라이언트 동작 원리

  1. 서버는 소켓을 생성한 후 클라이언트 접속을 기다린다.
    • 서버가 사용하는 소켓은 특정 포트 번호와 결합되어 있어 이 포트 번호로 접속하는 클라이언트만 수용 가능
    • ex) 9000
  2. 클라이언트가 서버에 접속
    • TCP 프로토콜 수준에서 연결 설정을 위한 패킷 교환이 일어남
    • SYN, SYN/ACK, ACK를 주고 받는다.
  3. 서버는 접속한 클라이언트와 통신할 수 있는 새로운 소켓을 생성
    • 이 소켓을 이용하여 서버와 클라이언트는 데이터를 주고 받음
    • 기존에 만들었던 소켓은 새로운 클라이언트의 접속을 수용하는데 사용
  4. 새 클라이언트가 서버에 접속
    • 서버에는 총 3개의 소켓이 존재하며, 두 개의 소켓은 각각의 클라이언트와 통신하는 용도로 사용

 

하나의 TCP 서버와 여러 TCP 클라이언트의 통신

  • 서버 측 소켓과 클라이언트 측 소겟이 일대일로 대응
  • 하나의 클라이언트가 둘 이상의 소켓을 사용하여 서버에 접속 가능

 

▷TCP 서버 - 클라이언트 구현

◆IPv4 기반 TCP 서버 -  클라이언트

  • 서버
    • 클라이언트가 보낸 데이터를 받아 이를 문자열로 간주하여 화면에 출력
    • 그런 다음 데이터 변경 없이 다시 클라이언트에 전송
    • 받은 데이터를 그대로 다시 보낸다는 뜻으로 에코 서버(Echo Server)라고 부름
  • 클라이언트
    • 사용자가 키보드로 입력한 문자열을 서버에 전송
    • 서버가 회신하면 클라이언트는 이를 화면에 출력
    • 에코 서버와 통신한다는 의미로 에코 클라이언트(Echo Client)라고 부름

 

서버 Code)

#include "../Common.h"

#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char *argv[])
{
        int retval;

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

        // bind()
        struct sockaddr_in serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
        serveraddr.sin_port = htons(SERVERPORT);
        retval = bind(listen_sock, (struct 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;
        struct sockaddr_in clientaddr;
        socklen_t addrlen;
        char buf[BUFSIZE + 1];

        while (1) {
                // accept()
                addrlen = sizeof(clientaddr);
                client_sock = accept(listen_sock, (struct sockaddr *)&clientaddr, &addrlen);
                if (client_sock == INVALID_SOCKET) {
                        err_display("accept()");
                        break;
                }

                // 접속한 클라이언트 정보 출력
                char addr[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
                printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
                        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", addr, ntohs(clientaddr.sin_port), buf);

                        // 데이터 보내기
                        retval = send(client_sock, buf, retval, 0);
                        if (retval == SOCKET_ERROR) {
                                err_display("send()");
                                break;
                        }
                }

                // 소켓 닫기
                close(client_sock);
                printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
                        addr, ntohs(clientaddr.sin_port));
        }

        // 소켓 닫기
        close(listen_sock);
        return 0;
}

클라이언트 Code)

#include "../Common.h"

char *SERVERIP = (char *)"127.0.0.1";
#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char *argv[])
{
        int retval;

        // 명령행 인수가 있으면 IP 주소로 사용
        if (argc > 1) SERVERIP = argv[1];

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

        // connect()
        struct sockaddr_in serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET;
        inet_pton(AF_INET, SERVERIP, &serveraddr.sin_addr);
        serveraddr.sin_port = htons(SERVERPORT);
        retval = connect(sock, (struct 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 = (int)strlen(buf);
                if (buf[len - 1] == '\n')
                        buf[len - 1] = '\0';
                if (strlen(buf) == 0)
                        break;
                // 데이터 보내기
                retval = send(sock, buf, (int)strlen(buf), 0);
                if (retval == SOCKET_ERROR) {
                        err_display("send()");
                        break;
                }
                printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);

                // 데이터 받기
                retval = recv(sock, buf, retval, MSG_WAITALL);
                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);
        }

        // 소켓 닫기
        close(sock);
        return 0;

Result)

 

2. TCP 서버 - 클라이언트 분석

응용 프로그램이 통신하려면 다음과 같은 요소가 필요

 

  • 프로토콜(protocol)
    • 통신 규약으로, 소켓을 생성할 때 결정
  • 지역(local) IP 주소와 지역 포트 번호
    • 서버 또는 클라이언트 자신의 주소
  • 원격(remote) IP 주소와 원격 포트 번호
    • 서버 또는 클라이언트가 통신하는 상대의 주소

 

1) TCP 서버 함수

TCP 서버 핵심 함수

  1. socket() 함수로 소켓을 생성함으로써 사용할 프로토콜 결정
  2. bind() 함수로 소켓의 지역 IP 주소와 지역 포트 번호를 결정
  3. listen() 함수로 소켓의 TCP 상태를 LISTENING으로 변경
  4. accept() 함수로 클라이언트 접속을 수용하고, 접속한 클라이언트와 통신할 수 있는 새로운 소켓 생성
    이때 원격 IP 주소와 원격 포트번호가 결정됨
  5. send() 함수와 recv() 함수 등의 데이터 전송 함수로 클라이언트와 통신을 수행한 후 close() 함수로 소켓을 닫음

 

2) TCP 클라이언트 함수

TCP 클라이언트 핵심 함수

  1. socket() 함수로 소켓을 생성함으로써 사용할 프로토콜 결정
  2. connect() 함수로 서버에 접속. 모든 IP 주소와 포트 번호가 결정됨
  3. send() 함수와 recv() 함수 등의 데이터 전송 함수로 클라이언트와 통신을 수행한 후 close() 함수로 소켓을 닫음

함수의 자세한 구조는 추후 설명 예정

 

3. TCP 서버 - 클라이언트(IPv6)

  • IPv4와 달라지는 것
    • 소켓 생성 시 AF_INET 대신 AF_INET6를 사용
    • 소켓 주소 구조체로 sockaddr_in 대신 sockaddr_in6를 사용
  • 이렇게 변경하고 나면 IPv6로 동작하는 서버와 클라이언트만 통신 가능하다.

 

서버 Code)

include "../Common.h"

#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char *argv[])
{
        int retval;

        // 소켓 생성
        SOCKET listen_sock = socket(AF_INET6, SOCK_STREAM, 0);
        if (listen_sock == INVALID_SOCKET) err_quit("socket()");

        // bind()
        struct sockaddr_in6 serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin6_family = AF_INET6;
        serveraddr.sin6_addr = in6addr_any;
        serveraddr.sin6_port = htons(SERVERPORT);
        retval = bind(listen_sock, (struct 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;
        struct sockaddr_in6 clientaddr;
        socklen_t addrlen;
        char buf[BUFSIZE + 1];

        while (1) {
                // accept()
                addrlen = sizeof(clientaddr);
                client_sock = accept(listen_sock, (struct sockaddr *)&clientaddr, &addrlen);
                if (client_sock == INVALID_SOCKET) {
                        err_display("accept()");
                        break;
                }

                // 접속한 클라이언트 정보 출력
                char addr[INET6_ADDRSTRLEN];
                inet_ntop(AF_INET6, &clientaddr.sin6_addr, addr, sizeof(addr));
                printf("\n[TCP 서버] 클라이언트 접속: IP 주소=%s, 포트 번호=%d\n",
                        addr, ntohs(clientaddr.sin6_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", addr, ntohs(clientaddr.sin6_port), buf);

                        // 데이터 보내기
                        retval = send(client_sock, buf, retval, 0);
                        if (retval == SOCKET_ERROR) {
                                err_display("send()");
                                break;
                        }
                }

                // 소켓 닫기
                close(client_sock);
        printf("[TCP 서버] 클라이언트 종료: IP 주소=%s, 포트 번호=%d\n",
            addr, ntohs(clientaddr.sin6_port));
        }

        // 소켓 닫기
        close(listen_sock);
        return 0;
}

클라이언트 Code)

#include "../Common.h"

char *SERVERIP = (char *)"::1";
#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char *argv[])
{
        int retval;

        // 명령행 인수가 있으면 IP 주소로 사용
        if (argc > 1) SERVERIP = argv[1];

        // 소켓 생성
        SOCKET sock = socket(AF_INET6, SOCK_STREAM, 0);
        if (sock == INVALID_SOCKET) err_quit("socket()");

        // connect()
        struct sockaddr_in6 serveraddr;
        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin6_family = AF_INET6;
        inet_pton(AF_INET6, SERVERIP, &serveraddr.sin6_addr);
        serveraddr.sin6_port = htons(SERVERPORT);
        retval = connect(sock, (struct 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 = (int)strlen(buf);
                if (buf[len - 1] == '\n')
                        buf[len - 1] = '\0';
                if (strlen(buf) == 0)
                        break;
                // 데이터 보내기
                retval = send(sock, buf, (int)strlen(buf), 0);
                if (retval == SOCKET_ERROR) {
                        err_display("send()");
                        break;
                }
                printf("[TCP 클라이언트] %d바이트를 보냈습니다.\n", retval);

                // 데이터 받기
                retval = recv(sock, buf, retval, MSG_WAITALL);
                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);
        }

        // 소켓 닫기
        close(sock);
        return 0;

 

Result)

 

※서버와 클라이언트 연결정보 확인하기

  • IPv4
    • netstat -a -n --tcp | grep 9000
  • IPv6
    • netstat -a -n -p tcpv6 | grep 9000