원작자: Ahmed Charfeddine
작성일: 2012. 05. 17
평점: 4.79 / 5.00 (20명 평가)
원문: A C++ Websocket server for realtime interaction with Web clients (Code Project) / A Websocket protocol implementation atop the Push Framework real time library plus a demo example featuring four types of communication workflows between the HTML5 web client and the server. (역: 웹 클라이언트와 실시간으로 상호작용하는 C++ 웹소켓 서버 / 실시간 푸쉬 프레임워크 라이브러리를 이용한 웹소켓 프로토콜 구현과 HTML5 웹 클라이언트와 통신하는 기능 데모 4가지)
Introduction
웹의 진화에서 웹 소켓 프로토콜의 도입은 이끌어내는 역사에 기록될 만한 흥미로운 것이다. 결과적으로 어떤 웹 페이지가 원격 서버와 양방향 통신을 할 수 있고, 폴링을 하지 않으면서 데이터를 비동기 수신할 수 있습니다. 웹 프론트 엔드를 구현하는 것 만으로도 수 많은 진화된 아이디어가 쉽게 구현되어질 수 있는 문을 열었고, 한가지 구현이 여러 종류의 장치를 타겟팅 할 수 있게 되었습니다. 또한 커스터마이징 된 서버 측 어플리케이션이 정말 많은 수의 클라이언트 커넥션은 처리하는 것이 가능하게 되었고, 심지어 저비용 서버에서도 동작할 수 있게 되었습니다.
이 글에서 우리는 웹소켓 서버 어플리케이션을 개발하고, 웹페이지와 상호작용하는 쇼케이스를 만들어 볼겁니다. 이 솔루션은 이전에 Push Framework에서 공개한 실시간 통신 라이브러리를 기반으로 개발자에 의해 쉽게 재사용될 수 있는 독립된 웹 소켓 라이브러리로 만들게 될겁니다.
Protocol Extension Layer
이 글의 기반이 된 Push Framework에서 제시한 솔루션은 우리가 많은 클라이언트를 관리하는 것을 보다 쉽게 만들어 줍니다. 이 프레임워크는 특정 프로토콜에 종속되지 않으며 몇 가지 추상 클래스에 대한 구현만 하게 되면 어떤 것이든 우리가 손쉽게 프로토콜을 구현할 수 있습니다. (푸쉬 프레임워크는 실시간 서버를 만들기 위한 라이브러리임)
1. IncomingPacket: 들어오는 메시지들을 표현하기 위한 프로토타입 클래스입니다. 클라이언트에 의해 전송된 메시지를 서버가 어떻게 반응해야 하는지를 구현합니다.
2. OutgoingPacket: 나가는 메시지를 표현하기 위한 프로토타입 클래스입니다. 대부분의 프로토콜은 대칭형으로 구현되며, 이것 역시도 IncomingPacket과 대칭되도록 구현되어야 합니다.
3. Protocol: 들어오거나 나가는 패킷들이 어떻게 인스턴싱되어야 하는지, 어떻게 엔코딩(혹은 디코딩)되어야 하는지에 대한 코드를 구현하게 됩니다.
PushFramework::Protocol을 완전한 클래스로 만들기 위해서는 아래 가상 메서드들이 구현되어야 합니다.
1. encodeOutgoingPacket: 이 메서드는 OutgoingPacket 인스턴스 하나를 받아서 바이너리로 엔코딩하는 동작을 수행해야 합니다.
2. frameOutgoingPacket: 이 메서드는 엔코딩된 OutgoingPacket을 여러 조각으로 나눠 그걸 패킷의 페이로드에 집어넣는 동작을 수행해야 합니다.
3. tryDefreameIncomingPacket: 수신된 데이터의 레퍼런스를 제공받아서 IncomingPacket 개체로 인스턴싱 될 수 있는지를 테스트하는 동작을 수행하며, 반환 값으로서 IncomingPacket의 인스턴스를 반환하도록 구현해야 합니다.
4. decodeIncomingPacket: 만약 tryDeframeIncomingPacket이 정상적인 IncomingPacket 인스턴스를 반환하면 그 내용을 디코딩하는 동작을 수행해야 합니다.
이 메서드들은 프로토콜을 구현할 때 필요한 요소를 충분히 추상화하여 제공하며 Serialization 혹은 Deserialization되어질 때 PF(Push Framework)에 의해 내부적으로 요청되어지게 됩니다. 웹 소켓 프로토콜을 구현할 때, 그게 프레임 기반 프로토콜 중 하나이기 때문에 반드시 엔코딩과 디코딩 파트가 서로 나눠져 구현되어야 하고, 각각의 파트는 데이터에 헤더를 붙여서 패킷의 페이로드로 탑재하거나 꺼내는 동작을 수행한 후 네트워크를 통해 송수신하게 될것입니다. 그러나 그건 페이로드가 어떻게 엔코딩될지를 결정하지는 않기 때문에 그에 대한 실질적인 동작은 frameOutgoingPacket 메서드와 tryDeframeIncomingPacket에서 수행하게 될 것입니다. 우리의 예제를 보면 우리는 엔코딩 단계에서 그렇게 큰 동작을 하지는 않기 때문에 그걸 알맞게 수정해서 사용하면 될 것입니다. 뭐, JSON 레이어를 추가한다던지 말이지요.
그러나 우린 웹소켓 프로토콜 스펙이 두 가지 통신 단계를 가지기 때문에 두 가지 데이터 구조를 생성해야 합니다.
1. 핸드쉐이크 메시지: 웹 소켓이 연결되면 핸드쉐이크가 일어나게 되는데 그 과정 중에 주고 받을 메시지입니다.
2. 웹 소켓 데이터 메시지: 핸드쉐이크 단계가 완료된 이후엔 이 구조를 이용해서 데이터를 주고받게 될 겁니다.
아래 코드는 받은 패킷을 위 에서 언급했던 메시지 타입중 어디에 해당되는지 확인하고, 그에 맞게 패킷을 해석하기 위한 코드입니다.
int WebsocketProtocol::tryDeframeIncomingPacket( PushFramework::DataBuffer& buffer,
PushFramework::IncomingPacket*& pPacket, int& serviceId,
unsigned int& nExtractedBytes, ConnectionContext* pContext )
{
if (buffer.GetDataSize() == 0)
return Protocol::eIncompletePacket;
WebsocketConnectionContext* pCxt = (WebsocketConnectionContext*) pContext;
if (pCxt->GetStage() == WebsocketConnectionContext::HandshakeStage) {
WebsocketHandshakeMessage* pMessage =
new WebsocketHandshakeMessage(buffer.GetBuffer(), buffer.GetDataSize());
serviceId = 0;
nExtractedBytes = buffer.GetDataSize();
pPacket = pMessage;
return Protocol::Success;
}
// 핸드쉐이크 단계가 아닌경우 아마도 데이터 메시지일 것으로 추정한다.
int nMinExpectedSize = 6;
if (buffer.GetDataSize() < nMinExpectedSize)
return Protocol::eIncompletePacket;
BYTE payloadFlags = buffer.getAt(0);
if (payloadFlags != 129)
return Protocol::eUndefinedFailure;
BYTE basicSize = buffer.getAt(1) & 0x7F;
unsigned __int64 payloadSize;
int masksOffset;
if (basicSize <= 125) {
payloadSize = basicSize;
masksOffset = 2;
}
else if (basicSize == 126) {
nMinExpectedSize += 2;
if (buffer.GetDataSize() < nMinExpectedSize)
return Protocol::eIncompletePacket;
payloadSize = ntohs( *(u_short*) (buffer.GetBuffer() + 2) );
masksOffset = 4;
}
else if (basicSize == 127) {
nMinExpectedSize += 8;
if (buffer.GetDataSize() < nMinExpectedSize)
return Protocol::eIncompletePacket;
payloadSize = ntohl( *(u_long*) (buffer.GetBuffer() + 2) );
masksOffset = 10;
}
else return Protocol::eUndefinedFailure;
nMinExpectedSize += payloadSize;
if (buffer.GetDataSize() < nMinExpectedSize)
return Protocol::eIncompletePacket;
BYTE masks[4];
memcpy(masks, buffer.GetBuffer() + masksOffset, 4);
char* payload = new char[payloadSize + 1];
memcpy(payload, buffer.GetBuffer() + masksOffset + 4, payloadSize);
for (unsigned __int64 i = 0; i < payloadSize; i++)
payload[i] = (payload[i] ^ masks[i%4]);
payload[payloadSize] = '\0';
WebsocketDataMessage* pMessage = new WebsocketDataMessage(payload);
serviceId = 1;
nExtractedBytes = nMinExpectedSize;
pPacket = pMessage;
delete payload;
return Protocol::Success;
}
The Websocket Server
웹 소켓 서버에서, 우리는 PushFramework::Server를 상속받아 구현된 개체를 인스턴싱 한 후 우리는 Protocol 클래스 인스턴스나 Service 인스턴스, ClientFactory 인스턴스를 생성하는 등 초기화 작업을 수행해야 합니다. 그리고 나서 ::Start 메서드를 호출함으로서 서버를 시작시키면 됩니다.
이 함수가 호출되었을 때 많은 리소스들이 거기에 들어가게 될겁니다.
1. 호 수락 쓰레드 (listening)
2. IO 이벤트 폴링을 처리할 서비스 쓰레드
3. 서버 전체 흐름을 처리할 주 쓰레드
4. 스트리밍 쓰레드 몇개. (이것들은 요청자로부터 받은 데이터를 브로드캐스팅 큐에 스트리밍하게 될 것입니다)
웹소켓 서버를 위한 Protocol 개체는 반드시 몇 가지 DLL 프로젝트로 나눠서 설계된 WebsocketProtocol을 상속받아 구현해야 합니다. ClientFactory 서브 클래스에서 연결된 클라이언트들의 라이프 싸이클(생명 주기, Life Cycle)을 관리하고 새로 연결된 클라이언트가 LogicalConnection 개체로 캡슐화 되어야 하는지 아닌지를 결정하게 될겁니다. 이 때 우리는 두가지 유효성 검사를 통해서 결정하게 될 것인데, 프로토콜 자체가 정의하는 핸드쉐이크가 성공적으로 이뤄졌는지 혹은 클라이언트가 보낸 어떤 로그인 정보에 의해 결정될 것입니다.
int WebsocketClientFactory::onFirstRequest( IncomingPacket& _request,
ConnectionContext* pConnectionContext, LogicalConnection*& lpClient,
OutgoingPacket*& lpPacket )
{
WebsocketConnectionContext* pCxt = (WebsocketConnectionContext*) pConnectionContext;
// 실제 연결이 이루어진 후 수신한 메시지인데,
// 아래 단계에서 이루어질 테스트에 통과되지 않으면
// LogicalConnection 개체로 캡슐화하지 않게 될 것입니다.
if (pCxt->GetStage() == WebsocketConnectionContext::HandshakeStage) {
WebsocketHandshakeMessage& request = (WebsocketHandshakeMessage&) _request;
if (!request.Parse()) {
return ClientFactory::RefuseAndClose;
}
WebsocketHandshakeMessage *pResponse = new WebsocketHandshakeMessage();
if (WebsocketProtocol::ProcessHandshake(request, *pResponse)) {
lpPacket = pResponse;
pCxt->SetStage(WebsocketConnectionContext::LoginStage);
}
// 이 경우 접속을 끊지는 않고, LogicalConnection 개체로 캡슐화 되기 위해서
// 로그인 메시지를 대기합니다.
return ClientFactory::RefuseRequest;
}
if (pCxt->GetStage() == WebsocketConnectionContext::LoginStage) {
WebsocketDataMessage& request = (WebsocketDataMessage&) _request;
WebsocketClient* pClient = new WebsocketClient(request.GetArg1());
lpClient = pClient;
WebsocketDataMessage *pResponse = new WebsocketDataMessage(LoginCommunication);
pResponse->SetArguments("Welcome " + request.GetArg1());
lpPacket = pResponse;
pCxt->SetStage(WebsocketConnectionContext::ConnectedStage);
return ClientFactory::CreateClient;
}
// 여기까지 도달할 수 없습니다.
}
서버가 수행할 동작에 대한 코드는 "Service" 클래스로 조직되어 들어가게 될 것입니다. 각각의 요청은 어떤 요청인가에 따라서 부분적으로 다르게 처리될 것입니다.
WebsocketServer server;
server.registerService(EchoCommunication, new EchoService, "echo");
server.registerService(Routedcommunication, new RoutedCommunicationService, "routed");
server.registerService(GroupCommunication, new GroupCommunicationService, "grouped");
server.registerService(StreamedCommunication, new StreamedCommunicationService, "streamed");
이렇게 어떤 요청인가에 따라서 서로 다른 "Service" 객체가 알맞은 처리 로직을 실행하게 될 것입니다. 위 코드에서 두번째 줄의 핸들러를 살펴보면,
void RoutedCommunicationService::handle( LogicalConnection* pClient, IncomingPacket* pRequest ) {
WebsocketDataMessage& request = (WebsocketDataMessage&)(*pRequest);
WebsocketClient& client = (WebsocketClient&) (*pClient);
LogicalConnection* pRecipient = FindClient(request.GetArg1().c_str());
if (pRecipient) {
WebsocketDataMessage response(Routedcommunication);
response.SetArguments(client.getKey(), request.GetArg2());
pRecipient->PushPacket(&response);
}
}
이렇게 단순히 Echo 처리만 하고 있습니다. 네번째 줄에서 정의한 streamed 요청에 대한 처리 코드는 아래와 같습니다. "subscribe" 라는 기능에 대한 요청인지, 아니면 "unsubscribe" 라는 기능에 대한 요청인지에 따라서 다르게 처리하고 있습니다.
void StreamedCommunicationService::handle( LogicalConnection* pClient, IncomingPacket* pRequest ) {
WebsocketDataMessage& request = (WebsocketDataMessage&)(*pRequest);
WebsocketClient& client = (WebsocketClient&) (*pClient);
string opType = request.GetArg1();
if (opType == "subscribe") {
broadcastManager.SubscribeConnectionToQueue(client.getKey(), "streamingQueue");
}
if (opType == "unsubscribe") {
broadcastManager.UnsubscribeConnectionFromQueue(client.getKey(), "streamingQueue");
}
}
사실 PF는 이미 브로드캐스팅을 계속 받을지, 아니면 더 이상 받지 않을지를 결정하는 메커니즘을 가지고 있기 때문에 어떤 메시지를 브로드캐스팅 큐에 넣을 때에 대해서만 신경쓰면 됩니다. 메시지 전송자들이 수신자에 대해서 잘 모르고, 수신자는 송신자에 대해서 잘 모르기 때문에 스트리밍 요청이 오면 그것을 받고자 하는 접속자에게만 그 스트리밍 메시지를 전달해 주면 되는 것입니다.
The Client
우리가 만들 웹 페이지는 네가지 탭을 가지고 있고, 그 탭들은 테스트를 위하 몇 가지를 구현하고 있습니다.
1. Echo Communication: 어떤 메시지를 전송하면 서버는 아무런 처리를 하지 않고 수신받은 그대로 클라이언트에게 전송합니다.
2. Routed Communication: 서버가 어떤 메시지를 일부 클라이언트들에게 전송 할 때 그 메시지들이 목적지에 잘 도착했는지를 관리하게 됩니다.
3. Group Communication: 어떤 메시지가 서버에 도착하면 그걸 브로드캐스팅 대기 큐에 등록하게 되는데, 우리는 원격으로 대기열에 등록된 모든 컨텐츠를 수신할 수 있습니다.
4. Streamed Communication: 브로드캐스팅 큐에 등록된 컨텐츠를 자동으로 계속 수신할지, 더이상 수신하지 않을지를 결정할 수 있습니다. 서버 쓰레드는 이걸로 계속 컨텐츠를 공급할 것이고, 우리는 클라이언트가 실시간 통신을 하는 것을 경험 할 수 있습니다.
로그인 하려면, 클라이언트에서 인증 정보를 입력하고, "Connect"를 누릅니다. 그러면 서버가 응답을 줄 겁니다.
독자분들 께서는 에코나 스트리밍 탭 들을 이용해서 서버가 구현된 4가지 형태의 예제를 테스트 할 수 있습니다. 이 웹 페이지들을 통해서 자동으로 생성한 실시간 스트리밍을 볼 수 있을 겁니다.
References
License
이 글과 관계된 모든 소스코드나 파일들은 The Apache License, Version 2.0을 따릅니다. - 역자 주: 이 라이센스 약관은 널리 퍼진거라 쉽게 번역본을 구할 수 있을 겁니다. 하하;;
위 Ahmed Charfeddine 씨가 원문 작성자이며, Code Project 홈페이지에서 만나보실(?) 수 있습니다.
다시한번 알려드리지만, 이 글(한글로 번역된 이 포스팅)의 원문 작성자는 제가 아니며 Ahmed Charfeddine 씨입니다. 원문의 라이센스가 Apache License 2.0을 따르기 때문에 번역본의 라이센스 역시 Apache License 2.0을 (라이센스 정책상)따르게 되었지만, 반드시 출처와 원문 저자, 역자 및 번역본 주소를 표기해 주시기 바랍니다.
아쉬웠던 점은 이 글에서 구현한 코드가 Push Framework라는 다른 라이브러리에 의존한다는 것이고, 또 하나는 Windows Specific 코드라는 점입니다. 이것을 Linux에서 돌릴 수 있고 다른 라이브러리에 대한 의존성이 없는 버젼으로 재구성해서 작성 해 볼만 한데, 평일에는 제가 시간이 충분치 못해서 아마도 주말로 미뤄야 할 듯합니다. 그리고 이걸 번역하면서 힘들었던 점은 영문 구사가 좀 독특했다는 점(핑계 죄송합니다!! 으앙...ㅜㅜ)입니다. 음,... 오역이 있을 수 있습니다. 아쉬운 부분에 대한 구현은 제가 직접 구현해서 주말에 다시 가지고 돌아오겠습니다. 내일은 이거 말고 다른 걸 들고 나올거에욧! 하핳;;
약속드렸던 것을 스크랩 자료로 대체합니다.(으익!! 주먹 치워주세여 ㅜㅜ)
[Scraps] Websocket Example under Linux with libwebsocket
출처 표기하는 방법:
A C++ Websocket Server for realtime interaction with Web client (Code Project, @charfeddine-ahmed) / 역: Jay K (http://www.jayks.ml/4)
이런 형태로 출처와 원문 저자, 역자 및 번역본 주소가 잘 표기되어 있으면 좋겠습니다.
감사합니다.
'Translations' 카테고리의 다른 글
How to create an Operating System - Bare Bones, Moving Forward&FAQ (0) | 2017.07.13 |
---|---|
How to create an Operating System - Bare Bones (0) | 2017.07.12 |
Creating a C++ Thread Class (0) | 2017.07.11 |
C++ Memory Leak Finder under Linux (0) | 2017.07.08 |