공부

20200901 공부

글로벌디노 2020. 9. 2. 00:07

1. 에코서버 만들기

2. 데이터 전송하기

3. 문제풀이

4. Win32 API 게임 작업

 

 

 

1. 에코서버 만들기

서버 코드

더보기
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

// 서버 프로그램
int main()
{
	// 윈속 초기화
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf_s("error WSAStartup()\n");
		return 1;
	}

	// 소켓생성
	SOCKET servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (servSock == INVALID_SOCKET)
	{
		printf_s("error socket()\n");
		return 1;
	}

	// bind()
	SOCKADDR_IN servAddr;
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(9000);
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

	int ret = bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr));
	if (ret == SOCKET_ERROR)
	{
		printf_s("error bind()\n");
		return 1;
	}

	// listen()
	ret = listen(servSock, SOMAXCONN);
	if (ret == SOCKET_ERROR)
	{
		printf_s("error listen()\n");
		return 1;
	}

	
	// 클라이언트 연결 대기
	SOCKET clientSock;
	SOCKADDR_IN clientAddr;
	int addrLen = sizeof(clientAddr);

	while (1)
	{
		puts("연결 대기...");

		// accept()
		clientSock = accept(servSock, (SOCKADDR*)&clientAddr, &addrLen);
		if (clientSock == INVALID_SOCKET)
		{
			printf_s("error accept()\n");
			break;
		}

		// 연결된 클라이언트 정보 출력
		const int bufSize = 256;
		char buf[bufSize];
		InetNtopA(clientAddr.sin_family, &clientAddr.sin_addr, buf, bufSize);
		
		printf_s("client addr : %s, port : %d \n", buf, ntohs(clientAddr.sin_port));


		// 데이터 수신 및 반송
		while (1)
		{
			ret = recv(clientSock, buf, bufSize, 0);
			if (ret == 0)
			{
				printf_s("connection close\n");
				break;
			}
			else if (ret < 0)
			{
				printf_s("recv failed: %d\n", WSAGetLastError());
				break;
			}

			printf_s("recv size : %d\n", ret);
			buf[ret] = '\0';

			printf_s("%s \n", buf);

			
			ret = send(clientSock, buf, ret, 0);
			if (ret == SOCKET_ERROR)
			{
				printf_s("send failed with error: %d \n", WSAGetLastError());
				break;
			}
		}

		closesocket(clientSock);
	}
	

	closesocket(servSock);

	WSACleanup();
	return 0;
}

 

클라이언트 코드

더보기
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

// 클라이언트 프로그램
int main()
{
	// 윈속 초기화
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf_s("error WSAStartup()\n");
		return 1;
	}

	// 소켓생성
	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET)
	{
		printf_s("error socket()\n");
		return 1;
	}

	// 연결
	SOCKADDR_IN servAddr;
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_port = htons(9000);
	InetPtonA(AF_INET, "127.0.0.1", &servAddr.sin_addr);

	int ret = connect(sock, (SOCKADDR*)&servAddr, sizeof(servAddr));
	if (ret == SOCKET_ERROR)
	{
		printf_s("error connect()\n");
		return 1;
	}

	// 문자열 입력받아서 전송 및 수신
	while (1)
	{
		printf_s("입력) ");
		const int bufSize = 256;
		char buf[bufSize];

		gets_s(buf, bufSize);

		ret = send(sock, buf, strlen(buf), 0);
		if (ret == SOCKET_ERROR)
		{
			printf_s("send failed with error: %d \n", WSAGetLastError());
			break;
		}


		ret = recv(sock, buf, bufSize, 0);
		if (ret == 0)
		{
			printf_s("connection close\n");
			break;
		}
		else if (ret < 0)
		{
			printf_s("recv failed: %d\n", WSAGetLastError());
			break;
		}

		printf_s("recv size : %d\n", ret);
		buf[ret] = '\0';

		printf_s("%s \n", buf);
	}

	closesocket(sock);
	WSACleanup();

	return 0;
}

 

socket() 함수

소켓을 생성함으로써 사용할 프로토콜을 결정한다

 

bind() 함수

소켓의 지역 IP 주소와 지역 포트 번호를 결정한다

 

listen() 함수

소켓의 TCP 포트 상태를 LISTENING으로 바꾼다. 이는 클라이언트 접속을 받아들일 수 있는 상태가 됨을 의미한다

 

accept() 함수

접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성해서 리턴한다. 또한 접속한 클라이언트의 주소 정보도 알려준다

접속한 클라이언트가 없을 경우 accept() 함수는 서버를 대기 상태(wait state 또는 suspended state)로 만든다. 이때 작업관리자를 실행해 CPU 사용률을 확인하면 0으로 표시된다. 클라이언트가 접속하면 서버는 깨어나고 accept() 함수는 비로소 리턴하게 된다

 

connect() 함수

