dayne의 블로그

[C]서버-클라이언트 Socket 통신 예제 (TCP) 본문

네트워크

[C]서버-클라이언트 Socket 통신 예제 (TCP)

dayne_ 2024. 10. 23. 17:38

목차

1. 소켓 통신 개요

2. 서버 코드

3. 클라이언트 코드

 

 


1. 소켓 통신 개요 (TCP)

출처 : https://recipes4dev.tistory.com/153

 

소켓이란 네트워크 상에서 데이터를 송수신하기 위한 양 끝단을 의미합니다.

각각의 소켓은 IP 주소와 포트 번호를 통해 고유하게 식별됩니다.

 

서버와 클라이언트의 소켓 통신은 네트워크 상에서 두 프로그램이 데이터를 주고받는 구조를 의미합니다. 

 

1.1 소켓 프로그래밍의 전체적인 과정 (TCP)

소켓 프로그래밍의 전체적인 흐름은 아래와 같습니다.

  1. 서버는 소켓을 생성하고, IP 주소와 포트 번호를 바인딩한 후 연결 요청을 대기합니다.
  2. 클라이언트는 서버와 '연결'을 시도합니다.
  3. 서버는 클라이언트의 '연결'을 수락하고, 양쪽은 데이터를 주고받습니다.
  4. 통신이 끝나면 각자 소켓을 닫아 연결을 종료합니다.

 

1.2 TCP 동작 과정

1. 서버 측에서 socket() 함수 및 bind() 함수를 호출하여, 클라이언트와 통신 시에 사용할 소켓을 생성하고, 특정 IP 주소와 포트 번호를 바인딩합니다. 이를 통해 서버 측에서 사용할 소켓이 커널에 등록됩니다.

 

2. 이후, 서버 측에서 listen() 함수를 호출하여, 클라이언트의 연결 요청을 받을 준비를 합니다.

서버는 연결 요청을 대기 중인 상태가 됩니다.

 

3. 클라이언트 측에서 connect() 함수를 호출합니다.

  • 이때, SYN 패킷이 서버로 전송됩니다.
  • SYN 패킷은 클라이언트가 서버와의 연결을 시작하고자 할 때 보내는 세그먼트입니다.

4. 이후, 서버 측에서 accept() 함수를 호출합니다.

  • 서버가 클라이언트의 SYN 패킷을 받으면, 서버는 클라이언트에게 SYN-ACK 패킷을 응답으로 보냅니다.
    • SYN-ACK 패킷은 서버가 클라이언트의 연결 요청을 받았음을 알리는 신호이며, 동시에 서버 측에서도 클라이언트와 연결을 설정하기 위한 SYN 패킷을 보냅니다.

5. 클라이언트 측에서 ACK 패킷 송신

  • 클라이언트가 서버로부터 SYN-ACK 패킷을 받으면, ACK 패킷을 서버로 보내 연결이 완료되었음을 알립니다.
  • 클라이언트가 ACK 패킷을 서버에 보내는 순간 3-way handshake가 완료되고, TCP 연결 설정이 완료됩니다.

 

 

흐름 제어

  • TCP는 수신 받은 데이터를 OS가 제공하는 버퍼에 순서대로 채웁니다. 버퍼에 있는 데이터들은 해당 포트에서 대기하고 있는 응용 프로그램들이 그것을 가져갈 때까지 버퍼에 유지됩니다.
  • 버퍼의 크기는 유한하며, 데이터가 가득 찬 경우에 TCP는 더 이상 데이터를 받을 수 없습니다.
  • 이러한 상황을 방지하고자, TCP는 수신 측의 버퍼에 공간이 있을 때만, 송신 측에서 데이터를 전송합니다. 이러한 절차와 행동 양식을 '흐름 제어'라고 합니다.

 

 

 

※ 소켓 프로그래밍 관련 함수 설명 (TCP)

