2025년 2월 15일 토요일

C++ winsock2

c++ 소켓은 winsock2로 구현할 수 있는데

자바의 소켓이랑은 많이 달랐음



클라이언트에서 데이터 보내기 및 받기 - Win32 apps | Microsoft Learn

예시 코드로는 ms 설명서에 제대로 나와있기는 한데



일단 winsock.h 과 winsock2.h 두 가지 버전으로 나뉨

전자는 옛날버전이고 후자는 기능 추가 한걸로 알고 있음



winsock.h는 windows.h 헤더에 이미 포함되어 있어

winsock2.h와 windows.h 헤더를 같이 쓰면 재정의 오류가 뜸




이를 해결하려면 #define WIN32_LEAN_AND_MEAN 를 추가하여

winsock.h 및 여러 불필요한 헤더들을 windows.h로 부터 제외 가능

이렇게 같이 사용할 수 있음


하지만 간접 헤더 포함될 때도 재정의 오류가 뜸...

그래서 모든 헤더에 #define 하기 그래서 함수 포인터를 씀

거기에 소켓 관련 함수들을 싹다 넣고 호출 



서버 코드

#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
#include <thread>
#include <vector>
#include <wchar.h>
#include <iostream>

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

#define SERVER_PORT 8001
#define BUFFER_SIZE 256

using namespace std;

struct ClientData
{
    SOCKET clientSocket = {};
    SOCKADDR_IN clientAddr = {};
    wchar_t clientName[10] = { L"player" };
    bool online;
};

vector<ClientData*> clients;

void SendToAll(char* typeMsg, wchar_t* chatMsg)
{
    int result = 0;
    int clsSize = clients.size();

    for (int i = 0; i < clsSize; i++)
    {
        if (!clients[i]->online)
        {
            char type3 = 0x03;
            wchar_t exitMsg[BUFFER_SIZE] = {};

            wcscat_s(exitMsg, clients[i]->clientName);
            wcscat_s(exitMsg, L" - ");
            wcscat_s(exitMsg, L"님이 퇴장했습니다.\n");
            SendToAll(&type3, exitMsg);

            delete clients[i];
            clients.erase(clients.begin() + i);
        }
    }

    for (int i = 0; i < clsSize; i++)
    {
        result = send(clients[i]->clientSocket, typeMsg, sizeof(char), 0);
        if (result == SOCKET_ERROR)
        {
            closesocket(clients[i]->clientSocket);
            continue;
        }

        result = send(clients[i]->clientSocket, (char*)chatMsg, BUFFER_SIZE * sizeof(wchar_t), 0);
        if (result == SOCKET_ERROR)
        {
            closesocket(clients[i]->clientSocket);
            continue;
        }
    }
}

