client - server 구현 (3)
알아야 할 것들
- Thread
- mutex
- chrono ( server 통신 timer로서 사용)
Thread 기본 이론
- 하나의 프로세스에서 여러일을 병렬처럼 사용하고자 사용
- 자원 공유 ( Heap 영역 / 전역변수 / 주소공간 / [시그널/ 파일] 핸들러 등등)
- 자원을 공유하기에 같은 곳 동시에 접근하지 않도록 적절한 관리가 필요
- 비동기처리를 할 때에 주로 사용됨
Mutex
- Thread에서 자원을 공유하는 부분에 동시에 접근하는 것을 막기 위해 사용
- 이번 예제에서는 출력하는 부분이 일정한 처리가 되지 않아 제대로 출력되지 않는 것에 있어서 mutex를 사용
- 너무 큰 범위의 mutex, 또는 너무 작은 범위의 mutex 사용은 관리의 힘듬을 야기할 수 있음
- mutex, semaphore, 조건변수 들을 활용하여 자원 공유 문제를 회피 할 수 있음
Chrono
- 정확한 시간값을 위하여 사용
- Server 타임아웃을 위해서 사용
Client 구현
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <chrono>
#include <stdexcept>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#pragma comment(lib, "Ws2_32.lib")
class Winsock {
public:
Winsock() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
}
}
~Winsock()
{
WSACleanup();
}
};
class ClientSocket {
public:
// 소켓 생성자
ClientSocket(const std::string& host, const std::string& port)
{
addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
addrinfo* result = nullptr;
int addrResult = getaddrinfo(host.c_str(), port.c_str(), &hints, &result);
if (addrResult != 0)
{
throw std::runtime_error("getaddrinfo failed with errro: " + std::to_string(addrResult));
}
for (addrinfo* ptr = result; ptr != nullptr; ptr = ptr->ai_next)
{
sock = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (sock == INVALID_SOCKET)
{
continue;
}
u_long mode = 1;
//ioctlsocket 함수 성공 -> 0 반환
//NO_ERROR == 0
if (ioctlsocket(sock, FIONBIO, &mode) != NO_ERROR)
{
closesocket(sock);
sock = INVALID_SOCKET;
continue;
}
if (connect(sock, ptr->ai_addr, (int)ptr->ai_addrlen) == SOCKET_ERROR)
{
int error = WSAGetLastError();
if (error != WSAEWOULDBLOCK)
{
closesocket(sock);
sock = INVALID_SOCKET;
continue;
}
}
else
{
break;
}
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(sock, &writeSet);
timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
int selectResult = select(0, nullptr, &writeSet, nullptr, &timeout);
if (selectResult > 0 && FD_ISSET(sock, &writeSet))
{
int error;
int len = sizeof(error);
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)&error, &len) == 0)
{
if (error == 0)
{
break;
}
else
{
closesocket(sock);
sock = INVALID_SOCKET;
}
}
}
else if (selectResult == 0)
{
throw std::runtime_error("connect timeout");
}
else
{
throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
}
}
freeaddrinfo(result);
if (sock == INVALID_SOCKET)
{
throw std::runtime_error("Unable to connect to server!");
}
}
~ClientSocket()
{
if (sock != INVALID_SOCKET)
{
closesocket(sock);
}
}
void send(const std::string& message)
{
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(sock, &writeSet);
timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int selectResult = select(0, nullptr, &writeSet, nullptr, &timeout);
if (selectResult > 0 && FD_ISSET(sock, &writeSet))
{
int sendResult = ::send(sock, message.c_str(), (int)message.size(), 0);
if (sendResult == SOCKET_ERROR)
{
throw std::runtime_error("send failed with error : " + std::to_string(WSAGetLastError()));
}
}
else if (selectResult == 0)
{
throw std::runtime_error("send timeout");
}
else
{
throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
}
}
std::string receive(size_t size)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(sock, &readSet);
timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
if (selectResult > 0 && FD_ISSET(sock, &readSet))
{
std::string buffer(size, '\0');
int recvResult = recv(sock, &buffer[0], (int)size, 0);
if (recvResult > 0)
{
buffer.resize(recvResult);
return buffer;
}
else if (recvResult == 0)
{
return {};
}
else
{
throw std::runtime_error("recv failed with error : " + std::to_string(WSAGetLastError()));
}
}
else if (selectResult == 0)
{
return {};
}
else
{
throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
}
}
private:
SOCKET sock = INVALID_SOCKET;
};
std::mutex outputMutex;
//쓰레드 사용 ( client 생성을 위한 thread )
void client_thread(int id)
{
try
{
ClientSocket socket("localhost", "27015");
std::string message = "Hello from Client" + std::to_string(id);
socket.send(message);
std::string response = socket.receive(512);
std::lock_guard<std::mutex> lock(outputMutex);
std::cout << "Client " << id << " recieved : " << response << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "Client " << id << " exception: " << e.what() << std::endl;
}
}
int main()
{
Winsock winsock;
//여러개의 thread를 생성하기 위해 vector 사용
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
//thread를 생성하고 vector에 추가
threads.emplace_back(client_thread, i+1);
//해당 Sleep부분은 서버의 timeout을 test하기 위해서 사용
Sleep(4000);
}
for (auto& thread : threads)
{
// thread 종료 대기
thread.join();
}
return 0;
}
SERVER 구현
#include <iostream>
#include <winsock2.h>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>
#pragma comment(lib, "Ws2_32.lib")
#define PORT 27015
#define BUFFER_SIZE 1024
std::string GetLastErrorAsString() {
DWORD errorMessageID = ::WSAGetLastError();
if (errorMessageID == 0) {
return std::string();
}
LPSTR messageBuffer = nullptr;
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL
);
std::string message(messageBuffer, size);
LocalFree(messageBuffer);
return message;
}
class Winsock
{
public:
Winsock()
{
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
throw std::runtime_error("WSAStartup failed with error : " + std::to_string(result));
}
}
~Winsock()
{
WSACleanup();
}
};
class ServerSocket
{
public:
ServerSocket()
{
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET)
{
WSACleanup();
throw std::runtime_error("Socket creation failed : " + GetLastErrorAsString());
}
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
closesocket(serverSocket);
throw std::runtime_error("Socket bind failed : " + GetLastErrorAsString());
}
if (listen(serverSocket, 100) == SOCKET_ERROR)
{
closesocket(serverSocket);
throw std::runtime_error("Socket listen failed : " + GetLastErrorAsString());
}
u_long mode = 1;
if (ioctlsocket(serverSocket, FIONBIO, &mode) != NO_ERROR)
{
closesocket(serverSocket);
throw std::runtime_error("ioctlsocket failed : " + GetLastErrorAsString());
}
std::cout << "Waiting for connection..." << std::endl;
}
void accept_connection()
{
// server client 연결 요청 대기 타임아웃 시간 10초 설정
auto time = std::chrono::steady_clock::now();
auto timeout_duration = std::chrono::seconds(10); // 10초 타임아웃
while (true)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(serverSocket, &readSet);
timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
if (selectResult > 0 && FD_ISSET(serverSocket, &readSet))
{
SOCKET acceptSocket = accept(serverSocket, nullptr, nullptr);
if (acceptSocket == INVALID_SOCKET)
{
int error = WSAGetLastError();
if (error != WSAEWOULDBLOCK)
{
throw std::runtime_error("Socket accept failed : " + GetLastErrorAsString());
}
continue;
}
else
{
//연결 요청이 있을경우, 타임 아웃 시간을 해당 시간부터 계산하기 위해 설정
time = std::chrono::steady_clock::now();
u_long mode = 1;
if (ioctlsocket(acceptSocket, FIONBIO, &mode) != NO_ERROR)
{
std::cerr << " ioctlsocket for acceptSocket failed : " + GetLastErrorAsString() << std::endl;
closesocket(acceptSocket);
continue;
}
//.detach 함수를 이용해 백그라운드에서 thread 실행
// handle_client 부분을 수행 후에 자동 종료되지만 그 전에 main 함수가 끝나지 않도록 주의
// 회피 방법 : thread 대기 종료 ( main이 먼저 종료될 경우 좀비프로세스가 될 수 있음 )
std::thread(&ServerSocket::handle_client, this, acceptSocket).detach();
}
}
else if (selectResult == 0)
{
//타임아웃 종료 매커니즘
auto current_time = std::chrono::steady_clock::now();
if (current_time - time >= timeout_duration)
{
std::cout << "No connection attempts within timeout period. Exiting accept loop." << std::endl;
break;
}
continue;
}
else
{
throw std::runtime_error("select failed with error : " + std::to_string(WSAGetLastError()));
}
}
}
void handle_client(SOCKET acceptSocket)
{
const char* message = "Hello from server";
char buffer[BUFFER_SIZE] = { 0, };
while (true)
{
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(acceptSocket, &readSet);
timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
int selectResult = select(0, &readSet, nullptr, nullptr, &timeout);
if (selectResult > 0 && FD_ISSET(acceptSocket, &readSet))
{
int recvResult = recv(acceptSocket, buffer, BUFFER_SIZE, 0);
if (recvResult > 0)
{
// recv 및 send 부분을 mutex로 통제하여 순서가 섞이게 출력되지 않도록 설정
std::lock_guard<std::mutex> lock(outputMutex);
std::cout << "Message from client : " << buffer << std::endl;
send(acceptSocket, message, strlen(message), 0);
std::cout << "(server-> client) send message" << std::endl;
}
else if (recvResult == 0)
{
std::lock_guard<std::mutex> lock(outputMutex);
std::cout << "Connection closed" << std::endl;
closesocket(acceptSocket);
break;
}
else
{
std::cerr << "recv failed with error : " << GetLastErrorAsString() << std::endl;
closesocket(acceptSocket);
break;
}
}
else if (selectResult == 0)
{
throw std::runtime_error("recv timeout");
closesocket(acceptSocket);
break;
}
else
{
throw std::runtime_error("select failed with error : " + GetLastErrorAsString());
closesocket(acceptSocket);
break;
}
}
}
~ServerSocket()
{
if (serverSocket != INVALID_SOCKET)
{
closesocket(serverSocket);
serverSocket = INVALID_SOCKET;
}
}
private:
SOCKET serverSocket = INVALID_SOCKET;
struct sockaddr_in serverAddr;
static std::mutex outputMutex;
};
//전역으로 mutex 설정
std::mutex ServerSocket::outputMutex;
int main()
{
try
{
Winsock winsock;
ServerSocket serverSocket;
serverSocket.accept_connection();
}
catch (const std::exception& e)
{
std::cerr << "Exception : " << e.what() << std::endl;
}
return 0;
}