게임서버에서는 여러 클라이언트에게 같은 데이터를 보내야할 일이 많아서 SenBuffer안에 SendBufferChunk shared_ptr을 만들어서, 서버가 여러 세션에게 같은 데이터를 보낼 때 효율적으로 했다. 그리고 세션이 SendBuffer shared_ptr queue를 가지고있어서 같은 데이터를 보낼 여러 세션이 있을 때 모든 세션이 send를 마치면 SendBuffer와 SendBufferChunk가 사라지도록 만든 것.

게임서버 강의에서의 Send과정을 분석해보자면

  1. 보내고자 하는 내용을 protobuf 패킷 클래스에 담는다.
  2. 클라이언트(서버)패킷핸들러가 protobuf 패킷 클래스를 받아서 SendBufferRef를 만들어서 return한다.
  3. 세션의 Send(SendBuffeRef) 함수로, 보낼 데이터를 SendBufferQueue에 저장.
  4. Register 시작 [ 세션이 들고있는 SendEvent의 sendbuffers 벡터로 보낼 데이터를 전부 옮긴다.
  5. wasbuf 벡터에 각 버퍼 시작주소, writesize 담는다. (뭐지? WSASend쓰려면 필요한것들인듯?)
  6. WSASend로 보냄. ] Register끝
  7. 커널이 다 보내고 나면 완료통지를 한다. -> processSend호출.
  8. 다 보냈으니 세션의 SendBuffer 클리어. 그리고 OnSend호출 (뭐지? 컨텐츠 코드에서 오버로딩이라는데 정확히 뭐하는거지?)
  9. 샌드큐가 또 찼으면 다시 Register. 없으면 끝.

 

이것을 epoll에서의 동작으로 바꿔서 정리해보자.

그런데 인증서버는 인증 관련 정보를 다른 클라이언트에게 보낼 일은 절대로 없을테니, 굳이 여러 클라이언트에게 보내기 좋은 구조로 만들 필요가 없을 것 같다. 그리고 일단은 에코서버를 만드는게 목표니까 필요없는 부분은 생략하자.

  1. [워커스레드] 보내고자 하는 내용을 protobuf 패킷 클래스에 담는다. (생략)
  2. 클라이언트(서버)패킷핸들러가 protobuf 패킷 클래스를 받아서 SendBufferRef를 만들어서 return한다. (생략)
  3. 세션의 Send(SendBuffeRef) 함수로 writeBuffers deque에 저장. epoll_ctl로 fd를 EPOLLIN | EPOLLOUT 로 설정
  4. [메인스레드 ] epoll_wait에서 write이벤트 발생. ProcessWrite 함수를 호출한다.
  5. writeBuffers의 모든 WriteBuffer를 write할때가지  SSL_write를 호출한다.
  6. write를 마치고 epoll_ctl로 EPOLLIN 설정

WriteBuffer

SSL_write_ex 함수를 호출해서 10글자를 보내려 헀는데 커널버퍼에 공간이 5밖에 남아있지 않다면 5글자만 쓰고 성공(1)이 리턴된다.

그렇기 때문에 성공 시에도 몇글자를 쓰는데 성공했는지 확인한 뒤에 아직 보내지 못한 데이터를 다음번에 보낼 수 있도록 처리할 수 있게 만들어야했다. 

그리고 Session에서 WriteBuffer deque을 들고있다. deque을 쓰는 이유는 pop front와 push_front를 모두 해야하기 때문.

#pragma once

#include "Types.h"
#include <vector>

class WriteBuffer
{
public:
	WriteBuffer(BYTE* buffer, int dataLen);
	~WriteBuffer();

	BYTE*	GetBuffer() { return &_buffer[_writePos]; }
	int		GetDataLen() { return _allocSize - _writePos; }
	bool	UpdateWritePos(int writeLen);
	
private:
	int					_writePos;
	int					_allocSize;
	std::vector<BYTE>	_buffer;
};
#include "WriteBuffer.h"
#include <cstring>
#include <iostream>

using namespace std;

WriteBuffer::WriteBuffer(BYTE* buffer, int dataLen) : _writePos(0), _allocSize(dataLen)
{
	_buffer.resize(dataLen);
	memcpy(&_buffer[0], buffer, dataLen);
}

WriteBuffer::~WriteBuffer() {}

bool	WriteBuffer::UpdateWritePos(int writeLen)
{
	_writePos += writeLen;
	if (_allocSize <= _writePos)
		return false;
	return true;
}

Send

워커스레드에서 db호출을 마치고 클라이언트에게 보낼 데이터를 만든 뒤 마지막에 사용할 함수.

Send함수로 Session의 writeBuffers에 writeBuffer를 넣고 epoll에 write 이벤트를 등록한다. 클라이언트에게 실제로 보내는건 메인스레드 담당.

void	Session::Send(WriteBufferRef writeBuffer)
{
	if (ServiceRef service = _service.lock())
	{
		std::lock_guard<std::mutex>	lock(m);
	
		_writeBuffers.push_back(writeBuffer);
		ModEvent(EventType::Write);
	}
}

ProcessWrite

워커스레드가 보낼 데이터를 넣어두면 메인스레드의epoll이 write이벤트를 감지하여 ProcessWrite함수를 실행시킨다.

지금까지 session의 writeBuffers deque에 쌓인 모든 버퍼를 front부터 차례대로 전송시도하며, 도중에 못하면 어디까지 보냈는지 설정하고, front로 집어넣어 다음에 보낼 때 가장 먼저 보내도록 만들었다.

 

void	Session::ProcessWrite()
{
	SSL*	ssl = _sslObject->GetSsl();
	while (!_writeBuffers.empty())//writebuffers가 빌때까지 반복
	{
		WriteBufferRef	writeBuffer;
		BYTE*			buffer;
		int				dataLen;
		{
			std::lock_guard<std::mutex>	lock(m);

			writeBuffer = _writeBuffers.front();
			buffer = writeBuffer->GetBuffer();
			dataLen = writeBuffer->GetDataLen();
			_writeBuffers.pop_front();
		}
		
		size_t	writeLen = 0;
		int rtn = SSL_write_ex(ssl, buffer, dataLen, &writeLen);
		if (rtn == 0)
		{// 실패
			int err = SSL_get_error(ssl, rtn);
			if (err == SSL_ERROR_WANT_WRITE){//커널 버퍼 공간 부족
				return;
			} else if (err == SSL_ERROR_ZERO_RETURN) {
				//??
				ProcessDisconnect(true);
				return;
			} else {//SSL에 심각한 에러 발생. SSL_shutdown 금지.
				ProcessDisconnect(false);
				return;
			}
		}
		else if (writeLen != dataLen)
		{//일부만 write 성공 시
			if (writeBuffer->UpdateWritePos(writeLen) == false)
			{
				ProcessDisconnect(true);
				return;
			}
			std::lock_guard<std::mutex>	lock(m);
			_writeBuffers.push_front(writeBuffer);
		}
		ModEvent(EventType::Read);
	}
}

+ Recent posts