외로운 Nova의 작업실

c 멀티 채팅 콘솔 앱 만들기 - 2 본문

Programming/C

c 멀티 채팅 콘솔 앱 만들기 - 2

Nova_ 2022. 4. 5. 13:40

안녕하세요. 저번 시간에 멀티 채팅 콘솔앱을 만들어보았습니다.

하지만 서버쪽에서 소켓의 정상적인 종료가 아니였음을 코딩하던중에 알게되었습니다.

이번시간에는 정상적인 종료가 무엇인지 저의 생각을 얘기하고 해결을 해보도록 하겠습니다.

비정상적인 종료는 클라이언트 쪽에서 문제가 있었습니다.

바로 recv()함수가 블로킹 상태였는데 closesocket()이 호출된것입니다.

 

클라이언트쪽에서 쓰레드가 2개였습니다.

하나는 send쓰레드 하나는 recv쓰레드이였습니다.

종료를 위해 q를 입력하면 send 쓰레드쪽에서 closesocket()을 호출하고 exit(1)을 했습니다.

하지만 위의 과정들이 진행되고있음에도 recv쓰레드에서 recv()함수는 계속 블로킹 상태였습니다.

결국 recv()함수가 블로킹상태임에도 closesocket()이 호출되었습니다.

이는 오류라고 생각한 클라이언트 소켓이 서버에게 비정상적으로 닫혔다고 신호를 보내고, 서버쪽에서 비정상적인 종료로 서버쪽의 recv()함수가 -1을 반환했다고 생각합니다.

 

이를 해결하기 위해서는 recv()함수가 무한블로킹이 안되어야하는데, 이를 해결해줄 recv()의 flag가 없었습니다.

 

이를 해결하기위해서는 select함수를 이용하는 방법이 있었습니다.

이를 활용해서 클라이언트를 다시 작성해보도록 하겠습니다.

//멀티 쓰레드 기반 채팅 클라이언트
#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string.h>
#include <process.h>
#include <Windows.h>
#define BUF_SIZE 1024
void ErrorHandling(char message[]);
unsigned WINAPI SendMsg(void* arg); //쓰레드가 호출할 함수
unsigned WINAPI RecvMsg(void* arg); //쓰레드가 호출할 함수
int check = 3 ; //쓰레드가 종료되었는지 확인하는 변수

SOCKET hostSocket; //클라이언트의 소켓 생성
fd_set recvFd, cpyRecvFd; //recv를 비동기로 처리하기위해 fdset선언
TIMEVAL timeOut; //select timeout구조체

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

	SOCKADDR_IN servAddr; //서버의 주소 구조체 생성
	WSADATA wsaData; //라이브러리 호환 구조체 선언
	HANDLE hThread1, hThread2; //쓰레드 핸들 선언
	int addrSize = sizeof(servAddr);

	FD_ZERO(&recvFd); //fdset 초기화

	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(9190); // 2번째 인자로 포트번호 설정

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

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

	FD_SET(hostSocket, &recvFd);
	timeOut.tv_sec = 2; //2초만 기다림
	timeOut.tv_usec = 0;

	hThread1 = (HANDLE)_beginthreadex(NULL, 0, SendMsg, NULL, 0, NULL); //메시지 보내는 쓰레드 실행
	hThread2 = (HANDLE)_beginthreadex(NULL, 0, RecvMsg, NULL, 0, NULL); //메시지 받는 쓰레드 실행

	check = WaitForSingleObject(hThread1, INFINITE);

	printf("send 쓰레드 종료, %d\n", check);

	WaitForSingleObject(hThread2, INFINITE);
	printf("recv쓰레드 종료\n");

	closesocket(hostSocket); // 소켓 종료

	WSACleanup();
	return 0;
}
//---------------함수 정의---------------------------
void ErrorHandling(char message[]) {
	fputs(message, stderr);
	exit(1);
}
unsigned WINAPI SendMsg(void* arg) {

	char buff[BUF_SIZE];

	while (1) {
		printf("이름과 메시지를 적어주세요 :");

		gets_s(buff, BUF_SIZE);
		if (!strcmp(buff, "q") || !strcmp(buff, "Q"))
			break;

		send(hostSocket, buff, BUF_SIZE, 0);
	}
	return 0;
}

