외로운 Nova의 작업실

c 소켓프로그래밍 공부 - 12(멀티플렉싱) 본문

Programming/C

c 소켓프로그래밍 공부 - 12(멀티플렉싱)

Nova_ 2022. 3. 31. 16:25

안녕하세요. 저번장에서는 멀티프로세서 기반 다중서버의 원리에대해서 배워보았습니다.

이번 장에서는 다중 서버 구현의 2번째 방법 멀티 플렉싱에대해서 배워보겠습니다.

멀티 플렉싱방법은 멀티프로세서와 다르게 윈도우에서도 구현이 되어있습니다.

따라서 구현되어있는 c언어 수준의 함수들과 사용방법들도 배워보고 구현도 한번 해보겠습니다.

 

먼저 멀티플렉싱이라는 단어가 생소하실 것 이라고 생각합니다.

저번 장에서 배운 멀티 프로세서는 부모 프로세서가 자식 프로세서를 만들어서 자식 프로세서에게 알아서 처리하라고 하는 방법이지만, 멀티 플렉싱방법은 부모프로세서가 다 처리하는 방법입니다.

즉, 사람으로 친다면 손이 2개여서 할 수 있는 일이 한정적이지만 멀티플렉싱을 적용시키면 손을 여러개 만들어주는 것입니다.

간단하게 한 프로세서가 멀티를 가능하게 해준다. 라고 이해하셔도 좋겠습니다.

 

 

다중 접속서버 멀티플렉싱 기반 echo서버의 원리를 살펴보겠습니다.

1. 서버는 요청이 들어오면 알림받을 애들을 정합니다 - ex)서버소켓

2. 서버는 실제로 요청이 들어오면 해당 소켓을 한곳에 모아놓습니다. - ex)서버소켓에 연결요청이 들어왔으니 0xff에 소켓을 저장해놔야지

3. 서버는 모아진 소켓들을 하나씩 처리하는데, 서버소켓이면 accept하고 반환된 클라이언트 소켓을 요청이 들어오면 알림받을 애로 정합니다.

4. 3번에 이어서 모아진 소켓들을 하나씩 처리하는데, 클라이언트 소켓이면 echo를 해주고 알림을 끄겟다고 합니다.

5. 모아진 소켓들을 다 처리했다면 또 요청이 들어와서 모아진 소켓들을 3,4,번에 의하여 처리합니다.

 

이제 원리에 대해서 구현해보겠습니다.

1번 순서에서 알림받을 소켓을 정합니다.를 구현 해 보겠습니다.

알림받을 소켓을 저장할 공간이 필요하겠죠? 그곳은 바로 fd_set자료형 입니다.

fd_set자료형은 소켓을 저장 할 수 있는 공간입니다.

그것도 배열로 되어있어서 여러개를 저장할 수 있습니다.

예를들어서 서버소켓을 저장 할 수 있습니다.

fd_set 자료형은 특별하게 소켓을 저장하고 삭제하는 함수등을 가지고있습니다.

그 함수들과 fd_set의 정의는 아래와 같습니다.

struct fd_set
{
	u_int fd_count; //fd셋에 들어있는 소켓의 갯수
	SOCKET fd_array[FD_SETSIZE] //SOCKET들이 들어있는 배열
}

FD_ZERO(fd_set *fdset) //fd_set의 모든 저장공간을 0으로 초기화한다.
FD_SET(int fd, fd_set *fdset) //fd의 핸들을 fdset에 저장한다.
FD_CLR(int fd, fd_set *fdset) //fd의 핸들을 fdset에서 삭제한다.
FD_ISSET(int fd, fd_set *fdset) //fdset에서 fd가 저장되어있으면 양수를 반환한다.

그럼 다음 2번순서의 실제로 요청이 들어오면 요청 들어온 소켓을 한 곳에 모아놓습니다를 구현해보겠습니다.

그럼 실제로 요청이 들어오면 모아놓는 기능이 있어야하는데 이 기능을 해주는 함수가 있습니다.

바로 select() 함수입니다.

select() 함수에게 알림받을 소켓을 모아논 fd_set자료형을 넘겨주면 이 중 실제로 요청이 들어오는 소켓들을 뺀 나머지들을 전달받은 fd_set에서 직접 지웁니다.

결론적으로는 알림받을 소켓을 fd_set에 모아두고 select()에게 전달하면 fd_set에는 실제 알림받은 소켓들만 남게됩니다.

다.

여기서 중요한점은 select()함수가 fd_set을 변형시키는 점입니다.

따라서 우리가 알림받을 fd_set을 미리 만들어놓고, 그 fd_set의 복사본을 select()함수에 전달해줘야 알림받을 fd_set이 남게됩니다.

아래는 select()함수의 정의입니다. 

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set, *exceptfds, struct timeval *timeout);
//nfds - 알림받을 소켓의 갯수
//fd_set *readfds - 입력버퍼가 왔을때 알림을 받으려고하는 소켓을 모아놓은 fd_set
//fd_set *writefds - 데이터 전송이 가능하다고 알림받으려고하는 소켓을 모아놓은 fd_set
//fd_set *exceptds - 예외상황이 발생했을때 알림받으려고하는 소켓을 모아놓은 fd_set
//struct timeval *time - 무한정 블로킹에 빠지지않도록 타임아웃을 설정하기위한 구조체

