프로그래밍/2024 네트워크 프로그래밍

11. udp 클라이언트/서버 통신 살펴보기

데일리 백수 2025. 1. 21. 22:36
#include "mylib.h"
#define BUFFER_SIZE 512
void handle_error(char *);
int main(int argc, char *argv[])
{
	int sockFd;
	struct sockaddr_in serverAddress, clientAddress;
	socklen_t clientAddressLen;
	char buffer[BUFFER_SIZE];
	int nRecv;
	if(argc != 2){
		printf("Usage: %s <port>\n", argv[0]);
		exit(1);
	}
	if((sockFd = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
		handle_error("socket() error.");	
	memset(&serverAddress, 0, sizeof(serverAddress));
	serverAddress.sin_family = AF_INET;
	serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddress.sin_port = htons(atoi(argv[1]));
	// sleep(10);
	if(bind(sockFd, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) < 0)
		handle_error("bind() error");
	for( ; ; ) {
		// sleep(10);
		clientAddressLen = sizeof(clientAddress);
		if((nRecv = recvfrom(sockFd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&clientAddress,&clientAddressLen)) < 0)
			handle_error("recvfrom() error.");
		buffer[nRecv] = 0; // Necessary for printf()
		printf("Message received from client: %d bytes, %s", nRecv, buffer);
		fflush(stdout);
	if(sendto(sockFd, buffer, nRecv, 0, (struct sockaddr *)&clientAddress, clientAddressLen)< 0)
 		handle_error("sendto() error.");
	}
	close(sockFd);
	return 0;
}
void handle_error(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

먼저 서버부터 살펴보도록 하겠습니다.

많이 등장한 부분에 대한 내용은 간단하게 설명하고 넘어가겠습니다.

Udp 통신에 사용할 소켓을 정의하고, 서버와 클라이언트 주소 정보를 저장할 구조체 변수와, 클라이언트 주소 구조체의 크기를 저장하는 변수를 선언해줍니다.

또한, 클라이언트로부터 수신한 메시지를 저장할 버퍼를 선언해주고, 수시된 바이트 수를 저장하는 변수도 생성하여 줍니다.

실행시 포트 번호를 인수로 받았는지 확인하는 부분입니다. 인수가 없으면 프로그램을 종료합니다.

Socket 메소드를 통해 udp소켓을 생성합니다. Sock_stream이 아닌, sock_dgram 옵션을 주어 udp소켓을 생성합니다.

serverAddress 구조체를 초기화 한 후, ip주소와 포트번호를 설정합니다. 

bind함수를 통해 서버 소켓을 특정 ip주소와 포트에 바인딩합니다.

자세히 살펴보아야 할 메시지 수신 및 송신 루프 부분입니다. 먼저 clientaddresslen변수를 초기화 해, 송신자의 주소 정보를 저장할 준비를 마친 후, recvfrom을 통해 수신된 바이트 수를 nRecv변수에 저장하고 있습니다. 

살펴 보아야 할 점은 tcp통신처럼 connection socket을 별도로 생성하지 않고, 맨 처음 생성해둔 소켓을 통해 데이터를 수신합니다. 이는 udp 통신의 특성때문인데, 클라이언트의 연결 요청을 받아들이기 위해 별도의 연결 소켓을 생성할 필요가 없습니다. 각 패킷에는 송신자의 주소 정보가 포함되어 있으며, 서버는 recvfrom함수를 통해 이 정보를 얻어. 데이터 핸들링에 사용합니다. 여기서는 해당 클라이언트로  받은 문자열을 반환해주고 있습니다. 

그 후 수신된 메시지를 문자열로 처리하기 위해 널문자를 버퍼에 추가해주고, 콘솔에 출력합니다. 또한 fflush를 통해 메시지를 즉각적으로 출력합니다.(출력 버퍼를 강제로 비우기) 엔터입력 감지를 못했을 경우, 출력이 안될 수 있기 때문입니다.

콘솔에 출력 후, sendto 메소드를 이용해 메시지를 전송한 클라이언트에 다시 메시지를 전송해주고 있습니다. 살펴 보아야 할 점은 tcp에서는 accept소켓을 이용하여 진행했으나. Udp 통신에서는 연결 설정없이 데이터를 전송하기 때문에 sendto에서 데이터 전송과 목적지 송신까지 이루어 지는 것을 확인 할 수 있습니다. 

슬립에 관하여 

바인드 전에 sleep을 걸고, 그 사이 클라이언트가 데이터를 송신하면, 서버는 수신할 수 없습니다. 당연히 주소 바인딩이 되기 전이라 패시브 소켓의 기능을 수행하지 못하기 때문입니다.

2번째 sleep은 메시지응답을 지연시킬 뿐, 데이터를 수신할 수는 있습니다. 리시브 버퍼는 bind 후에 생성되며, 프로그램이 sleep상태에 있더라도 클라이언트에서 전송한 데이터는 손실되지 않고 커널의 수신 버퍼에 저장됩니다. 즉 커널 레벨에서의 수신 버퍼에는 저장이 되지만 애플리케이션 레벨에서 수신은 지연이 발생합니다.

잘 작동하는 것을 볼 수 있습니다. 

 

#include "mylib.h"
#define BUFFER_SIZE 512
void handle_error(char *);
int main(int argc, char *argv[])
{
	int sockFd;
	struct sockaddr_in serverAddress, replyAddress;
	socklen_t replyAddressLen;
	char buffer[BUFFER_SIZE];
	int nRecv;
	if(argc != 3){
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	if((sockFd = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
		handle_error("socket() error.");
	memset(&serverAddress, 0, sizeof(serverAddress));
	serverAddress.sin_family = AF_INET;
	// serverAddress.sin_addr.s_addr = inet_addr(argv[1]);
	if(!inet_pton(AF_INET, argv[1], &serverAddress.sin_addr))
		handle_error("inet_pton() error.");
	serverAddress.sin_port = htons(atoi(argv[2]));
	for( ; ; ) {
		fputs("Type a message (Q/q to quit): ", stdout);
		fgets(buffer, BUFFER_SIZE, stdin);
		if(!strcmp(buffer, "Q\n") || !strcmp(buffer, "q\n"))
			break;
		if(sendto(sockFd, buffer, strlen(buffer), 0, (struct sockaddr *)&serverAddress,sizeof(serverAddress)) < 0)
 			handle_error("sendto() error.");
		replyAddressLen = sizeof(replyAddress);
		if((nRecv = recvfrom(sockFd, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&replyAddress,&replyAddressLen)) < 0)
			handle_error("recvfrom() error.");
		buffer[nRecv] = 0; // Necessary for printf()
		printf("Message received from server: %d bytes, %s", nRecv, buffer);
	}
	close(sockFd);
	return 0;
}
void handle_error(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

다음으로 클라이언트를 살펴보겠습니다.

서버와의 연결에 사용할 소켓을 정의하고, 서버의 주소와 응답을 받을 때 사용하는 임시 주소 구조체를 정의합니다.(서버 주소 구조체 크기를 저장하는 변수도 만듭니다) 사용자 입력 메시지 및 서버 응답 메시지를 저장하는 버퍼와 수신된 바이트 수를 저장하는 변수도 정의해줍니다.

사용자에게 데이터 통신을 진행할 서버 ip와 포트번호를 받았는지 검사합니다.

socket함수를 호출하여 udp소켓을 생성합니다. 역시 SOCK_DGRAM을 사용하는 것을 볼 수 있습니다. 

serverAddress구조체를 초기화 후, 서버ip주소와 포트 번호를 설정합니다. 

서버와 실제 메시지 송신 및 수신을 진행하는 루프입니다. 사용자에게 메시지 입력을 지시하고, 메시지를 입력받아 버퍼에 저장합니다(그 후 프로그램 종료를 검사합니다)

그후 sendto메소드를 이용하여 buffer에 저장된 메시지를 서버로 전송합니다. Udp는 연결을 설정하지 않기 때문에, 전송할 때마다 수신자 주소를 지정해야합니다.

문제없이 서버에 송신이 되었으면 , 서버는 그에 대한 응답 메시지를 전송해줍니다. 

이때 클라이언트에서는 recvfrom을 이용해 서버로부터의 응답 메시지를 수신하여 수신한 데이터를 buffer에 저장하고, 수신받은 바이트 수만큼 nRecv에 저장합니다. 

그 후 출력을 위해 버퍼에 null을 저장해주고, 콘솔에 수신한 내용을 표시합니다. 

사용자로부터 입력을 받아, 서버에 전송하고, 다시 수신받는 모습을 볼 수 있습니다.