unsigned WINAPI RecvMsg(void* arg) {

	char buff2[BUF_SIZE];

	while (1) {

		cpyRecvFd = recvFd; // recv되는 fdset을 select함수에 줄변수에 cpy
		select(0, &cpyRecvFd, NULL, NULL, &timeOut); //cpyRecvFd에 모아진 소켓들중에 recv된것만 남기고 삭제함, 만약 recv된게 없다면 2초동안 기다리고 아래 코드 진행

		if (FD_ISSET(hostSocket, &cpyRecvFd)) { //만약 남은것들중에 hostSocket이 있으면 아래 코드 실행

			recv(hostSocket, buff2, BUF_SIZE, 0);
			printf("%s\n", buff2);
		}
		if (check == 0) //만약 send쓰레드가 끝낫다면 반복문 break
			break;
	}

	return 0;
}

아래는 서버쪽 코드입니다.

//멀티 채팅 쓰레드 기반 서버
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#include <process.h>
#include <ws2tcpip.h>
#define MAX_CLINT 256
#define BUF_SIZE 1024
void ErrorHandling(char* message);
unsigned WINAPI HandleClnt(void* arg); //쓰레드가 호출 할 함수
void SendAllClnt(char* message); //연결된 모든 클라이언트에게 message를 보내는 함수 
SOCKET clntSocks[MAX_CLINT];// 연결되오있는 소켓들을 담을 소켓 배열 변수 - 임계영역 변수

int clntNum = 0; //연결된 클리이언트의 수에대한 값 - 임계영역 변수

//--------------- 뮤텍스 생성 코드 ------------------------------
HANDLE hMutex; //뮤텍스의 핸들

