일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- base from member
- return by reference
- std::cout
- pointer to member data
- virtual function table
- operator overloading
- C++
- new&delete
- discord bot
- virtual inheritance
- placement new
- dynamic_cast
- vector size
- delete function
- increment operator
- suffix return type
- c++ basic practice
- constructor
- 더 지니어스 양면포커
- std::ostream
- std::endl
- member function pointer
- conversion constructor
- vector capacity
- this call
- diamond inheritance
- virtual function
- std::vector
- c++ multi chatting room
- virtual destructor
- Today
- Total
I'm FanJae.
[C++ Server] Day 2. Thread 적용 및 Multi Chatting Room 구현 본문
[C++ Server] Day 2. Thread 적용 및 Multi Chatting Room 구현
FanJae 2024. 9. 25. 21:020.설계
0-1. 쓰레드 추가시 고려가 필요한 사항
① 쓰레드가 실행해야 하는 영역의 구분이 필요하다.
- 어떤 부분부터 쓰레드가 실행해야 하는지 구분해서 이를 처리해야 한다.
② 자원 동시 접근 방지
- 쓰레드는 서로 동시에 실행되는 것처럼 보이지만 실제로는 수 많은 Context Switching이 일어난다.
- 이 과정에서 어떤 쓰레드를 먼저 실행할지는 CPU의 스케줄링에 따라 달려있다.
- 따라서 이를 적당히 동기화(synchronization) 해주는 작업이 필요하다.
0-2. Multi Cheatting Room의 구현 기법
1) 과거 나의 생각
- 소켓 프로그래밍의 개념을 완전히 잘못 이해하던 시절에 짠 코드가 존재한다.
- 그 시절에는 이게 최선이라고 생각했고 내 나름대로 잘짰다고 생각했다....
void sendto_all(int *serv_sock, char *message, int size, client_list *user,struct sockaddr_in *clnt_adr,int room_number) // 모든 인원에게 메시지 전달.
{
int senduser = target_user(user,clnt_adr);
char temp[BUF_SIZE];
if((strcmp(message,"0000000000exit\n") == 0) || (strcmp(message,"0000000000exit") == 0)) {
room[room_number].clnt_cnt--;
user[senduser-1].clnt_adr.sin_addr.s_addr = 0;
user[senduser-1].clnt_adr.sin_port = 0;
user[senduser-1].connect = 0;
sprintf(temp,"0000000600System : %dp is logout\n",senduser);
printf("log : %dp logout\n",senduser);
}
else
sprintf(temp,"0000000600%dp : %s",senduser,message);
for(int i = 0; i < 4; i++) {
int clnt_adr_user_sz = sizeof(user[i].clnt_adr);
if(user[i].connect == 1) {
printf("Log : Ok It's Send it. ip : %s port : %d, temp : %s\n",inet_ntoa(user[i].clnt_adr.sin_addr),user[i].clnt_adr.sin_port,temp);
sendto(*serv_sock,temp,BUF_SIZE,0,(struct sockaddr*)&user[i].clnt_adr,clnt_adr_user_sz);
}
}
}
※ 이 당시의 코드는 다시봐도 알아보기가 힘들다.. 서버를 C로 짰느냐 아니냐의 문제와 별개로 상당히 난해하게 짰다.
당시 코드의 풀버전은 해당 링크를 통해서 확인할 수 있지만 굳이 권장하지 않는다.
※ 어쨌든 그 당시 내가 고전했던 이유는 다음과 같았다.
① Multi Room을 논리적으로 구현하는 방법 자체가 잘못되었음.
- 이것이 가장 치명적이였다. Multi Room이라는 이유로 Room 하나당 한 포트를 할당했다.
- 이 방식으로 처리하면서 메시지 처리를 하는 것 자체가 상당히 어려워졌다.
- 가장 좋은 방법은 같은 방에 있는 인원에 대한 소켓 정보를 받아두는 것으로 간단히 해결 가능하다.
② 방을 만들고 처리하는 방법이 비효율적
- ①의 연장선이다. 위와 같이 처리 했으니 방을 만드는 것이 얼마나 복잡했겠는가...
2) 개선 사항
① C++ std::thread 활용
- 기존에 짠 코드는 C로 작성된 코드이다. 따라서 STL에서 제공하는 라이브러리는 대부분 사용할 수 없었다.
- C++에서는 이를 활용해서 작성해보려고 한다.
② Multi Room을 나누는 로직 수정
- 채팅방에 입장할 때, 해당 인원의 소켓 정보만 가지고 있어도 문제되지 않는다. 이를 전제하여 구현하려고 한다.
※ 동기화는 우선 처리하지 않았다. 어느정도 로직을 확정낸 이후 동기화 처리를 하는것이 낫다고 판단했기 때문이다.
2. 쓰레드 추가에 따른 처리
2-1. 클라이언트 처리 함수 추가
#include <iostream>
#include <WinSock2.h>
#include <thread>
#include <map>
#include <set>
std::map<std::string, std::set<SOCKET>> chatRooms;
void ConnectClient(SOCKET clientSocket)
{
char buffer[1024];
std::string roomName;
int recv_length = recv(clientSocket, buffer, sizeof(buffer), 0);
if (recv_length <= 0)
{
closesocket(cClientSocket);
return;
}
buffer[recv_length] = '\0'; // 문자열 끝.
roomName = buffer;
chatRooms[roomName].insert(clientSocket);
while (true)
{
memset(buffer, 0, sizeof(buffer));
recv_length = recv(clientSocket, buffer, sizeof(buffer), 0);
if (recv_length <= 0)
{
break;
}
for (SOCKET socket : chatRooms[roomName])
{
if (socket != clientSocket)
{
send(socket, buffer, recv_length, 0);
}
}
}
chatRooms[roomName].erase(clientSocket);
if (chatRooms[roomName].empty()) {
chatRooms.erase(roomName);
}
closesocket(clientSocket);
}
- 이 코드 하나로 내가 과거에 처리해야 했던 비효율적인 작업 대부분이 해결된다.
2-1-1. 채팅방 입장
int recv_length = recv(clientSocket, buffer, sizeof(buffer), 0);
if (recv_length <= 0)
{
closesocket(cClientSocket);
return ;
}
buffer[recv_length] = '\0'; // 문자열 끝.
roomName = buffer;
chatRooms[roomName].insert(clientSocket);
- 채팅방에 소켓 정보를 담을때는 std::map을 활용하였다.
- key값은 중복되지 않기 때문에 적합한 자료형이다. Key에 해당하는 값을 찾을때 시간 복잡도도 거의 없다.
2-1-2. 해당 방 인원에게 메시지 전달.
while (true)
{
memset(buffer, 0, sizeof(buffer));
recv_length = recv(clientSocket, buffer, sizeof(buffer), 0);
if (recv_length <= 0)
{
break;
}
for (SOCKET socket : chatRooms[roomName])
{
if (socket != clientSocket)
{
send(socket, buffer, recv_length, 0);
}
}
}
- 채팅방 연결에 성공했다면, 본인을 제외한 모든 연결된 인원에게 메시지를 보낸다.
※ 이 로직은 GUI 에서는 수정하고자 한다.
2-1-3. 채팅방 인원 제거.
chatRooms[roomName].erase(clientSocket);
if (chatRooms[roomName].empty()) {
chatRooms.erase(roomName);
}
closesocket(clientSocket);
- 채팅방에서 빠져나온 인원은 자연스럽게 삭제하고, 만약 채팅방에 아무도 없으면 채팅방도 제거한다.
2-2. 서버 로직 수정
std::cout << "Server Start\n" << std::endl;
while (true)
{
SOCKET clientSocket;
SOCKADDR_IN clientAddr;
int clientAddrsize = sizeof(clientAddr);
clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &clientAddrsize);
if (clientSocket == INVALID_SOCKET)
{
std::cerr << "accept() Error" << std::endl;
continue;
}
std::thread worker(ConnectClient, clientSocket);
worker.detach();
}
- accept 되었다면, server는 해당 소켓에 대한 thread를 생성하여 처리한다.
3. 테스트용 클라이언트 (C++ CUI)
- 서버의 구성이 이전보다 복잡해지긴 했지만, 테스트 클라이언트는 이전에서 극히 일부 내용만 추가된다.
3-1. 연결 이후 처리 변경
std::string roomName;
std::cout << "참여할 채팅방 이름을 입력하세요: ";
std::getline(std::cin, roomName);
send(clientSocket, roomName.c_str(), roomName.length(), 0);
std::thread receiveThread(receiveMessages, clientSocket);
receiveThread.detach();
std::string message;
while (true)
{
std::getline(std::cin, message);
if (message == "/exit")
{
break;
}
send(clientSocket, message.c_str(), message.length(), 0);
}
- 첫번째 연결에서 보내는 메시지는 채팅방 입장 관련 메시지를 보내도록 한다.
- 이후 보내는 메시지는 다른 인원에게 보내는 메시지로 처리한다.
- 이때, receiveMessages라는 함수를 쓰레드로 실행시켜, 다른 인원이 보내는 메시지를 받는건 별개로 처리한다.
3-2. receiveThread
void receiveMessages(SOCKET clientSocket)
{
char buffer[1024];
while (true)
{
int recv_length = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (recv_length <= 0) {
std::cerr << "recv() Error" << std::endl;
break;
}
buffer[recv_length] = '\0';
std::cout << "받은 메시지: " << buffer << std::endl;
}
}
- 이와 같이 구성한다.
4. 테스트
- 서버를 틀고 프로그램 3개를 실행해서 2개는 fanjae라는 채팅방에 입장하였다.
- 한명은 fanjae2라는 채팅방에 입장한다. fanjae에 있는 인원끼리는 채팅이 공유된다.
- 하지만, fanjae2에 있는 인원은 공유되지 않는다.
5. TODO
1) Server
- 채팅방 기능 구현 및 동기화 처리 필요
2) Client
- C# Windows Forms을 이용한 GUI 구현 필요
'Toy Project > Multi Room Cheating Server' 카테고리의 다른 글
[C++ Server, C# Client] Day 3. GUI 클라이언트 설계 및 Server 버그 수정 (0) | 2024.09.26 |
---|---|
[C++ Server] Day 1. Echo Server 구현. (0) | 2024.09.25 |
[C++ Server, C# Client] Day 0. Multi Room Chatting Server 시작 (3) | 2024.09.24 |