반응형

pthread를 사용한 C언어 TCP 소켓 서버/클라이언트 예제입니다.



2021. 7.    4     최초작성

2023. 11. 18    코드 설명 추가




원본 코드 출처는 아래 깃허브 저장소입니다.  

https://github.com/shineyr/Socket




다음처럼 코드를 컴파일합니다.

 

$ gcc -o server server.c -lpthread

$ gcc -o client client.c -lpthread




터미널을 두개 사용하여 다음처럼 코드를 실행합니다.

 

1. 서버 프로그램 실행

     ./server

 

2. 클라이언트 프로그램 실행 - 같은 피시가 아니면 127.0.0.1 대신에 서버 프로그램이 실행된 PC의 IP를 사용합니다. 

    ./client 127.0.0.1



클라이언트 프로그램에서 입력한 문자열이 서버 프로그램으로 전송되고 서버 프로그램에서 입력한 문자열이 클라이언트 프로그램으로 전송됩니다.



서버 프로그램 또는 클라이언트 프로그램에서 exit를 입력하면 서버 프로그램 또는 클라이언트 프로그램이 종료됩니다.



server.c

#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)
    {
        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 구조체 설정
    bzero(&servaddr , 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);
        }
    }
}





코드 설명

// (1)
int listenfd , connfd;

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

 

connfd 변수는 서버가 클라이언트의 연결 요청을 수락(accept)한 후, 생성된 소켓의 파일 기술자를 저장합니다. 클라이언트와 데이터를 주고받을 때 이 소켓을 사용합니다.




      // (2)
pthread_t recv_tid;

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



// (3)
struct sockaddr_in servaddr , cliaddr;

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

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



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

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

 

socket() 함수를 호출하여 IPv4 인터넷 프로토콜을 사용하는 스트림 소켓을 생성합니다. 이 함수는 성공하면 소켓 파일 기술자를, 실패하면 -1을 반환합니다.

 

코드의 if 조건문은 socket() 함수가 -1보다 작은 값을 반환하는지 확인하여 소켓 생성이 실패했는지를 검사합니다. 소켓 생성에 실패하면, perror() 함수를 호출하여 "socket error"라는 메시지와 함께 오류를 출력합니다. 그리고 exit(1)을 호출하여 프로그램을 비정상적으로 종료시킵니다. exit(1)의 1은 오류가 발생했음을 나타내는 종료 상태 코드입니다.



// (5) sockaddr_in 구조체 설정
bzero(&servaddr , sizeof(servaddr));


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

sockaddr_in 구조체 설정합니다.

 

이 코드는 소켓 프로그래밍에서 서버의 인터넷 주소를 설정하는 데 사용됩니다. 각 줄은 다음과 같은 역할을 합니다:

 

1. `bzero(&servaddr , sizeof(servaddr));`

   - `bzero` 함수는 `servaddr` 구조체를 0으로 초기화합니다. 이는 구조체 내의 모든 필드를 안전하게 0으로 설정하여 깨끗한 상태로 시작할 수 있게 해줍니다. 이는 오래된 방식으로, 현대 C 프로그램에서는 `memset(&servaddr, 0, sizeof(servaddr));`으로 더 자주 대체됩니다.

 

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() 호출이 성공하면, 서버는 listen() 함수를 호출하여 연결 요청을 듣기 시작할 수 있으며, 이는 서버가 네트워크에서 연결을 수락할 준비가 되었음을 의미합니다.  그러나 이 함수 호출이 실패하면 (즉, 0보다 작은 값을 반환하면), 이는 에러 상황을 나타냅니다. 그 경우, `perror()` 함수를 사용하여 에러 메시지를 출력하고, 프로그램은 `exit(1)`를 통해 비정상적으로 종료됩니다. `perror()`는 발생한 시스템 에러에 대한 설명을 제공하는데 사용됩니다.



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

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

 

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

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

 

