유닉스(Unix)/네트워크 프로그래밍

폴링형 채팅 서버 프로그램

ddingz 2022. 1. 10. 19:23

tcp_chatserv_nonb.c

폴링형으로 구현한 채팅 서버 프로그램이다.
폴링 동작이 이루어지고 있는 것을 확인하기 위하여 100000번 폴링할 때마다 점(.)을 출력하도록 하였다.
한편 서버를 폴링형으로 구현하면 도착한 데이터가 없어도 계속 데이터 도착을 폴링하고 있으므로 CPU의 낭비가 발생한다는 것을 주의하기 바란다.

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/file.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define MAXLINE 511
#define MAX_SOCK 1024 // 솔라리스는 64

char *EXIT_STRING = "exit";
char *START_STRING = "Connected to chat_server\n";
int num_chat = 0; // 채팅 참가자 수
int clisock_list[MAX_SOCK]; // 채팅에 참가자 소켓번호 목록
int listen_sock;

// 새로운 채팅 참가자 처리
void addClient(int s, struct sockaddr_in *newcliaddr);
void removeClient(int); // 채팅 탈퇴 처리 함수
int set_nonblock(int sockfd); // 소켓을 넌블록으로 설정
int is_nonblock(int sockfd); // 소켓이 넌블록 모드인지 확인
int tcp_listen(int host, int port, int backlog); // 소켓 생성 및 listen
void errquit(char *mesg) {
    perror(mesg);
    exit(1);
}

int main(int argc, char *argv[]) {
    char buf[MAXLINE];
    int i, j, nbyte, count;
    int accp_sock, clilen;
    struct sockaddr_in cliaddr;

    if (argc != 2) {
        printf("사용법 : %s port\n", argv[0]);
        exit(0);
    }

    listen_sock = tcp_listen(INADDR_ANY, atoi(argv[1]), 5);
    if (listen_sock == -1)
        errquit("tcp_listen fail");
    if (set_nonblock(listen_sock) == -1)
        errquit("set_nonblock fail");

    for (count = 0; ; count++) {
        if (count == 100000) {
            putchar('.');
            fflush(stdout);
            count = 0;
        }

        clilen = sizeof(cliaddr);
        accp_sock = accept(listen_sock, (struct sockaddr *) &cliaddr, &clilen);
        if (accp_sock == -1 && errno != EWOULDBLOCK)
            errquit("accept fail");
        else if (accp_sock > 0) {
            // 통신용 소켓은 넌블록 모드가 아님
            if (is_nonblock(accp_sock) != 0 && set_nonblock(accp_sock) < 0)
                errquit("set_nonblock fail");

            addClient(accp_sock, &cliaddr);
            send(accp_sock, START_STRING, strlen(START_STRING), 0);
            printf("%d번째 사용자 추가.\n", num_chat);
        }

        // 클라이언트가 보낸 메시지를 모든 클라이언트에게 방송
        for (i = 0; i < num_chat; i++) {
            errno = 0;

            nbyte = recv(clisock_list[i], buf, MAXLINE, 0);
            if (nbyte == 0) {
                removeClient(i); // abrupt exit
                continue;
            }
            else if (nbyte == -1 && errno == EWOULDBLOCK)
                continue;

            // 종료문자 처리
            if (strstr(buf, EXIT_STRING) != NULL) {
                removeClient(i); // abrupt exit
                continue;
            }

            // 모든 채팅 참가자에게 메시지 방송
            buf[nbyte] = 0;
            for (j = 0; j < num_chat; j++)
                send(clisock_list[j], buf, nbyte, 0);
            printf("%s\n", buf);
        }
    }

    return 0;
}

// 새로운 채팅 참가자 처리
void addClient(int s, struct sockaddr_in *newcliaddr) {
    char buf[20];

    inet_ntop(AF_INET, &newcliaddr->sin_addr, buf, sizeof(buf));
    printf("new client: %s\n", buf);

    // 채팅 클라이언트 목록에 추가
    clisock_list[num_chat] = s;
    num_chat++;
}

// 채팅 탈퇴 처리
void removeClient(int i) {
    close(clisock_list[i]);

    if (i != num_chat - 1)
        clisock_list[i] = clisock_list[num_chat - 1];

    num_chat--;
    printf("채팅 참가자 1명 탈퇴. 현재 참가자 수 = %d\n", num_chat);
}

// 소켓이 nonblock 인지 확인
int is_nonblock(int sockfd) {
    int val;

    // 기존의 플래그 값을 얻어온다
    val = fcntl(sockfd, F_GETFL, 0);

    // 넌블록 모드인지 확인
    if (val & O_NONBLOCK)
        return 0;

    return -1;
}

// 소켓을 넌블록 모드로 설정
int set_nonblock(int sockfd) {
    int val;

    // 기존의 플래그 값을 얻어온다
    val = fcntl(sockfd, F_GETFL, 0);
    if (fcntl(sockfd, F_SETFL, val | O_NONBLOCK) == -1)
        return -1;

    return 0;
}

// listen 소켓 생성 및 listen
int tcp_listen(int host, int port, int backlog) {
    int sd;
    struct sockaddr_in servaddr;

    sd = socket(AF_INET, SOCK_STREAM, 0);
    if (sd == -1) {
        perror("socket fail");
        exit(1);
    }

    // servaddr 구조체의 내용 세팅
    bzero((char *)&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(host);
    servaddr.sin_port = htons(port);

    if (bind(sd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind fail");
        exit(1);
    }

    // 클라이언트로부터 연결요청을 기다림
    listen(sd, backlog);

    return sd;
}

실행 결과

폴링형 채팅 서버 프로그램을 이용하기 위한 클라이언트 프로그램으로는 앞에서 소개한 비동기형 채팅 클라이언트 프로그램 tcp_chatcli.c를 그대로 사용할 수 있다.