[C언어] 20강 소켓프로그래밍(2)

강지원·2025년 1월 27일

리눅스 기반 C언어

목록 보기
24/24

1. 채팅프로그램 구조

서버에 접속한 user 중 한 명이 뭔가를 말하면 서버에 접속한 나머지 user들에게 전달해주는 채팅 프로그램을 만들것이다

2. 저번 코드 복기(문제점)

server 문제점

user1이 접속하면 user1의 패킷만 받고 처리하고 있음.
user2가 들어오면 대기만 해야 함.

client 문제점

클라이언트는 미리 정해진 문자만 보낼 수밖에 없음.
채팅이 불가함.
받는 부분도 없음.

3. 문제점 고치기

server

accept 부분을 쓰레드로 돌려서, user 전용 쓰레드와 전부를 위한 send 쓰레드를 만들어줄 것임.

  • user1이 오면 user1 전용 쓰레드를 생성해주고 그때부터 user1은 전용쓰레드로 통신함.
  • user2가 오면 user2 전용 쓰레드를 생성해주고 그 때부터 전용쓰레드와 통신할 수 있도록 해줌.

이때 전용쓰레드는 read 만 가능하다.

client

송신 쓰레드 1(send)

  • 키보드 입력을 받고
  • 키보드 입력이 들어오면 send

수신 쓰레드 2(read)

  • 서버로 부터 온 내용을 받고
  • 모니터에 뿌려줌

4. server 코드

1) 헤더파일

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

2) 매크로, 전역변수

#define CLNT_MAX 10
#define BUFFSIZE 200

int g_clnt_socks[CLNT_MAX];
int g_clnt_count = 0;
pthread_mutex_t g_mutex;

3) 모든 클라이언트에게 메시지 전송

// 모든 클라이언트에게 메시지 전송
void send_all_clnt(char *msg, int str_len, int my_sock) {
    pthread_mutex_lock(&g_mutex);
    for (int i = 0; i < g_clnt_count; i++) {
        if (g_clnt_socks[i] != my_sock) {
            write(g_clnt_socks[i], msg, str_len);
        }
    }
    pthread_mutex_unlock(&g_mutex);
}

만약, user1이 메시지를 보내면 서버에서 나머지 클라이언트들에게 메시지를 전달하는 역할이다.

4) 클라이언트 연결 쓰레드

// 클라이언트 연결 처리
void *clnt_connection(void *arg) {
    int clnt_sock = *(int *)arg;
    free(arg);

    char msg[BUFFSIZE];
    int str_len;

    while (1) {
        str_len = read(clnt_sock, msg, sizeof(msg));
        if (str_len <= 0) {  // Client disconnected or error
            if (str_len == 0) {
                printf("Client[%d] disconnected\n", clnt_sock);
            } else {
                perror("Read error");
            }
            break;
        }
        send_all_clnt(msg, str_len, clnt_sock);
        printf("Message from client[%d]: %s", clnt_sock, msg);
    }

    // Remove client socket from the list
    pthread_mutex_lock(&g_mutex);
    for (int i = 0; i < g_clnt_count; i++) {
        if (g_clnt_socks[i] == clnt_sock) {
            for (int j = i; j < g_clnt_count - 1; j++) {
                g_clnt_socks[j] = g_clnt_socks[j + 1];
            }
            g_clnt_count--;
            break;
        }
    }
    pthread_mutex_unlock(&g_mutex);

    close(clnt_sock);
    pthread_exit(NULL);
}

main()에서 클라이언트가 연결요청이 되어 클라이언트가 clnt_sock으로 들어오면 이 유저 전용 쓰레드로 들어오게 됨.

  1. 클라이언트와 연결해서 read()로 클라이언트에서 보낸 메시지를 읽는다.
  2. 클라이언트에게 받은 메시지를 send_all_clnt()로 다른 클라이언트들에게 전송한다.
  3. 만약 클라이언트가 연결을 해제하면, 클라이언트 소켓 배열에서 제거를 한다.
    ex) g_clnt_count = 4일 때, user2가 연결을 해제하면
    user1,user3,user4 -> user1, user2, user3로 변경
    g_clnt_count = 3으로 변경

5) 소켓 연결, bind, listen

