외로운 Nova의 작업실

c 소켓프로그래밍 공부 - 5(UDP 연결) 본문

Programming/C

c 소켓프로그래밍 공부 - 5(UDP 연결)

Nova_ 2022. 3. 22. 11:43

안녕하세요, 이번시간에는 저번시간에 못했던 문제 해결과 UDP연결 방법에대해서 포스팅 해볼까합니다.

저번 시간에 echo서버 구현중 계속 클라이언트쪽에서 이상한 문자열이 반복되어 나타나는 현상이있었는데요.

그 현상은 바로 send(),recv()함수의 전달 문자열의 크기 때문이였습니다. 저번 코드를 잠깐 보자면,

아래는 서버 코드

(messLen = recv(clntSocket, message, BUF_SIZE - 1, 0)

send(clntSocket, message, BUF_SIZE, 0);

아래는 클라이언트 코드

send(hostSocket, message, strlen(message), 0);

messageLen = recv(hostSocket, recvMessage, BUF_SIZE - 1, 0)

이렇게 작성했는데, 첫번째로 클라이언트서버가 message의 길이 만큼 보냅니다. 그러면 서버쪽에서 BUF_SIZE -1 인 1023 바이트를 받습니다. 이때 만약, 보낸 메시지의 길이가 4바이트엿다면 남은 1019바이트는 쓰레기값으로 등록이 됩니다. 하지만 message에는 '\0'인 null값이 들어있기 때문에 상관없이 서버쪽에서 printf하면 잘 됫던갓입니다. 근데 이걸 서버에서 BUF_SIZE 만큼 보내게됩니다. 그러면 쓰레기값까지 보내게되는데 여기서 문제입니다. 서버에서 BUF_SIZE 바이트 수 만큼 보냈는데 클라이언트쪽에서 BUF_SIZE-1 만큼 받기때문에 남은 바이트수가 앞쪽으로 값이 들어가게되는 것입니다.

결론은 이러합니다.

  1. 소켓 통신에서 보낸 문자열의 크기가 받는 문자열의 크기보다 크면 안됩니다.
  2. send(),recv()는 NULL값도 같이 보냅니다.
  3. strlen의 값은 NULL이전 까지의 길이입니다.

4.fgets()함수로 엔터쳐서 입력받고 send()하면 '\n'값도 들어가게됩니다.

이러한 사실을 바탕으로 다시한번 echo 서버-클라이언트 프로그래밍을 해보았습니다.

항상  BUF_SIZE - 1값을  보내고 받게 설정하였습니다.

먼저 echo서버 코드입니다.

//echo server program code#include <stdio.h>#include <WinSock2.h>void ErrorHandling(char *message);

int main(int argc, char* argv[]) {

#define BUF_SIZE 1024//recv받음 메모리크기 설정

	SOCKET servSocket, clntSocket;//서버소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddr, clntAddr;//서버 주소 구조체와 클라이언트 주소 구조체 선언int clntAddrLen = sizeof(clntAddr);//클라이언트 주소 구주체의 크기 변수 초기화 선언
	WSADATA wsaData;// 라이브러리 버전 구조체 선언char message[BUF_SIZE];// 클라이언트로 부터 받은 메세지 받을 변수int messLen = 0;;//클라이언트로부터 메시지의 길이를 담을 변수int sendLen;

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)//라이브러리 버전 2.2버전 사용 함수
		ErrorHandling("WSAStartup() error!");

	servSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (servSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr));// 서버 주소 구조체 초기화
	servAddr.sin_family = AF_INET;//IPv4주소체계사용
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);//현재 컴퓨터 ip로 서버 열음
	servAddr.sin_port = htons(atoi(argv[1]));// 입력받은 포트로 서버 열음if (bind(servSocket, (SOCKADDR*) &servAddr, sizeof(servAddr)) == SOCKET_ERROR)//서버 소켓을 서버 주소 구조체에 담긴 주소로 연결
		ErrorHandling("bind() error!");

	if (listen(servSocket, 5) == SOCKET_ERROR)//서버 소켓 연결 시작
		ErrorHandling("listen() error!");

	for (int i = 1; i < 6; i++) {//5번만 실행

		clntSocket = accept(servSocket, (SOCKADDR*)&servAddr, &clntAddrLen);//연결요청을 받고 clntSocket 생성if (clntSocket == -1)//만약 실패하면 에러 출력, 정상이라면 몇번째인지 출력
			ErrorHandling("accpt() error!");
		else
			printf("connected cient : %d \\n", i);

		while ((messLen = recv(clntSocket, message, BUF_SIZE - 1, 0)) != 0) {//클라이언트로부터 메시지를받으면 그대로 반환printf("클라이언트로부터 받은 메시지 : %s받은 메시지 길이 : %d\\n", message, messLen);
			sendLen = send(clntSocket, message, BUF_SIZE-1, 0);
			printf("보낸 메시지 : %s보낸 메시지 길이 : %d\\n", message, sendLen);
		}

		closesocket(clntSocket);//메시지를 다 돌려줬으면 소켓 종료
	}

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