더보기
  • ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    • 연결된 socket으로부터 데이터를 수신할 때 사용하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : connect, accept로 연결된 socket descriptor
      • 2번째 인자 ( buf ) : 수신된 데이터를 담을 char 배열 타입 데이터 버퍼의 포인터
      • 3번째 인자 ( len ) : 읽을 데이터의 길이
      • 4번째 인자 ( flags ): 읽을 데이터 유형 또는 읽는 방법에 대한 옵션으로, 0 기입하면 일반 데이터 수신
    • 반환 값
      • 0 이상 : 정상적으로 데이터 수신, 실제로 수신한 데이터의 길이를 반환
      • -1 : 오류가 발생하였으며, 상세한 오류 내용은 errno에 저장

 

  • ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    • 연결된 socket으로 상대 시스템에 데이터를 전송할 때 사용하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : connect, accept로 연결된 socket descriptor
      • 2번째 인자 ( buf ) : 전송할 데이터를 담을 char 배열 타입 데이터 버퍼의 포인터
      • 3번째 인자 ( len ) : 전송할 데이터의 길
      • 4번째 인자 ( flags ): 전송할 데이터 또는 읽는 방법에 대한 옵션으로, 0 기입하면 일반 데이터 전송
    • 반환 값
      • 0 이상 : 정상적으로 데이터를 전송했으며, 실제로 전송된 데이터의 길이를 반환
      • -1 : 오류가 발생했으며, 상세한 오류 내용은 errno에 저장

 

<서버 구현에 필요한 함수>

  • int socket(int domain, int type, int protocol)
    • 소켓 디스크립터를 생성하는 함수
    • 매개변수
      • 1번째 인자 ( domain ) : AF_UNIX, AF_INET 등 어떤 영역에서 통신할 것인지에 대한 영역을 지정하는 매개변수
      • 2번째 인자 ( type ) : SOCK_STREAM, SOCK_DGRAM 등 TCP / UDP / ... 중에서 어떤 소켓을 사용할 것인지 지정하는 매개변수
      • 3번째 인자 ( protocol ) : 소켓에서 사용할 프로토콜을 지정하는 것으로, IPPROTO_TCP, IPPROTO_UDP, 0 등을 사용, 0은 type에서 미리 정해진 경우 이를 따른다는 의미
    • 반환 값
      • 0 이상 : 생성된 소켓의 소켓 디스크립터 반환, 정상적으로 소켓 생성했음을 의미
      • -1 : 소켓 생성 실패를 의미

 

  • int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    • 'socket descriptor'와 'IP 주소/포트 번호'를 결합하는 함수 
    • 매개변수
      • 1번째 인자 ( sockfd ) : socket() 함수로부터 반환받은 소켓 디스크립터 기입
      • 2번째 인자 ( myaddr ) :서버의 IP 주소 기입
      • 3번째 인자 ( addrlen ) : 주소의 길이 기입
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미

 

  • int listen(int sockfd, int backlog);
    • 대기 상태(클라이언트의 접속 요청을 받을 수 있는 상태)로 만드는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : socket() 함수로부터 반환받은 소켓 디스크립터 기입
      • 2번째 인자 ( backlog ) : 연결 요청 소켓들이 대기하는 연결 대기열의 크기를 지정
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미

 

  • int accept(int listenfd, struct sockaddr *client_addr, socklen_t client_addrlen);
    • 소켓 디스크립터에 클라이언트를 연결하는 함수
    • 매개변수
      • 1번째 인자 ( listenfd ) : listen() 함수를 통해 대기 상태로 변환된 listenfd 기입
      • 2번째 인자 ( client_addr ) : 클라이언트의 주소 정보를 담고있는 구조체 기입
      • 3번째 인자 ( client_addrlen ) : 2번째 인자 값(클라이언트의 주소 정보 구조체)의 길이 기입
    • 반환 값
      • 0 이상 : 연결 식별자(connection descriptor / connfd)를 반환, 성공했음을 의미
      • -1 이상 : 실패했음을 의미

