dayne의 블로그

[C]서버-클라이언트 Socket 통신 예제 (UDP) 본문

네트워크

[C]서버-클라이언트 Socket 통신 예제 (UDP)

dayne_ 2024. 10. 25. 10:06

목차

1. 소켓 통신 개요

2. 서버 코드

3. 클라이언트 코드

 

 


1. 소켓 통신 개요

출처 : https://blog.naver.com/tlsrka649/223117924160

 

간단한 개요는 TCP Socket 통신 예제 글에서 설명했으니 아래의 링크에서 확인할 수 있습니다.

 

참고 : https://dayne-w.tistory.com/17  

 

1.1 소켓 프로그래밍의 전체적인 과정 (UDP)

UDP 소켓 프로그래밍의 전체적인 흐름은 아래와 같습니다.

  1. 서버는 소켓을 생성하고, IP 주소와 포트 번호를 바인딩한 후 recvfrom() 함수를 호출하여 요청을 대기합니다.
  2. UDP 소켓 프로그래밍에서는 TCP에서와 달리,  연결(connect) 과정이 존재하지 않습니다. 따라서, 클라이언트에서는 sendto() 함수를 통해 서버로 데이터를 전송하고, recvfrom() 함수를 통해 서버로부터 데이터를 받습니다.
  3. 서버에서도, 클라이언트의 요청에 대해 sendto() 로 데이터를 전송합니다.
  4. 이후, 양단이 모두 close 되며 소켓 프로그래밍이 종료됩니다.

 

1.2 UDP 동작

  • UDP에서는 TCP에서 제공하는 순차적인 정보 전달, 흐름 제어 등의 서비스를 제공하지 않습니다.
  • 대신 포트 번호를 사용하여, 적절한 프로세스에게 전달해 주는 서비스와 같은 '전송 계층 프로토콜'이 제공해 주는 서비스 중 필수 서비스만을 제공합니다.
  • TCP는 위에서 말한 추가적인 서비스를 제공하기 위해 TCP 모듈 간의 정보를 공유해야 하고, 이를 위해 결국 네트워크 자원을 소모합니다.
  • 그에 비해서 UDP는 카운터 파트들 사이에 정보를 공유할 필요가 없으므로, 그만큼 자원의 소모가 덜하고 가볍게 동작할 수 있습니다.
  • TCP처럼 ACK 세그먼트로 연결 정보를 확인할 필요가 없기 때문에, UDP는 전송할 데이터가 생기면 그 즉시 상대방 UDP 모듈로 전송을 시도합니다.
  • UDP에서는 송신 측과 수신 측이 상대방에 대한 정보를 가지고 있지 않기 때문에, 데이터 전달 과정에서 상대방의 주소 정보를 포함해야 합니다.
  • 또한 주고받는 데이터를 '바이트 스트림'으로 취급하는 TCP와는 다르게 UDP는 '하나의 데이터그램' 단위로 읽기/쓰기 작업을 진행합니다.

 

※ 소켓 프로그래밍 관련 함수 설명 (UDP)

