멀티스레딩 없이 다중클라이언트 다루기

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

Server Studies

목록 보기
15/27

이 글을 보고 작성하였다.

큰 서버 큰 책임

지금까지 작성한 소켓통신코드들은 전부 1서버 1클라이언트 구조였다. 다만 현실에서 큰 서버를 운영하려면, 1서버가 n개의 클라이언트를 담당해야한다.

이 때 현명한 누군가는 '예전에 짠 코드를 전부 쓰레드화 시키고 클라이언트 연결할 때 마다 쓰레드를 생성하면 되는거 아니냐!' 라고 말할것이다. 물론 가능하다. 하지만 그렇게 구현하게 되면 몇 가지 단점이 있다.

  1. 쓰레드코드는 쓰기어렵고, 디버깅하기 어렵고, 가끔 예측불가능한 결과를 가지고 온다
  2. context switch의 오버헤드
  3. 엄청나게 많은 수의 클라이언트에는 쓸 수가 없다
  4. deadlock 가능성이 있다.

따라서 쓰레드 없이 다중클라이언트 다루는 코드를 알아보겠다.

select()

리눅스에서 다중클라이언트를 다루는 더 좋은 방법은 select()이다.

  • 셀렉트 커맨드는 다중 file descriptor를 모니터링 할 수 있게 해준다. fd가 active 될 때 까지 대기한다.

  • select는 interrupt handler로 작동하는데, fd가 데이터를 보내자마다 바로 작동한다.

fd_set

fd_set은 select를 위한 자료구조이다. fd_set의 함수는 다음과 같다.

fd_set readfds;

// Clear an fd_set
FD_ZERO(&readfds);  

// Add a descriptor to an fd_set
FD_SET(master_sock, &readfds);   

// Remove a descriptor from an fd_set
FD_CLR(master_sock, &readfds); 

//If something happened on the master socket , then its an incoming connection  
FD_ISSET(master_sock, &readfds); 

select는 다음과 같은 구조이다.

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

select함수의 원형은 다음과 같다.

int select (int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct imeval* timeout);

-readfds = 읽기 가능한지 확인
-writefds = 쓰기 가능한지 확인
-exceptfds = 예외가 발생했거나 대역을 넘어서는 데이터가 존재하는지 감시

만약 NULL을 준다면 감시를 시행하지 않는다는뜻이다. 따라서 우리가 작성한 activity는 readfds만 감시하고 나머지는 신경쓰지 않는다.

전체 코드

#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;
}

ssh로 서버실행후 linux 클라이언트, windows 클라이언트 두개를 동시에 연결시도 해보면 다음과 같이 나온다.


리눅스 클라이언트는 귀찮아서 안찍었다

검은화면이 server 아래 vs 화면이 window쪽 클라이언트이다.

보이는것처럼 서로 연결되고 메세지를 주고받는것을 알 수 있다.

다음글에서는 코드를 분석해보겠다.

꽤 길어서 오래걸릴듯하다.

0개의 댓글