TCP 프로토콜 수준에서 서버와 논리적 연결을 설정한다. 이때 원격 IP 주소와 원격 포트 번호는 물론, 지역 IP 주소와 지역 포트 번호도 결정된다

 

TCP 데이터 전송 함수

send() / recv()

sendto() / recvfrom()

WSASend*() / WSARecv*()

 

send() 함수

응용 프로그램 데이터를 운영체제의 송신 버퍼에 복사함으로써 데이터를 전송한다. send() 함수는 데이터 복사가 성공하면 곧바로 리턴한다. 따라서 send() 함수가 리턴 했다고 실제 데이터가 전송된 것은 아니며, 일정 시간이 지나야만 하부 프로토콜(예를 들면, TCP/IP)을 통해 전송이 완료된다

 

recv() 함수

운영체제의 수신 버퍼에 도착한 데이터를 응용 프로그램 버퍼에 복사한다

 

 

2. 데이터 전송하기

 

데이터 전송시 고려사항

 

1) 경계 구분 (데이터 끝 구분)

TCP처럼 메시지 경계를 구분하지 않는 프로토콜을 사용할 경우에는 응용프로그램 수준에서 메시지 경계를 구분하기 위한 추가 작업을 해야 한다.

 

방법 1) 송신자는 항상 고정 길이 데이터를 보낸다. 수신자는 항상 고정 길이 데이터를 읽는다.

방법 2) 송신자는 가변 길이 데이터를 보내고 끝 부분에 특별한 표시(EOR, End Of Record)를 붙인다. 수신자는 EOR이 나올 때까지 데이터를 읽는다.

방법 3) 송신자는 보낼 데이터 크기를 고정 길이 데이터로 보내고, 이어서 가변 길이 데이터를 보낸다. 수신자는 고정 길이 데이터를 읽어서 뒤따라올 가변 데이터의 길이를 알아내고, 이 길이만큼 데이터를 읽는다.

방법 4) 송신자는 가변 길이 데이터 전송 후 접속을 정상 종료한다. 수신자는 recv() 함수의 리턴 값이 0 (정상 종료) 이 될 때까지 데이터를 읽는다.

 

 

2) 바이트 정렬

서로 다른 바이트 정렬 방식을 사용하는 시스템 사이에서 데이터를 교환할 때 바이트 정렬 방식을 통일하지 않으면 데이터 해석에 문제 발생 (빅엔디안, 리틀엔디안)

 

 

3) 구조체 멤버 맞춤

구조체 (C++의 클래스 포함) 멤버의 메모리 시작 주소를 결정하는 컴파일러의 규칙을 뜻함

#pragma pack 지시자를 사용하거나 프로젝트 속성을 설정하면 기본 구조체 멤버 맞춤 방식 변경 가능

 

 

다양한 데이터 전송 방식 실습

 

1) 고정길이 데이터 전송

서버와 클라이언트 모두 크기가 같은 버퍼를 정의해두고 데이터를 주고 받는다

 

클라이언트 데이터 전송 코드 수정

// 고정 길이 데이터 전송
const int bufSize = 50;
char buf[bufSize];

const char* testData[] =
{
	"안녕하세요",
	"반가워요",
	"오늘따라 할 이야기가 많을 것 같네요",
	"저도 그렇네요"
};

for (int i = 0; i < 4; i++)
{
	memset(buf, '#', bufSize);
	memcpy_s(buf, bufSize, testData[i], strlen(testData[i]));

	ret = send(sock, buf, bufSize, 0);
	if (ret == SOCKET_ERROR)
	{
		printf_s("send failed with error: %d \n", WSAGetLastError());
		return 1;
	}
}

 

서버 데이터 수신 코드 수정

// 사용자 정의함수 생성
int recvn(SOCKET s, char* buf, int len, int flags)
{
	int received;
	char* ptr = buf;
	int left = len;

	while (left > 0)
	{
		received = recv(s, ptr, left, flags);

		if (received == SOCKET_ERROR)
			return SOCKET_ERROR;
		else if (received == 0)
			break;

		left -= received;
		ptr += received;
	}

	return (len - left);
}

// 클라이언트의 데이터 전송길이와 버퍼크기를 맞춘다
// 끝에 '\0' 문자 포함시키기 위해 버퍼크기 + 1 배열 생성
const int bufSize = 50;
char buf[bufSize + 1];

// 데이터 수신
while (1)
{
	ret = recvn(clientSock, buf, bufSize, 0);
	if (ret == SOCKET_ERROR)
	{
		printf_s("recv failed: %d\n", WSAGetLastError());
		break;
	}
	else if (ret == 0)
	{
		printf_s("connection close\n");
		break;
	}

	printf_s("recv size : %d\n", ret);
	buf[bufSize] = '\0';

	printf_s("%s \n", buf);
}

 

실행결과

 

 

 

3. 문제풀이

 