//아래는 timeval 구조체의 정의
struct timeval
{
	long tv_sec; //seconds
    long tv_usec; //microseconds
}

그럼다음 3번째,4번째 순서는 이전에 배웠던것들로 구현하시면됩니다.

 

아래는 멀티 플렉싱 기반 echo 다중서버 코드입니다.

//echo 멀티플렉싱 기반 다중 server program code

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

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

	WSADATA wsaData;
	SOCKET servSock, clntSock;//소켓 생성
	SOCKADDR* pServAddr; //bind시 필요한 주소 구조체 포인터 생성
	SOCKADDR clntAddr; //클라이언트 주소 구조체 생성
	SOCKADDR_IN servAddrIn; //주소 구조체 선언
	fd_set read, cpyRead; //소켓을 담아놓을 fd_set 선언
	int check; // 함수들이 잘 작동되는지 체크하는 변수
	TIMEVAL timeOut; //select함수 인자 선언
	char str[1024]; //소켓 문자열 처리 버퍼
	int strLen; //받은 문자열의 길이를 받을 변수
	int clntAddrSize = sizeof(clntAddr); //클라이언트의 주소구조체의 길이 accept()함수에 활용
	
	//아래는 소켓 listen처리
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) //소켓 버전 초기화
		ErrorHandling("WSAStartup() error!");

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

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

	pServAddr = (SOCKADDR*)(&servAddrIn); //bind시 필요한 형변환 진행

	check = bind(servSock, pServAddr, sizeof(SOCKADDR));//bind 실시
	if (check == SOCKET_ERROR)
		ErrorHandling("bind() error!");

	check = listen(servSock, 5); //liten실시
	if (check == SOCKET_ERROR)
		ErrorHandling("listen() error!");

	//아래부터는 fd_set 설정 처리
	FD_ZERO(&read); //알림받을 소켓을 모아놓을 공간 초기화
	FD_SET(servSock, &read); // servSock을 알림받을 소켓을 모아놓은 공간에 넣어줌

	//아래부터는 실제 서버의 동작
	while (1) {
		
		cpyRead = read; //select함수에 들어갈 fd_set 생성
		timeOut.tv_sec = 5; //5초지나면 블로킹 풀리게설정
		timeOut.tv_usec = 5000; //5초 지나면 블로킹 풀리게 설정

		check = select(0, &cpyRead, 0, 0, &timeOut);//입력버퍼에 알림이 온 소켓을 제외한 나머지 소켓 없앰
		if (check == SOCKET_ERROR)
			ErrorHandling("select() error!");

		for (unsigned int i = 0; i < cpyRead.fd_count; i++) { //알림이 온 소켓의 개수만큼 진행

			if (cpyRead.fd_array[i] == servSock) { //만약 알림이 온게 서버 소켓이라면
				
				clntSock = accept(servSock, &clntAddr, &clntAddrSize); //클라이언트소켓을 받음
				
				FD_SET(clntSock, &read); //받은 클라이언트소켓을 알림받겟다고 등록
				printf("connected client : %d // 알림받은 파일의 갯수 : %d\n", i, cpyRead.fd_count);
			}
			else {

				strLen = recv(cpyRead.fd_array[i], str, BUF_SIZE, 0); //클라이언트 소켓의 입력버퍼에있는 값을 가져옴

				if (strLen == 0) { //소켓의 연결종료

					closesocket(cpyRead.fd_array[i]); //소켓 연결 종료
					FD_CLR(cpyRead.fd_array[i], &read); //알림 등록 취소
					printf("closed client : %d\n", i); 
				}
				else { //다시 되돌려서 보내줌

					send(cpyRead.fd_array[i], str, BUF_SIZE, 0); //echo
				}
			}
		}
	}
	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[BUF_SIZE]; //마지막으로 서버에게 보낼 문자열

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

	while (1) {

		printf("보낼 메시지를 입력해주세요 :");
		gets_s(sendMessage, sizeof(sendMessage));

		if (!strcmp(sendMessage, "q") || !strcmp(sendMessage, "Q"))
			break;

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

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

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

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

	}

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

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

아래는 먼저 서버 실행 화면입니다.

아래는 한번에 6개의 클라이언트가 접속후 마지막 6번째 클라이언트가 종료된 사진입니다.

 

서버쪽에서 connected client 의 값이 계속 0입니다.

이는 컴퓨터의 처리속도가 굉장히빨라서 나타는 현상입니다.

메시지를 보내면 1초도 안되서 처리해버리고 select를 기다리고 있을 것입니다.

결국엔 그래서 계속 0이 나타나는 것입니다.

 

이로써 멀티플렉싱 기반 다중 에코 서버의 구현을 마치도록 하겠습니다.

Comments