int main() 
{
    int result;

    // Winsock 초기화
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) 
    {
        std::cerr << "WSAStartup failed!" << std::endl;
        return 0;
    }

    // 소켓 생성
    SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (serverSocket == INVALID_SOCKET) {
        std::cerr << "Socket creation failed! Error: " << WSAGetLastError() << std::endl;
        WSACleanup();
        return 0;
    }

    // 서버 주소 설정
    SOCKADDR_IN serverAddr = {};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(SERVER_PORT);

    bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
    listen(serverSocket, SOMAXCONN);

    wcout.imbue(locale("kor"));

    ////논 블로킹으로 설정
    //u_long argp = 1;
    //ioctlsocket(clientSocket, FIONBIO, &argp);

    int repeatCount = 0;

    //새로운 클라이언트를 계속해서 받는 반복문
    while (true)
    {
		ClientData* newClientData = new ClientData;
		int clientAddrSize = sizeof(SOCKADDR_IN);

		newClientData->clientSocket = accept(serverSocket, (SOCKADDR*)&newClientData->clientAddr, &clientAddrSize);
		if (newClientData->clientSocket == INVALID_SOCKET) {
			std::cerr << "Accept failed! Error: " << WSAGetLastError() << std::endl;
			closesocket(serverSocket);
			WSACleanup();

			break;
		}

		wchar_t convNum[4] = {};

		_itow_s(repeatCount, convNum, _countof(convNum), 10);//카운트를 문자로
		wcscat_s(newClientData->clientName, convNum);//클라이언트 이름에 숫자 추가
		newClientData->online = true;//연결중임을 의미
		clients.push_back(newClientData);//클라이언트 배열에 추가

        char type3 = 0x03;
        wchar_t joinMsg[BUFFER_SIZE] = {};

        wcscat_s(joinMsg, newClientData->clientName);
        wcscat_s(joinMsg, L" - ");
        wcscat_s(joinMsg, L"님이 접속했습니다.\n");
        SendToAll(&type3, joinMsg);

        wcout << L"New Player Entered : " << newClientData->clientName << endl;

		//지속적으로 클라이언트로부터 메시지를 받아 다른 클라로 전달해주는 반복문
		thread recvThread
		(
			[](ClientData* cls)
			{
				while (true)
				{  
                    int tresult;
					char typeBuffer;
                    wchar_t nameAndBar[BUFFER_SIZE] = {};
					wchar_t strBuffer[BUFFER_SIZE] = {};

                    tresult = recv(cls->clientSocket, &typeBuffer, sizeof(char), 0);
					if (tresult == SOCKET_ERROR)
					{
						closesocket(cls->clientSocket);
                        cerr << WSAGetLastError() << endl;
                        cls->online = false;

                        for (int i = 0; i < clients.size(); i++)
                        {
                            if (!clients[i]->online)
                            {
                                char type3 = 0x03;
                                wchar_t exitMsg[BUFFER_SIZE] = {};

                                wcscat_s(exitMsg, clients[i]->clientName);
                                wcscat_s(exitMsg, L" - ");
                                wcscat_s(exitMsg, L"님이 퇴장했습니다.\n");

                                delete clients[i];
                                clients.erase(clients.begin() + i);

                                SendToAll(&type3, exitMsg);
                            }
                        }

						break;
					}

                    tresult = recv(cls->clientSocket, (char*)strBuffer, BUFFER_SIZE * sizeof(wchar_t), 0);
					if (tresult == SOCKET_ERROR)
					{
						closesocket(cls->clientSocket);
                        cerr << WSAGetLastError() << endl;
                        cls->online = false;
						break;
					}

                    wcscat_s(nameAndBar, cls->clientName);
                    wcscat_s(nameAndBar, L" - ");
                    wcscat_s(strBuffer, L"\n");

                    SendToAll(&typeBuffer, nameAndBar);
					SendToAll(&typeBuffer, strBuffer);
				}
			}, newClientData
		);

		recvThread.detach();

		repeatCount++;//카운트 증가
    }
 
    std::cout << "Server ShutDown" << std::endl;

    // 소켓 닫기
    closesocket(serverSocket);
    WSACleanup();

    return 0;
}

메시지 받은걸 모든 접속자에게 돌려주는 프로그램

을.. 실행하고, 채팅 주고 받을 수 있는 클라로 접속하면 된다.



소켓 통신에서 데이터를 주고 받을 땐 send와 recv 함수가 있는데

기본적으로 blocking 이라는 데이터가 제대로 전달될 때 까지

기다리는 기능이 있는데 이것 때문에 메인 스레드는 못써먹게 됨



따라서 스레드 새로 생성해서 그쪽에서 돌려야 함



blocking을 없애는 것도 가능하다 //주석처리로 해둠

ioctlsocket 함수(winsock.h) - Win32 apps | Microsoft Learn

 

시연 이미지


같은 공유기를 쓰거나 내부 사설망의 경우 간단하게 접속 가능

아니라면 포트포워드


클라이언트 소스코드

GitHub - h117562/Tutorial_Socket

서버 프로그램은 data 폴더에 exe로 뒀고 포트는 8001 고정이다





댓글 없음:

댓글 쓰기

c++ thread.h

 c++에서 쓰레드 돌릴려면 thread.h 헤더를 쓰면 되는데 이 친구는 쓰레드가 아직 실행 중인지, 아니면 강제 종료하거나 하는 함수가 없어서 조금 아쉬운 애다. std::thread 는 로컬 변수로 선언하든 new 동적 할당을 하든 start 함...