1. new 와 delete 키워드가 하는 일을 순서대로 설명하시오

 

new는 메모리 할당 하고 가상함수 테이블 세팅, 생성자가 있는 자료형이면 생성자를 호출

delete는 소멸자가 있는 자료형이면 소멸자를 호출 후 메모리 해제

 

가상함수가 있는 경우 가상함수

 

 

2. '힙이 깨진다' 는 의미는?

 

할당받은 메모리의 범위를 넘어서는 데이터 수정이 있을 경우, 힙 메모리 관리정보 코드가 변경 될 위험이 있다. 만약 변경 된다면 나중에 힙 프리 (메모리 해제) 시 오류가 발생한다.

심각하면 winAPI 에서 메모리 할당 시에도 오류가 날 수 있다.

 

 

3. 캐시라인을 읽기와 쓰기 용도로 나눠야 하는 이유와 그 방법에 대해 설명하시오

 

참고

https://parksb.github.io/article/29.html

https://jungwoong.tistory.com/42

https://docs.microsoft.com/ko-kr/cpp/cpp/declspec?view=vs-2019

 

 

4. 

Data d1;

Data d2;

// d1 초기값 세팅

// d2 초기값 세팅

d1 = d2;

 

위와 같은 d1 = d2 식에서 32bit와 64bit식의 어셈블리어에서 문제가 있는가? 있다면, 어느 문제가 있는지 아는대로 설명하시오.

 

오류는 아니고 성능이 떨어진다

대입연산 vs  바이트복사

 

 

5. 아래 코드에서 컴파일 에러가 나는 부분과 그 이유는?

const int x = 1;
const int y = 1;
const int* p1 = &x; - (1)
p1 = &y;            - (2)
*p1 = y;            - (3)

(3) 에서 에러

p1 은 값을 변경할 수 없음

 

 

6. Little Endian 의 개념을 설명하고, 16진수와 메모리 형태를 이용하여 예를 들어보시오. ( 0x12345678 은 어떻게 메모리에 들어가는가? 0x100 번지부터 들어간다고 가정)

0x100 = 0x78

0x101 = 0x56

0x102 = 0x34

0x103 = 0x12

 

 

7. 어셈블리어에서 "call 함수이름" 과 같은 형태에서 call이 하는일에 대해 설명하시오.

IP(명령어 포인터) 스택에 백업 후 함수코드 위치로 IP 설정

 

 

8. 32bit 시스템 함수 호출 과정 설명

#include <stdio.h>

int PrintData(int _a, int _b)
{
	int a = _a;
	int b = _b;
	printf("%d %d\n", a, b);

	return a + b;
}


int main()
{
	int a = 1;
	int b = 2;

	int c = PrintData(a, b);

	return 0;
}

PrintData(a, b)  호출 부분

push b    // 오른쪽 인자값부터 전달

push a

call 함수  // 돌아올 IP 스택에 저장후 IP에 PrintData 함수 주소 설정

push ebp        // 되돌아올 함수의 베이스포인터 백업

mov ebp, esp  // 현재 스택포인터 위치를 베이스포인터로 설정

sub esp, 값     // 함수에서 필요로 하는 메모리만큼 스택포인터 위치 이동

함수 동작 후 ...

add esp, ebp   // 스택포인터를 옮기는 것으로 메모리 정리

pop ebp         // ebp에 이전 ebp 값 설정

ret     // IP 돌려놓기

add esp, 8  // 함수 인자값 정리

mov 주소, eax  // 리턴값이 있으면 eax에서 가져오기

 

 

 

4. Win32 API 게임 작업

 

이슈)

CreateWindow 함수 호출시 가로크기 640, 세로크기 480 으로 설정했지만, GetClientRect 호출하면 가로 624, 세로 441 사이즈가 나온다

이 설정은 상단 타이틀바와 좌우하단 윈도우 프레임을 포함한 크기라고 한다

 

해결)

AdjustWindowRect() 함수 사용

RECT wndRect = { 0, 0, 640, 480 };
AdjustWindowRect(&wndRect, WS_OVERLAPPEDWINDOW, FALSE);

int wndWidth = wndRect.right - wndRect.left;
int wndHeight = wndRect.bottom - wndRect.top;

CreateWindowW(
    szWindowClass, 
    szTitle, 
    WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
    CW_USEDEFAULT, 0, 
    wndWidth, wndHeight, 
    nullptr, nullptr, 
    hInstance, nullptr);

참고

https://m.blog.naver.com/darkness_54/140145793791

'공부' 카테고리의 다른 글

20200903 공부  (0) 2020.09.03
20200902 공부  (0) 2020.09.02
20200831 공부  (0) 2020.09.01
24bit BMP 파일 읽어서 윈도우 출력  (0) 2020.08.25
VirtualAlloc 함수를 이용한 Dynamic Array Design  (0) 2020.08.15