더보기
  • ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    • 지정한 소켓을 통해 특정 주소로 주어진 데이터를 전송하는 함수
    • 비연결형 UDP 소켓의 특성상, 상대방과 연결을 맺지 않고도 데이터 송신 가능
    • 매개변수
      • 1번째 인자 ( sockfd ) : socket() 함수로 생성된 '전송에 사용할 소켓 파일 디스크립터'를 기입
      • 2번째 인자 ( buf ) : 전송할 데이터를 담고 있는 버퍼의 포인터를 기입
      • 3번째 인자 ( len ) : 전송할 데이터의 크기(바이트 단위)를 기입
      • 4번째 인자 ( flags ) : 전송 플래그를 기입, 특별한 상황에서 플래그 옵션을 지정하며 일반적으로 0으로 설정
      • 5번째 인자 ( dest_addr ) : 데이터를 받을 대상 주소(IP 주소와 포트 정보)를 나타내는 sockaddr 구조체의 포인터를 기입
      • 6번째 인자 ( addrlen ) : dest_addr 구조체의 크기(일반적으로 sizeof(struct sockaddr_in)를 기입
    • 반환 값
      •  0 이상 : 작업이 성공했음을 의미, 전송한 바이트 수를 반환
      • -1 : 작업이 실패했음을 의미, 오류 원인은 errno에 저장됨
  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    • 지정한 소켓으로부터 데이터를 수신하고, 송신자의 주소 정보를 얻는 함수
    • 수신된 데이터는 지정된 버퍼에 저장
    • 매개변수
      • 1번째 인자 ( sockfd ) : 수신에 사용할 소켓 파일 디스크립터입니다. 소켓은 socket() 함수로 생성됩니다.
      • 2번째 인자 ( buf ) : 수신한 데이터를 저장할 버퍼의 포인터입니다.
      • 3번째 인자 ( len ) : 수신할 수 있는 최대 데이터 크기(바이트 단위)입니다.
      • 4번째 인자 ( flags ) : 수신 플래그로, 일반적으로 0으로 설정합니다. 특별한 상황에서 플래그 옵션을 추가할 수 있습니다.
      • 5번째 인자 ( src_addr ) : 데이터를 보낸 송신자의 주소 정보를 저장할 sockaddr 구조체의 포인터입니다. 송신자의 IP 주소와 포트가 포함됩니다. NULL로 설정하면 주소 정보를 저장하지 않습니다.
      • 6번째 인자 ( addrlen ) : src_addr 구조체의 크기를 나타내는 포인터입니다. 함수 호출 전에 socklen_t 변수에 구조체 크기를 설정하고, 함수 호출 후에는 수신한 주소 구조체의 크기로 변경됩니다.
    • 반환 값
      • 0 이상 : 작업이 성공했음을 의미, 수신한 바이트 수를 반환
      • -1 : 작업이 실패했음을 의미, 오류 원인은 errno에 저장됨

 

<서버 구현에 필요한 함수>

  • int socket(int domain, int type, int protocol)
    • 소켓 디스크립터를 생성하는 함수
    • 매개변수
      • 1번째 인자 ( domain ) : AF_UNIX, AF_INET 등 어떤 영역에서 통신할 것인지에 대한 영역을 지정하는 매개변수
      • 2번째 인자 ( type ) : SOCK_STREAM, SOCK_DGRAM 등 TCP / UDP / ... 중에서 어떤 소켓을 사용할 것인지 지정하는 매개변수, 여기서는 UDP를 사용할 예정이므로 SOCK_DGRAM으로 설정
      • 3번째 인자 ( protocol ) : 소켓에서 사용할 프로토콜을 지정하는 것으로, IPPROTO_TCP, IPPROTO_UDP, 0 등을 사용, 0은 type에서 미리 정해진 경우 이를 따른다는 의미
    • 반환 값
      • 0 이상 : 생성된 소켓의 소켓 디스크립터 반환, 정상적으로 소켓 생성했음을 의미
      • -1 : 소켓 생성 실패를 의미
  • int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    • 'socket descriptor'와 'IP 주소/포트 번호'를 결합하여 커널에 소켓을 등록하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : socket() 함수로부터 반환받은 소켓 디스크립터 기입
      • 2번째 인자 ( myaddr ) :서버의 IP 주소 기입
      • 3번째 인자 ( addrlen ) : 주소의 길이 기입
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미
  • ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    • 위에서의 설명과 동일, 클라이언트에게 수신된 데이터를 전송
  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    • 위에서의 설명과 동일, 클라이언트의 데이터를 수신 
  • int close(int sofckfd);
    • 소켓을 닫고 통신을 종료하는 함수
    • 매개변수
      • 1번째 인자 ( sockfd ) : 종료할 socket descriptor 기입
    • 반환 값
      • 0 : 성공했음을 의미
      • -1 : 실패했음을 의미

 

<클라이언트 구현에 필요한 함수>

  • int socket(int domain, int type, int protocol)
    • 위의 설명과 동일, socket 생성
  • ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    • 위의 설명과 동일, 클라이언트에게 수신된 데이터를 전송
  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    • 위의 설명과 동일, 클라이언트의 데이터를 수신
  • int close(int sofckfd);
    • 위의 설명과 동일, 소켓 연결 종료

 

 


2. 서버 코드

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

#define PORT 6001
#define MAX_LEN 1024

int main() {
    // (1)
	int sockfd;
	char buf[MAX_LEN];
	int recv_len, addr_len;
    
    // (2)
	struct sockaddr_in servaddr, cliaddr;

    // (3)
	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		perror("socket ");
		return 0;
	}

    // (4)
	memset(&servaddr, 0x00, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(PORT);

    // (5)
	if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
		perror("bind ");
		return 0;
	}

	printf("waiting for messages\n");

    // (6)
	addr_len = sizeof(cliaddr);
	if ((recv_len = recvfrom(sockfd, buf, MAX_LEN, 0, (struct sockaddr*)&cliaddr, &addr_len)) < 0) {
		perror("recvfrom ");
		return 0;
	}
	
    // (7)
	buf[recv_len] = '\0';

	printf("ip : %s\n", inet_ntoa(cliaddr.sin_addr));
	printf("received data : %s\n", buf);

    //(8)
	sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));

    // (9)
	close(sockfd);

	return 0;
}

 

