다중클라이언트 코드 분석

강한친구·2022년 3월 16일
0

Server Studies

목록 보기
16/27

전체 코드

#include <stdio.h> 
#include <string.h>   //strlen 
#include <stdlib.h> 
#include <errno.h> 
#include <unistd.h>   //close 
#include <arpa/inet.h>    //close 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros 

#define True 1
#define False 0
#define PORT 10000

int main(int argc, char *argv[]) {
    int opt = True;
    int master_socket , addrlen , new_socket , client_socket[30], max_clients = 30 , activity, i , valread , sd;
    int max_sd;
    struct sockaddr_in address;

    char buffer[1024];

    fd_set readfds;
    char *message = (char*)"Echo Daemon v1.0 \r\n";

    for (i = 0; i < max_clients; i++) {
        client_socket[i] = 0;
    }

    if ((master_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)) < 0 ) {
        perror("setsocketopt");
        exit(EXIT_FAILURE);
    }

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

    if (bind(master_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Listener on port %d \n", PORT);

    if (listen(master_socket, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    addrlen = sizeof(address);
    puts("waiting for connections...");

    while(True) {
        FD_ZERO(&readfds);
        FD_SET(master_socket, &readfds);
        max_sd = master_socket;

        for (i = 0; i < max_clients; i++) {
            sd = client_socket[i];

            if (sd > 0) FD_SET(sd, &readfds);
            if (sd > max_sd) max_sd = sd;
        }

        activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);

        if ((activity < 0) && (errno != EINTR)) {
            printf({"select Error"});
        }

        if (FD_ISSET(master_socket, &readfds)) {
            if ((new_socket = accept(master_socket, (struct sockaddr*)& address, (socklen_t*)& addrlen)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            printf("New connection , socket fd is %d , ip is : %s , port : %d\n" 
            , new_socket , inet_ntoa(address.sin_addr) , ntohs (address.sin_port));

            if (send (new_socket, message, strlen(message), 0) != strlen(message)) {
                perror("send");
            }

            puts("Welcome message sent sucessfully");

            for (i = 0; i < max_clients; i++) {
                if( client_socket[i] = 0) {
                    client_socket[i] = new_socket;
                    printf("Adding to list of socket as %d\n", i);

                    break;
                }
            }
        }

        for (i = 0; i < max_clients; i++) {
            sd = client_socket[i];

            if(FD_ISSET(sd, &readfds)) {
                if (valread = read(sd, buffer, 1024) == 0) {  
                    getpeername(sd , (struct sockaddr*)&address , \
                        (socklen_t*)&addrlen);  
                    printf("Host disconnected , ip %s , port %d \n" , 
                        inet_ntoa(address.sin_addr) , ntohs(address.sin_port));  
                    close( sd );  
                    client_socket[i] = 0;  
                } else {
                    buffer[valread] = '\0';
                    send(sd, buffer, strlen(buffer), 0);
                } 
            }
        }
    }
    return 0;
}

헤더

#include <errno.h> 
#include <sys/types.h> 
#include <sys/time.h> //FD_SET, FD_ISSET, FD_ZERO macros 

errno.h

error number의 약자로, 정적 메모리 위치에 저장도니 오류코드를 내보내서 오류상태를 보고 및 검색하기 위한 메크로를 정의한다.

sys/type.h

데이터타입 헤더이다.

sys/time.h

시간 타입 관련 헤더이다. fd_set 타입을 구조체 로 정의한다.

이 3개의 헤더가 이 코드에 새로 추가된 헤더이다.

선언부

    int opt = True;
    int master_socket , addrlen , new_socket , client_socket[30], max_clients = 30 , activity, i , valread , sd;
    int max_sd;
    struct sockaddr_in address;

새로 주목할만한건 client_socket 배열, sd, max_sd 정도이다.
client_socket은 소켓들의 fd번호를 기록해주기 위한 배열이고, sd, max_sd는 상호비교를 통해 fd_set을 지정해주기 위한 역할을 한다.

clinet_socket은 시작할 때 max_client의 길이만큼 0으로 초기화된다.

master_socket 지정

마스터소켓을 만들고, 그 소켓을 다중연결을 허용하는 소켓으로 지정하는 작업이다. 물론 이 과정이 없어도 되지만 있으면 좋다고 한다.

  if( (master_socket = socket(AF_INET , SOCK_STREAM , 0)) == 0)  
    {  
        perror("socket failed");  
        exit(EXIT_FAILURE);  
    }  
     
    if( setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, 
          sizeof(opt)) < 0 )  
    {  
        perror("setsockopt");  
        exit(EXIT_FAILURE);  
    }  

setsocketopt라는 새로운 함수가 나왔는데 이를 살펴보면

setsocketopt

소켓옵션 세팅 함수인데, 옵션을 가지고오는 getsocketopt도 있다.

int setsockopt(int s, int  level,  int  optname,  const  void  *optval, socklen_t optlen);
  • s : 소켓지정번호
  • level : 소켓의 레벨로 어떤 레벨의 소켓정보를 가져오거나 변경할 것인지를 명시한다. 보통 SOL_SOCKET와 IPPROTO_TCP 중 하나를 사용한다.
  • optname : 설정을 위한 소켓옵션의 번호
  • optval : 설정값을 저장하기 위한 버퍼의 포인터
  • optlen : optval 버퍼의 크기

로 구성되어있다.

우리가 실제로 사용한 예를 보자면, sol_socket 레벨에서 reuseaddr, 즉 bind가 정보를 또 쓸 수 있도록 해주는 옵션을 준 것을 알 수 있다.

위 과정을 거치고나면 우리가 평소에 하던데로 master_socket에 주소, 포트 지정해주고 bind, listen 까지 진행한 후, socket이 들어오기를 기다리면 된다.

여기서 listen 상태에 대기중이라는것을 유의해야한다. master_socket은 서버쪽 소켓이고 결국 대기를 통해 다른 소켓들이 들어오는 족족 받아주기 때문이다.

그 후 이제 본격적으로 다중 클라이언트 받기가 시작되는데 내용이 꽤 길어질거같아서 다음 글에서 설명하고 이 글에선 코드의 흐름만 한번 정리하고 마치겠다.

통신의 흐름

이런거 정리하려고 아이패드를 샀는데 글씨가 개판이라 맨날 컴퓨터로만 정리한다...

  1. 서버쪽 소켓 master소켓을 만든다 .

  2. master 소켓은 multiple connection이 가능하도록 설정한다.

  3. bind, listen과정을 거쳐 client socket을 받을 준비를 한다.

  4. while문 시작

  5. 마스터소켓을 selec를 위한 fd_set에 넣어주고
    다른 소켓들중에서 valid한 것이 있으면 마찬가지로 fd_set에 넣어준다. (1회차에서는 master 말고는 valid 소켓이 있을수 없다)

select를 작동시킨다. 여기서 우리는 따로 timeout을 지정하지 않았기에 어떠한 신호가 올 때 까지(클라이언트의 연결 요청) 무조건 대기한다.

6-1. master_socket이 fd_set에 있는 경우,
accpet를 통해 new_socket에 클라이언트 소켓 요청을 받아서 master와 client를 연결한다. 그리고 연결 내용을 출력해주고 send를 통해 메세지를 주고받는다.

그 후, client_socket 배열에서 가장 앞에 비어있는 배열 (0인 배열)에 해당 new_socket의 fd를 넣어준다. 그리고 break해서 빠져나간다.

6-2. 혹은 어떠한 신호가 온것이 새 연결요청이 아닌 기존 socket의 통신요청일 수 있다. 이 떄는 이들간의 통신이 연결되어 있는지 확인하고 메시지를 주고받는다.

GFG 코드 설명 해석

Code Explanation:

We have created a fd_set variable readfds, which will monitor all the active file descriptors of the clients plus that of the main server listening socket.
Whenever a new client will connect, master_socket will be activated and a new fd will be open for that client. We will store its fd in our client_list and in the next iteration we will add it to the readfds to monitor for activity from this client.
Similarly, if an old client sends some data, readfds will be activated and we will check from the list of existing client to see which client has send the data.
Alternatives:
There are other functions that can perform tasks similar to select. pselect , poll , ppoll

우리가 만든 fd_set variable readfds는 active fd와 main server listening socket을 체크할것이다.

언제든지 새로운 클라이언트가 연결되면, master_socket은 activated 될 것이며, 새로운 fd가 client를 위해 열릴것이다.
우리는 그러면 이 fd를 client_list에 저장하고, 다음 while문 순환때 readfds에 들어가서 client 통신을 체크하게 될 것이다.

마찬가지로, 이미 연결된 클라이언트가 데이터를 보내면, readfds가 작동하고 client_list 중 어느 클라이언트가 보낸 메세지인지 파악할 것이다.

이 기능은 pselect, poll, ppoll로도 구현이 가능하다.

0개의 댓글