Skip to content

Commit

Permalink
Improved Websocket Server
Browse files Browse the repository at this point in the history
This commit adds some important improvements to the websocket server
	- The websocket can now handled framing. Websockets have a maximum frame size, if a message is larger than that it will be split into multiple frames (which, in turn, might be split into multiple chunks each). Previously, the webserver would call the onMessage callback on every chunk. Now, the server gathers the entire message first and then calls the onMessage callback for the entire message.
	- The WS server now calls libwebsocket_callback_on_writable instead of libwebsocket_callback_on_writable_all_protocol where appropiate
	- A little refactoring where we added BLWebsocketConnection that gathers everything related to a single WS connection
	- CWWebServer has been refactored so the websocket callbacks are more readable
  • Loading branch information
Mario Schreiner committed Jan 23, 2015
1 parent 0baf165 commit b7b7ca0
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 203 deletions.
8 changes: 8 additions & 0 deletions Connichiwa.xcodeproj/project.pbxproj
Expand Up @@ -101,6 +101,8 @@
33618AC1195DAD37006FA51B /* CWRemoteLibraryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 33618ABF195DAD37006FA51B /* CWRemoteLibraryManager.m */; };
33618AC8195DEDFE006FA51B /* CWWebLibraryManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 33618AC6195DEDFE006FA51B /* CWWebLibraryManager.h */; };
33618AC9195DEDFE006FA51B /* CWWebLibraryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 33618AC7195DEDFE006FA51B /* CWWebLibraryManager.m */; };
3362DB801A72C8BE0038985E /* BLWebSocketConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 3362DB7E1A72C8BE0038985E /* BLWebSocketConnection.h */; };
3362DB811A72C8BE0038985E /* BLWebSocketConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 3362DB7F1A72C8BE0038985E /* BLWebSocketConnection.m */; };
3367186C194776C1000A7164 /* CWWebserverManagerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3367186B194776C1000A7164 /* CWWebserverManagerDelegate.h */; };
337FE7961977E07B00912293 /* weblib in Resources */ = {isa = PBXBuildFile; fileRef = 337FE7951977E07B00912293 /* weblib */; };
337FE7981977E2FA00912293 /* remote.html in Resources */ = {isa = PBXBuildFile; fileRef = 337FE7971977E2FA00912293 /* remote.html */; };
Expand Down Expand Up @@ -275,6 +277,8 @@
33618AC6195DEDFE006FA51B /* CWWebLibraryManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CWWebLibraryManager.h; sourceTree = "<group>"; };
33618AC7195DEDFE006FA51B /* CWWebLibraryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CWWebLibraryManager.m; sourceTree = "<group>"; };
33618ACA195E233D006FA51B /* CWWebLibraryManagerDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CWWebLibraryManagerDelegate.h; sourceTree = "<group>"; };
3362DB7E1A72C8BE0038985E /* BLWebSocketConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BLWebSocketConnection.h; sourceTree = "<group>"; };
3362DB7F1A72C8BE0038985E /* BLWebSocketConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BLWebSocketConnection.m; sourceTree = "<group>"; };
3367186B194776C1000A7164 /* CWWebserverManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CWWebserverManagerDelegate.h; sourceTree = "<group>"; };
337FE7951977E07B00912293 /* weblib */ = {isa = PBXFileReference; lastKnownFileType = folder; path = weblib; sourceTree = "<group>"; };
337FE7971977E2FA00912293 /* remote.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = remote.html; sourceTree = "<group>"; };
Expand Down Expand Up @@ -382,6 +386,8 @@
children = (
3317C6D61A714BCB008BD104 /* BLWebSocketsServer.h */,
3317C6D71A714BCB008BD104 /* BLWebSocketsServer.m */,
3362DB7E1A72C8BE0038985E /* BLWebSocketConnection.h */,
3362DB7F1A72C8BE0038985E /* BLWebSocketConnection.m */,
3317C6D81A714BCB008BD104 /* libwebsockets */,
);
path = BLWebSocketsServer;
Expand Down Expand Up @@ -615,6 +621,7 @@
333A80671953996D00420D5A /* CWBluetoothManager.h in Headers */,
3338A7A41A71204300898D4E /* GCDWebServerConnection.h in Headers */,
334AB747193FDADC00A15999 /* CWWebApplication.h in Headers */,
3362DB801A72C8BE0038985E /* BLWebSocketConnection.h in Headers */,
3338A7BC1A71204300898D4E /* GCDWebServerStreamedResponse.h in Headers */,
3317C6FF1A714BCB008BD104 /* getifaddrs.h in Headers */,
3338A7A61A71204300898D4E /* GCDWebServerFunctions.h in Headers */,
Expand Down Expand Up @@ -953,6 +960,7 @@
3317C6FB1A714BCB008BD104 /* extension-deflate-stream.c in Sources */,
3317C7061A714BCB008BD104 /* server-handshake.c in Sources */,
3317C7011A714BCB008BD104 /* libwebsockets.c in Sources */,
3362DB811A72C8BE0038985E /* BLWebSocketConnection.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
24 changes: 24 additions & 0 deletions Connichiwa/BLWebSocketsServer/BLWebSocketConnection.h
@@ -0,0 +1,24 @@
//
// BLWebSocket.h
// Connichiwa
//
// Created by Mario Schreiner on 23/01/15.
// Copyright (c) 2015 Mario Schreiner. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "BLWebSocketsServer.h"
#import "libwebsockets.h"



@interface BLWebSocketConnection : NSObject

@property (readonly) int ID;
@property (readonly) struct libwebsocket *socket;
@property (readwrite, copy) BLWebSocketsHandleRequestBlock handleRequestBlock;
@property (readwrite, strong) NSMutableData *currentMessage;

- (instancetype)initWithID:(int)ID socket:(struct libwebsocket *)socket;

@end
33 changes: 33 additions & 0 deletions Connichiwa/BLWebSocketsServer/BLWebSocketConnection.m
@@ -0,0 +1,33 @@
//
// BLWebSocket.m
// Connichiwa
//
// Created by Mario Schreiner on 23/01/15.
// Copyright (c) 2015 Mario Schreiner. All rights reserved.
//

#import "BLWebSocketConnection.h"



@interface BLWebSocketConnection()

@property (readwrite) int ID;
@property (readwrite) struct libwebsocket *socket;

@end

@implementation BLWebSocketConnection

- (instancetype)initWithID:(int)ID socket:(struct libwebsocket *)socket {
self = [super init];

self.ID = ID;
self.socket = socket;

return self;
}



@end
6 changes: 3 additions & 3 deletions Connichiwa/BLWebSocketsServer/BLWebSocketsServer.h
Expand Up @@ -8,7 +8,7 @@

#import <Foundation/Foundation.h>

typedef NSData *(^BLWebSocketsHandleRequestBlock)(int id, NSData * requestData);
typedef void (^BLWebSocketsHandleRequestBlock)(int connectionID, NSData *messageData);

@interface BLWebSocketsServer : NSObject

Expand All @@ -19,8 +19,8 @@ typedef NSData *(^BLWebSocketsHandleRequestBlock)(int id, NSData * requestData);
- (void)startListeningOnPort:(int)port withProtocolName:(NSString *)protocolName andCompletionBlock:(void(^)(NSError *error))completionBlock;
- (void)stopWithCompletionBlock:(void(^)())completionBlock;
- (void)setDefaultHandleRequestBlock:(BLWebSocketsHandleRequestBlock)handleRequestBlock;
- (void)setHandleRequestBlock:(BLWebSocketsHandleRequestBlock)handleRequestBlock forSession:(int)user;
- (void)push:(NSData *)data toSession:(int)session;
- (void)setHandleRequestBlock:(BLWebSocketsHandleRequestBlock)handleRequestBlock forConnection:(int)user;
- (void)push:(NSData *)data toConnection:(int)connectionID;
- (void)pushToAll:(NSData *)data;

@end
79 changes: 45 additions & 34 deletions Connichiwa/BLWebSocketsServer/BLWebSocketsServer.m
Expand Up @@ -10,6 +10,7 @@
#import "libwebsockets.h"
#import "private-libwebsockets.h"
#import "BLAsyncMessageQueue.h"
#import "BLWebSocketConnection.h"

static int pollingInterval = 20000;
static NSString * http_only_protocol = @"http-only";
Expand Down Expand Up @@ -42,7 +43,7 @@ @interface BLWebSocketsServer()
@property (nonatomic, assign) struct libwebsocket_context *context;
@property (nonatomic, strong) BLAsyncMessageQueue *asyncMessageQueue;
@property (nonatomic, strong, readwrite) BLWebSocketsHandleRequestBlock defaultHandleRequestBlock;
@property (nonatomic, strong, readwrite) NSMutableDictionary *handleRequestBlocks;
@property (nonatomic, strong, readwrite) NSMutableDictionary *connections;
/* Temporary storage for the server stopped completion block */
@property (nonatomic, strong) void(^serverStoppedCompletionBlock)();
/* Incremental value that defines the sessionId */
Expand All @@ -61,7 +62,7 @@ + (BLWebSocketsServer *)sharedInstance {
sharedInstance = [[self alloc] init];
sharedInstance.defaultHandleRequestBlock = NULL;
sharedInstance.asyncMessageQueue = [[BLAsyncMessageQueue alloc] init];
sharedInstance.handleRequestBlocks = [NSMutableDictionary dictionary];
sharedInstance.connections = [NSMutableDictionary dictionary];
});
return sharedInstance;
}
Expand Down Expand Up @@ -179,20 +180,21 @@ - (void)cleanup {

#pragma mark Session Management

- (void)setHandleRequestBlock:(BLWebSocketsHandleRequestBlock)handleRequestBlock forSession:(int)user {
NSString *key = [NSString stringWithFormat:@"%d", user];
BLWebSocketsHandleRequestBlock value = [handleRequestBlock copy];
[self.handleRequestBlocks setValue:value forKey:key];
- (void)setHandleRequestBlock:(BLWebSocketsHandleRequestBlock)handleRequestBlock forConnection:(int)connectionID {
id key = [BLWebSocketsServer keyForConnectionID:connectionID];
BLWebSocketConnection *connection = [self.connections objectForKey:key];
[connection setHandleRequestBlock:handleRequestBlock];
}

#pragma mark - Async messaging

- (void)push:(NSData *)data toSession:(int)session {
[self.asyncMessageQueue enqueueMessage:data forUserWithId:session];
- (void)push:(NSData *)data toConnection:(int)connectionID {
id key = [BLWebSocketsServer keyForConnectionID:connectionID];
[self.asyncMessageQueue enqueueMessage:data forUserWithId:connectionID];
BLWebSocketConnection *connection = [self.connections objectForKey:key];
dispatch_async(networkQueue, ^{
//TODO we should change this to "libwebsocket_callback_on_writable", but we need the libwebsocket object that
//TODO corresponds with the session id for that. we might need to store that in an additional dictionary
libwebsocket_callback_on_writable_all_protocol(&(self.context->protocols[1]));
//Make sure the new message is sent as soon as the socket is writable
libwebsocket_callback_on_writable(self.context, connection.socket);
});
}

Expand All @@ -203,6 +205,10 @@ - (void)pushToAll:(NSData *)data {
});
}

+ (id)keyForConnectionID:(int)connectionID {
return [NSNumber numberWithInt:connectionID];
}


@end

Expand All @@ -228,45 +234,51 @@ static int callback_websockets(struct libwebsocket_context * this,
void *user, void *in, size_t len) {
int *session_id = (int *) user;
switch (reason) {
case LWS_CALLBACK_ESTABLISHED:
NSLog(@"%@", @"Connection established");
case LWS_CALLBACK_ESTABLISHED: {
*session_id = sharedInstance.sessionIdIncrementalCount++;
[sharedInstance.asyncMessageQueue addMessageQueueForUserWithId:*session_id];

id sessionKey = [BLWebSocketsServer keyForConnectionID:*session_id];
BLWebSocketConnection *connection = [[BLWebSocketConnection alloc] initWithID:*session_id socket:wsi];
[sharedInstance.connections setObject:connection forKey:sessionKey];

break;
}
case LWS_CALLBACK_RECEIVE: {
//TODO we can use libwebsocket_is_final_fragment() and libwebsockets_remaining_packet_payload() to check if this is just a fragment of the message
//If so, we should add it to a buffer and only deliver it to the HandleRequestBlock after the entire message has been received
const size_t remaining = libwebsockets_remaining_packet_payload(wsi);
NSLog(@"Receive. Is Final: %d || Remain: %zu", libwebsocket_is_final_fragment(wsi), remaining);
NSData *data = [NSData dataWithBytes:(const void *)in length:len];
NSData *response = nil;
NSString *key = [NSString stringWithFormat:@"%d", *session_id];
BLWebSocketsHandleRequestBlock hrb = (BLWebSocketsHandleRequestBlock)[sharedInstance.handleRequestBlocks objectForKey:key];
if (!hrb) {
hrb = sharedInstance.defaultHandleRequestBlock;
}
if (hrb) {
response = hrb(*session_id, data);
}
if (response) {
write_data_websockets(response, wsi);
id sessionKey = [BLWebSocketsServer keyForConnectionID:*session_id];

BLWebSocketConnection *connection = [sharedInstance.connections objectForKey:sessionKey];

NSData *chunk = [NSData dataWithBytes:(const void *)in length:len];
if (connection.currentMessage == nil) connection.currentMessage = [NSMutableData data];
[connection.currentMessage appendData:chunk];

int isFinalFragment = libwebsocket_is_final_fragment(wsi);
const size_t remainingBytes = libwebsockets_remaining_packet_payload(wsi);
if (isFinalFragment && remainingBytes == 0) {
//This is the last (or only) chunk, the message is complete. We trigger the callback
BLWebSocketsHandleRequestBlock hrb = connection.handleRequestBlock;
if (!hrb) hrb = sharedInstance.defaultHandleRequestBlock;
if (hrb) hrb(*session_id, connection.currentMessage);
connection.currentMessage = nil;

}

break;
}
case LWS_CALLBACK_SERVER_WRITEABLE: {
NSData *message = [sharedInstance.asyncMessageQueue messageForUserWithId:*session_id];
if (message != nil) {
write_data_websockets(message, wsi);

//TODO we need this so the writeable callback is called again
//this might produce a lot of overhead, we should check if there are messages in the queue left
//also, we should change writable_all_protocol to writeable
libwebsocket_callback_on_writable_all_protocol(&(sharedInstance.context->protocols[1]));
//Make sure the rest of the message queue is processed
libwebsocket_callback_on_writable(sharedInstance.context, wsi);
}
break;
}
case LWS_CALLBACK_CLOSED: {
id sessionKey = [BLWebSocketsServer keyForConnectionID:*session_id];
[sharedInstance.connections removeObjectForKey:sessionKey];
[sharedInstance.asyncMessageQueue removeMessageQueueForUserWithId:*session_id];
break;
}
Expand All @@ -287,4 +299,3 @@ static int callback_http(struct libwebsocket_context *context,
return 0;
}


1 change: 1 addition & 0 deletions Connichiwa/CWUtil.m
Expand Up @@ -59,6 +59,7 @@ + (NSString *)escapedJSONStringFromDictionary:(NSDictionary *)dictionary

+ (NSDictionary *)dictionaryFromJSONData:(NSData *)JSON
{
if (JSON == nil) return [NSDictionary dictionary];
return [NSJSONSerialization JSONObjectWithData:JSON options:0 error:nil];
}

Expand Down

0 comments on commit b7b7ca0

Please sign in to comment.