네트워크에 대한 이해가 너무 부족하다고 생각했기 때문에, 간단하게 나마 네트워크의 기초를 이해해볼 수 있는 프로젝트를 하나 진행해보고 싶었다.
아무래도 채팅서버가 가장 간단하면서 서버의 개념을 잡기 좋은 프로젝트라고 생각하였고, 채팅서버를 하나 만들어보기로하였다.
메시지 입력, 출력, 렌더링 등의 기능을 편하게 구현하기 위해 IMGUI의 기능을 가져와서 활용하였다.
네트워크를 구축하는데 사용되는 라이브러리는 여러가지가 있겠지만, 본인은 Winsock2를 사용하였다.
윈도우에서 기본으로 지원해주는 라이브러리이기 때문에 추가적인 설치나 세팅이 필요하지 않았고 윈도우, 비주얼 스튜디오 환경에서 문제없이 돌아갈 것이라 생각하였기 때문이다.
네트워크 프로그램이기 때문에, 서버와 클라이언트를 분리해서 2개의 프로젝트를 생성해주었다.
서버 구현
먼저, Winsock2 헤더 파일을 추가해주었다.
#include <WinSock2.h>
#include <windows.h>
두 가지를 추가해주면 된다.
또한, Winsock2을 사용하기 위해선, ws2_32.lib 파일을 링크해주어야 한다.
#pragma comment(lib, "ws2_32.lib")
추가로 선언해주었다.
그런데, 여기서 주의해야 할 점이 하나 있다.
#include <windows.h>
#include <winsock2.h>
헤더 파일을 위 순서로 include하면 오류난다...
웃기지만, 사실이다. windows.h를 먼저 include하면 두 헤더파일이 충돌하게 된다.
원인은 무엇일까?
winsock2는 이름 그대로, winsock의 2번째 버전이다.
첫번째 버전인 그냥 winsock도 있다는 뜻이다.
이 winsock.h가 windows.h에 포함되어 있다고 한다.
구버전의 winsock을 추가한 상태에서 신버전의 winsock2를 추가하게 되면 서로 재정의를 하면서 충돌이 나는 것이다.
winsock2.h에는 winsock.h를 막는 코드가 포함되어 있기 때문에, winsock2.h를 먼저 include하게 되면
windows.h안에 있는 winsock.h이 무시된다고 한다. 그래서 순서에 따라 오류가 나는 것이라고 한다.
물론 순서를 계속 맞추는 것이 귀찮기 때문에, 이를 해결할 수 있는 방법이 있다.
#define _WINSOCKAPI_
#include <windows.h>
#include <WinSock2.h>
이렇게 상단에 매크로를 하나 추가하게 되면, 순서에 상관 없이 구버전의 winsock.h를 무시해준다고 한다.
헤더 파일도 추가했고, lib파일도 링크해주었으니 코드를 작성해보았다.
WSADATA Wsa;
int WsaStartResult = WSAStartup(MAKEWORD(2, 2), &Wsa);
if (WsaStartResult != 0)
{
std::cerr << "WSAStartup failed with error code: " << WsaStartResult << std::endl;
return 1;
}
먼저, WSAStartUp을 실행하여 초기화해주었다.
Winsock2의 경우 ws2_32.dll이라는 동적 라이브러리도 사용한다고 한다.
이 동적 라이브러리의 초기값을 설정해주는 역할이 WSAStartUp이라고 한다.
첫 번째 인자는 winsock의 어떤 버전을 사용할 지를 입력하는 것이라는데, 사실 버전의 차이를 구별할 정도의 지식은 아니라서 가장 최신버전인 2,2를 입력해주었다.
버전의 정수부와 실수부를 입력하면 내부에서 버전을 표기하는 방식으로 변환해준다고 한다.
두 번째 인자의 경우, 라이브러리 초기화를 한 뒤 WSADATA 구조체에 몇 가지 유용한 정보를 담아준다고 한다.
소켓은 최대 몇 개까지 열 수 있는가, winsock의 최신 버전이 몇인가 등의 정보가 있다는데 어디다가 써야하는 지는 잘 모르겠다.
WsaStartUp이 0이 아닌 다른 값을 반환했다면, 뭔가 문제가 있는 것이다.
당장 값을 확인해서 구글링을 해보자. 에러코드별 내용이 상세하게 구분되어있다.
SOCKET Server;
Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
이후, SOCKET 구조체를 하나 선언해준 뒤, socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 함수의 반환값을 저장하였다.
첫번쨰 인자는 IPv4를 기반으로 한 인터넷 통신을 하겠다는 의미이며, 두번째 인자는 연결 지향형 통신을 하겠다는 의미이고, 세번째는 TCP 프로토콜을 사용하겠다는 것이다.
근데 생각해보면 IPv4를 사용하며 연결 지향형 통신을 하는게 TCP인데 왜 또 세번째인자에선 TCP냐 UDP냐를 받는 것일까?
이를 찾아보니, 첫번째 인자를 일반적으로 IPv4를 사용하기 때문에 두번째 인자가 SOCK_STREAM이 되면 "반드시 TCP다" 라는 결론이 나오지만 첫번째 인자로 다른 프로토콜 체계를 선택한 경우, 연결 지향형 통신임에도 TCP로 단정할 수 없는 경우가 있다고 한다. 그래서 이를 구체화하기 위해 세번째 인자를 사용하는 것이라고 한다.
SOCKADDR_IN Addr = { 0, };
Addr.sin_addr.s_addr = htonl(INADDR_ANY);
Addr.sin_port = PORT;
Addr.sin_family = AF_INET;
int BindResult = bind(Server, reinterpret_cast<SOCKADDR*>(&Addr), sizeof(Addr));
if (BindResult != 0)
{
std::cerr << "Bind failed. error code : " << BindResult << std::endl;
return 1;
}
아무튼 소켓을 다 만들고, IP 주소와 포트번호를 소켓에 바인딩해주었다.
htonl이 뭔가 찾아봤더니, 호스트에선 리틀앤디안 방식을 사용하지만 네트워크에선 빅앤디안 방식을 사용하기 때문에 이를 변환해주는 함수라고 한다. hton은 host to network 의 준말이며, 마지막 l은 자료형이라고 한다. ( unsigned long )
INADDR_ANY은 하나의 PC에 여러개의 IP가 존재하는 있는 경우, 어떤 IP로 데이터를 수신받아도 처리할 수 있도록 해주는 것이라고 한다.
예를 들어, 컴퓨터 하나에 랜카드가 A, B 2개가 부착되어 있다고 할 때, 만약 소켓에 A의 IP를 정확히 박아버리면 B를 통해
들어오는 데이터는 처리할 수가 없게 된다.
반면, INADDR_ANY를 사용하게 되면 어떤 IP로 데이터가 들어오든 처리할 수 있도록 해준다고 한다.
또한, IP를 정확히 명시하게 되면 다른 컴퓨터에선 코드의 수정 없이는 사용할 수 없게 되는데, INADDR_ANY 매크로를 사용하게 되면 다른 컴퓨터에서도 수정없이 사용할 수 있어서 이식성이 좋아진다고 한다.
마지막으로 bind 결과에 대한 예외처리를 해주자.
소켓에 주소를 묶었으니 이제 연결을 기다릴 차례이다.
int ListenResult = listen(Server, SOMAXCONN);
if (ListenResult != 0)
{
std::cerr << "Listen failed. Error code : " << ListenResult << std::endl;
return 1;
}
std::thread(AcceptClient, std::ref(Server)).detach();
먼저, 연결 대기열을 만들어주자.
Listen 함수는 연결 대기열을 생성해주는 함수라고 한다.
서버에 100명이 연결을 요청한다고 하면, 요청 순서대로 listen이 만들어 놓은 queue에 쌓이게 되는 것이다.
listen의 두번째 인자는 연결 대기열의 길이를 설정할 수 있는 곳인데, SOMAXCONN을 입력하면 알아서 길이를 세팅해준다고 한다.
이후, 연결을 처리하기 위한 AcceptClient 함수를 백그라운드에서 돌려주었다.
해당 함수의 설명은 뒤쪽에서 하도록 하겠다.
이제, 클라이언트의 연결 요청을 처리하는 함수를 만들어야 한다.
먼저, 클라이언트가 서버에 접속 성공했을 때, 클라이언트의 정보를 보관하기 위한 클래스를 하나 만들어보았다.
class Client
{
public:
SOCKET ClientSock = { 0, };
SOCKADDR_IN Addr = { 0, };
int ClientSize = sizeof(Addr);
int Number = -1;
bool bIsDeath = false;
Client(){}
};
먼저, 소켓을 하나 선언하였다.
해당 클라이언트와 통신하기 위한 소켓이다.
클라이언트 별로 다른 소켓이 생성되기 때문에, 클라이언트 별로 소켓을 저장해놓을 필요가 있다.
SOCKADDR_IN은 클라이언트의 인터넷 정보를 저장하는 구조체이다.
서버에서 클라이언트에 대한 요청을 처리한 뒤, 해당 구조체에 정보를 넣어준다.
ClientSize는 서버에서 연결 요청을 처리한 뒤, 정보를 넣어준 구조체의 크기 또한 반환해주는데 이걸 저장해둘 변수이다.
Number는 클라이언트의 인덱스이다. 클라이언트를 편하게 관리하기 위해 클라이언트마다 인덱스를 부여해 줄 것이다.
bIsDeath는 이 클라이언트가 현재 연결이 끊겼는지를 알려주는 변수이다.
이 변수는 채팅서버 구현 과정에서 발견된 문제 하나를 해결하기 위한 임시방편이다.
문제에 대해선 뒤쪽에서 얘기해보도록 하겠다.
이제, 연결을 처리하는 함수를 만들어 보겠다.
std::vector<std::pair<Client, std::string>> Clients;
void AcceptClient(SOCKET& _Socket)
{
int Order = 0;
while (true)
{
Clients.push_back(std::pair<Client, std::string>{Client(), ""});
Clients[Order].first.ClientSock = accept(_Socket, reinterpret_cast<SOCKADDR*>(&Clients[Order].first.Addr), &Clients[Order].first.ClientSize);
Clients[Order].first.Number = Order;
Clients[Order].first.bIsDeath = false;
std::thread(RecvData, Clients[Order].first.ClientSock, Order).detach();
Order++;
}
}
클라이언트 구조체와 이름을 담는 벡터를 하나 선언하였다.
이후, 연결 요청이 들어오면 구조체에 소켓, 인터넷 정보, 인덱스 등을 모두 저장하였다.
그리고 해당 클라이언트와 1:1 통신을 하기 위해 만들어놓은 RecvData라는 함수를 백그라운드에서 돌려주었다.
처음엔 while문을 저렇게 계속 돌리는데 뭐가 제대로 되려나? 싶었는데 accept 함수의 경우 연결 요청이 없으면 그 자리에서 코드 진행을 막아준다고 한다.
위에서 bIsDeath라는 변수를 만들었던 이유가 여기서 나온다.
만약, 클라이언트가 연결이 끊긴다면 그 데이터를 자료구조에서 지우는 것이 일반적이다.
하지만, 벡터의 경우 자료구조 중간의 데이터를 지워버리면 그 뒤에 있는 원소들의 인덱스가 1씩 감소하게 된다.
위에서 멀티스레드 함수를 보면, 인자로 인덱스를 보내고 있다.
RecvData 함수가 최초에 입력받은 인덱스를 기반으로 실행되고 있는데, 중간에 인덱스가 바뀌어 버리면 문제가 발생하는 것이다.. 이를 임시방편으로 해결하기 위해 bIsDeath 변수를 추가하여 연결이 끊겼는지를 확인하여 예외처리를 두었다.
(하지만, 연결이 끊긴 클라이언트의 정보를 계속 가지고 있는 것이 메모리적으로 굉장한 손해이기 때문에 ,추후에는 새로운 접속이 들어오면 벡터를 앞에서부터 탐색하여 bIsDeath가 true인 인덱스의 데이터를 덮어씌우는 식으로 구조를 바꿔 인덱스의 변경 없이 메모리 낭비를 최소화해보려고 한다.)
std::vector<std::string> RecvChats;
std::mutex ChatsMutex;
void AddRecvChat(const std::string& _Chat)
{
std::lock_guard<std::mutex> lock(ChatsMutex);
RecvChats.push_back(_Chat);
}
void SendMsgToAllCLient(int _IgnoreIndex, std::string_view _Msg)
{
for (int i = 0; i < Clients.size(); i++)
{
if (i == _IgnoreIndex || Clients[i].first.bIsDeath == true)
{
continue;
}
send(Clients[i].first.ClientSock, _Msg.data(), sizeof(char) * PACKET_SIZE, 0);
}
}
void RecvData(SOCKET _Socket, int Num)
{
char Buffer[PACKET_SIZE] = { 0, };
recv(_Socket, Buffer, sizeof(Buffer), 0);
Clients[Num].second = Buffer;
AddRecvChat(Clients[Num].second + " join.");
std::string JoinMsg = Clients[Num].second;
JoinMsg += " join.";
SendMsgToAllCLient(Num, JoinMsg);
while (true)
{
ZeroMemory(Buffer, sizeof(Buffer));
int RecvReturn = recv(_Socket, Buffer, sizeof(Buffer), 0);
if (RecvReturn == 0)
{
Clients[Num].first.bIsDeath = true;
AddRecvChat(Clients[Num].second + " leave. \n");
std::string LeaveMsg = Clients[Num].second;
LeaveMsg += " Leave.";
SendMsgToAllCLient(Num, LeaveMsg);
shutdown(Clients[Num].first.ClientSock, SD_BOTH);
closesocket(Clients[Num].first.ClientSock);
break;
}
AddRecvChat(Clients[Num].second + " : " + Buffer + "\n");
std::string SendMsg = Clients[Num].second;
SendMsg += " : ";
SendMsg += Buffer;
SendMsgToAllCLient(Num, SendMsg);
}
}
이는 RecvData 함수의 코드이다.
먼저, 수신한 채팅을 로그에 저장하기 위해 문자열을 담는 벡터 RecvChats를 선언하였다.
멀티스레딩을 사용하는 만큼 RecvChats에 대한 동기화를 달성하기 위해 ChatsMutex라는 이름으로 std::mutex도 하나 선언해주었다.
채팅 메세지를 송신받으면 lock_guard를 이용하여 메모리 영역을 동기화한 뒤, 메세지를 삽입하도록 했다.
SendMsgToAllClient는 수신한 채팅을 그대로 다른 클라이언트들에게 보내주어야 하기 위해 만든 기능이다.
메세지를 보낸 사람을 제외한 나머지 클라이언트에게 메세지를 송신한다.
(채팅은 서버를 통해 이루어지기 때문에, 내가 보낸 채팅을 서버에서 송신해주지 않으면 상대방은 메세지를 받을 수 없다.)
RecvData에선 맨 처음으로 recv를 한다.
recv는 소켓을 통해 클라이언트에게 데이터를 받는 함수이다.
클라이언트 쪽에선 연결되면 처음으로 이름을 송신하도록 하였기 때문에 처음 recv에선 설정된 이름을 받게 된다.
이후, 해당 이름을 자료구조에 넣어주었다.
그리고 해당 이름의 유저가 채팅서버에 접속되었다는 알림을 모든 클라이언트에게 송신해주었다.
그 다음은 반복문을 통해 계속 Recv를 해주었다.
메세지가 들어온다면, "이름 : 메세지" 의 형태로 모든 클라이언트에게 송신해주었다.
그런데 위에 보면 예외처리가 하나 있는데, RecvReturn이 0일때 이다.
Recv함수는 기본적으로 받은 메세지의 길이를 반환해준다. 아무것도 없는 메세지여도 \0 (널) 하나는 저장되어 있기 때문에 최소 1의 길이는 갖게 된다.
Recv함수가 0을 반환할 때는 클라이언트가 접속을 끊었다는 얘기이다.
Recv가 0을 반환하면 해당 유저가 떠났음을 모든 클라이언트에게 공지한 뒤, 반복문을 탈출하여 해당 함수를 종료하고 스레드를 파괴해주었다.
참고로 통신을 끊을 때 shutdown함수와 closesocket함수를 반드시 호출해주어야 한다.
몇 가지 시행착오가 있었는데, 해당 함수를 제대로 호출해주지 않으면 실제론 연결이 끊겼는데 계속 허공에다가 recv를 하는 상황이 발생할 수 있다. 이 떄문에 로그에 빈 문자열이 프레임마다 출력되는 참사가 발생하기도 하였었다.
void PrintLog()
{
ImGui::Begin("Server");
ImGui::Text("Log");
if (RecvChats.size() > 20)
{
EraseRecvChat(RecvChats.begin());
}
for (int i = 0; i < RecvChats.size(); i++)
{
ImGui::Text(RecvChats[i].c_str());
}
ImGui::End();
}
이후, 매 프레임마다 자료구조에 저장된 메세지를 IMGUI윈도우에 출력해주면 로그 출력까지 마무리된다.
본인은 로그가 너무 길어지는 것을 막기 위해 최대 20개까지만 저장하고 출력하도록 하였다.
이로서 서버 구현은 완료되었다.
아래는 코드 전문이다.
헤더파일
더보기
#pragma once #pragma comment(lib, "ws2_32.lib") //Windows.h 에는 WinSock.h(WinSock2.h의 구버전)이 포함되어 있어 중복 정의가 발생. //구버전을 무시하겠다는 의미. #define _WINSOCKAPI_ //이렇게 그냥 WinSock2.h 를 먼저 include 해도 됨. (WinSock2.h 내부에는 WinSock.h의 포함을 막는 코드가 있다고 함) #include <WinSock2.h> #include <windows.h> #include <iostream> #include <thread> #include <vector> #include <string> class Client { public: SOCKET ClientSock = { 0, }; SOCKADDR_IN Addr = { 0, }; int ClientSize = sizeof(Addr); int Number = -1; bool bIsDeath = false; Client(){} }; bool CreateDeviceD3D(HWND hWnd); void CleanupDeviceD3D(); void CreateRenderTarget(); void CleanupRenderTarget(); void PrintLog(); int WindowInit(HINSTANCE& _hInstance); int ServerInit(); void RecvData(SOCKET _Socket, int Num); void AcceptClient(SOCKET& _Socket);
void AddRecvChat(const std::string& _Chat); void EraseRecvChat(const std::vector<std::string>::iterator& _ChatIter);
std::vector<std::pair<Client, std::string>> Clients;
CPP파일
더보기
// WindowsProject1.cpp : 애플리케이션에 대한 진입점을 정의합니다. #define _WINSOCK_DEPRECATED_NO_WARNINGS #pragma comment(lib, "ws2_32.lib") #include "framework.h" #include "resource.h" #include "imgui.h" #include "imgui_impl_win32.h" #include "imgui_impl_dx11.h" #include "ServerHeader.h" #include <d3d11.h> #include <tchar.h> #include <filesystem> #include <vector> #include <string> #include <string_view> #include <iostream> #include <thread> #include <mutex> #include <WinSock2.h> #pragma comment(lib, "d3d11.lib") #define MAX_LOADSTRING 100 #define PACKET_SIZE 1024 #define PORT 9090 static ID3D11Device* g_pd3dDevice = nullptr; static ID3D11DeviceContext* g_pd3dDeviceContext = nullptr; static IDXGISwapChain* g_pSwapChain = nullptr; static UINT g_ResizeWidth = 0, g_ResizeHeight = 0; static ID3D11RenderTargetView* g_mainRenderTargetView = nullptr; // 전역 변수: HINSTANCE hInst; WCHAR szTitle[MAX_LOADSTRING]; WCHAR szWindowClass[MAX_LOADSTRING]; WNDCLASSEXW wc; HWND hwnd; ImVec4 clear_color; LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); SOCKET Server; std::vector<std::string> RecvChats; std::mutex ChatsMutex; int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); if (WindowInit(hInstance) == 1) { return 1; } if (ServerInit() == 1) { return 1; } char Name[PACKET_SIZE] = { 0, }; char Message[PACKET_SIZE] = { 0, }; // Main loop bool done = false; while (!done) { // Poll and handle messages (inputs, window resize, etc.) // See the WndProc() function below for our to dispatch events to the Win32 backend. MSG msg; while (::PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); if (msg.message == WM_QUIT) done = true; } if (done) break; // Handle window resize (we don't resize directly in the WM_SIZE handler) if (g_ResizeWidth != 0 && g_ResizeHeight != 0) { CleanupRenderTarget(); g_pSwapChain->ResizeBuffers(0, g_ResizeWidth, g_ResizeHeight, DXGI_FORMAT_UNKNOWN, 0); g_ResizeWidth = g_ResizeHeight = 0; CreateRenderTarget(); } // Start the Dear ImGui frame ImGui_ImplDX11_NewFrame(); ImGui_ImplWin32_NewFrame(); ImGui::NewFrame(); PrintLog(); // Rendering ImGui::Render(); const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w }; g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr); g_pd3dDeviceContext->ClearRenderTargetView(g_mainRenderTargetView, clear_color_with_alpha); ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); g_pSwapChain->Present(1, 0); } ImGui_ImplDX11_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); CleanupDeviceD3D(); ::DestroyWindow(hwnd); ::UnregisterClassW(wc.lpszClassName, wc.hInstance); WSACleanup(); return 0; } bool CreateDeviceD3D(HWND hWnd) { // Setup swap chain DXGI_SWAP_CHAIN_DESC sd; ZeroMemory(&sd, sizeof(sd)); sd.BufferCount = 2; sd.BufferDesc.Width = 0; sd.BufferDesc.Height = 0; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; UINT createDeviceFlags = 0; //createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; D3D_FEATURE_LEVEL featureLevel; const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, }; HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); if (res == DXGI_ERROR_UNSUPPORTED) // Try high-performance WARP software driver if hardware is not available. res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, createDeviceFlags, featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext); if (res != S_OK) return false; CreateRenderTarget(); return true; } void CleanupDeviceD3D() { CleanupRenderTarget(); if (g_pSwapChain) { g_pSwapChain->Release(); g_pSwapChain = nullptr; } if (g_pd3dDeviceContext) { g_pd3dDeviceContext->Release(); g_pd3dDeviceContext = nullptr; } if (g_pd3dDevice) { g_pd3dDevice->Release(); g_pd3dDevice = nullptr; } } void CreateRenderTarget() { ID3D11Texture2D* pBackBuffer = nullptr; g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView); pBackBuffer->Release(); } void CleanupRenderTarget() { if (g_mainRenderTargetView) { g_mainRenderTargetView->Release(); g_mainRenderTargetView = nullptr; } } extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) return true; switch (msg) { case WM_SIZE: if (wParam == SIZE_MINIMIZED) return 0; g_ResizeWidth = (UINT)LOWORD(lParam); // Queue resize g_ResizeHeight = (UINT)HIWORD(lParam); return 0; case WM_SYSCOMMAND: if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu return 0; break; case WM_DESTROY: ::PostQuitMessage(0); return 0; } return ::DefWindowProcW(hWnd, msg, wParam, lParam); } int WindowInit(HINSTANCE& _hInstance) { // 전역 문자열을 초기화합니다. LoadStringW(_hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadStringW(_hInstance, IDC_IMGUISERVER, szWindowClass, MAX_LOADSTRING); //IMGUI wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, GetModuleHandle(nullptr), nullptr, nullptr, nullptr, nullptr, L"ImGui Example", nullptr }; ::RegisterClassExW(&wc); hwnd = ::CreateWindowW(wc.lpszClassName, L"Dear ImGui DirectX11 Example", WS_OVERLAPPEDWINDOW, 100, 100, 300, 400, nullptr, nullptr, wc.hInstance, nullptr); // Initialize Direct3D if (!CreateDeviceD3D(hwnd)) { CleanupDeviceD3D(); ::UnregisterClassW(wc.lpszClassName, wc.hInstance); return 1; } // Show the window ::ShowWindow(hwnd, SW_SHOWDEFAULT); ::UpdateWindow(hwnd); IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; std::filesystem::path MyPath = std::filesystem::current_path(); MyPath += "\\malgun.ttf"; io.Fonts->AddFontFromFileTTF(MyPath.string().c_str(), 17.0f, NULL, io.Fonts->GetGlyphRangesKorean()); ImGui::StyleColorsDark(); ImGui_ImplWin32_Init(hwnd); ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); bool show_demo_window = true; bool show_another_window = false; clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); return 0; } int ServerInit() { //서버 WSADATA Wsa; int WsaStartResult = WSAStartup(MAKEWORD(2, 2), &Wsa); if (WsaStartResult != 0) { std::cerr << "WSAStartup failed with error code: " << WsaStartResult << std::endl; return 1; } Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); SOCKADDR_IN Addr = { 0, }; Addr.sin_addr.s_addr = htonl(INADDR_ANY); Addr.sin_port = PORT; Addr.sin_family = AF_INET; int BindResult = bind(Server, reinterpret_cast<SOCKADDR*>(&Addr), sizeof(Addr)); if (BindResult != 0) { std::cerr << "Bind failed. error code : " << BindResult << std::endl; return 1; } int ListenResult = listen(Server, SOMAXCONN); if (ListenResult != 0) { std::cerr << "Listen failed. Error code : " << ListenResult << std::endl; return 1; } std::thread(AcceptClient, std::ref(Server)).detach(); return 0; } void AcceptClient(SOCKET& _Socket) { int Order = 0; while (true) { Clients.push_back(std::pair<Client, std::string>{Client(), ""}); Clients[Order].first.ClientSock = accept(_Socket, reinterpret_cast<SOCKADDR*>(&Clients[Order].first.Addr), &Clients[Order].first.ClientSize); Clients[Order].first.Number = Order; Clients[Order].first.bIsDeath = false; std::thread(RecvData, Clients[Order].first.ClientSock, Order).detach(); Order++; } } void SendMsgToAllCLient(int _IgnoreIndex, std::string_view _Msg) { for (int i = 0; i < Clients.size(); i++) { if (i == _IgnoreIndex || Clients[i].first.bIsDeath == true) { continue; } send(Clients[i].first.ClientSock, _Msg.data(), sizeof(char) * PACKET_SIZE, 0); } } void RecvData(SOCKET _Socket, int Num) { char Buffer[PACKET_SIZE] = { 0, }; recv(_Socket, Buffer, sizeof(Buffer), 0); Clients[Num].second = Buffer; AddRecvChat(Clients[Num].second + " join."); std::string JoinMsg = Clients[Num].second; JoinMsg += " join."; SendMsgToAllCLient(Num, JoinMsg); while (true) { ZeroMemory(Buffer, sizeof(Buffer)); int RecvReturn = recv(_Socket, Buffer, sizeof(Buffer), 0); if (RecvReturn == 0) { Clients[Num].first.bIsDeath = true; AddRecvChat(Clients[Num].second + " leave. \n"); std::string LeaveMsg = Clients[Num].second; LeaveMsg += " Leave."; SendMsgToAllCLient(Num, LeaveMsg); shutdown(Clients[Num].first.ClientSock, SD_BOTH); closesocket(Clients[Num].first.ClientSock); break; } AddRecvChat(Clients[Num].second + " : " + Buffer + "\n"); std::string SendMsg = Clients[Num].second; SendMsg += " : "; SendMsg += Buffer; SendMsgToAllCLient(Num, SendMsg); } } void PrintLog() { ImGui::Begin("Server"); ImGui::Text("Log"); if (RecvChats.size() > 20) { EraseRecvChat(RecvChats.begin()); } for (int i = 0; i < RecvChats.size(); i++) { ImGui::Text(RecvChats[i].c_str()); } ImGui::End(); }
void AddRecvChat(const std::string& _Chat) { std::lock_guard<std::mutex> lock(ChatsMutex); RecvChats.push_back(_Chat); } void EraseRecvChat(const std::vector<std::string>::iterator& _ChatIter) { std::lock_guard<std::mutex> lock(ChatsMutex); RecvChats.erase(_ChatIter); }
간단한 프로그램 하나 만드는데도 배워야 할 지식이 많고, 고민이 많다는 것은 아직 나아가야 할 길이 한참 멀다는 의미인 것 같다. 서버에 대한 기초 지식이 늘었음에 뿌듯하기도 하지만, 앞으로 더 많은 것을 배워야 한다. 멈추지 말고 끝없이 정진하자.
다음 게시글에선 클라이언트 구현을 해보겠다.