void ErrorHandling(char* message) {

	fputs(message, stderr);
	fputc('\\n', stderr);
	exit(1);
}

아래는 클라이언트 코드입니다.

//echo client socket programming#include <stdio.h>#include <WinSock2.h>#include <WS2tcpip.h>#include <string.h>#define BUF_SIZE 1024
void ErrorHandling(char message[]);
int main(int argc, char* argv[]) {

	SOCKET hostSocket;//클라이언트의 소켓 생성
	SOCKADDR_IN servAddr;//서버의 주소 구조체 생성
	WSADATA wsaData;//라이브러리 호환 구조체 선언char message[BUF_SIZE];//콘솔창에서 메세지를 입력받아서 저장할 변수int messageLen;//메세지의길이를 담을 변수char recvMessage[BUF_SIZE];//서버로부터 오는 문자열을 받을 변수int sendLen;

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)//소켓 버전 2.2 설정
		ErrorHandling("WSAData() error!");

	hostSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);// 호스트 소켓 생성if (hostSocket == INVALID_SOCKET)//에러 처리
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr));//통신할 서버 주소 구조체설정
	servAddr.sin_family = AF_INET;//IPv4 주소 체계 사용
	servAddr.sin_port = htons(atoi(argv[2]));// 2번째 인자로 포트번호 설정if (inet_pton(AF_INET, argv[1], &servAddr.sin_addr) != 1)//통신할 서버 주소 구조체의 ip주소 할당
		ErrorHandling("inet_pton() error!");

	if (connect(hostSocket, (SOCKADDR*) &servAddr, sizeof(servAddr)) == SOCKET_ERROR)//서버와 통신 연결 시도
		ErrorHandling("connect() error!");
	else
		puts("connectign....");

	while (1) {

		fputs("input message(Q to Quit) :", stdout);//콘솔창에 q아니면 메시지입력하라고 뜨게함
		fgets(message, BUF_SIZE, stdin);// 입력을 하나 받음if (!strcmp(message, "q\\n") || !strcmp(message, "Q\\n"))//만약 입력한값이 q나Q면 반복문을 끝내고 소켓을 닫음break;

		sendLen = send(hostSocket, message, BUF_SIZE-1, 0);//콘솔창에서 받은 메시지를 서버로 전달printf("보낸 메세지 : %s보낸 메세지 길이 : %d\\n", message, sendLen);

		messageLen = recv(hostSocket, recvMessage, BUF_SIZE - 1, 0);//서버로부터 메시지를 받아서 recvMessage에 담음 크기는 버퍼사이즈-1해야함, 안하면 오버플로발생가능printf("서버로부터 받은 메시지 : %s", recvMessage);
		printf("받은메시지 길이 : %d , ", messageLen);
	}

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

void ErrorHandling(char message[]) {
	fputs(message, stderr);
	exit(1);
}

 

 

이제 UDP방식으로 통신하는 코드를 작성해보도록 하겠습니다. 

먼저 c언어 개발자들이 만든 UDP함수 흐름도를 작성해보겠습니다.

서버입장

