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.
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:
- 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).
Edit your Rail's
config > environment > development.rb
file and put in the following line somewhere like before theend
keyword:Rails.application.config.action_cable.allowed_request_origins = ['http://10.1.1.10:3000']
Start your Rails server using following command:
rails server -b 0.0.0.0
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.
// 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!
Actually, Here is the code snippet that i'm using to connect to action cable.
function WebSocketTest()
{
var ws = new WebSocket("ws://localhost:3000/cable");
ws.onopen = function(data)
{
var i = JSON.stringify({"command":"subscribe" , "identifier": JSON.stringify({"channel":"CHANNEL_NAME"})});
// send data request
var j = JSON.stringify({"command":"message","identifier": JSON.stringify({"channel":"CHANNEL_NAME"}),"data": {"message":"Hello World","action": "METHOD_NAME_IN_CHANNEL","email": "abc@xyz.com", "token" : "xxxxxxxxxxxxx", "id": {"id_message" : "something", "ddd" : "something"}}})
var response = ws.send(i);
setTimeout(function()
{
var response1 = ws.send(j);
}, 1000);
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
};
}
来源:https://stackoverflow.com/questions/35145429/connecting-to-actioncable-from-ios-app