Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Connecting to ActionCable from iOS app

I have been stuck on this all day. I have the very simple ActionCable example app (the chat app) by David Heinemeier Hansson working correctly (https://www.youtube.com/watch?v=n0WUjGkDFS0).

I am trying to hit the websocket connection with an iPhone app. I am able to receive pings when I connect to ws://localhost:3000/cable, but I'm not quite sure how to subscribe to channels from outside of a javascript context.

like image 363
Pan Avatar asked Feb 02 '16 05:02

Pan


2 Answers

Oh man, I went through this problem too after reading this question.

After a while, I finally found this magical Github issue page:

https://github.com/rails/rails/issues/22675

I do understand that this patch would break some tests. That is not surprising to me. But the original issue I believe is still relevant and shouldn't be closed.

The following JSON sent to the server should succeed:

{"command": "subscribe","identifier":{"channel":"ChangesChannel"}}

It does not! Instead you must send this:

{"command": "subscribe","identifier":"{\"channel\":\"ChangesChannel\"}"}

I finally got the iOS app to subscribe to room channel following the Github user suggestion about Rails problem.

My setup is as follow:

  • Objective C
  • Using PocketSocket framework for making web socket connection
  • Rails 5 RC1
  • Ruby 2.2.4p230

I assume you know how to use Cocoapods to install PocketSocket.

The relevant codes are as follow:

ViewController.h

#import <PocketSocket/PSWebSocket.h>

@interface ViewController : UIViewController <PSWebSocketDelegate, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate>

@property (nonatomic, strong) PSWebSocket *socket;

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    [self initViews];
    [self initConstraints];
    [self initSocket];
}

-(void)initSocket
{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"ws://localhost:3000/cable"]];

    self.socket = [PSWebSocket clientSocketWithRequest:request];
    self.socket.delegate = self;

    [self.socket open];
}

-(void)joinChannel:(NSString *)channelName
{
    NSString *strChannel = @"{ \"channel\": \"RoomChannel\" }";

    id data = @{
                @"command": @"subscribe",
                @"identifier": strChannel
                };

    NSData * jsonData = [NSJSONSerialization  dataWithJSONObject:data options:0 error:nil];
    NSString * myString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    NSLog(@"myString= %@", myString);

    [self.socket send:myString];
}

#pragma mark - PSWebSocketDelegate Methods -

-(void)webSocketDidOpen:(PSWebSocket *)webSocket
{
    NSLog(@"The websocket handshake completed and is now open!");

    [self joinChannel:@"RoomChannel"];
}

-(void)webSocket:(PSWebSocket *)webSocket didReceiveMessage:(id)message
{
    NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
    id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

    NSString *messageType = json[@"type"];

    if(![messageType isEqualToString:@"ping"] && ![messageType isEqualToString:@"welcome"])
    {
        NSLog(@"The websocket received a message: %@", json[@"message"]);

        [self.messages addObject:json[@"message"]];
        [self.tableView reloadData];
    }
}

-(void)webSocket:(PSWebSocket *)webSocket didFailWithError:(NSError *)error
{
    NSLog(@"The websocket handshake/connection failed with an error: %@", error);
}

-(void)webSocket:(PSWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean
{
    NSLog(@"The websocket closed with code: %@, reason: %@, wasClean: %@", @(code), reason, (wasClean) ? @"YES": @"NO");
}

Important Note:

I also digged a bit into the subscription class source code:

def add(data)
        id_key = data['identifier']
        id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access

        subscription_klass = connection.server.channel_classes[id_options[:channel]]

        if subscription_klass
          subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
        else
          logger.error "Subscription class not found (#{data.inspect})"
        end
      end

Notice the line:

connection.server.channel_classes[id_options[:channel]]

We need to use the name of the class for the channel.

The DHH youtube video uses "room_channel" for the room name but the class file for that channel is named "RoomChannel".

We need to use the class name not the instance name of the channel.

Sending Messages

Just in case others want to know how to send messages also, here is my iOS code to send a message to the server:

-(void)sendMessage:(NSString *)message
{
    NSString *strMessage = [[NSString alloc] initWithFormat:@"{ \"action\": \"speak\", \"message\": \"%@\" }", message];

    NSString *strChannel = @"{ \"channel\": \"RoomChannel\" }";

    id data = @{
                @"command": @"message",
                @"identifier": strChannel,
                @"data": strMessage
                };

    NSData * jsonData = [NSJSONSerialization  dataWithJSONObject:data options:0 error:nil];
    NSString * myString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    NSLog(@"myString= %@", myString);

    [self.socket send:myString];
}

This assumes you've hooked up your UITextField to handle pressing the return key or some "send" button somewhere on your UI.

This whole demo app was a quick hack, obviously, if I was to do it in a real app, I would make my code more cleaner, more reusable and abstract it into a class altogether.

Connecting to Rails server from real iPhone device:

In order for iPhone app to talk to Rails server on real device, not iPhone simulator.

Do the following:

  1. Check your computer's TCP/IP address. On my iMac for example, it might be 10.1.1.10 on some days (can change automatically in the future if using DHCP).
  2. Edit your Rail's config > environment > development.rb file and put in the following line somewhere like before the end keyword:

    Rails.application.config.action_cable.allowed_request_origins = ['http://10.1.1.10:3000']

  3. Start your Rails server using following command:

    rails server -b 0.0.0.0

  4. Build and run your iPhone app onto the iPhone device. You should be able to connect and send messages now :D

I got these solutions from following links:

Request origin not allowed: http://localhost:3001 when using Rails5 and ActionCable

Rails 4.2 server; private and public ip not working

Hope that helps others in the future.

like image 200
Zhang Avatar answered Oct 18 '22 16:10

Zhang


// open socket connection first

var ws = new WebSocket("ws://localhost:3000/cable");  

// subscribe to channel
// 'i' should be in json

var i = { 'command': 'subscribe', 'identifier': {'channel':'ProfileChannel', 'Param_1': 'Value_1',...}};

 ws.send(i);

// After that you'll receive data inside the 'onmessage' function.

Cheers!

like image 26
faisal bhatti Avatar answered Oct 18 '22 14:10

faisal bhatti