`listen()` 함수의 호출이 성공하면, 해당 소켓을 연결 요청을 수신하는 상태로 변경합니다. 그러나 이 함수 호출이 실패하면 (즉, 0보다 작은 값을 반환하면), 이는 에러 상황을 나타냅니다. 그 경우, `perror()` 함수를 사용하여 에러 메시지를 출력하고, 프로그램은 `exit(1)`를 통해 비정상적으로 종료됩니다. `perror()`는 발생한 시스템 에러에 대한 설명을 제공하는데 사용됩니다.




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

이 코드는 네트워크 프로그래밍에서 서버가 클라이언트의 연결 요청을 수락하는 과정을 나타냅니다. 각각의 부분은 다음과 같은 역할을 합니다:

 

1. socklen_t clilen;

    clilen = sizeof(cliaddr);

 

clilen 변수에 클라이언트 주소 구조체 cliaddr의 크기를 저장합니다. 이것은 accept 함수에 전달되어, 함수가 클라이언트 주소의 실제 크기를 알 수 있게 합니다. 주소 구조체의 크기를 저장하는데 사용되는 데이터 타입은 socklen_t입니다.

 

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

 

   - accept 함수의 첫 번째 인자 listenfd는 리스닝 소켓의 파일 기술자입니다.

   - 두 번째 인자 cliaddr는 클라이언트의 주소 정보를 담을 sockaddr 구조체의 포인터입니다.  (struct sockaddr *)&cliaddr에서 &cliaddr은  cliaddr의 주소라는 의미이며  (struct sockaddr *)struct sockaddr 포인터로 형 변환한다는 의미입니다.

   - 세 번째 인자 clilen는 클라이언트 주소 구조체의 크기를 담고 있는 변수 clilen의 주소라서 변수 clilen앞에 &가 붙어있습니다.  accept 함수가 반환될 때, 이 변수는 실제로 cliaddr에 채워진 주소 정보의 길이로 업데이트됩니다. 이렇게 업데이트된 clilen 값은 클라이언트의 주소 정보를 올바르게 해석하는 데 필요한 실제 길이를 갖고 있습니다. 

 

3. accept 함수가 실패하면 -1을 반환합니다. perror을 사용하여 실패 원인을 설명하는 시스템 오류 메시지를 사용자가 추가해놓은 문자열과 함께  표준 오류(stderr)로 출력합니다.  exit(1)을 호출하여 오류가 발생했음을 나타내며 프로그램을 종료합니다. exit 함수에 사용한 1은 비정상 종료를 나타내는 전통적인 코드입니다.




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

cliaddr.sin_addr는 클라이언트의 주소 구조체인 sockaddr_in의 멤버로, 클라이언트의 주소를 저장하고 있습니다. 클라이언트의 주소는 네트워크 바이트 순서로 되어 있는 IP 주소로 저장되어 있습니다.  inet_ntoa 함수를 사용하여 사람이 읽을 수 있는 점으로 구분된 IPv4 주소로 예를 들어 "127.0.0.1" 문자열로 변환합니다. 




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

pthread_create 함수는 새로운 스레드를 생성하는 함수입니다. 

  - &recv_tid : 생성된 스레드의 ID를 저장할 변수의 주소입니다.

  - NULL : 새 스레드의 속성을 지정하는데 사용되는 pthread_attr_t 타입의 변수입니다. NULL이면 기본 속성으로 스레드가 생성됩니다.

  - recv_message : 새 스레드가 실행할 함수의 이름입니다. 지정하는 함수는 void* 를 파라미터로 받고 void* 를 반환하는 형태여야 합니다.

  - &connfd : recv_message 함수로 전달될 파라미터의 주소입니다. 여기에선  연결 파일 디스크립터의 주소가 전달됩니다.

 

connfd 변수는 서버가 클라이언트의 연결 요청을 수락(accept)한 후, 생성된 소켓의 파일 기술자를 저장합니다. 클라이언트와 데이터를 주고받을 때 이 소켓을 사용합니다.



pthread_create 함수는 스레드 생성이 성공하면 0을 반환하고, 에러가 발생하면 -1을 반환합니다. 

 