※ listen으로 듣기 상태가 된 '듣기 식별자(listenfd)'는 한 번 생성되면 서버가 동작하는 동안 계속 존재하지만, accept로 생성된 '연결 식별자(connfd)'는 서버-클라이언트 연결 시에만 생성되며 클라이언트에 서비스하는 동안만 존재함

 

 

<클라이언트 구현에 필요한 함수>

  • int socket(int domain, int type, int protocol)
    • 서버에서와 동일하게 동작

 

  • int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);
    • 서버에 연결을 요청하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : 클라이언트 측에서 socket() 함수로 생성된 socket descriptor 기입
      • 2번째 인자 ( serv_addr ) : 서버의 주소 정보 기입
      • 3번째 인자 ( addrlen ) : 서버의 주소 정보에 대한 길이 기입
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미

 

  • int close(int sofckfd);
    • 소켓을 닫고 통신을 종료하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : 종료할 socket descriptor 기입
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미

2. 서버 코드

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>

const int MAX_LINE = 2048;
const int PORT = 6001;
const int BACKLOG = 10;
const int LISTENQ = 6666;
const int MAX_CONNECT = 20;


// 클라이언트의 메시지를 수신하여 화면에 출력하는 역할을 하는 함수로, 쓰레드 내에서 실행됨
void *recv_message(void *fd)
{
    int sockfd = *(int *)fd;
    char buf[MAX_LINE];

    while(1)
    {
    	클라이언트가 송신한 내용을 buf에 저장
        memset(buf , 0 , MAX_LINE);
        int n;
        if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
        {
            perror("recv error.\n");
            exit(1);
        }
        buf[n] = '\0';

        // 클라이언트가 갑자기 접속을 끊은 경우 처리
        if (n==0)
        {
            printf("Client closed.\n");
            close(sockfd);
            exit(1);
        }

        // quit 문자열을 받으면 종료
        if(strcmp(buf , "exit") == 0)
        {
            printf("Client closed.\n");
            close(sockfd);
            exit(1);
        }

        // 클라이언트가 보낸 메시지를 화면에 출력
        printf("\nClient: %s\n", buf);
    }
}


int main()
{

    // (1)
    int listenfd , connfd;
       
    // (2)
    pthread_t recv_tid ;

    // (3)
    struct sockaddr_in servaddr , cliaddr;

   
    // (4) 소켓 생성
    if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    {
        perror("socket error.\n");
        exit(1);
    }


    // (5) sockaddr_in 구조체 설정
    memset(&servaddr, 0x00, sizeof(servaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);


    // bind 오류 해결. 사용했던 ip라고 뜨는 에러.
    int val = 1;   
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *) &val, sizeof val) < 0) {
        perror("setsockopt");
        close(listenfd);
        return -1;
    }

    // (6) 소켓 생성
    if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
    {
        perror("bind error.\n");
        exit(1);
    }


    // (7)
    if(listen(listenfd , LISTENQ) < 0)
    {
        perror("listen error.\n");
        exit(1);
    }


    // (8) 클라이언트 연결 요청 수락
    socklen_t clilen = sizeof(cliaddr);
    if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
    {
        perror("accept error.\n");
        exit(1);
    }

    // (9) 서버에 접속한 클라이언트의 주소 출력
    printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr));


    // (10) 메시지 수신하는 스레드 생성
    if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1)
    {
        perror("pthread create error.\n");
        exit(1);
    }

    // (11) 서버가 입력한 메시지 전송
    char msg[MAX_LINE];
    memset(msg , 0 , MAX_LINE);
    while(fgets(msg , MAX_LINE , stdin) != NULL)
    {
        if(strcmp(msg , "exit\n") == 0)
        {
            close(connfd);
            exit(0);
        }

        if(send(connfd , msg , strlen(msg) , 0) == -1)
        {
            perror("send error.\n");
            exit(1);
        }
    }
}

 

 

※ 헤더 설명

