1) UDP Echo 프로그램 만들기
- 과제명
UDP Echo 프로그램 만들기
- UDP: 단순히 한 컴퓨터에서 다른 컴퓨터로 전송하는 것.
TCP와의 차이점: 커넥션이 없고, 손실을 감당해야 한다.
- 문제 설명
본 과제는 POSIX Socket API를 사용해 클라이언트-서버 프로그램을 실제 구현하여, 분산 시스템의 실제 동작 원리를 이해하는 것을 목표로 한다.
- POSIX: Portable Operating System InterFace for Unix의 약자.
IEEE에서 지정한 운영체제간 호환성을 유지하기 위한 표준.
POSIX를 준수하는 운영체제는 POSIX를 준수하는 다른 운영체제와 호환되어야 한다.
더 자세하게
: https://velog.io/@bjk1649/POSIX%EB%9E%80
- SOCKET: 프로세스가 네트워크 세계로 데이터를 내보내거나, 그 세계로부터 데이터를 받기 위한 실제적인 창구 역할이다. 프로세스가 데이터를 보내거나 받기 위해서는 반드시 소켓에 데이터를 써보내거나, 소켓으로부터 데이터를 읽어들여야 한다.
소켓은 플랫폼마다 다르다. 크게 POSIX socket와 WinSock가 있다. 전자는 BDS sockets = 버클리 소켓으로도 잘 알려져 있다. 여기에서 소켓은 파일로 취급되며, 소켓에 파일에서 사용되는 함수들을 사용해도 잘 작동한다.
소켓은 세 가지를 포함한다.
- 프로토콜: 어떤 시스템이 다른 시스템과 통신을 원활하게 수용하도록 해주는 통신규약.
- IP: 고유의 식별주소.
- 포트: 네트워크에서, 통신을 위해 호스트 내부적으로 할당받아야 하는 고유한 숫자. 한 호스트 내에서 네트워크 통신을 하고 있는 프로세스를 식별하기 위해 사용되는 값.
=> 즉, 소켓은 떨어져 있는 두 호스트를 연결해주는 인터페이스이고, 데이터를 주고받을 수 있는 구조체이다. 최종적으로 소켓을 통해 데이터 통로가 만들어진다.
이때 소켓은 서버 소켓과 클라이언트 소켓으로 구분된다.
- 서버 소켓: 클라이언트 소켓의 연결 요청을 대기한다. 연결 요청이 오면, 클라이언트 소켓을 생성해 통신을 가능하게 한다.
- socket(): 소켓 생성.
- bind(): ip, port 번호를 설정.
- listen(): 클라이언트 접근 요청 수신 대기열을 만듦. 몇개의 클라이언트를 대기시킬지 결정한다.
- accept(): 클라이언트 연결.
- 클라이언트 소켓: 실제로 데이터 송수신을 발생시킨다.
- socket(): 가장 먼저 소켓을 연다.
- connect(): 통신할 서버의 설정된 ip, port 번호에 통신을 시도한다.
- 통신을 시도한다면, 서버가 accpet()를 사용해 클라이언트의 socket descriptor(=소켓 번호)을 반환한다.
- 이를 통해 서버와 클라이언트가 서로 read(), write()를 사용해 통신한다.
소켓 종류는 두 가지가 있다.
- 스트림(TCP)
- 양방향으로 바이트 스트림을 전송하며, 연결 지향성을 갖는다.
- 오류 수정, 전송 처리, 흐름 제어를 보장한다.
- 송신된 순서에 따라 중복되지 않게 데이터를 수신한다 -> 오버헤드가 발생한다.
- 소량의 데이터보다 대량의 데이터 전송에 적합하다.
- 데이터그램(UDP)
- 비 연결형 소켓이다.
- 데이터의 크기에 제한이 있다.
- 확실하게 전달이 보장되지 않고, 데이터가 손실되더라도 오류가 발생하지 않는다.
- 실시간 멀티미디어 정보를 사용하기 위해 주로 사용된다.
HTTP 통신과 SOCKET통신을 비교해본다.
- HTTP 통신: 클라이언트의 요청이 있을 때만 서버가 응답해 해당 정보를 전송하고 곧장 연결을 종료하는 방식이다.
- 클라이언트가 요청을 보내는 경우에만 서버가 응답하는 단방향 통신이다.
- 서버로부터 응답을 받은 후에는 연결이 바로 종료된다.
- 실시간 연결이 아닌, 필요한 경우에만 서버로 요청을 보내는 상황에 적합하다.
- 요청을 보내 서버의 응답을 기다리는 애플리케이션의 개발에 주로 사용된다.
- SOCKET 통신: 클라이언트와 서버가 특정 port를 통해 실시간으로 양방향 소통을 한다.
- 클라이언트와 서버가 계속 연결을 유지하는 양방향 통신이다.
- 서버와 클라이언트가 실시간으로 데이터를 주고받는 상황이 필요한 경우에만 사용된다.
- 실시간 동영상 스트리밍, 온라인 게임 같은 경우에 자주 사용된다.
소켓 함수에 대해서 설명한다.
- SOCKET socket(int af, int type, int protocol);
- 소켓을 생성한다.
- int af: Adress Family의 약자. 주로 AF_INET(IPv4)을 사용한다.
- int type: 소켓의 종류 지정. SOCK_STREAM, SOCK_DGRAM 두 가지가 가장 많이 사용된다. 0을 넣으면, 타입에 맞게 자동 지정된다.
- int protocol: 이 소켓에서 사용되는 별도의 protocol이 필요할 경우 protocol 파라미터로 지정한다. 그런 것들이 없는 일반적인 경우 보통 0을 전달한다.
- struct sockaddr {uint16_t sa_family; char sa_data[14];}
- 송수신 과정에서 필요한 발신자, 수신자에 대한 정보를 주고받기 위한 구조체이다.
- uint16_t sa_family: 주소의 종류 = af의 값과 동일하다.
- char sa_data[14]: 데이터를 담고 있다.
- bind(SOCKET socket, const sockaddr* address, int address_len);
- 어떠한 소켓을 사용해 통신하기 위해서는, 소켓을 한 엔드포인트에 묶어둘 필요가 있다. 즉, 소켓에 주소를 할당하고 연결하는 역할.
- SOCKET socket: 생성한 소켓을 넣어준다.
- const sockaddr* address: 생성한 소켓 구조체를 넣어준다. 소켓에 ip, port 번호를 묶어주는 역할.
- int address_len: 포인터로 넣어준 구조체의 크기를 알려준다.
+ UDP의 경우: 비연결 - 패킷을 송수신 할 때마다 어디로 보내는 패킷인지 정보를 넘겨줄 필요가 있다.
- int sendto(SOCKET sock, const char* buf, int len, inf flags, const sockaddr* to, int tolen);
- int recvfrom(SOCKET sock, const char* buf, int len, inf flags, const sockaddr* from, int* fromlen);
더 자세하게:
https://lads.tistory.com/75
https://recipes4dev.tistory.com/153
https://helloworld-88.tistory.com/215
- POSIX Socket API: https://docs.oracle.com/cd/E19048-01/chorus5/806-6897/architecture-9/index.html
- 요구 사항
주어진 server.c의 while (!quit) {} 루프문 안에
1) read request의 type을 read reply로 변경하고
2) value 필드 를 “DDDCCCCBBBBAAAA”로 변경한 후
3) 클라이언트에 이를 전송하는 코드를 추가하세요.
- util.h
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <time.h>
// Constants
#define KEY_SIZE 16 // 사용할 KEY 크기이다. 16바이트.
#define VALUE_SIZE 16 // 사용할 VALUE 크기이다. 32바이트.
#define DATASET_SIZE 100000 // 데이터셋 크기
#define SET_SIZE 62 // 가능한 문자들의 수 (예: 영문 대소문자 + 숫자)
const char SET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// 가독성을 위해 메시지 타입별로 매크로를 만듬
#define READ_REQ 0
#define READ_REP 1
#define WRITE_REQ 2
#define WRITE_REP 3
struct KVS { // key value store 구조체
uint8_t type; // message type
char key[KEY_SIZE]; // key
char value[VALUE_SIZE]; // value
} __attribute__((packed));
- client.c
#include "util.h"
int SERVER_PORT; // 서버 포트번호
const char* dst_ip = "127.0.0.1"; // 하나의 host안에서 통신할 것이므로 서버주소는 localhost(i.e., 127.0.0.1)임
// 임의의 key를 생성해서 반환해줌
void generate_key(char* key) {
uint64_t number = rand() % DATASET_SIZE;
for (int i = 0; i < 5; ++i) number = ((number << 3) - number + 7) & 0xFFFFFFFFFFFFFFFF;
key[KEY_SIZE - 1] = '\0';
for (int i = KEY_SIZE - 2; i >= 0; i--) {
int index = number % SET_SIZE;
key[i] = SET[index];
number /= SET_SIZE;
}
}
int main(int argc, char *argv[]) {
// 프로그램 시작시 입력받은 매개변수를 parsing한다.
if ( argc < 2 ){ // 반드시 포트번호를 입력받아야하므로, argument가 없다면 에러를 띄운다.
printf("Input : %s port number\n", argv[0]);
return 1;
}
srand((unsigned int)time(NULL)); // 난수 발생기 초기화
/* 서버 구조체 설정 */
int SERVER_PORT = atoi(argv[1]); // 입력받은 argument를 포트번호 변수에 넣어준다.
struct sockaddr_in srv_addr; // 패킷을 수신할 서버의 정보를 담을 소켓 구조체를 생성한다.
memset(&srv_addr, 0, sizeof(srv_addr)); // 구조체를 모두 '0'으로 초기화해준다.
srv_addr.sin_family = AF_INET; // IPv4를 사용할 것이므로 AF_INET으로 family를 지정한다.
srv_addr.sin_port = htons(SERVER_PORT); // 서버의 포트번호를 넣어준다. 이 때 htons()를 통해 byte order를 network order로 변환한다.
inet_pton(AF_INET, dst_ip, &srv_addr.sin_addr); // 문자열인 IP주소를 바이너리로 변환한 후 소켓 구조체에 저장해준다.
/* 소켓 생성 */
int sock; // 소켓 디스크립터(socket descriptor)를 생성한다.
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { // socket()으로 IPv4(AF_INET), UDP(SOC_DGRAM)를 사용하는 소켓을 생성 시도한다.
printf("Could not create socket\n"); // sock으로 return되는 값이 -1이라면 소켓 생성에 실패한 것이다.
exit(1);
}
int n = 0;
struct KVS RecvMsg={0,}; // 수신용으로 쓸 메시지 구조체 생성 및 초기화
struct KVS SendMsg={0,}; // 송신용으로 쓸 메시지 구조체 생성 및 초기화
struct sockaddr_in src_addr; // 패킷을 수신하였을 때, 해당 패킷을 보낸 송신자(Source)의 정보를 저장하기 위한 소켓 구조체
socklen_t src_addr_len = sizeof(src_addr); // 수신한 패킷의 소켓 구조체 크기를 저장함. IPv4를 사용하므로 sockaddr_in 크기인 16바이트가 저장됨.
int cnt = 0; // 패킷 5개를 전송한다.
while(cnt < 5){
printf("Request ID: %d\n",cnt++);
SendMsg.type = READ_REQ; // 요청 타입을 읽기로 선언한다.
/*주의: 문자열의 마지막은 null-terminator \0가 들어가야 한다. 즉, 4바이트 문자열은 ABCD가 아니라 ABC로 보임. 실제 저장된 것은 ABC\0 */
generate_key(SendMsg.key); // key를 새로 생성한다.
strncpy(SendMsg.value, "AAAABBBBCCCCDDD", VALUE_SIZE-1); // value필드에 미리 생성해둔 value 값을 복사한다.
SendMsg.value[VALUE_SIZE - 1] = '\0'; // 명시적으로 널 터미네이터 추가
// strncpy는 \0을 자동으로 추가하지 않고, strcpy는 \0을 자동으로 추가해준다.
//strcpy(SendMsg.value,"AAAABBBBCCCCDDD");
printf("type: READ_REQ Key: %s Value: %s\n",SendMsg.key,SendMsg.value); // 생성한 key와 value를 출력해본다.
sendto(sock, &SendMsg, sizeof(SendMsg), 0, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); // 생성한 메시지를 서버로 송신한다.
n = recvfrom(sock, &RecvMsg, sizeof(RecvMsg), 0, (struct sockaddr *)&src_addr, &src_addr_len); // 서버로부터 답장을 수신한다.
if (n > 0) { // 만약 송신한 데이터가 0바이트를 초과한다면 (즉, 1바이트라도 수신했다면)
//printf("Received bytes: %d, Length: %d\n",n,src_addr_len);
char* type;
if(RecvMsg.type == READ_REQ) type ="READ_REQ";
else if(RecvMsg.type == READ_REP) type ="READ_REP";
else if(RecvMsg.type == WRITE_REQ) type ="WRITE_REQ";
else type ="WRITE_REP";
printf("Type: %s Key: %s Value: %s\n",type,RecvMsg.key, RecvMsg.value); // 수신한 내용을 출력한다.
}
}
close(sock); // 소켓을 닫아준다.
return 0;
}
- server.c
#include "util.h"
int SERVER_PORT; // 서버 포트번호
static volatile int quit = 0; // Trigger conditions for SIGINT
void signal_handler(int signum) {
if(signum == SIGINT){ // Functions for Ctrl+C (SIGINT)
quit = 1;
}
}
int main(int argc, char *argv[]) {
// 프로그램 시작시 입력받은 매개변수를 parsing한다.
if ( argc < 2 ){
printf("Input : %s port number\n", argv[0]);
return 1;
}
signal(SIGINT, signal_handler); // SIGINT에 대한 핸들러 등록
SERVER_PORT = atoi(argv[1]); // 입력받은 argument를 포트번호 변수에 넣어준다.
// 서버의 정보를 담을 소켓 구조체 생성 및 초기화
struct sockaddr_in srv_addr;
memset(&srv_addr, 0, sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(SERVER_PORT);
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 i.e., 자기 자신의 IP
// 소켓을 생성한다.
int sock;
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf("Could not create listen socket\n");
exit(1);
}
// 생성한 소켓에 소켓구조체를 bind시킨다.
if ((bind(sock, (struct sockaddr *)&srv_addr, sizeof(srv_addr))) < 0) {
printf("Could not bind socket\n");
exit(1);
}
int n = 0;
struct KVS RecvMsg={0,}; // 수신용으로 쓸 메시지 구조체 생성 및 초기화
struct sockaddr_in src_addr; // 패킷을 수신하였을 때, 해당 패킷을 보낸 송신자(Source)의 정보를 저장하기 위한 소켓 구조체
socklen_t src_addr_len = sizeof(src_addr);
while (!quit) {
n = recvfrom(sock, &RecvMsg, sizeof(RecvMsg), 0, (struct sockaddr*)&src_addr, &src_addr_len);
if (RecvMsg.type == READ_REQ) RecvMsg.type = READ_REP;
strcpy(RecvMsg.value, "DDDCCCCBBBBAAAA");
sendto(sock, &RecvMsg, n, 0, (struct sockaddr*)&src_addr, sizeof(src_addr));
}
printf("\nCtrl+C pressed. Exit the program after closing the socket\n");
close(sock);
return 0;
}