[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