더보기
  • #include <stdio.h>
    • 표준 입출력 함수들(printf, scanf, gets, fopen, fclose, ...)을 제공하는 헤더 
  • #include <stdlib.h>
    • 표준 라이브러리 함수들(malloc, free, ...)을 제공하는 헤더
  • #include <string.h>
    • 문자열 처리 및 메모리 조작에 필요한 함수들(strcpy, strcmp, memset, ...)을 제공하는 헤더 
  • #include <sys/socket.h>
    • 소켓 프로그래밍을 위한 함수들(socket(소켓 생성), bind(소켓 바인딩), accept(연결 수락), send/recv(데이터 송수신), ...)과 상수들이 정의된 헤더
  • #include <sys/types.h>
    • 시스템 호출에 사용되는 데이터 타입들(pid_t, size_t, ssize_t, tid_t, ...)이 정의된 헤더
  • #include <unistd.h>
    • 유닉스 계열 시스템에서 제공하는 운영체제 서비스에 접근하는 함수들(fork, exec, getpid, read, write, ...)을 제공하는 헤더
  • #include <errno.h>
    • 시스템 호출이나 라이브러리 함수가 실패할 경우, 전역 변수 errno에 해당 에러 번호 설정되고, 해당 값에 따라 어떠한 에러인지 판단할 수 있도록 해주는 헤더
  • #include <netinet/in.h>
    • 인터넷 프로토콜(IP) 주소 체계와 관련된 구조체와 상수들을 제공하는 헤더
    • sockaddr_in 구조체 (IPv4 주소를 표현), sin_port (포트 번호), sin_addr (IP 주소), ...
    • 네트워크와 호스트 간 바이트 순서 변환 함수(htonl, ntohl) 등도 제공
  • #include <netdb.h>
    • DNS 조회와 호스트 이름 관련 함수들(gethostbyname, getaddrinfo, ...)을 제공하는 헤더
    • 도메인 이름을 IP 주소로 변환하거나, 네트워크 서비스를 조회 가능
  • #include <arpa/inet.h>
    • IP 주소 변환과 관련된 함수를 제공하는 헤더
    • 문자열로 표현된 IP 주소를 네트워크 바이트 순서로 변환하는 함수(inet_aton, inet_addr) 와 그 반대 방향의 변환(inet_ntoa) 등을 지원
  • #include <pthread.h>
    • 쓰레드를 관리하는 함수들과 관련된 헤더
    • 쓰레드 생성(pthread_create), 쓰레드 종료(pthread_exit), 쓰레드 동기화(pthread_mutex_lock) 등의 함수를 제공

2.1 recv_message() 함수 설명

  • memset(buf,0, MAX_LINE)
    • 메시지 수신 이전에, 메시지 내용을 담을 버퍼를 비웁니다.
  • recv() 함수를 사용해서, 연결된 상태인 클라이언트 소켓의 데이터를 읽어옵니다. 
  • n==0일 경우, 클라이언트가 접속 끊었다는 것이므로, sockfd를 닫고 쓰레드를 종료합니다.
  • buf(수신한 데이터)의 내용이 "quit"인 경우, 소켓 sockfd를 닫고 쓰레드를 종료합니다.
  • 클라이언트가 보낸 메시지를 화면에 출력합니다.

 

2.2 main() 함수 설명

// (1)
int listenfd, connfd

 

listenfd 는 서버가 클라이언트의 연결 요청을 기다리는 상태일 때 사용되는 소켓의 file descriptor가 저장되는 변수입니다. socket() 함수를 호출하여 할당받은 값을 저장하며, 해당 소켓은 클라이언트가 서버에 연결될 때 사용됩니다.

 

connfd 는 서버가 클라이언트의 연결 요청을 수락(accept)한 후, 생성된 소켓의 file descriptor가 저장되는 변수입니다.

클라이언트와 데이터를 주고받을 때, 해당 소켓을 사용합니다.

 

// (2)
pthread_t recv_tid;

 

recv_tid는 소켓으로부터, 메시지를 수신하여 화면에 출력하는 쓰레드의 식별자를 저장할 변수입니다.

 

