From 126ebb888c2fb5516dc108de9a6ebbc8f44adc25 Mon Sep 17 00:00:00 2001 From: Chris Hannon Date: Tue, 25 Jun 2013 22:40:01 -0400 Subject: [PATCH] Implements a socket.io client extension, adds a test case and updates the android makefiles. Contains the following tasks: -initial socket.io extension commit -sioclientimpl subclasses websocket:delegate to respond to websocket events -implement static connect methods and basic client creation -move SocketIO class files into correct extension folder (network) -create SocketIO test in TestCpp -update project references -add missing static modifier to connect method -implement basic test methods -update extensions tests with SocketIO test entry -implement basic handshake and opensocket methods for SocketIO -add Delegate class to handle callbacks, implement virtual Delegate methods in test -implement socket and client registries for lookup when connecting to hosts and endpoints -connect delegate onOpen method by separating impl creation from connection -update test to demonstrate onOpen callback -create send and emit methods, move SIOClient into header file, add send and emit methods to test -implement basic socket.io message parsing -improve logging for events and messages -add logic to pull event name from payload -schedule heartbeat to keep connection alive, scheduled for 90% of the heartbeat interval from the server for safety -add onConnect handler to to catch socket.io onconnect vs websocket onopen -add disconnect and disconnectFromEndpoint methods to properly disconnect and destroy objects -modify SIOClientImpl to track _uri for easier lookup in registries -connect handler for onMessage to message event from socket.io, modify onError handler to take a string instead of WebSocket error code -create SIOEvent callback type, implement event registry in clients, add test for event registration and callback -update SIOEvent to use std::function and c++11, utilize cocos2d CC_CALLBACK method to bind selectors, this ensures that the *this reference is properly passed -check for connect before sending or emitting in the client, cleanup some codes -change connect logic to reuse existing socket connections instead of opening a new one -implements get and set Tag methods for clients for easy reference -improve endpoint handling, add endpoint tests to test layer -additional error handling within socket disconnect error and failure to open connection -fixes extracting endpoint from socket.io messages (in cases of the connect message, where there is 1 less colon for the connect message to the default namespace). Also fixes connecting to the default namespace "/" in the connectToEndpoint method -add disconnect and onClose handlers to client so that onClose is called in the delegate -add disconnect test methods to test layers -change c-style casts to static_casts when using a CCDICT_FOREACH -remove some unneeded namespace completion -add usage documentation -add handling for disconnect from server, cleanup some codes -update comments and documentation in the socketiotest -update includes so the NDK doesn't complain when compiling with c++11 -add socketio.cpp and test.cpp to the android makefiles -update test URL to my public server, test script can also be found in my repo at https://github.com/hannon235/socket.io-testserver.git Signed-off-by: Chris Hannon --- extensions/Android.mk | 1 + extensions/network/SocketIO.cpp | 714 ++++++++++++++++++ extensions/network/SocketIO.h | 179 +++++ extensions/proj.win32/libExtensions.vcxproj | 2 + .../proj.win32/libExtensions.vcxproj.filters | 6 + samples/Cpp/TestCpp/Android.mk | 1 + .../Classes/ExtensionsTest/ExtensionsTest.cpp | 3 + .../NetworkTest/SocketIOTest.cpp | 268 +++++++ .../ExtensionsTest/NetworkTest/SocketIOTest.h | 51 ++ .../Cpp/TestCpp/proj.win32/TestCpp.vcxproj | 2 + .../proj.win32/TestCpp.vcxproj.filters | 6 + 11 files changed, 1233 insertions(+) create mode 100644 extensions/network/SocketIO.cpp create mode 100644 extensions/network/SocketIO.h create mode 100644 samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.cpp create mode 100644 samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.h diff --git a/extensions/Android.mk b/extensions/Android.mk index e95186884c..187b362b84 100644 --- a/extensions/Android.mk +++ b/extensions/Android.mk @@ -78,6 +78,7 @@ GUI/CCEditBox/CCEditBox.cpp \ GUI/CCEditBox/CCEditBoxImplAndroid.cpp \ network/HttpClient.cpp \ network/WebSocket.cpp \ +network/SocketIO.cpp \ physics_nodes/CCPhysicsDebugNode.cpp \ physics_nodes/CCPhysicsSprite.cpp \ LocalStorage/LocalStorageAndroid.cpp \ diff --git a/extensions/network/SocketIO.cpp b/extensions/network/SocketIO.cpp new file mode 100644 index 0000000000..727f7fd657 --- /dev/null +++ b/extensions/network/SocketIO.cpp @@ -0,0 +1,714 @@ +/**************************************************************************** + Copyright (c) 2010-2013 cocos2d-x.org + Copyright (c) 2013 Chris Hannon + + http://www.cocos2d-x.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*based on the SocketIO library created by LearnBoost at http://socket.io +*using spec version 1 found at https://github.com/LearnBoost/socket.io-spec + + ****************************************************************************/ + +#include "SocketIO.h" +#include "cocos-ext.h" +#include "network/WebSocket.h" +#include + +NS_CC_EXT_BEGIN + +//class declarations + +/** + * @brief The implementation of the socket.io connection + * Clients/endpoints may share the same impl to accomplish multiplexing on the same websocket + */ +class SIOClientImpl : + public Object, + public WebSocket::Delegate +{ +private: + int _port, _heartbeat, _timeout; + std::string _host, _sid, _uri; + bool _connected; + + WebSocket *_ws; + + Dictionary* _clients; + +public: + SIOClientImpl(const std::string& host, int port); + virtual ~SIOClientImpl(void); + + static SIOClientImpl* create(const std::string& host, int port); + + virtual void onOpen(cocos2d::extension::WebSocket* ws); + virtual void onMessage(cocos2d::extension::WebSocket* ws, const cocos2d::extension::WebSocket::Data& data); + virtual void onClose(cocos2d::extension::WebSocket* ws); + virtual void onError(cocos2d::extension::WebSocket* ws, const cocos2d::extension::WebSocket::ErrorCode& error); + + void connect(); + void disconnect(); + bool init(); + void handshake(); + void handshakeResponse(HttpClient *sender, HttpResponse *response); + void openSocket(); + void heartbeat(float dt); + + SIOClient* getClient(const std::string& endpoint); + void addClient(const std::string& endpoint, SIOClient* client); + + void connectToEndpoint(const std::string& endpoint); + void disconnectFromEndpoint(const std::string& endpoint); + + void send(std::string endpoint, std::string s); + void emit(std::string endpoint, std::string eventname, std::string args); + + +}; + + +//method implementations + +//begin SIOClientImpl methods +SIOClientImpl::SIOClientImpl(const std::string& host, int port) : + _port(port), + _host(host), + _connected(false) +{ + _clients = Dictionary::create(); + _clients->retain(); + + std::stringstream s; + s << host << ":" << port; + _uri = s.str(); + + _ws = NULL; + +} + +SIOClientImpl::~SIOClientImpl() { + + if(_connected) disconnect(); + + CC_SAFE_DELETE(_clients); + CC_SAFE_DELETE(_ws); + +} + +void SIOClientImpl::handshake() { + CCLog("SIOClientImpl::handshake() called"); + + std::stringstream pre; + pre << "http://" << _uri << "/socket.io/1"; + + HttpRequest* request = new HttpRequest(); + request->setUrl(pre.str().c_str()); + request->setRequestType(HttpRequest::kHttpGet); + + request->setResponseCallback(this, httpresponse_selector(SIOClientImpl::handshakeResponse)); + request->setTag("handshake"); + + CCLog("SIOClientImpl::handshake() waiting"); + + HttpClient::getInstance()->send(request); + + request->release(); + + return; +} + +void SIOClientImpl::handshakeResponse(HttpClient *sender, HttpResponse *response) { + + CCLog("SIOClientImpl::handshakeResponse() called"); + + if (0 != strlen(response->getHttpRequest()->getTag())) + { + CCLog("%s completed", response->getHttpRequest()->getTag()); + } + + int statusCode = response->getResponseCode(); + char statusString[64] = {}; + sprintf(statusString, "HTTP Status Code: %d, tag = %s", statusCode, response->getHttpRequest()->getTag()); + CCLog("response code: %d", statusCode); + + if (!response->isSucceed()) + { + CCLog("SIOClientImpl::handshake() failed"); + CCLog("error buffer: %s", response->getErrorBuffer()); + + DictElement* el = NULL; + + CCDICT_FOREACH(_clients, el) { + + SIOClient* c = static_cast(el->getObject()); + + c->getDelegate()->onError(c, response->getErrorBuffer()); + + } + + return; + } + + CCLog("SIOClientImpl::handshake() succeeded"); + + std::vector *buffer = response->getResponseData(); + std::stringstream s; + + for (unsigned int i = 0; i < buffer->size(); i++) + { + s << (*buffer)[i]; + } + + CCLog("SIOClientImpl::handshake() dump data: %s", s.str().c_str()); + + std::string res = s.str(); + std::string sid; + int pos; + int heartbeat, timeout; + + pos = res.find(":"); + if(pos >= 0) { + sid = res.substr(0, pos); + res.erase(0, pos+1); + } + + pos = res.find(":"); + if(pos >= 0){ + heartbeat = atoi(res.substr(pos+1, res.size()).c_str()); + } + + pos = res.find(":"); + if(pos >= 0){ + timeout = atoi(res.substr(pos+1, res.size()).c_str()); + } + + _sid = sid; + _heartbeat = heartbeat; + _timeout = timeout; + + openSocket(); + + return; + +} + +void SIOClientImpl::openSocket() { + + CCLog("SIOClientImpl::openSocket() called"); + + std::stringstream s; + s << _uri << "/socket.io/1/websocket/" << _sid; + + _ws = new WebSocket(); + if(!_ws->init(*this, s.str())) + { + CC_SAFE_DELETE(_ws); + } + + return; +} + +bool SIOClientImpl::init() { + + CCLog("SIOClientImpl::init() successful"); + return true; + +} + +void SIOClientImpl::connect() { + + this->handshake(); + +} + +void SIOClientImpl::disconnect() { + + if(_ws->getReadyState() == WebSocket::kStateOpen) { + + std::string s = "0::"; + + _ws->send(s); + + CCLog("Disconnect sent"); + + _ws->close(); + + } + + Director::sharedDirector()->getScheduler()->unscheduleAllForTarget(this); + + _connected = false; + + SocketIO::instance()->removeSocket(_uri); + +} + +SIOClientImpl* SIOClientImpl::create(const std::string& host, int port) { + + SIOClientImpl *s = new SIOClientImpl(host, port); + + if(s && s->init()) { + + return s; + + } + + return NULL; + +} + +SIOClient* SIOClientImpl::getClient(const std::string& endpoint) { + + return static_cast(_clients->objectForKey(endpoint)); + +} + +void SIOClientImpl::addClient(const std::string& endpoint, SIOClient* client) { + + _clients->setObject(client, endpoint); + +} + +void SIOClientImpl::connectToEndpoint(const std::string& endpoint) { + + std::string path = endpoint == "/" ? "" : endpoint; + + std::string s = "1::" + path; + + _ws->send(s); + +} + +void SIOClientImpl::disconnectFromEndpoint(const std::string& endpoint) { + + _clients->removeObjectForKey(endpoint); + + if(_clients->count() == 0 || endpoint == "/") { + + CCLog("SIOClientImpl::disconnectFromEndpoint out of endpoints, checking for disconnect"); + + if(_connected) this->disconnect(); + + } else { + + std::string path = endpoint == "/" ? "" : endpoint; + + std::string s = "0::" + path; + + _ws->send(s); + + } + +} + +void SIOClientImpl::heartbeat(float dt) { + + std::string s = "2::"; + + _ws->send(s); + + CCLog("Heartbeat sent"); + +} + + +void SIOClientImpl::send(std::string endpoint, std::string s) { + std::stringstream pre; + + std::string path = endpoint == "/" ? "" : endpoint; + + pre << "3::" << path << ":" << s; + + std::string msg = pre.str(); + + CCLog("sending message: %s", msg.c_str()); + + _ws->send(msg); + +} + +void SIOClientImpl::emit(std::string endpoint, std::string eventname, std::string args) { + + std::stringstream pre; + + std::string path = endpoint == "/" ? "" : endpoint; + + pre << "5::" << path << ":{\"name\":\"" << eventname << "\",\"args\":" << args << "}"; + + std::string msg = pre.str(); + + CCLog("emitting event with data: %s", msg.c_str()); + + _ws->send(msg); + +} + +void SIOClientImpl::onOpen(cocos2d::extension::WebSocket* ws) { + + _connected = true; + + SocketIO::instance()->addSocket(_uri, this); + + DictElement* e = NULL; + + CCDICT_FOREACH(_clients, e) { + + SIOClient *c = static_cast(e->getObject()); + + c->onOpen(); + + } + + Director::sharedDirector()->getScheduler()->scheduleSelector(schedule_selector(SIOClientImpl::heartbeat), this, (_heartbeat * .9), false); + + CCLog("SIOClientImpl::onOpen socket connected!"); + +} + +void SIOClientImpl::onMessage(cocos2d::extension::WebSocket* ws, const cocos2d::extension::WebSocket::Data& data) { + + CCLog("SIOClientImpl::onMessage received: %s", data.bytes); + + int control = atoi(&data.bytes[0]); + + std::string payload, msgid, endpoint, s_data, eventname; + payload = data.bytes; + + int pos, pos2; + + pos = payload.find(":"); + if(pos >=0 ) { + payload.erase(0, pos+1); + } + + pos = payload.find(":"); + if(pos > 0 ) { + msgid = atoi(payload.substr(0, pos+1).c_str()); + } + payload.erase(0, pos+1); + + pos = payload.find(":"); + if(pos >= 0) { + + endpoint = payload.substr(0, pos); + payload.erase(0, pos+1); + + } else { + + endpoint = payload; + } + + if(endpoint == "") endpoint = "/"; + + + s_data = payload; + SIOClient *c = NULL; + c = getClient(endpoint); + if(c == NULL) CCLog("SIOClientImpl::onMessage client lookup returned NULL"); + + switch(control) { + case 0: + CCLog("Received Disconnect Signal for Endpoint: %s\n", endpoint.c_str()); + if(c) c->receivedDisconnect(); + disconnectFromEndpoint(endpoint); + break; + case 1: + CCLog("Connected to endpoint: %s \n",endpoint.c_str()); + if(c) c->onConnect(); + break; + case 2: + CCLog("Heartbeat received\n"); + break; + case 3: + CCLog("Message received: %s \n", s_data.c_str()); + if(c) c->getDelegate()->onMessage(c, s_data); + break; + case 4: + CCLog("JSON Message Received: %s \n", s_data.c_str()); + if(c) c->getDelegate()->onMessage(c, s_data); + break; + case 5: + CCLog("Event Received with data: %s \n", s_data.c_str()); + + if(c) { + eventname = ""; + pos = s_data.find(":"); + pos2 = s_data.find(","); + if(pos2 > pos) { + s_data = s_data.substr(pos+1, pos2-pos-1); + std::remove_copy(s_data.begin(), s_data.end(), + std::back_inserter(eventname), '"'); + } + + c->fireEvent(eventname, payload); + } + + break; + case 6: + CCLog("Message Ack\n"); + break; + case 7: + CCLog("Error\n"); + if(c) c->getDelegate()->onError(c, s_data); + break; + case 8: + CCLog("Noop\n"); + break; + } + + return; +} + +void SIOClientImpl::onClose(cocos2d::extension::WebSocket* ws) { + + if(_clients->count() > 0) { + + DictElement *e; + + CCDICT_FOREACH(_clients, e) { + + SIOClient *c = static_cast(e->getObject()); + + c->receivedDisconnect(); + + } + + } + + this->release(); + +} + +void SIOClientImpl::onError(cocos2d::extension::WebSocket* ws, const cocos2d::extension::WebSocket::ErrorCode& error) { + + +} + +//begin SIOClient methods +SIOClient::SIOClient(const std::string& host, int port, const std::string& path, SIOClientImpl* impl, SocketIO::SIODelegate& delegate) + : _host(host) + , _port(port) + , _path(path) + , _socket(impl) + , _connected(false) + , _delegate(&delegate) +{ + + +} + +SIOClient::~SIOClient(void) { + + if(_connected) { + _socket->disconnectFromEndpoint(_path); + } + +} + +void SIOClient::onOpen() { + + if(_path != "/") { + + _socket->connectToEndpoint(_path); + + } + +} + +void SIOClient::onConnect() { + + _connected = true; + _delegate->onConnect(this); + +} + +void SIOClient::send(std::string s) { + + if(_connected) { + _socket->send(_path, s); + } else { + _delegate->onError(this, "Client not yet connected"); + } + +} + +void SIOClient::emit(std::string eventname, std::string args) { + + if(_connected) { + _socket->emit(_path, eventname, args); + } else { + _delegate->onError(this, "Client not yet connected"); + } + +} + +void SIOClient::disconnect() { + + _connected = false; + + _socket->disconnectFromEndpoint(_path); + + _delegate->onClose(this); + + this->release(); + +} + +void SIOClient::receivedDisconnect() { + + _connected = false; + + _delegate->onClose(this); + + this->release(); + +} + +void SIOClient::on(const std::string& eventName, SIOEvent e) { + + _eventRegistry[eventName] = e; + +} + +void SIOClient::fireEvent(const std::string& eventName, const std::string& data) { + + CCLog("SIOClient::fireEvent called with event name: %s and data: %s", eventName.c_str(), data.c_str()); + + if(_eventRegistry[eventName]) { + + SIOEvent e = _eventRegistry[eventName]; + + e(this, data); + + return; + } + + CCLog("SIOClient::fireEvent no event with name %s found", eventName.c_str()); + +} + +//begin SocketIO methods +SocketIO *SocketIO::_inst = NULL; + +SocketIO::SocketIO() { + + _sockets = Dictionary::create(); + _sockets->retain(); + +} + +SocketIO::~SocketIO(void) { + CC_SAFE_DELETE(_sockets); + delete _inst; +} + +SocketIO* SocketIO::instance() { + + if(!_inst) + _inst = new SocketIO(); + + return _inst; + +} + +SIOClient* SocketIO::connect(SocketIO::SIODelegate& delegate, const std::string& uri) { + + std::string host = uri; + int port, pos; + + pos = host.find("//"); + if(pos >= 0) { + host.erase(0, pos+2); + } + + pos = host.find(":"); + if(pos >= 0){ + port = atoi(host.substr(pos+1, host.size()).c_str()); + } + + pos = host.find("/", 0); + std::string path = "/"; + if(pos >= 0){ + path += host.substr(pos + 1, host.size()); + } + + pos = host.find(":"); + if(pos >= 0){ + host.erase(pos, host.size()); + }else if((pos = host.find("/"))>=0) { + host.erase(pos, host.size()); + } + + std::stringstream s; + s << host << ":" << port; + + SIOClientImpl* socket = NULL; + SIOClient *c = NULL; + + socket = SocketIO::instance()->getSocket(s.str()); + + if(socket == NULL) { + //create a new socket, new client, connect + socket = SIOClientImpl::create(host, port); + + c = new SIOClient(host, port, path, socket, delegate); + + socket->addClient(path, c); + + socket->connect(); + + + + } else { + //check if already connected to endpoint, handle + c = socket->getClient(path); + + if(c == NULL) { + + c = new SIOClient(host, port, path, socket, delegate); + + socket->addClient(path, c); + + socket->connectToEndpoint(path); + + } + + } + + return c; + +} + +SIOClientImpl* SocketIO::getSocket(const std::string& uri) { + + return static_cast(_sockets->objectForKey(uri)); + +} + +void SocketIO::addSocket(const std::string& uri, SIOClientImpl* socket) { + _sockets->setObject(socket, uri); +} + +void SocketIO::removeSocket(const std::string& uri) { + _sockets->removeObjectForKey(uri); +} + +NS_CC_EXT_END \ No newline at end of file diff --git a/extensions/network/SocketIO.h b/extensions/network/SocketIO.h new file mode 100644 index 0000000000..1fadc762a3 --- /dev/null +++ b/extensions/network/SocketIO.h @@ -0,0 +1,179 @@ +/**************************************************************************** + Copyright (c) 2010-2013 cocos2d-x.org + Copyright (c) 2013 Chris Hannon http://www.channon.us + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +*based on the SocketIO library created by LearnBoost at http://socket.io +*using spec version 1 found at https://github.com/LearnBoost/socket.io-spec + +Usage is described below, a full working example can be found in TestCpp under ExtionsTest/NetworkTest/SocketIOTest + +creating a new connection to a socket.io server running at localhost:3000 + + SIOClient *client = SocketIO::connect(*delegate, "ws://localhost:3000"); + +the connection process will begin and if successful delegate::onOpen will be called +if the connection process results in an error, delegate::onError will be called with the err msg + +sending a message to the server + + client->send("Hello!"); + +emitting an event to be handled by the server, argument json formatting is up to you + + client->emit("eventname", "[{\"arg\":\"value\"}]"); + +registering an event callback, target should be a member function in a subclass of SIODelegate +CC_CALLBACK_2 is used to wrap the callback with std::bind and store as an SIOEvent + + client->on("eventname", CC_CALLBACK_2(TargetClass::targetfunc, *targetclass_instance)); + +event target function should match this pattern, *this pointer will be made available + + void TargetClass::targetfunc(SIOClient *, const std::string&) + +disconnect from the endpoint by calling disconnect(), onClose will be called on the delegate once complete +in the onClose method the pointer should be set to NULL or used to connect to a new endpoint + + client->disconnect(); + + ****************************************************************************/ + +#ifndef __CC_SOCKETIO_H__ +#define __CC_SOCKETIO_H__ + +#include "ExtensionMacros.h" +#include "cocos2d.h" + +NS_CC_EXT_BEGIN + +//forward declarations +class SIOClientImpl; +class SIOClient; + +/** + * @brief Singleton and wrapper class to provide static creation method as well as registry of all sockets + */ +class SocketIO +{ +public: + SocketIO(); + virtual ~SocketIO(void); + + static SocketIO *instance(); + + /** + * @brief The delegate class to process socket.io events + */ + class SIODelegate + { + public: + virtual ~SIODelegate() {} + virtual void onConnect(SIOClient* client) = 0; + virtual void onMessage(SIOClient* client, const std::string& data) = 0; + virtual void onClose(SIOClient* client) = 0; + virtual void onError(SIOClient* client, const std::string& data) = 0; + }; + + /** + * @brief Static client creation method, similar to socketio.connect(uri) in JS + * @param delegate The delegate which want to receive events from the socket.io client + * @param uri The URI of the socket.io server + * @return An initialized SIOClient if connected successfully, otherwise NULL + */ + static SIOClient* connect(SocketIO::SIODelegate& delegate, const std::string& uri); + + SIOClientImpl* getSocket(const std::string& uri); + void addSocket(const std::string& uri, SIOClientImpl* socket); + void removeSocket(const std::string& uri); + +private: + + static SocketIO *_inst; + + Dictionary* _sockets; + +}; + +//c++11 style callbacks entities will be created using CC_CALLBACK (which uses std::bind) +typedef std::function SIOEvent; +//c++11 map to callbacks +typedef std::map EventRegistry; + +/** + * @brief A single connection to a socket.io endpoint + */ +class SIOClient + : public Object +{ +private: + int _port; + std::string _host, _path, _tag; + bool _connected; + SIOClientImpl* _socket; + + SocketIO::SIODelegate* _delegate; + + EventRegistry _eventRegistry; + +public: + SIOClient(const std::string& host, int port, const std::string& path, SIOClientImpl* impl, SocketIO::SIODelegate& delegate); + virtual ~SIOClient(void); + + SocketIO::SIODelegate* getDelegate() { return _delegate; }; + + void onOpen(); + void onConnect(); + void receivedDisconnect(); + + /** + * @brief Disconnect from the endpoint, onClose will be called on the delegate when comlpete + */ + void disconnect(); + /** + * @brief Send a message to the socket.io server + */ + void send(std::string s); + /** + * @brief The delegate class to process socket.io events + */ + void emit(std::string eventname, std::string args); + /** + * @brief Used to resgister a socket.io event callback + * Event argument should be passed using CC_CALLBACK2(&Base::function, this) + */ + void on(const std::string& eventName, SIOEvent e); + void fireEvent(const std::string& eventName, const std::string& data); + + inline void setTag(const char* tag) + { + _tag = tag; + }; + + inline const char* getTag() + { + return _tag.c_str(); + }; + +}; + +NS_CC_EXT_END + +#endif /* defined(__CC_JSB_SOCKETIO_H__) */ diff --git a/extensions/proj.win32/libExtensions.vcxproj b/extensions/proj.win32/libExtensions.vcxproj index cf6856cfed..d08da80057 100644 --- a/extensions/proj.win32/libExtensions.vcxproj +++ b/extensions/proj.win32/libExtensions.vcxproj @@ -166,6 +166,7 @@ + @@ -287,6 +288,7 @@ + diff --git a/extensions/proj.win32/libExtensions.vcxproj.filters b/extensions/proj.win32/libExtensions.vcxproj.filters index 6862bbd040..ff707f1d83 100644 --- a/extensions/proj.win32/libExtensions.vcxproj.filters +++ b/extensions/proj.win32/libExtensions.vcxproj.filters @@ -354,6 +354,9 @@ CCArmature\external_tool + + network + @@ -712,6 +715,9 @@ CCArmature\external_tool + + network + diff --git a/samples/Cpp/TestCpp/Android.mk b/samples/Cpp/TestCpp/Android.mk index 55e2bdd3c2..c8550b0a56 100644 --- a/samples/Cpp/TestCpp/Android.mk +++ b/samples/Cpp/TestCpp/Android.mk @@ -63,6 +63,7 @@ Classes/ExtensionsTest/ComponentsTest/ProjectileController.cpp \ Classes/ExtensionsTest/ComponentsTest/SceneController.cpp \ Classes/ExtensionsTest/NetworkTest/HttpClientTest.cpp \ Classes/ExtensionsTest/NetworkTest/WebSocketTest.cpp \ +Classes/ExtensionsTest/NetworkTest/SocketIOTest.cpp \ Classes/ExtensionsTest/EditBoxTest/EditBoxTest.cpp \ Classes/ExtensionsTest/TableViewTest/TableViewTestScene.cpp \ Classes/ExtensionsTest/TableViewTest/CustomTableViewCell.cpp \ diff --git a/samples/Cpp/TestCpp/Classes/ExtensionsTest/ExtensionsTest.cpp b/samples/Cpp/TestCpp/Classes/ExtensionsTest/ExtensionsTest.cpp index 2b6b849a56..52ed969c78 100644 --- a/samples/Cpp/TestCpp/Classes/ExtensionsTest/ExtensionsTest.cpp +++ b/samples/Cpp/TestCpp/Classes/ExtensionsTest/ExtensionsTest.cpp @@ -12,6 +12,7 @@ #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) || (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) #include "NetworkTest/WebSocketTest.h" +#include "NetworkTest/SocketIOTest.h" #endif #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) || (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) || (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_TIZEN) @@ -61,6 +62,8 @@ static struct { #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) || (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) { "WebSocketTest", [](Object *sender){ runWebSocketTest();} }, + { "SocketIOTest", [](Object *sender){ runSocketIOTest();} + }, #endif #if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) || (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID) || (CC_TARGET_PLATFORM == CC_PLATFORM_MAC) || (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32) || (CC_TARGET_PLATFORM == CC_PLATFORM_TIZEN) { "EditBoxTest", [](Object *sender){ runEditBoxTest();} diff --git a/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.cpp b/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.cpp new file mode 100644 index 0000000000..940492a9ab --- /dev/null +++ b/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.cpp @@ -0,0 +1,268 @@ +// +// SocketIOTest.cpp +// TestCpp +// +// Created by Chris Hannon on 6/26/13. +// +// + + +#include "SocketIOTest.h" +#include "../ExtensionsTest.h" + +USING_NS_CC; +USING_NS_CC_EXT; + +SocketIOTestLayer::SocketIOTestLayer(void) + : _sioClient(NULL) + , _sioEndpoint(NULL) +{ + //set the clients to NULL until we are ready to connect + + Size winSize = Director::sharedDirector()->getWinSize(); + + const int MARGIN = 40; + const int SPACE = 35; + + LabelTTF *label = LabelTTF::create("SocketIO Extension Test", "Arial", 28); + label->setPosition(ccp(winSize.width / 2, winSize.height - MARGIN)); + addChild(label, 0); + + Menu *menuRequest = Menu::create(); + menuRequest->setPosition(PointZero); + addChild(menuRequest); + + // Test to create basic client in the default namespace + LabelTTF *labelSIOClient = LabelTTF::create("Open SocketIO Client", "Arial", 22); + MenuItemLabel *itemSIOClient = MenuItemLabel::create(labelSIOClient, CC_CALLBACK_1(SocketIOTestLayer::onMenuSIOClientClicked, this)); + itemSIOClient->setPosition(ccp(VisibleRect::left().x + labelSIOClient->getContentSize().width / 2 + 5, winSize.height - MARGIN - SPACE)); + menuRequest->addChild(itemSIOClient); + + // Test to create a client at the endpoint '/testpoint' + LabelTTF *labelSIOEndpoint = LabelTTF::create("Open SocketIO Endpoint", "Arial", 22); + MenuItemLabel *itemSIOEndpoint = MenuItemLabel::create(labelSIOEndpoint, CC_CALLBACK_1(SocketIOTestLayer::onMenuSIOEndpointClicked, this)); + itemSIOEndpoint->setPosition(ccp(VisibleRect::right().x - labelSIOEndpoint->getContentSize().width / 2 - 5, winSize.height - MARGIN - SPACE)); + menuRequest->addChild(itemSIOEndpoint); + + // Test sending message to default namespace + LabelTTF *labelTestMessage = LabelTTF::create("Send Test Message", "Arial", 22); + MenuItemLabel *itemTestMessage = MenuItemLabel::create(labelTestMessage, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestMessageClicked, this)); + itemTestMessage->setPosition(ccp(VisibleRect::left().x + labelTestMessage->getContentSize().width / 2 + 5, winSize.height - MARGIN - 2 * SPACE)); + menuRequest->addChild(itemTestMessage); + + // Test sending message to the endpoint '/testpoint' + LabelTTF *labelTestMessageEndpoint = LabelTTF::create("Test Endpoint Message", "Arial", 22); + MenuItemLabel *itemTestMessageEndpoint = MenuItemLabel::create(labelTestMessageEndpoint, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestMessageEndpointClicked, this)); + itemTestMessageEndpoint->setPosition(ccp(VisibleRect::right().x - labelTestMessageEndpoint->getContentSize().width / 2 - 5, winSize.height - MARGIN - 2 * SPACE)); + menuRequest->addChild(itemTestMessageEndpoint); + + // Test sending event 'echotest' to default namespace + LabelTTF *labelTestEvent = LabelTTF::create("Send Test Event", "Arial", 22); + MenuItemLabel *itemTestEvent = MenuItemLabel::create(labelTestEvent, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestEventClicked, this)); + itemTestEvent->setPosition(ccp(VisibleRect::left().x + labelTestEvent->getContentSize().width / 2 + 5, winSize.height - MARGIN - 3 * SPACE)); + menuRequest->addChild(itemTestEvent); + + // Test sending event 'echotest' to the endpoint '/testpoint' + LabelTTF *labelTestEventEndpoint = LabelTTF::create("Test Endpoint Event", "Arial", 22); + MenuItemLabel *itemTestEventEndpoint = MenuItemLabel::create(labelTestEventEndpoint, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestEventEndpointClicked, this)); + itemTestEventEndpoint->setPosition(ccp(VisibleRect::right().x - labelTestEventEndpoint->getContentSize().width / 2 - 5, winSize.height - MARGIN - 3 * SPACE)); + menuRequest->addChild(itemTestEventEndpoint); + + // Test disconnecting basic client + LabelTTF *labelTestClientDisconnect = LabelTTF::create("Disconnect Socket", "Arial", 22); + MenuItemLabel *itemClientDisconnect = MenuItemLabel::create(labelTestClientDisconnect, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestClientDisconnectClicked, this)); + itemClientDisconnect->setPosition(ccp(VisibleRect::left().x + labelTestClientDisconnect->getContentSize().width / 2 + 5, winSize.height - MARGIN - 4 * SPACE)); + menuRequest->addChild(itemClientDisconnect); + + // Test disconnecting the endpoint '/testpoint' + LabelTTF *labelTestEndpointDisconnect = LabelTTF::create("Disconnect Endpoint", "Arial", 22); + MenuItemLabel *itemTestEndpointDisconnect = MenuItemLabel::create(labelTestEndpointDisconnect, CC_CALLBACK_1(SocketIOTestLayer::onMenuTestEndpointDisconnectClicked, this)); + itemTestEndpointDisconnect->setPosition(ccp(VisibleRect::right().x - labelTestEndpointDisconnect->getContentSize().width / 2 - 5, winSize.height - MARGIN - 4 * SPACE)); + menuRequest->addChild(itemTestEndpointDisconnect); + + // Sahred Status Label + _sioClientStatus = LabelTTF::create("Not connected...", "Arial", 14, CCSizeMake(320, 100), kTextAlignmentLeft); + _sioClientStatus->setAnchorPoint(ccp(0, 0)); + _sioClientStatus->setPosition(ccp(VisibleRect::left().x, VisibleRect::rightBottom().y)); + this->addChild(_sioClientStatus); + + // Back Menu + MenuItemFont *itemBack = MenuItemFont::create("Back", CC_CALLBACK_1(SocketIOTestLayer::toExtensionsMainLayer, this)); + itemBack->setPosition(ccp(VisibleRect::rightBottom().x - 50, VisibleRect::rightBottom().y + 25)); + Menu *menuBack = Menu::create(itemBack, NULL); + menuBack->setPosition(PointZero); + addChild(menuBack); + +} + + +SocketIOTestLayer::~SocketIOTestLayer(void) +{ +} + +//test event callback handlers, these will be registered with socket.io +void SocketIOTestLayer::testevent(SIOClient *client, const std::string& data) { + + CCLog("SocketIOTestLayer::testevent called with data: %s", data.c_str()); + + std::stringstream s; + s << client->getTag() << " received event testevent with data: " << data.c_str(); + + _sioClientStatus->setString(s.str().c_str()); + +} + +void SocketIOTestLayer::echotest(SIOClient *client, const std::string& data) { + + CCLog("SocketIOTestLayer::echotest called with data: %s", data.c_str()); + + std::stringstream s; + s << client->getTag() << " received event echotest with data: " << data.c_str(); + + _sioClientStatus->setString(s.str().c_str()); + +} + +void SocketIOTestLayer::toExtensionsMainLayer(cocos2d::Object *sender) +{ + ExtensionsTestScene *pScene = new ExtensionsTestScene(); + pScene->runThisTest(); + pScene->release(); + + if(_sioEndpoint) _sioEndpoint->disconnect(); + if(_sioClient) _sioClient->disconnect(); + +} + +void SocketIOTestLayer::onMenuSIOClientClicked(cocos2d::Object *sender) +{ + //create a client by using this static method, url does not need to contain the protocol + _sioClient = SocketIO::connect(*this, "ws://channon.us:3000"); + //you may set a tag for the client for reference in callbacks + _sioClient->setTag("Test Client"); + + //register event callbacks using the CC_CALLBACK_2() macro and passing the instance of the target class + _sioClient->on("testevent", CC_CALLBACK_2(SocketIOTestLayer::testevent, this)); + _sioClient->on("echotest", CC_CALLBACK_2(SocketIOTestLayer::echotest, this)); + +} + +void SocketIOTestLayer::onMenuSIOEndpointClicked(cocos2d::Object *sender) +{ + //repeat the same connection steps for the namespace "testpoint" + _sioEndpoint = SocketIO::connect(*this, "ws://channon.us:3000/testpoint"); + //a tag to differentiate in shared callbacks + _sioEndpoint->setTag("Test Endpoint"); + + //demonstrating how callbacks can be shared within a delegate + _sioEndpoint->on("testevent", CC_CALLBACK_2(SocketIOTestLayer::testevent, this)); + _sioEndpoint->on("echotest", CC_CALLBACK_2(SocketIOTestLayer::echotest, this)); + +} + +void SocketIOTestLayer::onMenuTestMessageClicked(cocos2d::Object *sender) +{ + //check that the socket is != NULL before sending or emitting events + //the client should be NULL either before initialization and connection or after disconnect + if(_sioClient != NULL) _sioClient->send("Hello Socket.IO!"); + +} + +void SocketIOTestLayer::onMenuTestMessageEndpointClicked(cocos2d::Object *sender) +{ + + if(_sioEndpoint != NULL) _sioEndpoint->send("Hello Socket.IO!"); + +} + +void SocketIOTestLayer::onMenuTestEventClicked(cocos2d::Object *sender) +{ + //check that the socket is != NULL before sending or emitting events + //the client should be NULL either before initialization and connection or after disconnect + if(_sioClient != NULL) _sioClient->emit("echotest","[{\"name\":\"myname\",\"type\":\"mytype\"}]"); + +} + +void SocketIOTestLayer::onMenuTestEventEndpointClicked(cocos2d::Object *sender) +{ + + if(_sioEndpoint != NULL) _sioEndpoint->emit("echotest","[{\"name\":\"myname\",\"type\":\"mytype\"}]"); + +} + +void SocketIOTestLayer::onMenuTestClientDisconnectClicked(cocos2d::Object *sender) +{ + + if(_sioClient != NULL) _sioClient->disconnect(); + +} + +void SocketIOTestLayer::onMenuTestEndpointDisconnectClicked(cocos2d::Object *sender) +{ + + if(_sioEndpoint != NULL) _sioEndpoint->disconnect(); + +} + +// Delegate methods + +void SocketIOTestLayer::onConnect(cocos2d::extension::SIOClient* client) +{ + CCLog("SocketIOTestLayer::onConnect called"); + + std::stringstream s; + s << client->getTag() << " connected!"; + _sioClientStatus->setString(s.str().c_str()); + +} + +void SocketIOTestLayer::onMessage(cocos2d::extension::SIOClient* client, const std::string& data) +{ + CCLog("SocketIOTestLayer::onMessage received: %s", data.c_str()); + + std::stringstream s; + s << client->getTag() << " received message with content: " << data.c_str(); + _sioClientStatus->setString(s.str().c_str()); + +} + +void SocketIOTestLayer::onClose(cocos2d::extension::SIOClient* client) +{ + CCLog("SocketIOTestLayer::onClose called"); + + std::stringstream s; + s << client->getTag() << " closed!"; + _sioClientStatus->setString(s.str().c_str()); + + //set the local pointer to NULL or connect to another client + //the client object will be released on its own after this method completes + if(client == _sioClient) { + + _sioClient = NULL; + } else if(client == _sioEndpoint) { + + _sioEndpoint = NULL; + } + +} + +void SocketIOTestLayer::onError(cocos2d::extension::SIOClient* client, const std::string& data) +{ + CCLog("SocketIOTestLayer::onError received: %s", data.c_str()); + + std::stringstream s; + s << client->getTag() << " received error with content: " << data.c_str(); + _sioClientStatus->setString(s.str().c_str()); +} + + + +void runSocketIOTest() +{ + Scene *pScene = Scene::create(); + SocketIOTestLayer *pLayer = new SocketIOTestLayer(); + pScene->addChild(pLayer); + + Director::sharedDirector()->replaceScene(pScene); + pLayer->release(); +} diff --git a/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.h b/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.h new file mode 100644 index 0000000000..fc1ebd8b2e --- /dev/null +++ b/samples/Cpp/TestCpp/Classes/ExtensionsTest/NetworkTest/SocketIOTest.h @@ -0,0 +1,51 @@ +// +// SocketIOTest.h +// TestCpp +// +// Created by Chris Hannon on 6/26/13. +// +// +#ifndef __TestCpp__SocketIOTest__ +#define __TestCpp__SocketIOTest__ + +#include "cocos2d.h" +#include "cocos-ext.h" +#include "network/SocketIO.h" + +class SocketIOTestLayer + : public cocos2d::Layer + , public cocos2d::extension::SocketIO::SIODelegate +{ +public: + SocketIOTestLayer(void); + virtual ~SocketIOTestLayer(void); + + virtual void onConnect(cocos2d::extension::SIOClient* client); + virtual void onMessage(cocos2d::extension::SIOClient* client, const std::string& data); + virtual void onClose(cocos2d::extension::SIOClient* client); + virtual void onError(cocos2d::extension::SIOClient* client, const std::string& data); + + void toExtensionsMainLayer(cocos2d::Object *sender); + + void onMenuSIOClientClicked(cocos2d::Object *sender); + void onMenuTestMessageClicked(cocos2d::Object *sender); + void onMenuTestEventClicked(cocos2d::Object *sender); + void onMenuTestClientDisconnectClicked(cocos2d::Object *sender); + + void onMenuSIOEndpointClicked(cocos2d::Object *sender); + void onMenuTestMessageEndpointClicked(cocos2d::Object *sender); + void onMenuTestEventEndpointClicked(cocos2d::Object *sender); + void onMenuTestEndpointDisconnectClicked(cocos2d::Object *sender); + + + void testevent(cocos2d::extension::SIOClient *client, const std::string& data); + void echotest(cocos2d::extension::SIOClient *client, const std::string& data); + + cocos2d::extension::SIOClient *_sioClient, *_sioEndpoint; + + cocos2d::LabelTTF *_sioClientStatus; +}; + +void runSocketIOTest(); + +#endif /* defined(__TestCpp__SocketIOTest__) */ \ No newline at end of file diff --git a/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj b/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj index 09a603fc73..d1d7c02be5 100644 --- a/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj +++ b/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj @@ -150,6 +150,7 @@ xcopy /Y /Q "$(ProjectDir)..\..\..\..\external\libwebsockets\win32\lib\*.*" "$(O + @@ -258,6 +259,7 @@ xcopy /Y /Q "$(ProjectDir)..\..\..\..\external\libwebsockets\win32\lib\*.*" "$(O + diff --git a/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj.filters b/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj.filters index 967126a591..c5bdcb4e17 100644 --- a/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj.filters +++ b/samples/Cpp/TestCpp/proj.win32/TestCpp.vcxproj.filters @@ -549,6 +549,9 @@ Classes\ExtensionsTest\Scale9SpriteTest + + Classes\ExtensionsTest\NetworkTest + @@ -1043,5 +1046,8 @@ Classes\ExtensionsTest\Scale9SpriteTest + + Classes\ExtensionsTest\NetworkTest + \ No newline at end of file