[UDP] echo server 및multicast 방식의 채팅 프로그램 구현하기

UDP 기반 소켓 통신을 이해하고 2가지 echo 서버를 구현해본 뒤, multicast 방식의 채팅 프로그램을 구현해보자.
Hi's avatar
Apr 11, 2024
[UDP] echo server 및multicast 방식의 채팅 프로그램 구현하기

✅ 목표

  • UDP 기반 echo socket program 구조 및 동작 이해
  • connected UDP 기반 echo socket program 구조 및 동작 이해
  • UDP 기반 multicast 방식의 채팅 program 구조 및 동작 이해
 
 

✅ UDP 기반 통신

서버를 구현하기에 앞서, UDP 기반 통신의 특징을 알아보자.
UDP는 User Datagram Protocol의 약자이며 전송 계층의 비연결지향 프로토콜을 의미한다. 비연결지향 프로토콜이란 데이터를 주고받을 때 우선적으로 연결 절차를 거치지 않고 발신자가 일방적으로 수신자에게 데이터를 송신하는 방식을 말한다.
이와 다르게 TCP는 연결지향 프로토콜이다. 따라서 데이터 송수신 전에 호스트 간 연결 절차를 거친 뒤 데이터 송수신이 진행된다.
UDP는 TCP와는 다르게 연결하는 과정이 없기 때문에 비교적 빠른 전송이 가능하다는 장점이 있다. 하지만 데이터가 유실될 수 있고, 데이터 패킷의 순서를 보장해주지 않는다는 단점이 있다. 이는 데이터 신뢰성을 보장해주지 않는다는 것을 의미한다.
 