// (3)
struct sockaddr_in servaddr, cliaddr;

 

struct sockaddr_in은 소켓 프로그래밍에서 인터넷 주소를 정의할 때 사용하는 구조체입니다.

sockaddr_in 구조체의 인스턴스로, 각각 서버의 IP 주소와 포트번호, 클라이언트의 주소를 저장하는 데 사용됩니다.

 

// (4) 소켓 생성
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
	perror("socket error.\n");
    exit(1);
}

 

서버가 클라이언트의 연결을 받기 위해 소켓을 생성하는 코드입니다.

 

socket 함수를 호출하여 IPv4 인터넷 프로토콜을 사용하는 스트림 소켓을 생성합니다.

소켓이 정상적으로 생성되지 않을 경우 -1이 반환되므로, 이러한 경우에 대해 에러 처리를 진행합니다.

 

// (5) sockaddr_in 구조체 설정
memset(&servaddr, 0x00, sizeof(servaddr));

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);

 

sockaddr_in 구조체를 설정합니다.

이 코드를 통해 소켓 프로그래밍에서 서버의 인터넷 주소를 설정할 수 있습니다.

 

1.memset(&servaddr, 0x00, sizeof(servaddr));

  • memset() 함수를 통해 `servaddr` 구조체를 0으로 초기화합니다.
  • 이는 구조체 내의 모든 필드를 안전하게 0으로 설정하여 깨끗한 상태로 시작할 수 있게 해 줍니다.

2. servaddr.sin_family = AF_INET;

  • `sin_family` 필드는 주소 체계(address family)를 설정합니다.
  • `AF_INET`은 IPv4 인터넷 프로토콜을 사용하겠다는 것을 의미합니다.

3. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  • `sin_addr` 필드는 인터넷 주소를 저장합니다.
  • `s_addr`는 실제 IP 주소를 나타냅니다.
  • `htonl(INADDR_ANY)`는 "호스트 바이트 순서"에서 "네트워크 바이트 순서"로 긴 정수를 변환하는 함수입니다.
  • `INADDR_ANY`는 서버가 모든 네트워크 인터페이스를 통해 들어오는 연결을 수락하겠다는 것을 의미합니다. 즉, 서버는 특정 IP 주소에 바인딩되지 않고, 모든 인터페이스의 주소로 들어오는 클라이언트의 연결을 수락할 준비가 됩니다.

4. servaddr.sin_port = htons(PORT);

  • `sin_port` 필드는 포트 번호를 저장합니다.
  • `htons()`는 호스트 바이트 순서에서 네트워크 바이트 순서로 짧은 정수를 변환합니다.
  • `PORT`는 서버가 연결을 수락할 포트 번호입니다.

이렇게 설정된 servaddr 구조체는, 이후 bind() 함수 호출 시에 사용되어, 서버 소켓이 해당 주소와 포트에 바인딩되도록 합니다.

 

// (6)
if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
	perror("bind error.\n");
	exit(1);
}

 

외부에서 서버로의 네트워크 연결을 받아들일 준비를 합니다.

 

서버가 자신의 주소 구조체인 servaddr를 사용하여 소켓(listenfd)을 시스템의 포트에 연결합니다.

bind() 함수는 소켓에 IP 주소와 포트 번호를 할당합니다.

이 경우, servaddr 구조체는 서버가 INADDR_ANY를 통해 모든 인터페이스에서 들어오는 연결을 받아들일 수 있도록 설정됩니다. 이는 서버가 자신의 IP 주소를 명시하지 않고, 어떤 인터페이스의 IP 주소를 통해서든 연결을 받아들이겠다는 것을 의미합니다.

 

bind() 함수의 호출 결과가 0 미만일 경우, 작업을 실패했다는 의미이므로 이에 대한 에러 처리를 진행합니다.

 

// (7)
if(listen(listenfd , LISTENQ) < 0)
{
	perror("listen error.\n");
	exit(1);
}

 

