#import #import #import "../AsyncSocket.h" #include #include #pragma mark Declarations @interface Echo : NSObject { BOOL shouldExitLoop; AsyncSocket *socket; NSMutableString *text; } -(id)init; -(void)dealloc; -(void)runLoop; -(void)stopRunLoop; -(void)readFromServer; -(void)readFromStdIn; -(void)doTextCommand; -(void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err; -(void)onSocketDidDisconnect:(AsyncSocket *)sock; -(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port; -(void)onSocket:(AsyncSocket *)sock didReadData:(NSData*)data withTag:(long)t; @end void showHelp(); #pragma mark - #pragma mark Implementation @implementation Echo : NSObject /* This method creates the socket. Echo reuses this one socket throughout its life. Echo also sets up the input. While a command-line app is waiting for input, it is usually blocked; I make the input non-blocking so that the run-loop remains active. */ - (id)init { self = [super init]; // Create socket. NSLog (@"Creating socket."); socket = [[AsyncSocket alloc] initWithDelegate:self]; // Create command buffer. text = [[NSMutableString alloc] init]; // Set up stdin for non-blocking. if (fcntl (STDIN_FILENO, F_SETFL, O_NONBLOCK) == -1) { NSLog (@"Can't make STDIN non-blocking."); exit(1); } return self; } /* I release allocated resources here, including the socket. The socket will close any connection before releasing itself, but it will not need to. I explicitly close any connections in the "quit" command handler. */ - (void)dealloc { [socket release]; [text release]; [super dealloc]; } /* Echo spends one second handling any run-loop activity (i.e. socket activity) and then comes up for air to check if any new commands have been entered and, if so, executing them. Wash, rinse, repeat. Note the use of the shouldExitLoop flag to control when the run-loop ends and the app quits. I could have just called exit(), but this allows the app to clean up after itself properly. You should use a similar technique if you create a thread for socket activity and processing. */ - (void)runLoop { shouldExitLoop = NO; while (!shouldExitLoop) { [self readFromStdIn]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; } } /* This method just abstracts the stop-run-loop operation. */ - (void)stopRunLoop { shouldExitLoop = YES; } /* This method simply abstracts the read-from-server operation. It is called from -onSocket:didReadData:withTag: to set up another read operation. If it did not set up another read operation, AsyncSocket would not do anything with any further packets from Echo Server. You should not use "\n" as a packet delimiter in your own code. I explain why in EchoServerMain.c. */ - (void)readFromServer { NSData *newline = [@"\n" dataUsingEncoding:NSASCIIStringEncoding]; [socket readDataToData:newline withTimeout:-1 tag:0]; } /* This method queues up a message that will be sent immediately upon connecting to Echo Server. Note that the message consists of two write requests. They will be sent consecutively, though, and will appear as one packet to Echo Server, because Echo Server looks for the "\n" to determine when the packet ends. */ - (void)prepareHello { // Yes, you can call methods on NSString constants. NSData *message = [@"Hello, Echo Server! " dataUsingEncoding:NSASCIIStringEncoding]; NSData *newline = [@"I am a new client.\n" dataUsingEncoding:NSASCIIStringEncoding]; [socket writeData:message withTimeout:-1 tag:0]; [socket writeData:newline withTimeout:-1 tag:0]; }; /* I only runs the run-loop for a second at a time, because I need to receive and act on input from the user (which won't result in run-loop activity). That happens here. */ - (void)readFromStdIn { Byte c; while (read (STDIN_FILENO, &c, 1) == 1) { [text appendString: [NSString stringWithFormat:@"%c", c]]; if (c == '\n') [self doTextCommand]; } } /* This method sends text to Echo Server, or executes a command. */ - (void)doTextCommand { NSArray *params = [text componentsSeparatedByString:@" "]; if ([text hasPrefix: @"quit"] || [text hasPrefix: @"exit"]) { // I don't technically need to call -disconnect here. When the app quits, // the socket will disconnect itself if needed. But it is easier to keep // track of here. Note that -onSocket:willDisconnectWithError: will NOT // be called here, though -onSocketDidDisconnect: will be. // I stop the run loop like a gentleman instead of simply calling exit() // so that the app exits cleanly. This is also technically unnecessary; // the OS will destroy the socket connection when it cleans up after the // process. NSLog (@"Disconnecting & exiting."); [socket disconnect]; [self stopRunLoop]; } else if ([text hasPrefix: @"disconnect"]) { NSLog (@"Disconnecting."); [socket disconnect]; } else if ([text hasPrefix: @"connect"]) { UInt16 port; NSString *host; if ([params count] == 3) { port = [[params objectAtIndex:2] intValue]; host = [params objectAtIndex:1]; } else { port = [[params objectAtIndex:1] intValue]; host = @"localhost"; } // This starts to establish a connection to the server. // The connection will not be finished until later, when // -onSocket:didConnectToPort:host: is called. But even so, you can // immediately queue a read or write operation here. It will be // performed when the connection is established. // // Note that I enclose the connect method call in a @try block. An // exception will be thrown if the socket is already connected. Usually, // if an exception was thrown, that indicates programmer error. In this // case, an exception would indicate that I did not forsee a user trying // to make a new connection when one already exists. I could and should // have used -isConnected to check for that case in advance, but I wanted // to demonstrate the exception-handling. // // Note also, that I call -prepareHello (which writes to the socket) // before the socket is actually connected. The write request will be // queued up and transmitted as soon as the connection is complete. @try { NSError *err; if ([socket connectToHost:host onPort:port error:&err]) { NSLog (@"Connecting to %@ port %u.", host, port); [self prepareHello]; } else { NSLog (@"Couldn't connect to %@ port %u (%@).", host, port, err); } } @catch (NSException *exception) { NSLog ([exception reason]); } } else if ([text hasPrefix: @"dump"]) { // This demonstrates AsyncSocket's -description method. NSLog (@"%@", socket); } else if ([text hasPrefix: @"help"]) { showHelp(); } else { // Anything other than a command is sent to Echo Server. Note that data // will include the final press of the Return key, which is "\n". That // is why I use "\n" verbatim as the packet delimiter, instead of // specifying CRLF or something. NSData *data = [text dataUsingEncoding:NSASCIIStringEncoding]; [socket writeData:data withTimeout:-1 tag:0]; } [text setString:@""]; } #pragma mark - #pragma mark AsyncSocket Delegate Methods /* This will be called whenever AsyncSocket is about to disconnect. In Echo Server, it does not do anything other than report what went wrong (this delegate method is the only place to get that information), but in a more serious app, this is a good place to do disaster-recovery by getting partially-read data. This is not, however, a good place to do cleanup. The socket must still exist when this method returns. */ -(void) onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err { if (err != nil) NSLog (@"Socket will disconnect. Error domain %@, code %d (%@).", [err domain], [err code], [err localizedDescription]); else NSLog (@"Socket will disconnect. No error."); } /* Normally, this is the place to release the socket and perform the appropriate housekeeping and notification. But I intend to re-use this same socket for other connections, so I do nothing. */ -(void) onSocketDidDisconnect:(AsyncSocket *)sock { NSLog (@"Disconnected."); } /* This method is called when Echo has connected to Echo Server. I immediately wait for incoming data from the server, but I already have two write requests queued up (from -prepareHello), and will also be sending data when the user gives me some to send. */ -(void) onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port; { NSLog (@"Connected to %@ %u.", host, port); [self readFromServer]; } /* This method is called when Echo has finished reading a packet from Echo Server. It prints it out and immediately calls -readFromServer, which will queue up a read operation, waiting for the next packet. You'll note that I do not implement -onSocket:didWriteDataWithTag:. That is because Echo does not care about the data once it is transmitted. AsyncSocket will still send the data, but will not notify Echo when that it done. */ -(void) onSocket:(AsyncSocket *)sock didReadData:(NSData*)data withTag:(long)t { NSString *str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; printf ([str cString]); fflush (stdout); [str release]; [self readFromServer]; } @end #pragma mark - #pragma mark C Functions void showHelp() { printf ("ECHO by Dustin Voss copyright 2003. Sample code for using AsyncSocket."); printf ("\nReads and writes text to an ECHO SERVER. The following commands are available:"); printf ("\n\tquit, exit -- exit the program"); printf ("\n\thelp -- display this message"); printf ("\n\tconnect host port -- connects to the server on the given host and port"); printf ("\n\tdisconnect -- disconnects from the current server"); printf ("\n\tdump -- displays the socket's status"); printf ("\nAnything else gets transmitted to the server. Begin!\n"); fflush (stdout); } int main (int argc, const char * argv[]) { showHelp(); NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; Echo *e = [[Echo alloc] init]; [e runLoop]; [e release]; [pool release]; return 0; }