iOS TCP Server 编程要点
一.编程结构
一般使用GCDAsyncSocket 库,这个是对CFNetworks库的直接封装,如果对于POSIX Socket编程很熟的话,这个流程相当熟悉的.
1.1 数据结构
#import "GCDAsyncSocket.h"
dispatch_queue_t socketQueue; //执行socket处理的GCD队列,这样socket处理不占用主线程时间
GCDAsyncSocket *listenSocket; //服务器socket
NSMutableArray *connectedSockets; //客户端socket列表
1.1 初始化socket
isRunning = NO;
socketQueue = dispatch_queue_create("socketQueue", NULL);
listenSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:socketQueue];
[listenSocket setAutoDisconnectOnClosedReadStream:NO];
// Setup an array to store all accepted client connections
connectedSockets = [[NSMutableArray alloc] initWithCapacity:1];
1.3 开始侦听
NSInteger port = 12345; //侦听端口
NSError *error = nil;
if(![listenSocket acceptOnPort:port error:&error])
{
// [self logError:FORMAT(@"Error starting server: %@", error )];
NSLog(@"Error starting server: %@", error );
return;
}
1.4 处理客户端联接请求
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
// This method is executed on the socketQueue (not the main thread)
@synchronized(connectedSockets)
{
[connectedSockets addObject:newSocket];
}
NSString *host = [newSocket connectedHost];
UInt16 port = [newSocket connectedPort];
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
NSLog(@"Accepted client %@:%hu", host, port);
}
});
NSString *welcomeMsg = @"Welcome to the AsyncSocket Echo Server\r\n";
NSData *welcomeData = [welcomeMsg dataUsingEncoding:NSUTF8StringEncoding];
//向客户端发送欢迎字符串
[newSocket writeData:welcomeData withTimeout:-1 tag:WELCOME_MSG];
//一直等待客户端数据,一直收到\r\n才返回.
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:0];
}
1.4 处理客户端发送数据
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
// This method is executed on the socketQueue (not the main thread)
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
NSData *strData = [data subdataWithRange:NSMakeRange(0, [data length] - 2)];
NSString *msg = [[NSString alloc] initWithData:strData encoding:NSUTF8StringEncoding];
if (msg)
{
[self logMessage:msg];
}
else
{
[self logError:@"Error converting received data into UTF-8 String"];
}
}
});
// 将数据原样发回客户端
[sock writeData:data withTimeout:-1 tag:ECHO_MSG];
}
1.5关闭客户端联接
[newSocket disconnect];
二.常见问题
2.1 客户端socket突然关闭.
GCDAsyncSocket 可以设置
[listenSocket setAutoDisconnectOnClosedReadStream:NO];
防止在客户端主动关闭socket时断开服务端的相应socket.
但是在实际编程中仍然发现断开情况,还有两种情况比较隐藏,一种上述connectedSockets对象未初始化,当客户端触发didAcceptNewSocket时也能联接上,但是很快断开,调用读数据方法,在几十次联接会有一次读取成功,因此这个错误比较隐蔽,以为是编程哪里碰到不对,实际还是操作空对象问题.
第二种情况GCDAsycncSocket还有一个selector是
- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag
elapsed:(NSTimeInterval)elapsed
bytesDone:(NSUInteger)length
这个返回值返回0.0表示一直等待数据接收,而返回值大于零表示多少秒后没有读到数据就会主动断开联接!
2.2 收不到数据
现象是触发不了- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag ;这样就无法接收数据.
一种原因在在联接服务器,或服务端accept时,没有触发接收读.
触发的方式有多种,比如要用下列之一
[newSocket readDataWithTimeout:-1 tag:0]; //有数据接收就触发
[newSocket readDataToLength:20 withTimeout:60 tag:0]; //读到指定长度才触发
[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:1]; //读到回车换行符才触发
还有一种可能丢数据,我看到有人建议是文件一定加换行符
三.Mac 测试工具
Windows下socket测试相当多,但是Mac下很少,我个人还写一个命令行的工具,但是效果不好,现在我用的开源Qt开发的 sockit. http://code.google.com/p/sockit
当然还有一些缺点,比如对十六进制处理不好,因此我升级一下.
以下是一个TCP echo server的iOS版,
http://download.csdn.net/detail/work4blue/9462382