1. socket() SOCK_DGRAM, IPPROTO_UDP

2. bind()

3. sendto(),recvfrom()

4. closesocket()

클라이언트입장

1.socket() SOCK_DGRAM, IPPROTO_UDP

2.sendto()/recvfrom()

3.closesocket()

 

UDP방식은 TCP와는 다르게 함수흐름이 흘러갑니다.

또한 UDP방식에서 조심해주셔야할건 sendto()호출갯수와 recvfrom()호출갯수가 같아야합니다.

예를들어 클라이언트가 sendto()를 2번 호출하면 서버에서 recvfrom()을 2번 호출해야 모든 데이터가 받아집니다.

또한 TCP와 가장 큰 차이점은 서버에서는 클라이언트 소켓을 반환하지않고 클라이언트에서는 자신의 소켓에 주소를 할당하지않는다는 점입니다. 둘다 주소가 할당된 소켓이 없음.

이점만 유의하고 2장 맨 밑단에있는 sendto(),recvfrom()함수의 정의를 보면서 코딩하시면됩니다.

 

아래는 UDP기반 echo서버 코드입니다.

 

//echo server program code

#include <stdio.h>
#include <WinSock2.h>
void ErrorHandling(char *message);

int main(int argc, char* argv[]) {

#define BUF_SIZE 1024 //recv받음 메모리크기 설정

	SOCKET servSocket; //서버소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddr, clntAddr; //서버 주소 구조체와 클라이언트 주소 구조체 선언
	int clntAddrLen = sizeof(clntAddr); //클라이언트 주소 구주체의 크기 변수 초기화 선언
	WSADATA wsaData; // 라이브러리 버전 구조체 선언
	char message[BUF_SIZE]; // 클라이언트로 부터 받은 메세지 받을 변수
	int messLen = 0;; //클라이언트로부터 메시지의 길이를 담을 변수
	int sendLen;



	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //라이브러리 버전 2.2버전 사용 함수
		ErrorHandling("WSAStartup() error!");

	servSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (servSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");
	
	memset(&servAddr, 0, sizeof(servAddr)); // 서버 주소 구조체 초기화
	servAddr.sin_family = AF_INET; //IPv4주소체계사용
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //현재 컴퓨터 ip로 서버 열음
	servAddr.sin_port = htons(atoi(argv[1])); // 입력받은 포트로 서버 열음

	if (bind(servSocket, (SOCKADDR*) &servAddr, sizeof(servAddr)) == SOCKET_ERROR) //서버 소켓을 서버 주소 구조체에 담긴 주소로 연결
		ErrorHandling("bind() error!");
	
	while (1) {


		messLen = recvfrom(servSocket, message, BUF_SIZE - 1, 0, (SOCKADDR*) &clntAddr, &clntAddrLen);
		printf("받은 메시지 : %s받은 메시지 길이 : %d\n", message, messLen);

		sendLen = sendto(servSocket, message, BUF_SIZE - 1, 0, (SOCKADDR*) &clntAddr, clntAddrLen);
		printf("보낸메시지 : %s보낸 메시지 길이 : %d\n", message, sendLen);
	}

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

void ErrorHandling(char* message) {

	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

아래는 클라이언트 코드입니다.

//echo client socket programming

#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string.h>
#define BUF_SIZE 1024
void ErrorHandling(char message[]);
int main(int argc, char* argv[]) {

	SOCKET hostSocket; //클라이언트의 소켓 생성
	SOCKADDR_IN servAddr; //서버의 주소 구조체 생성
	WSADATA wsaData; //라이브러리 호환 구조체 선언
	char message[BUF_SIZE]; //콘솔창에서 메세지를 입력받아서 저장할 변수
	int messageLen; //메세지의길이를 담을 변수
	char recvMessage[BUF_SIZE]; //서버로부터 오는 문자열을 받을 변수
	int sendLen;
	int addrSize = sizeof(servAddr);
	

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //소켓 버전 2.2 설정
		ErrorHandling("WSAData() error!");

	hostSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 호스트 소켓 생성

	if (hostSocket == INVALID_SOCKET) //에러 처리
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr)); //통신할 서버 주소 구조체설정
	servAddr.sin_family = AF_INET; //IPv4 주소 체계 사용
	servAddr.sin_port = htons(atoi(argv[2])); // 2번째 인자로 포트번호 설정

	if (inet_pton(AF_INET, argv[1], &servAddr.sin_addr) != 1) //통신할 서버 주소 구조체의 ip주소 할당
		ErrorHandling("inet_pton() error!");

	else
		puts("connectign....");

	while (1) {

		fputs("input message(Q to Quit) :", stdout); //콘솔창에 q아니면 메시지입력하라고 뜨게함
		fgets(message, BUF_SIZE, stdin); // 입력을 하나 받음

		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) //만약 입력한값이 q나Q면 반복문을 끝내고 소켓을 닫음
			break;

		sendLen = sendto(hostSocket, message, BUF_SIZE - 1, 0, (SOCKADDR*)&servAddr, addrSize);
		printf("보낸 메세지 : %s보낸 메세지 길이 : %d\n", message, sendLen);

		messageLen = recvfrom(hostSocket, recvMessage, BUF_SIZE - 1, 0, (SOCKADDR*)&servAddr, &addrSize);
		printf("서버로부터 받은 메시지 : %s", recvMessage);
		printf("받은메시지 길이 : %d\n", messageLen);
	}

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

void ErrorHandling(char message[]) {
	fputs(message, stderr);
	exit(1);
}

아래는 실행시 콘솔창입니다. 위가 서버고 아래가 클라이언트입니다.

이렇듯 UDP기반 서버를 코딩해보았습니다. 그런데 UDP는 데이터를 서로 받거나 줄때 항상 주소를 할당해야함으로 하나의 주소와 계속 데이터를 주고받을때는 효율성이 떨어집니다.

이를 위해 connected UDP 방식이 존재하는데 이는 클라이언트 측면에서의 흐름이 바뀝니다.

원래 클라이언트 UDP흐름은 아래와같습니다.

1. socket() SOCK_DGRAM, IPPROTO_UDP

2. sendto(),recvfrom()

3. closesocket()

하지만 클라이언트 connected UDP흐름은 아래와 같습니다.

1. socket() SOCK_DGRAM, IPPROTO_UDP

2. connect()

3. send(),recv()

4. closesocket()

connect()함수로 소켓에 주소를 할당해주는 것 입니다.

그렇다면 클라이언트 쪽에서 connected UDP로 send를 3번 보내면 서버 클라이언트에서는 몇번 recvfrom()을 해야할까요?

한번 실험해보겠습니다.

클라이언트에서 3개의 스트링배열을 3번에 나눠서 send()하면 서버쪽에서 recvfrom()으로 한번에 받을 수 있게 코드를 짜 봤습니다. 

아래는 서버 코드입니다.

//echo server program code

#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
void ErrorHandling(char *message);

int main(int argc, char* argv[]) {

#define BUF_SIZE 1024 //recv받음 메모리크기 설정

	SOCKET servSocket; //서버소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddr, clntAddr; //서버 주소 구조체와 클라이언트 주소 구조체 선언
	int clntAddrLen = sizeof(clntAddr); //클라이언트 주소 구주체의 크기 변수 초기화 선언
	WSADATA wsaData; // 라이브러리 버전 구조체 선언
	char message[BUF_SIZE]; // 클라이언트로 부터 받은 메세지 받을 변수
	int messLen = 0;; //클라이언트로부터 메시지의 길이를 담을 변수
	int sendLen;



	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //라이브러리 버전 2.2버전 사용 함수
		ErrorHandling("WSAStartup() error!");

	servSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

	if (servSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");
	
	memset(&servAddr, 0, sizeof(servAddr)); // 서버 주소 구조체 초기화
	servAddr.sin_family = AF_INET; //IPv4주소체계사용
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY); //현재 컴퓨터 ip로 서버 열음
	servAddr.sin_port = htons(atoi(argv[1])); // 입력받은 포트로 서버 열음

	if (bind(servSocket, (SOCKADDR*) &servAddr, sizeof(servAddr)) == SOCKET_ERROR) //서버 소켓을 서버 주소 구조체에 담긴 주소로 연결
		ErrorHandling("bind() error!");

		Sleep(23454);

		messLen = recvfrom(servSocket, message, BUF_SIZE - 1, 0, (SOCKADDR*) &clntAddr, &clntAddrLen);
		printf("받은 메시지 : %s받은 메시지 길이 : %d\n", message, messLen);

		sendLen = sendto(servSocket, message, BUF_SIZE - 1, 0, (SOCKADDR*) &clntAddr, clntAddrLen);
		printf("보낸메시지 : %s보낸 메시지 길이 : %d\n", message, sendLen);

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

void ErrorHandling(char* message) {

	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

아래는 클라이언트 코드입니다. connect()함수를 사용해 connected UDP를 구현했습니다.

//echo client socket programming

#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string.h>
#define BUF_SIZE 1024
void ErrorHandling(char message[]);
int main(int argc, char* argv[]) {

	SOCKET hostSocket; //클라이언트의 소켓 생성
	SOCKADDR_IN servAddr; //서버의 주소 구조체 생성
	WSADATA wsaData; //라이브러리 호환 구조체 선언
	char message[BUF_SIZE]; //콘솔창에서 메세지를 입력받아서 저장할 변수
	int messageLen; //메세지의길이를 담을 변수
	char recvMessage[BUF_SIZE]; //서버로부터 오는 문자열을 받을 변수
	int sendLen;
	int addrSize = sizeof(servAddr);

	char str1[] = "hello my name is a\0";
	char str2[] = "my favorit color is red\0";
	char str3[] = "nice meet you\0";

	int str1Len = sizeof(str1);
	int str2Len = sizeof(str2);
	int str3Len = sizeof(str3);
 	

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //소켓 버전 2.2 설정
		ErrorHandling("WSAData() error!");

	hostSocket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); // 호스트 소켓 생성

	if (hostSocket == INVALID_SOCKET) //에러 처리
		ErrorHandling("socket() error!");

	memset(&servAddr, 0, sizeof(servAddr)); //통신할 서버 주소 구조체설정
	servAddr.sin_family = AF_INET; //IPv4 주소 체계 사용
	servAddr.sin_port = htons(atoi(argv[2])); // 2번째 인자로 포트번호 설정

	if (inet_pton(AF_INET, argv[1], &servAddr.sin_addr) != 1) //통신할 서버 주소 구조체의 ip주소 할당
		ErrorHandling("inet_pton() error!");

	if(connect(hostSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	sendLen = send(hostSocket, str1, str1Len, 0);
	printf("보낸 메세지 : %s보낸 메세지 길이 : %d\n", str1, sendLen);
	sendLen = send(hostSocket, str2, str2Len, 0);
	printf("보낸 메세지 : %s보낸 메세지 길이 : %d\n", str2, sendLen);
	sendLen = send(hostSocket, str3, str3Len, 0);
	printf("보낸 메세지 : %s보낸 메세지 길이 : %d\n", str3, sendLen);

	messageLen = recvfrom(hostSocket, recvMessage, BUF_SIZE - 1, 0, (SOCKADDR*)&servAddr, &addrSize);
	printf("서버로부터 받은 메시지 : %s", recvMessage);
	printf("받은메시지 길이 : %d\n", messageLen);

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

void ErrorHandling(char message[]) {
	fputs(message, stderr);
	exit(1);
}

이렇게 코드를 짠다음 실행 시켜봤습니다.

아래는 실행결과입니다. 위는 서버, 아래는 클라이언트입니다.

서버는 하나의 str 즉 send()만 받아졌습니다.

이로써 클라이언트 쪽에서 connected UDP로 구현하여도 서버쪽에서는 send()갯수에따라 recvfrom()을 해야한다는 것을 알 수 있습니다.

 

이번 시간에는 UDP연결에대해서 배웠습니다.

다음시간에는 지연된 소켓 종료에대해서 배워보겠습니다.

Comments