코딩/Network

[네트워크] UDP 서버-클라이언트

나야, 웅이 2023. 6. 5. 23:37
728x90
반응형
SMALL

목차

1. UDP 서버-클라이언트 구조

2. UDP 서버-클라이언트 분석

 

 

 

UDP 서버-클라이언트 구조

 

TCP와 UDP 공통점

  • 전송 계층 프로토콜
  • 포트 번호를 이용해 주소를 지정
    • 두 으용 프로그램이 TCP나 UDP를 이용해 통신하려면 반드시 포트 번호를 결정해야함
  • 데이터 오류를 체크
    • TCP와 UDP는 헤더는 물론, 데이터에 대한 오류도 체크
    • Cf) IP계층은 헤더의 오류만 체크 = 단편화

 

UDP의 특징

  • 연결 설정을 하지 않으므로 connect() 함수가 불필요함
  • 프로토콜 수준에서 신뢰성 있는 데이터 전송을 보장하지 않으므로, 필요하다면 응용 프로그램 수준에서 신뢰성 있는 데이터 전송 기능을 구현해야 함
  • 간단한 소켓 함수 호출 절차만 따르면 다자간 통신을 쉽게 구현할 수 있음
  • TCP와 달리 응용 프로그램이 데이터 경계구분을 위한 작업을 별도로 할 필요가 없음

 

TCP와 UDP의 차이점

 

 

UDP 서버-클라이언트 동작 원리

  • 멀티스레드 등의 기법을 사용하지 않고도, 한 소켓으로 여러 클라이언트를 처리 가능
  • 클라이언트는 별도의 연결설정 없이 곧바로 서버와 통신

 

  • 두 번째 클라이언트도 별도의 연결설정 없이 곧바로 서버와 통신
  • TCP 서버와 다르게 UDP 서버는 소켓을 한 개만 사용

 

UDP 에코 서버-클라이언트 (실습) - server

#include "../../common.h"


#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char* argv[])
{
	int retval = -1;

	//	initialize winsock
	WSADATA wsa;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
		return 1;

	//	create socket
	//	SOCK_DGRAM
	//	IPPROTO_UDP
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (INVALID_SOCKET == sock)
		err_quit("socket()");

	//	bind socket
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = bind(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	if (retval == SOCKET_ERROR)
		err_quit("bind()");

	//	TCP에서는 listen() / UDP에선 없음
	printf("I'm running!!!\n");

	//	data communication
	struct sockaddr_in clientaddr;
	int addrlen;
	char buf[BUFSIZE + 1];

	while (1)
	{
		//	TCP에서는 accept() / UDP에서는 없음
		//	recvfrom()
		addrlen = sizeof(clientaddr);
		retval = recvfrom(sock, buf, BUFSIZE, 0, (struct sockaddr*)&clientaddr, &addrlen);
		//	인자 :    소켓, 받을 버퍼, 버퍼크기, 옵션, 클라이언트 주소 정보, 주소길이
		//	retval = 수신한 데이터 길이

		//	recv error
		if (SOCKET_ERROR == retval)
		{
			err_display("recvfrom()");
			break;
		}

		//	print the received data
		//	수신한 buf의 retval(수신한데이터길이) = 수신한 데이터의 마지막에 널문자 넣음
		buf[retval] = '\0';
		char addr[INET_ADDRSTRLEN] = { 0, };
		inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
		printf("[UDP/%s:%d] %s\n", addr, ntohs(clientaddr.sin_port), buf);

		//	send data
		//	sendto()
		retval = sendto(sock, buf, retval, 0, (struct sockaddr*)&clientaddr, sizeof(clientaddr));
		//	인자 :				retval 데이터길이			clinet 정보
		if (SOCKET_ERROR == retval)
		{
			err_display("sendto()");
			break;
		}
	}

	//	close socket
	closesocket(sock);

	//	close winsock
	WSACleanup();

	return 0;
}

 

UDP 에코 서버-클라이언트 (실습) - client

#include "../../common.h"

char* SERVERIP = (char*)"127.0.0.1";
#define SERVERPORT 9000
#define BUFSIZE    512

int main(int argc, char* argv[])
{
	int retval = -1;

	//	get IP address from users
	if (argc > 1)
		SERVERIP = argv[1];

	//	initialize winsock
	WSADATA wsa;
	if (0 != WSAStartup(MAKEWORD(2, 2), &wsa))
		return 1;

	//	create socket
	//	SOCK_DGRAM
	//	IPPROTO_UDP
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if (INVALID_SOCKET == sock)
		err_quit("socket()");

	// TCP에서 connect() 부분
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	inet_pton(AF_INET, SERVERIP, &serveraddr.sin_addr);
	serveraddr.sin_port = htons(SERVERPORT);

	//	데이터 통신에 사용할 변수 초기화
	struct sockaddr_in peeraddr;
	int addrlen = 0;
	char buf[BUFSIZE + 1] = { 0, };
	int len = 0;

	while (1)
	{
		//	get data to send
		printf("\n[data to send] ");
		if (fgets(buf, BUFSIZE + 1, stdin) == NULL)
			break;

		//	remove null terminate character
		len = (int)strlen(buf);
		if ('\n' == buf[len - 1])
			buf[len - 1] = '\0';
		//	마지막 엔터 \n 문자를 \0 null로 대체

		//	no data to send
		if (0 == strlen(buf))
			break;

		//	send data
		retval = sendto(sock, buf, (int)strlen(buf), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
		//	       sock, 보낼버퍼, 버퍼크기, 옵션,           보낼 서버 주소 정보,    서버 주소 크기
		if (SOCKET_ERROR == retval)
		{
			err_display("sendto()");
			break;
		}
		printf("[UDP client] %d bytes sent.\n", retval);

		//	receive data
		addrlen = sizeof(peeraddr);
		retval = recvfrom(sock, buf, BUFSIZE, 0, (struct sockaddr*)&peeraddr, &addrlen);
		//									 recvfrom() 하면서 받아온 주소 정보
		if (SOCKET_ERROR == retval)
		{
			err_display("recvfrom()");
			break;
		}

		//	print the received data
		if (memcmp(&peeraddr, &serveraddr, sizeof(peeraddr)))
		{
			printf("[ERROR] invalide data\n");
			break;
		}

		buf[retval] = '\0';
		printf("[UDP client] receive %d bytes.\n", retval);
		printf("[received data] %s\n", buf);
	}

	//	close socket
	closesocket(sock);

	//	close socket
	WSACleanup();

	return 0;
}

 

 

 

 

 

 

UDP 서버-클라이언트 분석

 

 

응용프로그램이 통신을 수행하기 위해 결정해야 할 요소 (TCP와 동일)

  • 프로토콜 : 통신규약으로, 소켓을 생성할 때 결정
  • 지역 IP주소와 지역 포트 번호 : 서버 또는 클라이언트 자신의 주소
  • 원격 IP주소와 원격 포트 번호 : 서버 또는 클라이언트가 통신하는 상대의 주소

 

소켓 데이터 구조체

 

 

 

UDP 서버-클라이언트 모델

 

UDP 서버

1. socket()으로 소켓 생성 -> 프로토콜 지정

2. bind()로 지역 IP주소와 지역 포트 번호를 결정

3. client가 보낸 데이터를 recvfrom()으로 수신

(이 때 원격 IP와 원격 포트 번호, 즉 client의 주소를 알 수 있음)

4. 받은 데이터를 처리한 결과를 sendto()로 보냄

5. 모든 작업을 마치면 closesocket()으로 소켓 닫음

 

 

UDP 클라이언트

1. socket()으로 소켓 생성 -> 프로토콜 지정

2. sendto()로 서버에 데이터 전송

(이 때, 원격 IP주소와 원격 포트 번호와 지역 IP 주소와 지역 포트 번호 결정)

3. 서버가 처리하여 보낸 데이터를 recvfrom()으로 수신

4. 모든 작업을 마치면 closesocket()으로 소켓 닫음

 

connect()와 send(), recv()를 사용할 수는 있음

 

 

 

 

sendto() 함수

  • 응용 프로그램 데이터를 운영체제의 송신 버퍼에 복사함으로써 데이터를 전송
  • 소켓의 지역 IP주소와 지역 포트 번호가 아직 결정되지 않은 상태라면 운영체제가 자동으로 결정
int sendto(
  [in] SOCKET         s,
  [in] const char     *buf,
  [in] int            len,
  [in] int            flags,
  [in] const sockaddr *to,
  [in] int            tolen
);

[in] s

소켓

[in] buf

전송할 데이터를 포함하는 버퍼에 대한 포인터

[in] len

buf의 길이(바이트)

[in] flags

호출이 이루어지는 방식을 지정하는 플래그

[in] to

보낼 대상 소켓에 대한 구조체

[in] tolen

보낼 대상 소켓 to가 가리키는 주소크기(바이트)

 

 

  • UDP, TCP에서도 사용가능 (TCP에서 사용하는 경우 addr과 addrlen은 무시해도됨)
  • snedto()로 보낸 데이터는 독립적인 UDP 데이터그램으로 만들어져 전송되며, 수신측에서는 recvfrom()을 한 번 호출하여 데이터를 읽을 수 있음 = 메시지 경계를 구분할 필요 없음
  • sendto()를 호출할 때 한 번에 보낼 수 있는 최대 데이터 크기는 65507bytes = ( 65535 - 20(IP헤더) - 8(UDP헤더) ) **512 bytes 권장
  • sendto() 리턴 $ /neq $ 전송보장

 

sendto() 함수 사용 예

 

 

recvfrom() 함수

  • 운영체제의 수신 버퍼에 도착한 데이터를 응용 프로그램 버퍼에 복사
  • UDP 패킷 데이터를 한 번에 하나만 읽을 수 있음
int recvfrom(
  [in]                SOCKET   s,
  [out]               char     *buf,
  [in]                int      len,
  [in]                int      flags,
  [out]               sockaddr *from,
  [in, out, optional] int      *fromlen
);

[in] s

바인딩된 소켓

[out] buf

받은 데이터에 대한 버퍼

[in] len

buf의 길이

[in] flags

연결된 소켓에 대해 지정된 옵션 이상으로 함수 호출 동작을 수정하는 옵션

[out] from

반환 시 원본 주소를 정보를 가지고 있는 sockaddr 구조체에 대한 포인터

[in, out, optional] fromlen

form 매개 변수가 가리키는 버퍼의 크기에 대한 포인터

 

 

recvfrom() 함수 사용 예

728x90
반응형
LIST