서버가 클라이언트로부터 들어오는 연결 요청을 수신하기 시작하도록 지시합니다.

 

`listenfd`는 이전에 `socket()` 함수를 통해 생성되고, `bind()` 함수를 통해 특정 포트에 바인딩된 소켓의 파일 기술자입니다. 이 소켓은 연결 요청을 받기 위한 '리스닝 소켓'으로 사용됩니다.

 

`LISTENQ`는 이 소켓에서 동시에 대기할 수 있는 최대 대기열(큐)의 크기를 정의합니다. 즉, 한 번에 수락되지 않고 대기할 수 있는 최대 연결 요청 수입니다.

 

listen() 함수의 반환 값이 0 미만일 경우, 작업을 실패했다는 의미이기 때문에 이에 대한 에러 처리를 진행합니다.

 

// (8) 클라이언트 연결 요청 수락
socklen_t clilen;
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
{
	perror("accept error.\n");
	exit(1);
}

 

서버가 클라이언트의 연결 요청을 수락하는 코드입니다.

 

clilen 변수에 클라이언트 주소 구조체 cliaddr의 길이를 저장합니다.

 

이후, accept() 함수를 호출하여 서버의 리스닝 소켓 listenfd으로 들어오는 클라이언트의 연결 요청을 기다립니다. 클라이언트와 서버 간에 연결이 되면, 새로운 소켓 기술자 connfd를 반환하고, 이후 connfd를 사용하여 클라이언트와 데이터를 주고받습니다.

 

accept() 함수의 반환 값이 0 미만일 경우, 작업에 실패했음을 의미하기 때문에 이에 대한 에러 처리를 진행합니다.

 

// (9) 서버에 접속한 클라이언트의 주소 출력
printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr));

 

cliaddr.sin_addr는 클라이언트의 주소 구조체인 sockaddr_in의 필드로, 클라이언트의 주소를 저장하고 있습니다.

클라이언트의 주소는 네트워크 바이트 순서로 되어 있는 IP 주소로 저장되어 있습니다.

 

inet_ntoa 함수를 사용해, 사람이 읽을 수 있는 점으로 구분된 IPv4 주소 형태의 문자열로 변환합니다. 

 

// (10) 메시지 수신하는 스레드 생성
if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1)
{
	perror("pthread create error.\n");
    exit(1);
}

 

서버가 클라이언트의 연결 요청을 수락한 후, 새로 생성된 소켓의 file descriptor인 connfd를 매개변수로 가지는 recv_message 함수를 실행하는 쓰레드를 생성합니다.

 

pthread_create() 함수의 반환 값이 -1인 경우, 쓰레드 생성이 실패했음을 의미하기 때문에 이에 대한 에러 처리를 진행합니다.

 

// (11) 서버가 입력한 메시지 전송
char msg[MAX_LINE];
memset(msg , 0 , MAX_LINE);

// fgets 함수를 사용하여 입력된 문자열 msg가 “exit\n” 인 경우 소켓을 닫고 프로그램을 종료합니다.  
// 엔터때문에 문자열 끝에 ‘\n’이 추가되어 있습니다. 
while(fgets(msg , MAX_LINE , stdin) != NULL)
{
	if(strcmp(msg , "exit\n") == 0)
    {
        close(connfd);
        exit(0);
    }

    if(send(connfd , msg , strlen(msg) , 0) == -1)
    {
    	perror("send error.\n");
        exit(1);
    }
}

 

서버 프로그램에서 키보드로 입력한 메시지를 전송하는 코드입니다.

 

fgets() 함수는 입력 스트림에서 한 줄의 문자열을 읽습니다.

개행 문자를 만나거나 버퍼 크기만큼의 문자를 읽었을 때, 또는 스트림의 끝(EOF)에 도달하였을 때 읽기를 중단합니다.

입력 스트림은 표준 입력(stdin)으로 설정합니다.

 

