ddingz 2022. 1. 10. 18:19

TCP 서버 프로그램에서 소켓 개설, 서버주소 바인딩, listen() 함수의 호출은 자주 등장하므로 이들을 묶어 tcp_listen(AF_INET, servport, backlog)라는 함수를 정의하였다.
tcp_listen()은 소켓을 개설하고 listen()을 호출한 후 listen 상태에 있는 소켓을 리턴한다.
TCP 채팅 서버 프로그램에서는 무한 루프를 돌면서 채팅 가입자 처리와 채팅 메시지 방송을 수행하여야 하는데 이러한 작업을 하나의 프로세스가 다중처리하기 위해 select()를 사용하여 소켓을 비동기 모드로 바꾸었다.
select()에서는 읽기 변화만 감지하면 되므로 select()의 두 번째 fd_set 타입의 인자 read_fds만 지정하고 세 번째와 네 번째 인자 즉, 쓰기 및 예외발생에 해당하는 fd_set는 NULL로 지정하면 된다.


tcp_chatserv.c

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

#define MAXLINE 511
#define MAX_SOCK 1024 // 솔라리스의 경우 64

char *EXIT_STRING = "exit"; // 클라이언트의 종료 요청 문자열
char *START_STRING = "Connected to chat_server\n"; // 클라이언트 환영 메시지
int maxfdp1; // 최대 소켓번호 + 1
int num_chat = 0; // 채팅 참가자 수
int clisock_list[MAX_SOCK]; // 채팅에 참가자 소켓번호 목록
int listen_sock; // 서버의 리슨 소켓

// 새로운 채팅 참가자 처리
void addClient(int s, struct sockaddr_in *newcliaddr);
int getmax(); // 최대 소켓번호 찾기
void removeClient(int s); // 채팅 탈퇴 처리 함수
int tcp_listen(int host, int port, int backlog); // 소켓 생성 및 listen
void errquit(char *mesg) {
    perror(mesg);
    exit(1);
}

int main(int argc, char *argv[]) {
    struct sockaddr_in cliaddr;
    char buf[MAXLINE + 1];
    int i, j, nbyte, accp_sock, addrlen = sizeof(struct sockaddr_in);
    fd_set read_fds; // 읽기를 감지할 fd_set 구조체

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

    // tcp_listen(host, port, backlog) 함수 호출
    listen_sock = tcp_listen(INADDR_ANY, atoi(argv[1]), 5);

    while (1) {
        FD_ZERO(&read_fds);
        FD_SET(listen_sock, &read_fds);
        for (i = 0; i < num_chat; i++)
            FD_SET(clisock_list[i], &read_fds);
        maxfdp1 = getmax() + 1; // maxfdp1 재 계산
        puts("wait for client");
        if (select(maxfdp1, &read_fds, NULL, NULL, NULL) < 0)
            errquit("select fail");

        if (FD_ISSET(listen_sock, &read_fds)) {
            accp_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &addrlen);
            if (accp_sock == -1)
                errquit("accept 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++) {
            if (FD_ISSET(clisock_list[i], &read_fds)) {
                nbyte = recv(clisock_list[i], buf, MAXLINE, 0);
                if (nbyte <= 0) {
                    removeClient(i); // 클라이언트의 종료
                    continue;
                }
                buf[nbyte] = 0;

                // 종료문자 처리
                if (strstr(buf, EXIT_STRING) != NULL) {
                    removeClient(i); // 클라이언트 종료
                    continue;
                }

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

                printf("%s\n", buf);
            }
        }
    } // end of while

    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 s) {
    close(clisock_list[s]);

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

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

// 최대 소켓번호 찾기
int getmax() {
    // Minimum 소켓번호는 가장 먼저 생성된 listen_sock
    int max = listen_sock;
    int i;

    for (i = 0; i < num_chat; i++)
        if (clisock_list[i] > max)
            max = clisock_list[i];

    return max;
}

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

실행 결과