Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (3) TCP 기반 서버 및 클라이언트 - 2

로파이 2021. 3. 28. 17:31

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

TCP 소켓의 입출력 버퍼

 

각 소켓은 입력 버퍼와 출력 버퍼를 관리하고 recv()를 통해 입력 버퍼에서 수신된 데이터를 읽어드리고 send()를 통해 출력 버퍼로 송신할 데이터를 쓴다.

 

소켓을 통한 데이터 수신 처리

 

- 데이터 수신

int recv(SOCKET s, const char* buf, int len, int flags);

- recv함수는 성공 시 수신된 데이터의 바이트 수만큼 반환하고 그렇지 않으면 SOCKET_ERROR를 반환한다.

// 한번에 100 바이트를 수신함
int recvLen = recv(hServSock, message, 100, 0);

위 코드를 통해 사용자가 수신해야할 데이터가 100바이트라면, 100바이트의 데이터가 입력 버퍼에 완전히 수신되어야 한 번에 수신할 수 있다.

 

하지만 네트워크 지연으로 입력 버퍼에 완전한 데이터가 수신되어있지 않을 수 도 있기 때문에 다음과 같이 필요한 양만큼 데이터가 수신될 때까지 recv()를 호출해야한다.

int recvLen = 0;
while(recvLen < 100)
{
	// recv 함수는 성공시 수신된 바이트를 반환한다.
	recvlen += recv(hServSock, &message[recvLen], 100, 0);
}

TCP 프로토콜의 흐름 제어로 입력 버퍼를 초과하여 데이터를 전송하지 않기 때문에 입력 버퍼만 잘 읽어드리는 것으로 완전한 데이터를 수신할 수 있다.

 

어플리케이션 프로토콜의 사용자 정의

 

잘 알려진 파일의 확장자는 (이미지: bmp/png/jpg, 음성 파일: mp3/wav, 메쉬: ply, pcd) 그 데이터에 대한 형식을 약속하고 있다. 예를 들어, 어떤 이미지 데이터는 첫 번째 줄에 이미지 가로와 세로 사이즈에 대한 정의와 픽셀 형식 그 다음 줄부터 차례대로 행을 구성하는 각 열의 픽셀 정보가 차례대로 담겨 있을 수 있다.

 

네트워크를 통해 통신할 데이터도 송신자와 수신자간의 약속된 데이터의 형식이 있어야한다. 가령 구조체로 정의된 어떤 데이터가 같은 정의로 사용되어 멤버만 순서대로 잘 채워 송신하면 수신자는 같은 정의로된 구조체의 바이트 크기만큼 읽어드려 구조체의 정의에 따라 데이터를 수신할 수 있을 것이다.

 

어떤 방식으로든 송신자와 수신자는 보내고자하는 데이터와 수신하고자 하는 데이터에 대한 약속된 프로토콜이 필요하고 이는 실제 어플리케이션에서 정의하여 원할한 정보를 주고 받도록한다. 

 

TCP 서버-클라이언트 예제 - 2

피연산자와 연산자를 수신하여 그 결과를 전송하는 TCP 서버 클라이언트에 대한 어플리케이션 프로토콜을 정의하고 예시를 보자.

 

- 클라이언트에서 서버로의 메세지

1. 첫번째 데이터는 1 바이트 크기로 피연산자 수 N를 정의한다.

2. 각 피연산자 수 N에 대해 4 바이트의 정수형을 담는다.

3. 연산자를 1 바이트 크기로 담는다.

 

- 구현부


// 메세지 전송 부분
/*
	계산기를 위한 어플리케이션 프로토콜 사용자 정의
	피연산자 갯수 N (1 바이트)
	피연산자 (4xN 바이트)
	연산자 종류 (1 바이트)
*/
char message[BUF_SIZE] = {};

int operCnt = 0, offset = 0;
cout << "피연산자 갯수 : ";
cin >> operCnt;
message[offset++] = (char)operCnt;

for (int i = 0; i < operCnt; ++i)
{
	cout << "피연산자 (정수형) :";
	cin >> *((int*)(message + offset));
	offset += 4;
}
cin.get();

cout << "연산자 종류 : ";
cin.read(message + offset, 1);
++offset;

send(hSocket, message, offset, 0);

int result = 0;
if(recv(hSocket, (char*)&result, 4, 0) == SOCKET_ERROR)
{
	printf("Socket Error\n");
}

printf("Operation result %d\n", result);
system("pause");

- 서버에서 클라이언트 메세지

1. 피연산자 수 N을 수신한다.

2. N에 대해 총 4*N 바이트의 정수형 데이터를 수신한다.

3. 연산 결과를 4 바이트의 정수형으로 보낸다.

 

- 구현부

// 메세지를 수신하여 계산하는 부분
// 피연산자 갯수 수신
char opndCnt = 0;
recv(hClntSock, &opndCnt, 1, 0);

int recvLen = 0;
// 총 4*N + 1 바이트를 수신
while (recvLen < 4 * opndCnt + 1)
{
	int recvCnt = recv(hClntSock, &message[recvLen], BUF_SIZE - 1, 0);
	recvLen += recvCnt;
}

const auto calculate = [&]() -> int
{
	// 피연산자
	int* opnds = reinterpret_cast<int*>(message);
	int res = opnds[0], i;
	// 연산자
	char op = message[recvLen - 1];
	switch (op)
	{
	case '+':
		for (i = 1; i < opndCnt; ++i) res += opnds[i];
		break;
	case '-':
		for (i = 1; i < opndCnt; ++i) res -= opnds[i];
		break;
	case '*':
		for (i = 1; i < opndCnt; ++i) res *= opnds[i];
		break;
	}
	return res;
};

// 결과 계산 및 전송
int result = calculate();
int sendLen = send(hClntSock, (char*)&result, 4, 0);
if (sendLen == SOCKET_ERROR)
{
	printf("Socket Error Send()\n");
}
else 
{
	printf("%d bytes sent\n", sendLen);
}

closesocket(hClntSock);

 

참고: 윤성우 저 TCP/IP 소켓 프로그래밍