표준 입력(키보드)으로 입력한 내용이 exit일 경우, 소켓 디스크립터를 닫고, 프로그램을 종료합니다.

 

send() 함수를 통해 연결된 상태의 클라이언트에게 데이터를 전송합니다.

함수의 반환 값이 -1인 경우, 작업을 실패했다는 의미이기 때문에, 이에 대한 에러 처리를 진행합니다.

 

 

 


3. 클라이언트 코드

서버 측에서 구현한 프로그램과 차이점을 위주로 설명하겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>

const int MAX_LINE = 2048;
const int PORT = 6001;


// 서버의 메시지를 수신하여 화면에 출력하는 역할을 하는 함수로, 쓰레드 내에서 실행
void *recv_message(void *fd)
{
    int sockfd = *(int *)fd;
    char buf[MAX_LINE];

    while(1)
    {
        memset(buf , 0 , MAX_LINE);
        int n;
        if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
        {
            perror("recv error.\n");
            exit(1);
        }
        
        buf[n] = '\0';

        if (n == 0)
        {
            printf("Server is closed.\n");
            close(sockfd);
            exit(0);
        }
       
        if(strcmp(buf , "exit") == 0)
        {
            printf("Server is closed.\n");
            close(sockfd);
            exit(0);
        }


        printf("\nServer: %s\n", buf);
    }
}


int main(int argc , char **argv)
{

    // (1)
    int sockfd;

    // (2)
    pthread_t recv_tid;

    // (3)
    struct sockaddr_in servaddr;

    // (4)
    if(argc != 2)
    {
        perror("usage:tcpcli <IPaddress>");
        exit(1);
    }

    // (5)
    if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    {
        perror("socket error");
        exit(1);
    }

    // (6)
    memset(&servaddr, 0x00, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);

    // (7)
    if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0)
    {
        printf("inet_pton error for %s\n",argv[1]);
        exit(1);
    }

    // (8)
    if( connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
    {
        perror("connect error");
        exit(1);
    }

    // (9) 메시지 수신하는 스레드 생성
    if(pthread_create(&recv_tid , NULL , recv_message, &sockfd) == -1)
    {
        perror("pthread create error.\n");
        exit(1);
    }

    // (10) 클라이언트가 입력한 메시지 전송
    char msg[MAX_LINE];
    memset(msg , 0 , MAX_LINE);

    while(fgets(msg , MAX_LINE , stdin) != NULL)
    {
        if(strcmp(msg , "exit\n") == 0)
        {
            close(sockfd);
            exit(0);
        }

        if(send(sockfd , msg , strlen(msg) , 0) == -1)
        {
            perror("send error.\n");
            exit(1);
        }
   
    }
}

 

3.1 recv_message() 함수

서버에서 구현한 함수와 동일하게 동작합니다.

 

3.2 main() 함수 설명

int main(int argc, char **argv) : 실행 파일로 실행 시에, 인자로 서버 프로그램이 실행된 PC의 IP 주소를 인자 받습니다.

 

// (1)
int sockfd;

 

sockfd는 서버에 연결하기 위한 소켓의 파일 디스크립터를 저장할 변수입니다.

 

// (3)
struct sockaddr_in servaddr;

 

servaddr는 서버의 인터넷 주소(IPv4) 정보를 저장하는 구조체입니다.

 

클라이언트는 구조체 servaddr에 저장된 정보를 사용하여 connect() 함수를 통해 서버에 연결을 시도하고, 서버는 bind() 함수를 통해 특정 포트에 소켓을 연결하는 데에 사용합니다.

 

// (4)
if(argc != 2)
{
    perror("usage:tcpcli <IPaddress>");
    exit(1);
}

 

프로그램 실행 시, 연결할 서버의 IP 주소를 인자로 주어야 합니다.

포트 번호는 전역변수 PORT에 저장한 값인 6001로 고정되어 있습니다.

 

// (6)
memset(&servaddr, 0x00, sizeof(servaddr));

servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);

 