✅ uecho server & client 구현

  1. 우분투에서 2개의 terminal을 연다. 하나는 server 용, 하나는 client 용
  1. 디렉토리를 하나 생성한다.
  1. 디렉토리 안에 uecho_server.c 및 uecho_client.c를 각각 만들고 컴파일하여 실행 파일을 만든다.
  1. server용 터미널에서 uecho_server 코드를 먼저 실행하고, client용 터미널에서 uecho_client 코드를 실행한다.
      • server: ./uecho_server 9190 (port 번호는 임의로)
      • client: ./uecho_client 127.0.0.1 9190 (서버에서 적은 포트 번호)
  1. client 터미널에서 message를 입력하여 해당 message가 echo 되는 것을 확인한다.
  1. client 터미널에서 Q를 입력하면 client 코드가 종료되는 것을 확인한다.
  1. server 터미널에서 control + c 를 입력하여 server 코드를 강제 종료한다.
 
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message); int main(int argc, char *argv[]) { int serv_sock; char message[BUF_SIZE]; int str_len; socklen_t clnt_adr_sz; struct sockaddr_in serv_adr, clnt_adr; if (argc != 2) { printf("Usage: %s <PORT>\n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_DGRAM, 0); if (serv_sock == -1) { error_handling("UDP socket creation error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) { error_handling("bind() error"); } while (1) { clnt_adr_sz = sizeof(clnt_adr); str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz); } close(serv_sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
uecho_server.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message); int main(int argc, char *argv[]) { int sock; char message[BUF_SIZE]; int str_len; socklen_t adr_sz; struct sockaddr_in serv_adr, from_adr; if (argc != 3) { // 인자를 제대로 입력하지 않음 printf("Usage: %s <IP> <PORT>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { // 소켓 생성 실패 error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); while (1) { fputs("Insert message(q to quit): ", stdout); fgets(message, sizeof(message), stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); adr_sz = sizeof(from_adr); str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz); message[str_len] = 0; printf("Message from server: %s", message); } close(sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
uecho_client.c
 
실행화면
실행화면
 

✅ connected uecho server & client 구현

  1. uecho_client.c 대신 uecho_con_client.c를 이용하여 동일한 방법으로 실행한다.
  1. echo_client.c, uecho_client.c, uecho_con_client.c 소스 코드를 비교하여 차이점을 찾는다.
 
[echo_client.c, uecho_client.c, uecho_con_client.c의 차이점 분석]
  1. Client 종류:
      • echo_client.c
      • uecho_client.c
      • uecho_con_client.c
  1. 통신 방식의 차이:
      • echo_client.c는 TCP 소켓을 사용하여 서버와의 연결을 설정하고, 데이터를 주고 받는다.
      • uecho_client.c는 UDP 소켓을 사용하여 서버와 통신한다.
      • uecho_con_client.c는 UDP 소켓을 사용하며, connect() 함수를 사용하여 소켓을 연결하고 있다. 하지만 UDP는 연결 지향형이 아닌 비연결형 프로토콜이므로, 실제로 연결이 수립되는 것이 아니라 주소를 목적지 주소로 설정하는 것뿐이다.
  1. 데이터 송수신 방식의 차이:
      • echo_client.c에서는 TCP 소켓이므로 read()write() 함수를 사용하여 데이터를 송수신한다.
      • uecho_client.c에서는 UDP 소켓이므로 sendto()recvfrom() 함수를 사용하여 데이터를 송수신한다.
      • uecho_con_client.c에서는 UDP 소켓을 사용하며, connect() 함수로 소켓을 연결하였기 때문에 연결 설정 후 write()read() 함수를 사용하여 데이터를 송수신한다. 하지만 이 방식은 UDP의 특성에 어긋나며, 실제로는 UDP에서 제공하는 비연결성과는 거리가 있다.
 
 
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define BUF_SIZE 30 void error_handling(char *message); int main(int argc, char *argv[]) { int sock; char message[BUF_SIZE]; int str_len; socklen_t adr_sz; struct sockaddr_in serv_adr, from_adr; if (argc != 3) { // 인자를 제대로 입력하지 않음 printf("Usage: %s <IP> <PORT>\n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { // 소켓 생성 실패 error_handling("socket() error"); } memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); // UDP 소켓을 대상으로 connect() 수행 connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)); while (1) { fputs("Insert message(q to quit): ", stdout); fgets(message, sizeof(message), stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) { break; } // sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr)); // sendto 대신 write 함수를 사용하여 서버로 메시지 전송 write(sock, message, strlen(message)); adr_sz = sizeof(from_adr); // str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz); // recvfrom 대신 read를 사용하여 서버로부터 메시지 수신 str_len = read(sock, message, sizeof(message) - 1); message[str_len] = 0; printf("Message from server: %s", message); } close(sock); return 0; } void error_handling(char *message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }
uecho_con_client.c
 
실행화면 (중간에 버퍼 사이즈를 넘어서 문자열이 깨짐)
실행화면 (중간에 버퍼 사이즈를 넘어서 문자열이 깨짐)
 

✅ multicast 방식의 chatting program 구현

  1. 하나의 terminal에서 디렉토리를 하나 생성한다.
  1. 디렉토리 안에 multicast.c를 만들고 컴파일하여 실행파일을 만든다.
  1. 추가로 2개의 터미널을 연다.
  1. 각 터미널에서 실행파일을 동작한다.
    1. ./multicast IP# Port# name
      → IP: 239.0.3.3 / Port: 3000 / name은 채팅 시 사용할 영문 이름
  1. 3개의 터미널에서 채팅을 테스트한다. 하나의 터미널에서 입력한 메시지가 다른 터미널 모두 동시에 출력되는지를 확인한다.
  1. 종료를 원하면 해당 터미널에서 Control + c 를 입력하여 강제 종료한다.
 
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #define MAXLINE 1024 int main(int argc, char *argv[]) { int send_s, recv_s; // 송신용 소켓, 수신용 소켓 int pid; unsigned int yes = 1; struct sockaddr_in mcast_group; // 멀티캐스트 그룹 주소 struct ip_mreq mreq; char line[MAXLINE]; char name[10]; // 채팅 참가자 이름 int n, len; if (argc != 4) { printf("Usage: %s multicast_address port My_name \n", argv[0]); exit(0); } sprintf(name, "[%s]", argv[3]); /* 멀티캐스트 수신용 소켓 개설 */ memset(&mcast_group, 0, sizeof(mcast_group)); mcast_group.sin_family = AF_INET; mcast_group.sin_port = htons(atoi(argv[2])); mcast_group.sin_addr.s_addr = inet_addr(argv[1]); if ((recv_s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("error: Can't create receive socket\n"); exit(0); } /* 멀티캐스트 그룹에 가입 */ mreq.imr_multiaddr = mcast_group.sin_addr; mreq.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(recv_s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { printf("error: add membership\n"); exit(0); } /* 소켓 재사용 옵션 지정 */ if (setsockopt(recv_s, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) { printf("error: reuse setsocketopt\n"); exit(0); } /* 소켓 바인드 */ if (bind(recv_s, (struct sockaddr *)&mcast_group, sizeof(mcast_group)) < 0) { printf("error: bind receive socket\n"); exit(0); } /* 멀티캐스트 메시지 송신용 소켓 개설 */ if ((send_s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("error: Can't create send socket\n"); exit(0); } /* fork() 실행: child는 수신 담당 parent는 송신 담당 */ if ((pid = fork()) < 0) { printf("error: fork\n"); exit(0); } else if (pid == 0) { /* child process: 채팅 메시지 수신 담당 */ struct sockaddr_in from; char message[MAXLINE+1]; for (;;) { printf("receiving message...\n"); len = sizeof(from); if ((n = recvfrom(recv_s, message, MAXLINE, 0, (struct sockaddr *)&from, &len)) < 0) { printf("error: recvfrom\n"); exit(0); } message[n] = 0; printf("Received Message: %s\n", message); } } else { /* parent process: 키보드 입력 및 메시지 송신 담당 */ char message[MAXLINE+1]; char line[MAXLINE+1]; printf("Send Message: "); while (fgets(message, MAXLINE, stdin) != NULL) { sprintf(line, "%s %s", name, message); len = strlen(line); if (sendto(send_s, line, strlen(line), 0, (struct sockaddr *)&mcast_group, sizeof(mcast_group)) < len) { printf("error: sendto\n"); exit(0); } } } }
multicast.c
 
실행 화면
실행 화면
Share article

soultree