외로운 Nova의 작업실

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

Programming/C

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

Nova_ 2022. 4. 4. 18:20

안녕하세요. 저번시간까지 소켓프로그래밍을 배웠습니다.

배운 소켓프로그래밍으로 이제 멀티 채팅 콘솔앱을 만들어보겠습니다.

보낸 소켓에 이름을 부여하고 그 소켓이 메시지를 보내오면 서버쪽에서 이름과 함께 메시지를 연결된 클라이언트에게 같이 보내는 서버를 구현하도록 하겠습니다.

이후에는 c#의 winform을 활용해서 ui까지 가지고있는 winform앱도 만들어보겠습니다.

먼저 이름을 부여하지않은 멀티 채팅의 원리는 이러합니다.

1. 서버가 클라이언트를 받는다.

2. 부모 서버는 받은 클라이언트를 클라이언트만 모아놓은 소켓 배열에 추가한다.

3. 이후 받은 클라이언트를 처리하는 쓰레드를 만들고 클라이언트를 쓰레드에 맡겨놓는다.

4-1. 쓰레드는 클라이언트로부터 recv하고 만약 0이 전달되면 클라이언트만 모아놓은 소켓배열에서 없애고 소켓을 닫고 쓰레드를 종료한다.

4-2. 만약 0이 아닌 값이 전달되면 그 값을 연결되어있는 클라이언트들에게 다 보낸다.

5. 1~4를 반복한다.

 

위를 한번 코드적으로 구현해보겠습니다.

관련 함수와 구조체, 자료형등은 저의 소켓프로그래밍 공부 2장에 다 있습니다.

먼저 서버 코드입니다.

//멀티 채팅 쓰레드 기반 서버
#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); //클라이언트로부터 메시지 받음
		
		if (messLen < 0) { //만약 클라이언트가 종료한다고 햇을경우
			WaitForSingleObject(hMutex, INFINITE);

			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;
				}
			}
			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);

}

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

//멀티 쓰레드 기반 채팅 클라이언트
#include <stdio.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <string.h>
#include <process.h>
#define BUF_SIZE 1024
void ErrorHandling(char message[]);
unsigned WINAPI SendMsg(void *arg); //쓰레드가 호출할 함수
unsigned WINAPI RecvMsg(void *arg); //쓰레드가 호출할 함수

SOCKET hostSocket; //클라이언트의 소켓 생성

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

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

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

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

	WaitForSingleObject(hThread1, INFINITE);
	printf("send 쓰레드 종료\n");
	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);
	}
	closesocket(hostSocket);
	exit(1);
}

unsigned WINAPI RecvMsg(void *arg) {

	char buff2[BUF_SIZE];

	while (1) {

		recv(hostSocket, buff2, BUF_SIZE, 0);

		printf("%s\n", buff2);
	}

	return 0;
}

아래는 실행 화면입니다.

먼저 서버 화면입니다.

아래는 클라이언트3개의 화면입니다.

이로써 콘솔앱버전 making을 맞치도록 하겠습니다.

다음엔 c#을 활요해 winform으로 채팅 서버와 클라이언트를 구현해보도록 하겠습니다.

-----------------------------------------------------------------------------------------------------------------------------

이번에 개발하면서 한가지알게된점이있다.

소켓의 생성도 중요하지만 종료도 중요하다는 점이다.

위와같은 코드를 짜서 클라이언트에서 종료하면 서버측에서 재대로된 종료가 안된다.

WSAGetLastError 함수로 서버쪽에서 제대로 종료됫는지 보면 10053이나 10054오류가 뜬다.

뭐 방화벽 오류라고는 하는데... 어떻게해야 소켓이 제대로 종료되는지 궁금하다.

recv함수로 소캣의 종료를 알 수 있는데 이는 아래와같다.

"소켓이 연결 지향적이고 원격 측이 연결을 정상적으로 종료하고 모든 데이터가 수신된 경우 recv 는 수신된 0바이트로 즉시 완료됩니다. 연결이 재설정 된 경우 WSAECONNRESET 오류와 함께 recv 가 실패합니다 ."

라고 ms에 적혀있다.

그럼 정상적으로 종료한다는것은 무슨 의미일까?

Comments