memset() 함수를 사용해, servaddr 구조체의 모든 바이트를 0으로 초기화합니다.

 

// (7)
if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0)
{
    printf("inet_pton error for %s\n",argv[1]);
    exit(1);
}

 

inet_pton() 함수를 사용해, 문자열로 된 IP 주소를 네트워크 바이트 순서로 변환합니다.

 

inet_pton() 함수를 사용하여, 실행 시에 인자로 전달된 문자열 형식의 IP 주소 (argv[1]) 를 네트워크 바이트 순서의 이진 숫자 형식으로 변환하고, 결과를 servaddr.sin_addr에 저장합니다.

 

// (8)
if( connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
    perror("connect error");
    exit(1);
}

 

connect() 함수를 사용해, 지정된 소켓(sockfd)을 서버의 주소(servaddr)와 연결하는 작업을 수행합니다.

 

connect() 함수의 반환 값이 -1인 경우, 연결 작업이 실패했음을 의미하기 때문에 이에 대한 에러 처리를 진행합니다.

 

// (9) 메시지 수신하는 스레드 생성
if(pthread_create(&recv_tid , NULL , recv_message, &sockfd) == -1)
{
    perror("pthread create error.\n");
    exit(1);
}

 

매개변수로 sockfd를 가지는 recv_message() 함수를 실행할 쓰레드를 새로 생성하여, 서버로부터 수신한 메시지를 화면에 출력합니다.

 

 


참고

https://webnautes.tistory.com/1498

 

pthread 사용한 C언어 TCP Socket 서버/클라이언트 예제

pthread를 사용한 C언어 TCP 소켓 서버/클라이언트 예제입니다. 2021. 7. 4 최초작성 2023. 11. 18 코드 설명 추가 원본 코드 출처는 아래 깃허브 저장소입니다. https://github.com/shineyr/Socket 다음처럼 코드를

webnautes.tistory.com

 

https://dongwooklee96.github.io/post/2021/08/04/tcp-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EB%B0%8F-%ED%8A%B9%EC%A7%95.html

 

TCP 프로토콜 및 특징 | 개발자 이동욱

TCP 소켓 전송 계층 프로토콜의 핵심 기능은 호스트 안에서의 프로세스 식별을 통한 데이터의 배달이라고 볼 수 있다. 네트워크를 통해 배달된 데이터가 어떤 프로세스에게 배달된 것인지를 확

dongwooklee96.github.io

 

 

https://www.it-note.kr/123

 

recv(2) - 데이터를 수신하는 함수

recv(2) #include #include ssize_t recv(int sockfd, void *buf, size_t len, int flags); connect(2) 또는 accept(2) 등으로 연결된 socket으로부터 데이터를 수신합니다. 일반적인 데이터를 읽을 때에는 read(2) 함수를 사용할

www.it-note.kr

 

https://velog.io/@gojaegaebal/210124-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%8048%EC%9D%BC%EC%B0%A8-%EC%BB%B4%ED%93%A8%ED%84%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C-11%EC%9E%A5-%EC%9B%B9%EC%84%9C%EB%B2%84-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B83-%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%84%9C%EB%B2%84-%EB%B0%8F-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EB%A5%BC-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%8D%B0-%ED%95%84%EC%9A%94%ED%95%9C-%ED%95%A8%EC%88%98

 

210124 개발일지(48일차) - 컴퓨터 시스템 11장 웹서버 프로젝트(3) : 소켓 프로그래밍 (서버 및 클라

지난 번 포스팅에서 소켓 인터페이스에 대해 간략히 알아봤다.이번에는 서버와 클라이언트를 만들 때 필요한 함수들을 자세히 뜯어보며 공부해보자.소켓 디스크립터는 소켓의 파일 디스크립터

velog.io

'네트워크' 카테고리의 다른 글

[C]서버-클라이언트 Socket 통신 예제 (UDP)  (0) 2024.10.25
OSI 7계층 와 TCP/IP 계층  (0) 2024.10.22