[UDP] echo server 및multicast 방식의 채팅 프로그램 구현하기
UDP 기반 소켓 통신을 이해하고 2가지 echo 서버를 구현해본 뒤, multicast 방식의 채팅 프로그램을 구현해보자.
Apr 11, 2024
✅ 목표
- UDP 기반 echo socket program 구조 및 동작 이해
- connected UDP 기반 echo socket program 구조 및 동작 이해
- UDP 기반 multicast 방식의 채팅 program 구조 및 동작 이해
✅ UDP 기반 통신
서버를 구현하기에 앞서, UDP 기반 통신의 특징을 알아보자.
UDP는 User Datagram Protocol의 약자이며 전송 계층의
비연결지향 프로토콜
을 의미한다. 비연결지향 프로토콜
이란 데이터를 주고받을 때 우선적으로 연결 절차를 거치지 않고 발신자가 일방적으로 수신자에게 데이터를 송신하는 방식을 말한다. 이와 다르게 TCP는
연결지향 프로토콜
이다. 따라서 데이터 송수신 전에 호스트 간 연결 절차를 거친 뒤 데이터 송수신이 진행된다. UDP는 TCP와는 다르게 연결하는 과정이 없기 때문에 비교적 빠른 전송이 가능하다는 장점이 있다. 하지만 데이터가 유실될 수 있고, 데이터 패킷의 순서를 보장해주지 않는다는 단점이 있다. 이는 데이터 신뢰성을 보장해주지 않는다는 것을 의미한다.
✅ uecho server & client 구현
- 우분투에서 2개의 terminal을 연다. 하나는 server 용, 하나는 client 용
- 디렉토리를 하나 생성한다.
- 디렉토리 안에 uecho_server.c 및 uecho_client.c를 각각 만들고 컴파일하여 실행 파일을 만든다.
- server용 터미널에서 uecho_server 코드를 먼저 실행하고, client용 터미널에서 uecho_client 코드를 실행한다.
- server:
./uecho_server 9190
(port 번호는 임의로) - client:
./uecho_client 127.0.0.1 9190
(서버에서 적은 포트 번호)
- client 터미널에서 message를 입력하여 해당 message가 echo 되는 것을 확인한다.
- client 터미널에서 Q를 입력하면 client 코드가 종료되는 것을 확인한다.
- 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); }
#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); }
✅ connected uecho server & client 구현
- uecho_client.c 대신 uecho_con_client.c를 이용하여 동일한 방법으로 실행한다.
- echo_client.c, uecho_client.c, uecho_con_client.c 소스 코드를 비교하여 차이점을 찾는다.
[echo_client.c, uecho_client.c, uecho_con_client.c의 차이점 분석]
- Client 종류:
echo_client.c
uecho_client.c
uecho_con_client.c
- 통신 방식의 차이:
echo_client.c
는 TCP 소켓을 사용하여 서버와의 연결을 설정하고, 데이터를 주고 받는다.
uecho_client.c
는 UDP 소켓을 사용하여 서버와 통신한다.
uecho_con_client.c
는 UDP 소켓을 사용하며,connect()
함수를 사용하여 소켓을 연결하고 있다. 하지만 UDP는 연결 지향형이 아닌 비연결형 프로토콜이므로, 실제로 연결이 수립되는 것이 아니라 주소를 목적지 주소로 설정하는 것뿐이다.
- 데이터 송수신 방식의 차이:
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); }
✅ multicast 방식의 chatting program 구현
- 하나의 terminal에서 디렉토리를 하나 생성한다.
- 디렉토리 안에 multicast.c를 만들고 컴파일하여 실행파일을 만든다.
- 추가로 2개의 터미널을 연다.
- 각 터미널에서 실행파일을 동작한다.
./multicast IP# Port# name
→ IP: 239.0.3.3 / Port: 3000 / name은 채팅 시 사용할 영문 이름
- 3개의 터미널에서 채팅을 테스트한다. 하나의 터미널에서 입력한 메시지가 다른 터미널 모두 동시에 출력되는지를 확인한다.
- 종료를 원하면 해당 터미널에서
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); } } } }
Share article