-1을 반환시 perror을 사용하여 실패 원인을 설명하는 시스템 오류 메시지를 사용자가 추가해놓은 문자열과 함께  표준 오류(stderr)로 출력합니다.  exit(1)을 호출하여 오류가 발생했음을 나타내며 프로그램을 종료합니다. exit 함수에 사용한 1은 비정상 종료를 나타내는 전통적인 코드입니다.




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


    // (11-1)
    while(fgets(msg , MAX_LINE , stdin) != NULL)
    {

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


        // send 함수는 msg에 저장된 문자열을 네트워크를 통해 클라이언트로 전송합니다. 
        // connfd : 이미 연결된 상태의 소켓 파일 디스크립터로 클라이언트와 데이터를 주고 받을 때 사용됩니다. 
        // msg : 전송할 메시지가 저장된 버퍼입니다.
        // strlen(msg) : strlen 함수는 msg 버퍼에 저장된 문자열의 길이를 반환합니다 (널 종료자를 제외한). 
        //               이는 `send` 함수에게 전송할 바이트 수를 알려줍니다.
        // 0 : 이는 전송에 사용되는 플래그 옵션으로, 일반적인 경우에는 0을 사용합니다.
        //
        // send 함수는 성공적으로 데이터를 보내면 전송된 바이트 수를 반환하고, 실패하면 -1을 반환합니다.
        if(send(connfd , msg , strlen(msg) , 0) == -1)
        {
            perror("send error.\n");
            exit(1);
        }
    }

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

 

(11-1)

fgets 함수는 입력 스트림에서 한 줄의 문자열을 읽어들입니다. 이 함수는 개행 문자(`\n`)를 만나거나 버퍼 크기만큼인 MAX_LINE - 1개의 문자를 읽었을 때( MAX_LINE이 아니라 MAX_LINE-1인 이유는 버퍼 마지막 위치에 널 종료자(‘\0’)을 채워야 하기 때문) , 또는 스트림의 끝(EOF)에 도달했을 때 읽기를 중단합니다. 

 

`fgets` 함수는 입력 스트림으로부터 문자열을 읽는 데 사용됩니다. 이 입력 스트림은 사용자의 키보드 입력에 대응하는 표준 입력(`stdin`)이 될 수도 있으며, 또는 `fopen` 함수 등을 사용하여 열린 파일로부터의 데이터 입력일 수도 있습니다. 

 

파일의 끝에 도달하거나 읽기 오류가 발생하면 `fgets` 함수는 `NULL`을 반환합니다. 따라서 `while` 루프는 `fgets`가 `NULL`을 반환할 때까지 계속 반복하여 실행됩니다.



fgets 함수의 아규먼트

  • msg : fgets 함수가 문자열을 저장하는 버퍼입니다. 이 버퍼는 최대 MAX_LINE 길이의 문자열을 저장할 수 있습니다.
  • MAX_LINE : 읽을 수 있는 최대 문자 수를 정의하는 상수입니다. 이는 `msg` 버퍼의 크기를 의미하며, `fgets`가 한 번에 읽을 수 있는 최대 문자 수를 결정합니다.
  • stdin : 이는 표준 입력을 가리키는 FILE 포인터입니다. 일반적으로 키보드 입력에 대응됩니다.




// 클라이언트의 메시지를 수신하여 화면에 출력하는 역할을 하는 함수로 스레드내에서 실행됩니다.
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';

        // 클라이언트가 갑자기 접속을 끊은 경우 소켓 sockfd를 닫고 스레드를 종료합니다.
        if (n==0)
        {
            printf("Client closed.\n");
            close(sockfd);
            exit(1);
        }

        // 클라이언트로부터 quit 문자열을 수신 받으면 소켓 sockfd를 닫고 스레드를 종료합니다.
        if(strcmp(buf , "quit") == 0)
        {
            printf("Client closed.\n");
            close(sockfd);
            exit(1);
        }

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

클라이언트의 메시지를 수신하여 화면에 출력하는 역할을 하는 함수로 스레드내에서 실행됩니다. 