//---------------------------------------------------------------

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

	//--------------- 구조체 선언 --------------------------------
	WSADATA wsaData;
	SOCKET servSock, clntSock; //서버 소켓과 클라이언트 소켓 선언
	SOCKADDR_IN servAddrIn, clntAddrIn; //서버 주소 구조체, 클라이언트 주소 구조체 선언
	SOCKADDR_IN* pClntAddrIn = &clntAddrIn; //accept 할때 필요한 자료형 선언
	SOCKADDR* pServAddr; //bind할때 필요한 자료형 선언
	//---------------------------------------------------------------

	//--------------- 뮤텍스 생성 코드 -----------------------------
	hMutex = CreateMutex(0, FALSE, NULL);
	//---------------------------------------------------------------



	//--------------- 쓰레드 변수 코드 ------------------------------
	HANDLE hThread;

	//---------------- 변수 선언 ------------------------------------
	int check = 1; //함수가 잘 작동했는지 반환값을 받아서 확인하는 변수
	int clntSize = sizeof(clntAddrIn); //클라이언트 주소체의 크기를 담을 변수
	char addres[BUF_SIZE];
	//--------------------------------------------------------------

	//----------------아래부터는 서버 코드---------------------------
	check = WSAStartup(MAKEWORD(2, 2), &wsaData); //소켓 버전 설정
	if (check != 0)
		ErrorHandling("WSAStartup() Error!");

	memset(&servAddrIn, 0, sizeof(servAddrIn));; //구조체 초기화
	servAddrIn.sin_family = AF_INET;
	servAddrIn.sin_port = htons(9190);
	inet_pton(AF_INET, "127.0.0.1", &(servAddrIn.sin_addr.s_addr));

	pServAddr = (SOCKADDR*)(&servAddrIn); //형변환 후 포인터 저장

	servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); //소켓 생성
	if (servSock == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	check = bind(servSock, pServAddr, sizeof(servAddrIn)); //소켓에 주소 바인딩
	if (check == SOCKET_ERROR)
		ErrorHandling("bind() error!");

	check = listen(servSock, 5); //소켓 리슨 상태로 변경
	if (check == SOCKET_ERROR)
		ErrorHandling("listen() error!");

	while (1) {

		clntSock = accept(servSock, (SOCKADDR*)pClntAddrIn, &clntSize); //클라이언트연결요청 허용
		if (clntSock == INVALID_SOCKET)
			ErrorHandling("accept() error!");

		inet_ntop(AF_INET, &(clntAddrIn.sin_addr.s_addr), addres, BUF_SIZE);
		printf("connected client adress : %s\n", addres);

		WaitForSingleObject(hMutex, INFINITE); //쓰레드와 공유하는 변수들은 임계영역 처리
		clntNum++;
		clntSocks[clntNum] = clntSock;
		ReleaseMutex(hMutex); //임계영역 끝

		printf("클라이언트 추가 완료\n");

		hThread = (HANDLE)_beginthreadex(NULL, 0, HandleClnt, (void*)&clntSock, 0, NULL); //쓰레드 생성 후 시작

	}

	closesocket(servSock);
	WSACleanup();
	return 0;

}
//---------------함수 정의--------------------------------
void ErrorHandling(char* message) {

	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
//---------------쓰레드 함수-----------------------------
unsigned WINAPI HandleClnt(void* arg) {

	char messageBuf[BUF_SIZE]; //recv 메시지를 담을 버퍼
	SOCKET hostSock = *((SOCKET*)arg); //인자로 받은 소켓포인터를 소켓으로 변환
	int messLen = 0; //메시지길이 변수

	while (1) {

		messLen = recv(hostSock, messageBuf, BUF_SIZE, 0); //클라이언트로부터 메시지 받음

		printf("받은 메시지 길이 : %d\n", messLen);

		if (messLen == 0) { //만약 클라이언트가 정상 종료한다고 햇을경우
			WaitForSingleObject(hMutex, INFINITE);
			printf("클라이언트 정상 종료대기\n");

			for (int i = 1; i <= clntNum; i++) { //종료하기때문에 그 클라이언트소켓을 연결된 클라이언트소켓 배열에서 삭제해야함

				if (hostSock == clntSocks[i]) { //만약 클라이언트 소켓배열이 쓰레드가 처리하는 소켓이라면

					printf("disconnected client : %d\n", i);

					while (i < clntNum) {

						clntSocks[i] = clntSocks[i + 1]; //그 소켓을 기준으로 다음 소켓들을 왼쪽으로 시프트
						i++;
					}

					clntNum--;

					break;
				}
			}
			printf("클라이언트 종료 끝\n");
			ReleaseMutex(hMutex);
			closesocket(hostSock);

			return 0;
		}
		else if (messLen < 0) { //만약 클라이언트가 비정상 종료일경우

			WaitForSingleObject(hMutex, INFINITE);
			printf("클라이언트 비정상 종료대기\n");

			for (int i = 1; i <= clntNum; i++) { //종료하기때문에 그 클라이언트소켓을 연결된 클라이언트소켓 배열에서 삭제해야함

				if (hostSock == clntSocks[i]) { //만약 클라이언트 소켓배열이 쓰레드가 처리하는 소켓이라면

					printf("disconnected client : %d\n", i);

					while (i < clntNum) {

						clntSocks[i] = clntSocks[i + 1]; //그 소켓을 기준으로 다음 소켓들을 왼쪽으로 시프트
						i++;
					}

					clntNum--;

					break;
				}
			}
			
			printf("클라이언트 종료 끝\n");
			ReleaseMutex(hMutex);
			closesocket(hostSock);

			return 0;

		}
		else { //클라이언트가 문자열을 보냇을 경우


			SendAllClnt(messageBuf);
		}

	}
}

void SendAllClnt(char* message) {

	WaitForSingleObject(hMutex, INFINITE);
	for (int i = 1; i <= clntNum; i++) {

		send(clntSocks[i], message, BUF_SIZE, 0);

	}
	ReleaseMutex(hMutex);

}

아래는 실행 화면입니다.

처음은 서버쪽 화면입니다.

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

select()함수로 recv를 무한정 블로킹에 빠지지않게 만들고, 소켓의 정상적인 종료로 이끌었습니다.

다음 시간에는 이름을 저장하는 기능을 추가해보도록 하겠습니다.

Comments