헤더에 대한 설명은 이전 글인 TCP 소켓 통신에서 확인할 수 있습니다.

참고 : https://dayne-w.tistory.com/17  

 

// (1)
int sockfd;
char buf[MAX_LEN];
int recv_len, addr_len;

 

sockfd는 서버가 클라이언트와 데이터 송수신 시에 사용되는 소켓의 file descriptor가 저장되는 변수입니다. socket() 함수를 호출하여 할당받은 값을 저장합니다.

 

buf는 클라이언트가 송신한 데이터를 저장할 변수입니다.

 

recv_len은 클라이언트로부터 수신받은 데이터의 길이가 저장되는 변수입니다.

addr_len은 cliaddr 구조체의 길이가 저장되는 변수입니다.

 

// (2)
struct sockaddr_in servaddr, cliaddr;

 

struct sockaddr_in 은 소켓 프로그래밍에서 인터넷 주소를 정의할 때 사용하는 구조체입니다.

 

servaddr과 cliaddr은 sockaddr_in 구조체의 인스턴스로, 각각 서버의 IP 주소와 포트 번호, 클라이언트의 주소를 저장하는 데 사용됩니다.

 

// (3)
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror("socket ");
    return 0;
}

 

서버가 클라이언트의 연결을 받기 위해서 소켓을 생성하는 코드입니다.

 

SOCK_DGRAM을 인자로 갖는 socket() 함수를 호출하여, UDP 통신을 위한 소켓을 생성합니다.

소켓이 정상적으로 생성되지 않을 경우 -1이 반환되므로, 이러한 경우에 대해 에러 처리를 진행합니다.

 

// (4)
memset(&servaddr, 0x00, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);

 

sockaddr_in 구조체를 설정합니다.

이 코드를 통해 소켓 프로그래밍에서 서버의 인터넷 주소를 설정할 수 있습니다.

 

1. memset(&servaddr, 0x00, sizeof(servaddr));

  • memset() 함수를 통해 `servaddr` 구조체를 0으로 초기화합니다.
  • 이는 구조체 내의 모든 필드를 안전하게 0으로 설정하여 깨끗한 상태로 시작할 수 있게 해 줍니다.

2. servaddr.sin_family = AF_INET;

  • `sin_family` 필드는 주소 체계(address family)를 설정합니다.
  • `AF_INET`은 IPv4 인터넷 프로토콜을 사용하겠다는 것을 의미합니다.

3. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  • `sin_addr` 필드는 인터넷 주소를 저장합니다.
  • `s_addr`는 실제 IP 주소를 나타냅니다.
  • `htonl(INADDR_ANY)`는 "호스트 바이트 순서"에서 "네트워크 바이트 순서"로 긴 정수를 변환하는 함수입니다.
  • `INADDR_ANY`는 서버가 모든 네트워크 인터페이스를 통해 들어오는 연결을 수락하겠다는 것을 의미합니다. 즉, 서버는 특정 IP 주소에 바인딩되지 않고, 모든 인터페이스의 주소로 들어오는 클라이언트의 연결을 수락할 준비가 됩니다.

4. servaddr.sin_port = htons(PORT);

  • `sin_port` 필드는 포트 번호를 저장합니다.
  • `htons()`는 호스트 바이트 순서에서 네트워크 바이트 순서로 짧은 정수를 변환합니다.
  • `PORT`는 서버가 연결을 수락할 포트 번호입니다.

이렇게 설정된 servaddr 구조체는, 이후 bind() 함수 호출 시에 사용되어, 서버 소켓이 해당 주소와 포트에 바인딩되도록 합니다.

 

// (5)
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
	perror("bind ");
	return 0;
}

 

외부에서 서버로의 '네트워크를 통한 데이터 전송'을 받아들일 준비를 합니다.

 

서버가 자신의 주소 구조체인 servaddr를 사용하여 소켓을 시스템의 포트에 연결합니다.