client.c

#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);
        }
       
        // exit 문자열을 받으면 종료
        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)
    bzero(&servaddr , 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);
        }
   
    }
}




코드 설명

    // (1)
    int sockfd;

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



    // (2)
    pthread_t recv_tid;

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



    // (3)
    struct sockaddr_in servaddr;

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

 

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



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

프로그램 실행시 연결할 서버의 IP 주소를 인자로 주어야 합니다. 포트번호는 6001로 고정되어 있습니다.



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

socket 함수는 소켓을 생성하고, 생성된 소켓에 대한 파일 디스크립터를 반환합니다. 소켓은 네트워크 통신을 위한 엔드포인트로 작동합니다.

  • AF_INET : 주소 체계를 지정합니다. AF_INET은 IPv4 인터넷 프로토콜을 사용하는 주소 체계입니다. 
  • SOCK_STREAM :  소켓 타입을 지정합니다. SOCK_STREAM은 연결 지향형 소켓을 나타내며, TCP(Transmission Control Protocol) 통신에 사용됩니다.
  • 0 : 프로토콜을 지정합니다. 0은 지정한 소켓 타입에 대한 기본 프로토콜을 사용하겠다는 의미입니다. SOCK_STREAM의 경우 기본 프로토콜은 TCP입니다.

 

-  성공적으로 소켓이 생성되면 sockfd 변수에 유효한 파일 디스크립터 값이 저장됩니다. 오류가 발생한 경우 sockfd 변수에는 -1이 저장됩니다.

 

socket 함수가 -1을 리턴한 경우 perror 함수는 아규먼트로 전달된 메시지와 함께 시스템 오류 메시지를 출력합니다.

exit 함수는 프로그램을 즉시 종료합니다. 인자로 전달된 1은 오류로 인한 비정상 종료를 나타냅니다.



    // (6)
    bzero(&servaddr , sizeof(servaddr));

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

bzero 함수는 servaddr 구조체의 모든 바이트를 0으로 초기화합니다.

  • &servaddr : servaddr 구조체의 주소를 의미합니다.
  • sizeof(servaddr) : servaddr 구조체의 전체 크기를 바이트 단위로 계산합니다. 이는 bzero가 올바른 메모리 크기를 0으로 설정할 수 있도록 합니다.

 

servaddr.sin_family : sockaddr_in 구조체의 sin_family 필드를 AF_INET으로 설정합니다. 이는 소켓이 IPv4 인터넷 프로토콜을 사용하는 주소 체계임을 나타냅니다.

 

servaddr.sin_port :  sockaddr_in 구조체의 sin_port 필드를 설정합니다. 이 필드는 네트워크 통신에서 사용될 포트 번호를 저장합니다. htons 함수는 호스트 바이트 순서(host byte order)의 포트 번호를 네트워크 바이트 순서(network byte order, 일반적으로 빅 엔디언)로 변환합니다. PORT는 서버가 클라이언트의 접속을 대기하고 있을 포트 번호를 상수로 정의한 값입니다.



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

문자열로 된 IP 주소를 네트워크 바이트 순서로 변환합니다.

 

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

  • AF_INET : 변환할 주소의 주소 체계를 지정합니다. AF_INET은 IPv4 주소를 의미합니다.
  • argv[1] : 명령줄에서 입력받은 두 번째 인수로, 프로그램을 실행시 제공된 서버의 IP 주소를 나타냅니다. 예를 들어, 프로그램을 `./program 192.168.1.1`과 같이 실행했다면, `argv[1]`은 "192.168.1.1"이 됩니다.
  • &servaddr.sin_addr : 변환된 이진 주소를 저장할 sockaddr_in 구조체 내의 sin_addr 필드의 주소입니다.

 

inet_pton 함수는 성공 시 1을, 주소가 유효하지 않은 문자열일 경우 0을, 오류가 발생했을 경우 -1을 반환합니다.



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

