Threading and Sockets in Objective-C
NOTE: I've edited my question. I've got it to connect and perform the first callback, but subsequent callbacks don't go through at all.
This is my first time writing Objective-C (with GNUstep; it's for a homework assignment). I've got the solution working, but I am trying to add something more to it. The app is a GUI client that connects to a server and gets data from it. Multiple clients can connect to the same server. If any one of the clients changes data that is residing on the server, the server sends a callback to all registered clients. This solution was originally implemented in Java (both client and server) and for the latest assignment, the professor wanted us to write an Objective-C client for it. He said that we don't need to handle callbacks, but I wanted to try anyway.
I am using NSThread
and I wrote something that looks like this:
CallbackInterceptorThread.h
#import <Foundation/Foundation.h>
#import "AppDelegate.h"
@interface CallbackInterceptorThread : NSThread {
@private
NSString* clientPort;
AppDelegate* appDelegate;
}
- (id) initWithClientPort: (NSString*) aClientPort
appDelegate: (AppDelegate*) anAppDelegate;
- (void) main;
@end
CallbackInterceptorThread.m
#import <Foundation/Foundation.h>
#import "CallbackInterceptorThread.h"
#define MAXDATASIZE 4096
@implementation CallbackInterceptorThread
- (id) initWithClientPort: (NSString*) aClientPort
appDelegate: (AppDelegate*) anAppDelegate {
if((self = [super init])) {
[clientPort autorelease];
clientPort = [aClientPort retain];
[appDelegate autorelease];
appDelegate = [anAppDelegate retain];
}
return self;
}
- (void) main {
GSRegisterCurrentThread();
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
char* buffer = malloc(MAXDATASIZE);
Cst420ServerSocket* socket = [[Cst420ServerSocket alloc] initWithPort: clientPort];
[socket retain];
NSString* returnString;
while(YES) {
printf("Client waiting for callbacks on port %s\n", [clientPort cString]);
if([socket accept]) {
printf("Connection accepted!\n");
while(YES) {
printf("Inner loop\n");
sleep(1);
returnString = [socket receiveBytes: buffer maxBytes: MAXDATASIZE beginAt: 0];
printf("Received from Server |%s|\n", [returnString cString]);
if([returnString length] > 0) {
printf("Got a callback from server\n");
[appDelegate populateGui];
}
printf("Going to sleep now\n");
sleep(1);
}
[socket close];
}
}
}
@end
Cst420ServerSocket
has been provided to us by the instructor. It looks like this:
#import "Cst420Socket.h"
#define PORT "4444"
/**
* Cst420Socket.m - objective-c class for manipulating stream sockets.
* Purpose: demonstrate stream sockets in Objective-C.
* These examples are buildable on MacOSX and GNUstep on top of Windows7
*/
// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa){
if (sa->sa_family == AF_INET) {
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
@implementation Cst420ServerSocket
- (id) initWithPort: (NSString*) port{
self = [super init];
int ret = 0;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // use my IP
const char* portStr = [port UTF8String];
if ((rv = getaddrinfo(NULL, portStr, &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
ret = 1;
}else{
for(p = servinfo; p != NULL; p = p->ai_next) {
if ((sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP))==-1){
perror("server: socket create error");
continue;
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
#if defined(WINGS)
closesocket(sockfd);
#else
close(sockfd);
#endif
perror("server: bind error");
continue;
}
break;
}
if (p == NULL) {
fprintf(stderr, "server: failed to bind\n");
ret = 2;
}else{
freeaddrinfo(servinfo); // all done with this structure
if (listen(sockfd, BACKLOG) == -1) {
perror("server: listen error");
ret = 3;
}
}
if (ret == 0){
return self;
} else {
return nil;
}
}
}
- (BOOL) accept {
BOOL ret = YES;
#if defined(WINGS)
new_fd = accept(sockfd, NULL, NULL);
#else
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
#endif
if (new_fd == -1) {
perror("server: accept error");
ret = NO;
}
connected = ret;
return ret;
}
- (int) sendBytes: (char*) byteMsg OfLength: (int) msgLength Index: (int) at{
int ret = send(new_fd, byteMsg, msgLength, 0);
if(ret == -1){
NSLog(@"error sending bytes");
}
return ret;
}
- (NSString* ) receiveBytes: (char*) byteMsg
maxBytes: (int) max
beginAt: (int) at {
int ret = recv(new_fd, byteMsg, max-1, at);
if(ret == -1){
NSLog(@"server error receiving bytes");
}
byteMsg[ret+at] = '\0';
NSString * retStr = [NSString stringWithUTF8String: byteMsg];
return retStr;
}
- (BOOL) close{
#if defined(WINGS)
closesocket(new_fd);
#else
close(new_fd);
#endif
connected = NO;
return YES;
}
- (void) dealloc {
#if defined(WINGS)
closesocket(sockfd);
#else
close(sockfd);
#endif
[super dealloc];
}
@end
Our professor also provided us an example of a simple echo server and client (the server just spits back whatever the client sent it) and I've used the same pattern in the thread.
My initial problem was that my callback interceptor thread didn't accept any (callback) connections from the server. The server said that it could not connect back to the client (ConnectException
from Java; it said "Connection refused"). I was able to fix this by changing my instructor's code. In the connect
function (not shown), he had set the hints to use AF_UNSPEC
instead of AF_INET
. So Java was seeing my localhost IP come through as 0:0:0:0:0:0:0:1
(in IPv6 format). When Java tried to connect back to send a callback, it received an exception (not sure why it cannot connect to an IPv6 address).
After fixing this problem, I tried out my app again and this time the callback from the server was received by my client. However, subsequent callbacks fail to work. After receiving the first callback, the busy-loop keeps running (as it should). But when the server sends a second callback, it looks like the client cannot read it in. On the server side I can see that it sent the callback to the client successfully. It's just that the client is having trouble reading in the data. I added some print statements (see above) for debugging and this is what I get:
Client waiting for callbacks on port 2020
Connection a开发者_开发百科ccepted!
Inner loop
Received from Server |A callback from server to 127.0.0.1:2020|
Got a callback from server
Going to sleep now
Inner loop
Received from Server ||
Going to sleep now
Inner loop
Received from Server ||
Going to sleep now
Inner loop
... (and it keeps going regardless of the second callback being sent)
Here is how I am starting the thread (from the GUI):
CallbackInterceptorThread* callbackInterceptorThread = [[CallbackInterceptorThread alloc] initWithClientPort: clientPort appDelegate: self];
[callbackInterceptorThread start];
I think I've got it working. So from the Java side (the server), this was what I was doing:
Socket socket = new Socket(clientAddress, clientPort);
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());
out.write(("A callback from server to " + clientAddress + ":" + clientPort).getBytes());
out.flush();
out.close();
I put some debugging print-statements in my professor's code and noticed that in receiveBytes
, recv
was returning 0. The return value of recv
is the length of the message that it received. So it received a zero-length string. But a return value of 0 also means that the peer closed the connection properly (which is exactly what I had done from the Java side with out.close()
). So I figured that if I needed to respond to the second callback, I would need to accept
the connection again. So I changed my busy loop to this:
printf("Client waiting for callbacks on port %s\n", [clientPort cString]);
while([socket accept]) {
printf("Connection accepted!\n");
returnString = [socket receiveBytes: buffer maxBytes: MAXDATASIZE beginAt: 0];
printf("Received from Server |%s|\n", [returnString cString]);
if([returnString length] > 0) {
printf("Got a callback from server\n");
[appDelegate populateGui];
}
}
[socket close];
and that seemed to do the trick. I am not sure if this is the right way to do it, so I am open to suggestions for improvement!
精彩评论