bind() 함수는 소켓에 IP 주소와 포트 번호를 할당합니다.

이 경우, servaddr 구조체는 서버가 INADDR_ANY를 통해 모든 인터페이스에서 들어오는 데이터를 받아들일 수 있도록 설정됩니다. 이는 서버가 자신의 IP 주소를 명시하지 않고, 어떤 인터페이스의 IP 주소를 통해서든 데이터를 받아들이겠다는 것을 의미합니다.

 

bind() 함수의 호출 결과가 0 미만일 경우, 작업을 실패했다는 의미이므로 이에 대한 에러 처리를 진행합니다.

소와 포트 번호를 할당합니다.

 

// (6)
addr_len = sizeof(cliaddr);
if ((recv_len = recvfrom(sockfd, buf, MAX_LEN, 0, (struct sockaddr*)&cliaddr, &addr_len)) < 0) {
	perror("recvfrom ");
	return 0;
}

 

sockaddr_in 타입 구조체의 인스턴스인 cliaddr의 길이를 addr_len 변수에 저장합니다.

 

이후, recvfrom() 함수를 호출하여, 클라이언트가 송신한 데이터를 읽어오고, 읽어온 데이터의 길이를 recv_len 변수에 저장합니다.

 

함수의 반환 값이 0 미만인 경우, 작업을 실패했다는 의미이기 때문에 이에 대한 에러 처리를 해줍니다.

 

// (7)
buf[recv_len] = '\0';

 

클라이언트로부터 읽어온 데이터의 끝에 '\0'을 넣어 끝을 표시해 줍니다.

 

//(8)
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cliaddr, sizeof(cliaddr));

 

sendto() 함수를 통해 클라이언트에게 데이터를 전송합니다.

 

함수의 반환 값이 -1인 경우, 작업을 실패했다는 의미이기 때문에, 이에 대한 에러 처리를 진행합니다.

 

// (9)
close(sockfd);

 

클라이언트와 데이터 송수신 시에 사용했던 소켓을 해제합니다.

 

 

 

 


3. 클라이언트 코드

서버 측에서 구현한 프로그램과 차이점을 위주로 설명하겠습니다.

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

#define PORT 6001
#define MAX_LEN 1024

int main(int argc, char** argv) {
	// (1)
	int sockfd;
	char buf[MAX_LEN];
	int recv_len, addr_len;
    
    // (2)
	struct sockaddr_in target_addr;

	const char* msg = "hello, network";

	// (3)
	if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		perror("socket ");
		return 0;
	}

	// (4)
	memset(&target_addr, 0x00, sizeof(target_addr));

	target_addr.sin_family = AF_INET;
	target_addr.sin_prot = htons(PORT);

	// (5)
	if (inet_pton(AF_INET, argv[1], &target_addr.sin_addr) < 0) {
		printf("inet_pton");
		return 0;
	}

	// (6)
	addr_len = sizeof(target_addr);

	sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&target_addr, addr_len);

	// (7)
	if ((recv_len = recvfrom(sockfd, buf, MAX_LEN, 0, (struct sockaddr*)&target_addr, &addr_len)) < 0) {
		perror("recvfrom ");
		return 0;
	}

	// (8)
	buf[recv_len] = '\0';

	printf("ip : %s\n", inet_ntoa(target_addr.sin_addr));
	printf("received data : %s\n", buf);

	// (9)
	close(sockfd);

	return 0;
}

 

// (1)
int sockfd;
char buf[MAX_LEN];
int recv_len, addr_len;

 

sockfd는 클라이언트가 서버와 데이터 송수신 시에 사용되는 소켓의 file descriptor가 저장되는 변수입니다. socket() 함수를 호출하여 할당받은 값을 저장합니다.

 

buf는 서버 측에서 송신한 데이터를 저장할 변수입니다.

 

recv_len은 서버로부터 수신받은 데이터의 길이가 저장되는 변수입니다.

addr_len은 target_addr 구조체의 길이가 저장되는 변수입니다.

 

// (2)
struct sockaddr_in target_addr;

const char* msg = "hello, network";

 

struct sockaddr_in 은 서버 예제 코드에서 보았듯이, 소켓 프로그래밍에서 인터넷 주소를 정의할 때 사용하는 구조체입니다.

 

target_addr은 sockaddr_in 구조체의 인스턴스로, 통신할 서버의 IP 주소와 포트 번호를 저장하는 데 사용됩니다.

 