connect 함수는 클라이언트가 서버에 연결을 시도할 때 사용되는 함수입니다. 

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

  • sockfd : 서버와의 연결을 시도할 로컬 소켓의 파일 디스크립터입니다. 
  • (struct sockaddr *)&servaddr : 서버의 주소 정보를 담고 있는 sockaddr_in 구조체를 sockaddr 구조체로 형변환한 포인터입니다. connect 함수는 sockaddr 타입의 포인터를 요구하기 때문에, 주소를 형변환 해야합니다.
  • sizeof(servaddr)는 connect 함수에 전달되는 서버 주소 구조체의 크기입니다. connect 함수가 주소의 크기를 알아야 할 때 사용됩니다.

 

서버와 연결이 성공 시 0을 반환하고, 연결 시도가 실패하면 -1을 반환합니다.



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

스레드를 생성하여 서버로부터 수신한 메시지를 화면에 출력하는 recv_message 함수를 실행합니다.

 

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


    // (10-1)
    while(fgets(msg , MAX_LINE , stdin) != NULL)
    {

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


        // send 함수는 msg에 저장된 문자열을 네트워크를 통해 클라이언트로 전송합니다. 

        // connfd : 이미 연결된 상태의 소켓 파일 디스크립터로 클라이언트와 데이터를 주고 받을 때 사용됩니다. 
        // msg : 전송할 메시지가 저장된 버퍼입니다.
        // strlen(msg) : strlen 함수는 msg 버퍼에 저장된 문자열의 길이를 반환합니다 (널 종료자를 제외한). 
        //               이는 `send` 함수에게 전송할 바이트 수를 알려줍니다.
        // 0 : 이는 전송에 사용되는 플래그 옵션으로, 일반적인 경우에는 0을 사용합니다.
        //
        // send 함수는 성공적으로 데이터를 보내면 전송된 바이트 수를 반환하고, 실패하면 -1을 반환합니다.
        if(send(sockfd , msg , strlen(msg) , 0) == -1)
        {
            perror("send error.\n");
            exit(1);
        }
   
    }

사용자가 키보드로 입력한 문자열을 서버로 전송합니다. "exit" 문자열을 입력받으면 소켓을 닫고 프로그램을 종료합니다.



(10-1)

fgets 함수는 입력 스트림에서 한 줄의 문자열을 읽어들입니다. 이 함수는 개행 문자(`\n`)를 만나거나 버퍼 크기만큼인 MAX_LINE - 1개의 문자를 읽었을 때( MAX_LINE이 아니라 MAX_LINE-1인 이유는 버퍼 마지막 위치에 널 종료자(‘\0’)을 채워야 하기 때문) , 또는 스트림의 끝(EOF)에 도달했을 때 읽기를 중단합니다. 

 

`fgets` 함수는 입력 스트림으로부터 문자열을 읽는 데 사용됩니다. 이 입력 스트림은 사용자의 키보드 입력에 대응하는 표준 입력(`stdin`)이 될 수도 있으며, 또는 `fopen` 함수 등을 사용하여 열린 파일로부터의 데이터 입력일 수도 있습니다. 

 

파일의 끝에 도달하거나 읽기 오류가 발생하면 `fgets` 함수는 `NULL`을 반환합니다. 따라서 `while` 루프는 `fgets`가 `NULL`을 반환할 때까지 계속 반복하여 실행됩니다.



fgets 함수의 아규먼트

  • msg : fgets 함수가 문자열을 저장하는 버퍼입니다. 이 버퍼는 최대 MAX_LINE 길이의 문자열을 저장할 수 있습니다.
  • MAX_LINE : 읽을 수 있는 최대 문자 수를 정의하는 상수입니다. 이는 `msg` 버퍼의 크기를 의미하며, `fgets`가 한 번에 읽을 수 있는 최대 문자 수를 결정합니다.
  • stdin : 이는 표준 입력을 가리키는 FILE 포인터입니다. 일반적으로 키보드 입력에 대응됩니다.




반응형

'socket programming' 카테고리의 다른 글

파이썬 클라이언트 / 서버 소켓 예제  (0) 2023.10.19

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts