본문 바로가기
프로그래밍/C

윈도우 소켓 ] UDP 서버, 클라이언트

by eteo 2022. 12. 27.

 

📝 UDP 소켓과 TCP 소켓의 차이점

 

TCP 소켓 (연결지향형 SOCK_STREAM)

  • 중간에 데이터가 소멸되지 않는다.
  • 전송 순서대로 데이터가 수신된다.
  • 데이터의 경계가 존재하지 않는다.
  • 소켓 대 소켓의 연결은 반드시 1 대1의 구조이다.

 

 

UDP 소켓 (비 연결지향형 SOCK_DGRAM)

  • Flow Control 이 없다. 그러므로 SEQ, ACK 같은 메시지 전달을 하지 않는다.
  • 연결의 설정과 해제의 과정이 존재하지 않는다.
  • 데이터 분실 및 파손의 위험이 존재한다.
  • 데이터 전송이 빠름
  • 한번에 전송할 수 있는 데이터의 크기가 제한된다.
  • 서버소켓과 클라이언트의 소켓 구분이 없이 하나의 소켓으로 둘 이상의 노드와 데이터 송수신이 가능하다.

 

출처 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍

 

 

 

UDP 송수신 함수

#include <winsock2.h>

int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
// 성공시 전송된 바이트 수, 실패시 SOCKET_ERROR 반환

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
// 성공시 전송된 바이트 수, 실패시 -1 반환

 

 

#include <winsock2.h>

int recvfrom(SOCKET s, char* buff, int len, int flags, struct sockaddr* from, int* fromlen);
// 성공시 수신한 바이트 수, 실패시 SOCKET_ERROR 반환

#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen);
// 성공시 수신한 바이트 수, 실패시 -1 반환

 

 

UDP 소켓은 연결의 개념이 있지 않으므로, sendto() 함수로 데이터를 전송할 때마다 목적지에 대한 정보를 전달해야 하고, recvfrom() 함수로 데이터 수신 후 데이터의 전송지가 어디인지 확인할 필요가 있다.

 

 

 

 

 

UDP 에코 서버와 클라이언트 소스코드

 

UDP 서버

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>			//윈속 헤더파일 포함
#pragma comment(lib,"ws2_32")	//윈속 라이브러리 참조

#define _CRT_SECURE_NO_WARNINGS
#define BUF_SIZE 30

void ErrorHandling(const char* message);

int main(int argc, char* argv[])
{
	WSADATA wsadata;
	SOCKET servSock;
	char message[BUF_SIZE];
	int strLen;
	int clntAdrSz;

	SOCKADDR_IN servAdr, clntAdr;
	if (argc != 2)
	{
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)	//윈속 초기화
		ErrorHandling("WSAStartup() error!");

	servSock = socket(PF_INET, SOCK_DGRAM, 0);	// UDP 소켓생성 SOCK_DGRAM
	if (servSock == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error!");

	while (1)
	{
		clntAdrSz = sizeof(clntAdr);
		strLen = recvfrom(servSock, message, BUF_SIZE, 0, (SOCKADDR*)&clntAdr, &clntAdrSz);
		sendto(servSock, "hi\n", 4, 0, (SOCKADDR*)&clntAdr, sizeof(clntAdr));
	}

	closesocket(servSock);
	WSACleanup();		//윈속 해제
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

1. socket() : 소켓 생성

2. bind() : 소켓 주소 할당

3. recvfrom() / sendto() : 데이터 송수신

4. close() : 연결 종료

 

수신한 데이터의 전송지 정보를 그대로 참조하여 데이터를 에코하고 있다.

 

 

UDP 클라이언트

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32")

#define BUF_SIZE 30

void ErrorHandling(const char* message);


int main(int argc, char* argv[])
{
	WSADATA wsadata;
	SOCKET sock;

	char message[BUF_SIZE];
	int strLen;

	SOCKADDR_IN servAdr;
	if (argc != 3)
	{
		printf("Usage : %s <IP> <Port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0)
		ErrorHandling("WSAStartup() error!");

	sock = socket(PF_INET, SOCK_DGRAM, 0);
	if (sock == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = inet_addr(argv[1]);
	servAdr.sin_port = htons(atoi(argv[2]));

	connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));

	while (1)
	{
		fputs("Insert message(q to quit): ", stdout);
		fgets(message, sizeof(message), stdin);
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;

		send(sock, message, strlen(message), 0);
		strLen = recv(sock, message, sizeof(message) - 1, 0);
		message[strLen] = 0;
		printf("Message from server: %s", message);
	}


	closesocket(sock);
	WSACleanup();
	return 0;
}

void ErrorHandling(const char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

✔ UDP는 데이터의 경계가 존재하기 때문에 한번의 recvfrom 함수 호출을 통해 하나의 메시지를 완전히 읽어 들인다.

 

✔ 위 UDP 클라이언트 코드에서는 명시적으로 bind를 하고 있지 않다. 그럼에도 recvfrom 호출시 운영체제에서 어플리케이션에 전달할 UDP 패킷을 식별할 수 있는 이유는 sendto 호출시 운영체제가 소켓에 임의의 로컬 포트 번호를 할당하고 해당 로컬 주소와 암시적으로 바인딩하기 때문이다. 즉, recvfrom 호출 전 sendto가 먼저 호출되면 문제가 없다.

 

 

✔ 위 UDP 클라이언트 코드는 Connected UDP 소켓을 사용하고 있다. Unconnected UDP 소켓과 Connected UDP 소켓의 차이점은 뭘까?

보통 Unconnected UDP 소켓은 다음의 과정을 거친다.

1. UDP 소켓에 목적지 IP와 port번호 등록
2. 데이터 전송
3. UDP 소켓에 등록된 목적지 정보 삭제

하지만 connect() 함수를 사용해서 Connected UDP 소켓으로 만들면 목적지에 대한 정보가 등록이 되므로 TCP 기반에서 사용했던 함수인 send 함수와 recv를 호출하여 송수신할 수 있다. (UDP 소켓에 connect() 호출시 목적지 정보만 등록하고 바로 리턴하며, TCP와 같이 상대 소켓과의 실제 연결을 의미하지는 않는다. 다만, 운영체제는 해당 소켓이 특정한 목적지와 연결되었다고 간주하기 때문에 sendto와 recvfrom대신 send와 recv를 호출할 수 있는 듯 하다.)

 

 

 

출처 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