버그 해결을 위한 모든 질문을 던져
0 votes
301 views

안녕하세요!

취업 준비를 위해 C++ 서버를 만들고 있습니다. 말 그대로 맨땅에 헤딩하면서 작성 중이라 코드 곳곳에 유혈이 낭자하고 있네요. 그동안 별별 오류가 다 생겨 꿋꿋하게 잡아왔지만 이번 오류는 감조차 오질 않아 질문드리게 되었습니다. 채팅 서버인데요. 클라이언는 두 개로 테스트 중입니다. C1, C2라고 하겠습니다. 오류 지점은 4번에 등장합니다!

1. C1에서 로그인 한 뒤 로비에 입장합니다.

2. C2에서 로그인 한 뒤 로비에 입장합니다.

   ㄱ. C1 로비 유저 목록에는 C2의 유저 아이디가 추가됩니다(기존 유저는 새 유저의 아이디가 추가됩니다).

   ㄴ. C2의 로비 유저 목록에는 C1, C2의 유저 아이디가 함께 추가됩니다(새 유저는 자신을 포함한 모든 유저의 아이디를 받습니다).

3. C1에서 로비를 나갑니다(C1의 로비 유저 목록은 전부 사라지며, C2의 로비 유저 목록에서는 C1의 유저 아이디가 사라집니다).

4. C1에서 다시 로비를 입장하기 위해 [입장] 버튼을 누르면 응답하지를 않습니다. 몇 번 더 연타해야 서버에서 처리됩니다.

 

C1이 로비를 재입장할 때(4번) 서버의 패킷받는 부분에 중단점을 걸어보니 받아야 할 바이트 수는 6바이트로 올바르게 나오지만 패킷 아이디, 바디 사이즈, 문자열 등 패킷이 가지고 있어야 할 데이터가 없습니다. 서버의 수신 버퍼의 문제일까 싶지만 버퍼 크기를 넉넉하게 잡았고 로그를 통해 수신 가용한 크기와 수신 버퍼의 포인터를 확인해보면 문제가 없는 것 같습니다. 소스 코드 어디를 보여드려야 할지 어렵네요. 다음은 워커 스레드의 일부입니다. 수신이 완료되면 AddPacket()를 호출하는데요. 

case IoMode::READ:
{
	if (io_data->bytes_ == SOCKET_ERROR)
		CloseSocket(io_data->index_, true);
	else if (io_data->bytes_ == 0)
		CloseSocket(io_data->index_, false);
	else
	{
		logger_->Write(LogType::L_INFO, "%s | session index:%d | 수신완료 바이트:%ld",
			__FUNCTION__, io_data->index_, io_data->bytes_);
		AddPacketQueue(io_data->index_);
	}
	break;
}

case IoMode::WRITE:
{
	if (io_data->bytes_ == SOCKET_ERROR)
		CloseSocket(io_data->index_, true);
	else if (io_data->bytes_ == 0)
		CloseSocket(io_data->index_, false);
	else
	{
		logger_->Write(LogType::L_INFO, "%s | session index:%d | 송신완료 바이트:%ld",
			__FUNCTION__, io_data->index_, io_data->bytes_);
		PostRead(io_data->index_);
	}
	break;
}
void IOCP::AddPacketQueue(const int session_index)
{
	auto& session = client_session_pool_[session_index];
		logger_->Write(LogType::L_WARN, "%s | session index:%ld | 수신버퍼 위치:%p", 
		__FUNCTION__, session_index, session.io_data_.recv_rear_);

	PacketHeader* header = (PacketHeader*)session.io_data_.recv_rear_;
	RecvPacketInfo packet_info;
	packet_info.session_index_ = session.index_;
	packet_info.packet_id_ = header->id_;
	packet_info.packet_body_size_ = header->packet_size_ - kPacketHeaderSize;
	packet_info.data_ = &session.io_data_.recv_rear_[kPacketHeaderSize];
	packet_queue_.push(packet_info);
}

C1에서 로비에 재입장하려고 [입장]버튼을 누르면 PacketHeader* header로 값이 들어오지 않습니다. 서너 번 더 클릭을 하면 처리가 됩니다. 그렇게 입장/퇴장을 테스트하다 보면 처리가 안 될 때 버튼을 한 번 더 클릭하면 처리가 되는 상황이 계속 일어납니다.

기본적인 동작은요, 패킷을 수신완료하면 AddPacketQueue()을 호출하고, 외부 스레드에서 packet_queue_에 있는 패킷을 꺼내 필요한 처리를 한 뒤 클라이언트에 송신합니다. 송신 완료되면(case IoMode::WRITE), PostRead()를 호출해 WSARecv()를 커널에 요청합니다. 즉 패킷 송신이 완료될 때마다 WSARecv()가 호출되며 수신 버퍼는 수신이 완료된 바이트 수만큼 포인터를 이동해줍니다.

왜 클라이언트에서 보내는 패킷이 바로바로 처리가 안 되는 순간이 생길까요? [패킷을 받는다 -> 가공한 뒤 클라이언트로 보낸다 -> 송신이 완료되면 커널에 WSARecv()를 요청한다] 이 순서면 클라이언트에서 패킷을 보냈을 때 서버가 받지 못하는 불상사가 안 생길 것 같은데, 그렇게 믿고 있다 오류가 생겨 버리니까 어디가 어떻게 문제인지 감이 안 오네요. 소스를 다 보여드리지 못하는 상태에서 드리는 질문이라 많이 제한적이지만..도움 요청드립니다!

asked (24 point)
수정됨 , 301 views

2 answers

0 votes
Queue의 사이즈를 넘겨서 두번 수신하려고 대기 하는건 아닌가요?
answered (16 point)

말씀이 잘 이해가 안 됩니다! AddPacketQueue()에서 packet_queue_에 들어간 패킷은 외부에서 다음과 같이 처리되고 있습니다. Main::Run()은 스레드 생성을 통해 실행시킨 상태입니다.

void Main::Run()
{
	server_->Run(); // 스레드
	while (is_run_)
	{			
		auto packet_info = server_->GetPacket();
                // 아직 이유는 모르나 쓰레기 값 때문에 음수가 넘어올 때 있음
		if (packet_info.packet_id_ > 0) 
			packet_process_->Process(packet_info);

		Sleep(1);
	}
}

 

+2 votes
문제를 해결했기에 간단하게 자문자답하겠습니다.

IOCP를 거의 독학하다시피 하고 있어서, 기존 에코 서버를 채팅 서버로 확장하면서

구조적인 실수가 있었습니다.

READ가 완료되면 WSARecv()를 호출해서 커널에 요청해야 했는데 그러고 있지 않았구요.

오류가 발생한 결정적인 이유는 overlapped 변수가 있는 IoData 구조체를 WSASend와 WSARecv에서 공유하고 있었습니다 :(

분명 Recv에 대한 패킷을 받아야 하는데, GQCS에서 WRITE가 뜨는 걸 보고 알아차렸습니다. 5일 걸렸네요 :(
answered (24 point)

버그 해결을 위해 도움을 구하고, 도움을 주세요. 우리는 그렇게 발전합니다.

throw bug 는 프로그래밍에 대한 전분야를 다룹니다. 질문,논의거리,팁,정보공유 모든 것이 가능합니다. 프로그래밍과 관련이 없는 내용은 환영받지 못합니다.

239 질문
367 answers
376 댓글
470 users