int main() {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_size;

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(7989);

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("Bind failed");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

    if (listen(serv_sock, 5) == -1) {
        perror("Listen failed");
        close(serv_sock);
        exit(EXIT_FAILURE);
    }

6) accept

	pthread_mutex_init(&g_mutex, NULL);
    printf("Server started on port 7989\n");

    while (1) {
        clnt_addr_size = sizeof(clnt_addr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        if (clnt_sock == -1) {
            perror("Accept failed");
            continue;
        }

assept()로 serv_sock에 클라이언트 연결 요청을 수락한다.
그 반환값을 clnt_sock에 넘겨준다.

7) thread

		pthread_mutex_lock(&g_mutex);
        g_clnt_socks[g_clnt_count++] = clnt_sock;
        pthread_mutex_unlock(&g_mutex);

        int *arg = malloc(sizeof(int));
        if (arg == NULL) {
            perror("Malloc failed");
            close(clnt_sock);
            continue;
        }
        *arg = clnt_sock;

        pthread_t t_thread;
        if (pthread_create(&t_thread, NULL, clnt_connection, arg) != 0) {
            perror("Thread creation failed");
            free(arg);
            close(clnt_sock);
            continue;
        }
        pthread_detach(t_thread);
    }

    close(serv_sock);
    pthread_mutex_destroy(&g_mutex);
    return 0;
}
  1. 클라이언트와 연결되면 전역변수인 g_clnt_count를 +1 해주고, g_clnt_socks[]배열에 클라이언트 정보를 넘겨준다.
  2. pthread_create()으로 클라이언트 연결 쓰레드로 넘겨준다.
  3. pthread_detache()로 쓰레드를 분리해준다.

5. client 코드

1) 헤더파일, #define

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>

#define BUFFSIZE 200
#define NAMESIZE 20

2) 메시지 수신 쓰레드

// 메시지 수신 쓰레드 함수
void *rcv(void *arg) {
    int sock = *(int *)arg;
    char buff[BUFFSIZE];
    int len;

    while (1) {
        len = read(sock, buff, sizeof(buff));
        if (len <= 0) { // Server closed connection or error
            if (len == 0) {
                printf("Server closed connection\n");
            } else {
                perror("Read error");
            }
            break;
        }
        printf("%s", buff); // Display received message
    }
    pthread_exit(NULL);
}

3) 메시지 전송 쓰레드

void *snd(void *arg) {
    int sock = *(int *)arg;
    char chat[BUFFSIZE];
    char msg[BUFFSIZE + NAMESIZE];
    char id[NAMESIZE];

    printf("Enter your ID: ");
    fgets(id, NAMESIZE, stdin);
    id[strcspn(id, "\n")] = '\0'; // Remove newline character

    while (1) {
        printf("Enter message: ");
        if (fgets(chat, sizeof(chat), stdin) == NULL) {
            if (feof(stdin)) {
                printf("EOF reached\n");
            } else {
                perror("Input error");
            }
            break;
        }
        chat[strcspn(chat, "\n")] = '\0'; // Remove newline character

        snprintf(msg, sizeof(msg), "[%s]: %s\n", id, chat);
        if (write(sock, msg, strlen(msg)) <= 0) {
            perror("Write error");
            break;
        }
    }
    pthread_exit(NULL);
}

4) 소켓 설정

int main() {
    int sock;
    struct sockaddr_in serv_addr;
    pthread_t rcv_thread, snd_thread;

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) {
        perror("Socket creation failed");
        return -1;
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serv_addr.sin_port = htons(7989);

5) 서버 연결

// 서버에 연결
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
        perror("Connection failed");
        close(sock);
        return -1;
    }

6) 송신, 수신 쓰레드 생성

 // 수신 및 송신 쓰레드 생성
    if (pthread_create(&rcv_thread, NULL, rcv, (void *)&sock) != 0) {
        perror("Receiver thread creation failed");
        close(sock);
        return -1;
    }

    if (pthread_create(&snd_thread, NULL, snd, (void *)&sock) != 0) {
        perror("Sender thread creation failed");
        pthread_cancel(rcv_thread);
        close(sock);
        return -1;
    }

7) 송신 및 수신 쓰레드 종료 대기

// 송신 및 수신 쓰레드 종료 대기
    pthread_join(snd_thread, NULL);
    pthread_cancel(rcv_thread);

    close(sock);
    return 0;
}

6. 실행결과

0개의 댓글