외로운 Nova의 작업실

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

Programming/C

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

Nova_ 2022. 4. 5. 17:24

안녕하세요. 저번시간에 이어서 오늘은 이름을 입력받고 그이름과 함께 메시지를 전달하는 기능을 추가해보도록 하겠습니다.

아래는 기능의 원리입니다.

1. 이름을 순차적으로 저장할 2차원 배열을 생성

2. 클라이언트가 첫번째로 메시지를 보내오면 이름이기때문에 클라이언트 번호에 맞게 배열에 저장

3. 클라이언트가 종료하면 소켓 배열에서 없애는것처럼 이름 배열도 시프트연산으로 없애줌

4. 반복

 

c언어에서 문자열을 다루기는 좀 까다롭습니다.

그래서 문자열에 관한 함수들을 좀 정리해 볼까합니다.

#include <string.h>

//문자열 합치는 함수
erron_t  strcat_s ( char * s1 ,size_t size, const char *s2 );
//s2를 s1뒤에 size만큼 저장

//문자열을 복사하는 함수
erron_t  strcpy_s ( char * s1 ,size_t size, const char *s2 );
//s2를 s1에 size만큼 복사

위 함수를 이용해서 코드를 짜면 아래와 같습니다.

먼저 서버 코드입니다.

//멀티 채팅 쓰레드 기반 서버
#include <stdio.h>
#include <WinSock2.h>
#include <Windows.h>
#include <process.h>
#include <ws2tcpip.h>
#include <string.h>
#include <assert.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];// 연결되오있는 소켓들을 담을 소켓 배열 변수 - 임계영역 변수
char names[MAX_CLINT][BUF_SIZE]; //이름을 담을 배열

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 메시지를 담을 버퍼
	char realMessage[BUF_SIZE]; //sendallclnt에게 줄 이름과 함께 있는 진짜 메시지
	SOCKET hostSock = *((SOCKET*)arg); //인자로 받은 소켓포인터를 소켓으로 변환
	int messLen = 0; //메시지길이 변수
	int first = 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]; //그 소켓을 기준으로 다음 소켓들을 왼쪽으로 시프트
						strcpy_s(names[i], BUF_SIZE, names[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]; //그 소켓을 기준으로 다음 소켓들을 왼쪽으로 시프트
						strcpy_s(names[i], BUF_SIZE, names[i + 1]); // 이름 배열도 그 이름을 기준으로 왼쪽으로 시프트
						i++;
					}

					clntNum--;

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

			return 0;

		}
		else if (first == 0) { //클라이언트가 첫번째로 문자열을 보냇을경우 이름등록

			for (int i = 1; i <= clntNum; i++) {

				if (clntSocks[i] == hostSock) {

					WaitForSingleObject(hMutex, INFINITE);
					strcpy_s(names[i],BUF_SIZE,  messageBuf);
					first++;
					ReleaseMutex(hMutex);
					printf("이름 등록 완료 %s, %d\n", names[i], i);
					break;
				}
			}
		} 
		else { //클라이언트가 두번째로 문자열을 보냇을 경우


			for (int i = 1; i <= clntNum; i++) {

				if (clntSocks[i] == hostSock) {

					memset(realMessage, 0, sizeof(realMessage));

					WaitForSingleObject(hMutex, INFINITE);

					strcat_s(realMessage,BUF_SIZE, names[i]);
					strcat_s(realMessage,BUF_SIZE, messageBuf);

					ReleaseMutex(hMutex);

					SendAllClnt(realMessage);

					break;
				}
			}
		}

	}
}

void SendAllClnt(char* message) {

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

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

	}
	ReleaseMutex(hMutex);

}

여기서 주의할점은 문자열끼리는 = 연산자를 써서는 안됩니다.

만약 = 연산자를 쓰면 주소가 문자열의 주소가 복사되기때문입니다.

꼭 strcpy_s()를 써줘야합니다.

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

//멀티 쓰레드 기반 채팅 클라이언트
#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];

	printf("이름을 적어주세요:");

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

	send(hostSocket, buff, BUF_SIZE, 0);

	while (1) {

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

아래는 실행 화면입니다.

먼저 서버쪽 화면입니다.

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

민지가 먼저 연결을 끊고 수진이 hi를 한번 더 보내도 이름은 유효합니다.

이는 이름 배열의 처리가 잘 이뤄짐을 알 수 있습니다.

다음시간에는 다른사람이 연결을 끊으면 클라이언트에게 이사람이 연결을 끊었다고 알려주는 기능을 추가해보도록 하겠습니다.

Comments