msg 변수에는 서버로 보낼 데이터(문자열)을 저장합니다.

 

// (3)
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror("socket ");
    return 0;
}

 

클라이언트가 서버로 데이터를 전송하기 위해 사용할 소켓을 생성합니다.

이때도 서버 예제와 마찬가지로, SOCK_DGRAM 값을 인자로 줘서 UDP 통신으로 지정합니다.

 

// (4)
memset(&target_addr, 0x00, sizeof(target_addr));

target_addr.sin_family = AF_INET;
target_addr.sin_prot = htons(PORT);

 

 

통신할 서버의 IP 주소와 포트 번호를 저장할 때 사용할 target_addr 구조체의 내용을 0으로 초기화해 줍니다.

 

IPv4를 사용하겠다는 설정과, 포트 번호를 설정해 주는 코드는 서버 예시와 동일합니다.

 

// (5)
if (inet_pton(AF_INET, argv[1], &target_addr.sin_addr) < 0) {
    printf("inet_pton");
    return 0;
}

 

클라이언트 실행 파일 실행 시에, 명령어의 첫 번째 인자로 통신하고자 하는 대상(서버)의 IP 주소를 받도록 로직을 작성하였습니다.

첫 번째 인자로 받은 문자열 형식의 IPv4 주소를 이진 형식으로 변환하고, 이를 target_addr의 sin_addr 필드에 저장합니다.

 

  • 반환값
    • 1인 경우, 작업이 성공했음을 의미합니다.
    • 0인 경우, 변환할 IP 주소가 유효하지 않음을 의미합니다.
    • -1인 경우, 에러가 발생했음을 의미합니다.

 

// (6)
addr_len = sizeof(target_addr);

sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&target_addr, addr_len);

 

target_addr 구조체의 크기를 addr_len 변수에 저장합니다.

 

이후, target_addr의 정보(= 통신하고자 하는 대상, 서버)와, 기존에 설정해 두었던 msg를 매개변수 값으로 가지는 sendto() 함수를 사용하여, 서버로 데이터를 전송합니다.

 

// (7)
if ((recv_len = recvfrom(sockfd, buf, MAX_LEN, 0, (struct sockaddr*)&target_addr, &addr_len)) < 0) {
    perror("recvfrom ");
    return 0;
}

 

통신하고자 하는 대상(target_addr)으로부터 수신한 데이터를 buf에 저장하는 함수 recvfrom()을 호출합니다.

 

수신 작업이 성공적으로 완료되면, 반환 값은 수신한 데이터의 길이가 되고, 이를 recv_len에 저장합니다.

반환 값이 0 미만일 경우, 에러가 발생했음을 의미하기 때문에 이에 대한 처리를 진행합니다.

 

 

// (8)
buf[recv_len] = '\0';

printf("ip : %s\n", inet_ntoa(target_addr.sin_addr));
printf("received data : %s\n", buf);

close(sockfd);

 

서버로부터 수신한 데이터를 저장한 buf의 데이터 끝에 '\0'을 넣어줘서, 데이터의 끝을 표시해 줍니다.

 

이후, inet_ntoa() 함수를 호출하여 통신한 대상(서버)의 이진 형식 IPv4 주소를 문자열 형식으로 변환 후 출력하고, buf에 저장된 서버로부터 수신한 데이터를 출력합니다.

 

이후 close(sockfd)를 통해 서버와 통신 시에 사용했던 소켓을 해제합니다.

 


참고

https://dongwooklee96.github.io/post/2021/08/09/udp-%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D.html

 

UDP 소켓 프로그래밍 | 개발자 이동욱

“출발지 주소를 설정하지 않고 sendto 함수를 호출한 경우, 소켓에 자동으로 IP 주소와 포트번호가 할당된다.”

dongwooklee96.github.io

 

https://limjunho.github.io/2021/05/10/C-socket.html

 

C TCP/UDP socket - limjunho

Summry 본 문서에서는 클라이언트와 서버를 만들어 TCP/UDP Socket 통신을 구현한다. send me email if you have any questions. TCP/IP 통신 함수 사용 순서 Server socket() - socket 생성 만들어진 server_socket 은 단지 socke

limjunho.github.io

 

 

'네트워크' 카테고리의 다른 글

[C]서버-클라이언트 Socket 통신 예제 (TCP)  (0) 2024.10.23
OSI 7계층 와 TCP/IP 계층  (0) 2024.10.22