외로운 Nova의 작업실

c 소켓프로그래밍 공부 - 6(종료의 지연) 본문

Programming/C

c 소켓프로그래밍 공부 - 6(종료의 지연)

Nova_ 2022. 3. 23. 21:06

안녕하세요. 오늘은 지난시간에 이어 half-close()함수에대해 배워보겠습니다.

half-close()함수를 설명하기에 앞서 필요성에대해 설명해보겠습니다.

서버와 클라이언트 사이에 통신할때 아래와 같은 상황이면 서버는 마지막 클라이언트의 문자열을 받지못합니다.

그전에 EOF라는게 있는게 EOF란 End Of File 이라는 뜻입니다.

소켓에서는 closesocket()함수를 사용할때 상대에게 EOF가 전달이됩니다.

그럼 한번 흐름을 봅시다.

1. 클라이언트 ---연결요청 --> 서버

2. 서버 --- 파일데이터 --> 클라이언트

3. 서버 ---EOF(closesocket()) --> 클라이언트

4. 클라이언트 ---"Thank you" --> 서버

 

위 상황에서 3번의 흐름에 서버는 closesocket()을 통해 EOF를 클라이언트에게 전달합니다.

그런데도 클라이언트는 "Thank you"라는 문자열을 서버에게 전달합니다.

하지만 서버는 소켓을 닫은 상태이기때문에 4번흐름의 문자열을 받지못하게됩니다.

이 문제를 해결하기위해서는 EOF를 보내고, 데이터를 보내진 못하지만 데이터를 잠시 받을 수 있게하는 함수가 있습니다.

바로 shutdown()함수입니다.

shutdown()함수의 정의는 아래와 같습니다.

int shutdown(int socket. int howto);
//socket을 howto 모드에 맞춰 종료하는 함수
//아래는 howto에 들어갈 상수
SD_RECEIVE //데이터를 수신하는 스트림 종료
SD_SEND //데이터를 전송하는 스트림 종료
SD_BOTH //데이터를 전송,수신하는 스트림 종료

이 함수를 이용하면 아래와 같은 흐름으로 바꿀 수 있게 됩니다.

1. 클라이언트 ---연결요청 --> 서버

2. 서버 ---파일데이터 --> 클라이언트

3. 서버 ---EOF(shutdown()) -->클라이언트

4. 클라이언트 ---"Thank you" -->서버

5. 서버의 closesocket() 완전한 종료

 

이전과 다르게 서버는 클라이언트의 "Thank You"문자열을 받고 완전히 종료할 수 있습니다.

이를 지연된 소켓 종료라고 한다.

 

그럼 실제로 문제점이 있는 코드를 작성해보고 실습해 보겠습니다.

먼저 아래는 문제점이 있는 서버 코드입니다.

//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, clntSocket; //서버소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddr, clntAddr; //서버 주소 구조체와 클라이언트 주소 구조체 선언
	int clntAddrLen = sizeof(clntAddr); //클라이언트 주소 구주체의 크기 변수 초기화 선언
	WSADATA wsaData; // 라이브러리 버전 구조체 선언
	char message[] = "file data"; // 클라이언트로 부터 받은 메세지 받을 변수
	int sendLen = 0;; //클라이언트에게 보내는 메시지의 길이를 담을 변수



	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!");

	clntSocket = accept(servSocket, (SOCKADDR*)&clntAddr, &clntAddrLen);


	if (clntSocket == INVALID_SOCKET)
		ErrorHandling("accept() error!");

	sendLen = send(clntSocket, message, sizeof(message), 0);

	printf("클라이언트에게 보낸 메시지 : %s\n", message);
	printf("보낸 메시지의 길이 : %d\n", 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 recvMessage[BUF_SIZE]; //서버로부터 오는 문자열을 받을 변수
	int sendLen;
	int recvLen;
	int addrSize = sizeof(servAddr);
	char sendMessage[] = "Thank you"; //마지막으로 서버에게 보낼 문자열
 	

	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!");

	recvLen = recv(hostSocket, recvMessage, BUF_SIZE , 0);
	
	printf("서버로부터 온 메시지 : %s\n", recvMessage);
	printf("메시지 길이 : %d\n", recvLen);

	sendLen = send(hostSocket, sendMessage, sizeof(sendMessage), 0);

	printf("서버에게 보낸 메시지 : %s\n", sendMessage);
	printf("메시지의 길이 : %d", sendLen);

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

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

이제 실행 시켜보도록 하겠습니다.

먼저 서버 프로그램 프롬프트 입니다.

서버는 클라이언트가 접속하면 "file data" 문자열을 보냅니다. 그리고는 closesocket()으로 소켓을 종료해버려 클라이언트의 소켓을 받지못합니다. 애초에 close를 해버리니 recv함수도 쓰지 못합니다.

그러면 클라이언트 프롬프트입니다.

클라이언트는 서버로부터 "file data" 문자열을 받고 그 응답으로 "Thank you"를 보냅니다. 하지만 메시지의 길이가 -1로 보내기에 실패했다는 것을 알 수 있습니다. 

 

그렇다면 shutdown()함수를 이용하여 서버가 EOF를 보내고도 "Thank you" 문자열을 받도록 코드를 작성해보도록 하겠습니다.

 

아래는 문제없는 서버코드입니다.

//문제없는 서버 코드

#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, clntSocket; //서버소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddr, clntAddr; //서버 주소 구조체와 클라이언트 주소 구조체 선언
	int clntAddrLen = sizeof(clntAddr); //클라이언트 주소 구주체의 크기 변수 초기화 선언
	WSADATA wsaData; // 라이브러리 버전 구조체 선언
	char message[] = "file data"; // 클라이언트에게 보낼 메세지 변수
	char recvMessage[BUF_SIZE];
	int sendLen = 0; //클라이언트에게 보내는 메시지의 길이를 담을 변수
	int recvLen = 0; //클라이언트에게 받는 메시지의 길이를 담을 변수



	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!");

	clntSocket = accept(servSocket, (SOCKADDR*)&clntAddr, &clntAddrLen);


	if (clntSocket == INVALID_SOCKET)
		ErrorHandling("accept() error!");

	sendLen = send(clntSocket, message, sizeof(message), 0);

	printf("클라이언트에게 보낸 메시지 : %s\n", message);
	printf("보낸 메시지의 길이 : %d\n", sendLen);
	
	shutdown(clntSocket, SD_SEND);

	recvLen = recv(clntSocket, recvMessage, BUF_SIZE, 0);

	printf("클라이언트에게 받은 메시지 : %s\n", recvMessage);
	printf("받은 메시지의 길이 : %d\n", recvLen);

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

void ErrorHandling(char* message) {

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

shutdown()함수 호출로 EOF보내면서 recv를 한번 해서 "Thank you" 문자열을 클라이언트로부터 받을 수 있게 코딩해 봤습니다.

아래는 실행시킨 서버쪽 프롬프트 사진입니다.

이와 같이 클라이언트로 부터 "Thank you"문자열을 받고 소켓을 종료함을 알 수 있습니다.

아래는 클라이언트 프롬프트 사진입니다.

클라이언트쪽에서도 아까는 -1로 떳던 메시지의길이가 10으로 변경되어 잘 송신했다고 메시지의 길이를 반환합니다.

 

결국 이번 장에서 중요한건 "서버쪽에서 EOF를 보내면서도 recv()나 send()함수 호출을 할 수있는 방법은 shutdown()함수를 이용하면된다"입니다.

 

다음장에서는 도메인 주소와 ip에 관련된 내용을 다루도록 하겠습니다.

Comments