iOS 简单的聊天程序

在这篇文章中,我将介绍如何使用TCP/IP协议让iPhone与服务器实现通信,同时以一个简单的聊天程序作为例子进行说明。


首先使用Xcode常见一个基于视图(View)的应用程序项目,取名Network

使用网络通信流

使用套接字在网络上通信最简单的方法是使用NSStream类,NSStream类是一个表示流的抽象类,你可以使用它读写数据,它可以用在内存、文件或网络上。使用NSStream类,你可以向

服务器写数据,也可以从服务器读取数据。

Mac OS X上,可以使用NSHostNSStream对象建立到服务器的连接,如:

NSInputStream *iStream;
NSOutputStream *oStream;
uint portNo = 500;
NSURL *website = [NSURL URLWithString:urlStr];
NSHost *host = [NSHost hostWithName:[website host]];
[NSStream getStreamsToHost:host port:portNo inputStream:&iStream outputStream:&oStream];


NSStream类有一个方法getStreamsToHost:port:inputStream:outputStream:,它创建一个到服务器的输入和输出流,但问题是iPhone OS不支持getStreamsToHost:port:inputStream:outputStream:方法,因此上面的代码在iPhone应用程序中是不能运行的。

为了解决这个问题,你可以增加一个类别到现有的NSStream类上,替换getStreamsToHost:port:inputStream:outputStream:方法提供的功能。

XcodeClasses上点击右键,添加一个文件NSStreamAdditions.m,在NSStreamAdditions.h文件中,增加下面的代码:

#import <UIKit/UIKit.h>

@interface NSStream (MyAdditions)

+ (void)getStreamsToHostNamed:(NSString *)hostName port:(NSInteger)port inputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr;

@end

NSStreamAdditions文件中加入以下代码:

#import "NSStreamAdditions.h"

@implementation NSStream (MyAdditions)

+ (void)getStreamsToHostNamed:(NSString *)hostName port:(NSInteger)port inputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr
{
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    assert(hostName != nil);
    assert((port > 0) && (port < 65536));
    assert((inputStreamPtr != NULL) || (outputStreamPtr != NULL));
    readStream = NULL;
    writeStream = NULL;
    CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef) hostName, port, ((inputStreamPtr  != nil) ? &readStream : NULL),((outputStreamPtr != nil) ? &writeStream :NULL));
    if (inputStreamPtr != NULL)
    {
        *inputStreamPtr  = [NSMakeCollectable(readStream) autorelease];
    }
    if (outputStreamPtr != NULL)
    {
        *outputStreamPtr = [NSMakeCollectable(writeStream) autorelease];
    }
}

@end


上面的代码为NSStream类增加了一个getStreamsToHostNamed:port:inputStream:outputStream:方法,现在你可以在你的iPhone应用程序中使用这个方法,使用TCP协议连接到服务器。

NetworkViewController.m文件中,插入下面的代码:

#import "NetworkViewController.h"
#import "NSStreamAdditions.h"

@implementation NetworkViewController

NSMutableData *data;
NSInputStream *iStream;
NSOutputStream *oStream;

// 定义connectToServerUsingStream:portNo:方法,以便连接到服务器,然后创建输入和输出流对象:
- (void)connectToServerUsingStream:(NSString *)urlStr portNo:(uint)portNo
{
    if (![urlStr isEqualToString:@""])
    {
        NSURL *website = [NSURL URLWithString:urlStr];
        if (!website)
        {
            NSLog(@"%@ is not a valid URL");
            return;
        }
        else
        {
            [NSStream getStreamsToHostNamed:urlStr port:portNo inputStream:&iStream outputStream:&oStream];
            [iStream retain];
            [oStream retain];
            [iStream setDelegate:self];
            [oStream setDelegate:self];
            [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [iStream open];
        }
    }
}

在一个运行循环中,你可以调度输入和输出流接收事件,这样可以避免阻塞。

使用CFNetwork框架

使用TCP协议建立到服务器的连接,还有一种办法是使用CFNetwork框架,CFNetwork是核心服务框架(C)中的一个框架,它为网络协议提供了抽象,如HTTPFTPBSD套接字。

为了弄清楚如何使用CFNetwork框架中的各种类,在NetworkViewController.m文件中增加下面的代码:

#import "NetworkViewController.h"
#import "NSStreamAdditions.h"

@implementation NetworkViewController

NSMutableData *data;
NSInputStream *iStream;
NSOutputStream *oStream;
CFReadStreamRef readStream = NULL;
CFWriteStreamRef writeStream = NULL;

// 然后使用下面的代码定义connectToServerUsingCFStream:portNo::
- (void)connectToServerUsingCFStream:(NSString *)urlStr portNo:(uint)portNo
{
    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)urlStr, portNo, &readStream, &writeStream);
    if (readStream && writeStream)
    {
        CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
        CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
        iStream = (NSInputStream *)readStream;
        [iStream retain];
        [iStream setDelegate:self];
        [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [iStream open];
        
        oStream = (NSOutputStream *)writeStream;
        [oStream retain];
        [oStream setDelegate:self];
        [oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [oStream open];
    }
}

你第一次使用CFStreamCreatePairWithSocketToHost() 方法创建一个可读写的流,通过TCP/IP 连接到服务器,这个方法返回这个可读写流(readStream writeStream) 的引用 ,它们和Objective C 中的NSInputStream NSOutputStream 是等效的。


发送数据

使用NSOutputStream对象向服务器发送数据,如:

- (void)writeToServer:(const uint8_t *)buf
{
    [oStream write:buf maxLength:strlen((char*)buf)];
}

这个方法向服务器发送一组无符号整数字节。

读取数据

从服务器接收数据时,将会触发stream:handleEvent:方法,因此可以使用这个方法接收所有入站数据,这个方法实现如下:

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode)
    {
        case NSStreamEventHasBytesAvailable:
        {
            if (data == nil)
            {
                data = [[NSMutableData alloc] init];
            }
            uint8_t buf[1024];
            unsigned int len = 0;
            len = [(NSInputStream *)stream read:buf maxLength:1024];
            if (len)
            {
                [data appendBytes:(const void *)buf length:len];
                int bytesRead;
                bytesRead += len;
            }
            else
            {
                NSLog(@"No data.");
            }
            NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSLog(str);
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"From server" message:str delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
            [alert release];
            [str release];
            [data release];
            data = nil;
        }
    }
}

这个方法包括两个参数:一个是NSStream实例,一个是NSStreamEvent常量,NSStreamEvent常量可以是以下的值:

NSStreamEventNone:无事件发生。

NSStreamEventOpenCompleted:打开事件已经成功完成。

NSStreamEventHasBytesAvailable:已经读取的流字节数。

NSStreamEventHasSpaceAvailable:流接收的可写入的字节数。

NSStreamEventErrorOccurred:在流上发生了错误。

NSStreamEventEndEncountered:已经抵达流的结尾。

读取入站数据时,你应该检查NSStreamEventHasBytesAvailable常量,在这个方法中,你可以读取入站数据流,然后UIAlertView对象显示接收到的数据。

stream:handleEvent:方法也是检查连接错误的一个好方法,例如,如果connectToServerUsingStream:portNo:方法连接到服务器时失败了,错误将使用stream:

handleEvent:方法通知,NSStreamEvent常量设置为NSStreamEventErrorOccurred


断开连接

为了断开与服务器的连接,定义如下的断开方法:

- (void) disconnect
{
    [iStream close];
    [oStream close];
}

然后将下面的代码添加到dealloc分发中:

- (void)dealloc
{
    [self disconnect];
    [iStream release];
    [oStream release];
    if (readStream)
    {
        CFRelease(readStream);
    }
    if (writeStream)
    {
        CFRelease(writeStream);
    }
    [super dealloc];
}

测试应用程序

现在可以将所有代码集合到一起进行测试了,在NetworkViewController.h文件中,声明下面的出口和行为:

#import
@interface NetworkViewController : UIViewController
{
    IBOutlet UITextField *txtMessage;
}

@property (nonatomic, retain) UITextField *txtMessage;

- (void)btnSend:(id)sender;

@end


双击NetworkViewController.xib,在Interface Builder中打开编辑它,在View窗口中,使用下面的视图填充它,如图1所示。

文本区域(Text Field)

圆形按钮(Round Rect Button)

  

  图 1 填充:使用视图填充View窗口

执行下面的操作

1、在File’s Owner上点击,将其拖到文本区域视图中,选择txtMessage。

2、选中圆形按钮视图,将其拖到File’s Owner上,选择btnSend。

在File’s Owner上点击右键,验证它的连接,如图2所示。

  

  图 2 验证:验证File’s Owner上的连接

回到NetworkViewController.m文件,将下面的代码添加到viewDidLoad方法中。

- (void)viewDidLoad 
{
    [self connectToServerUsingStream:@"192.168.1.102" portNo:500];
    //---OR---
    //[self connectToServerUsingCFStream:@"192.168.1.102" portNo:500];
    [super viewDidLoad];
}

上面的代码假设你正连接到一个ip地址为192.168.1.102的服务器的500端口上。btnSend:方法的代码如下:

-(IBAction) btnSend: (id) sender
{
    const uint8_t *str = (uint8_t *) [txtMessage.text cStringUsingEncoding:NSASCIIStringEncoding];
    [self writeToServer:str];
    txtMessage.text = @"";
}

在dealloc方法中重新发布txtMessage出口。

- (void)dealloc 
{
  [txtMessage release];
  [self disconnect];
  [iStream release];
  [oStream release];
  if (readStream) 
   {
       CFRelease(readStream);
    }
  if (writeStream)
    {
       CFRelease(writeStream);
    }
  [super dealloc];
}

构建服务器

现在已经构建好一个可以在iPhone上运行的客户端,并已经可以通过它向服务器发送一些文本信息,但为了测试这个应用程序还需要一个服务端程序,我使用C#构建了一个非常简单的控制台服务器,下面是Program.cs文件的代码。

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
namespace Server_CS
{
    class Program
   {
        const int portNo = 500;
        static void Main(string[] args)
        {
            System.Net.IPAddress localAdd = System.Net.IPAddress.Parse("192.168.1.102");
            TcpListener listener = new TcpListener(localAdd, portNo);
            listener.Start();
            while (true)
            {
                TcpClient tcpClient = listener.AcceptTcpClient();
                NetworkStream ns = tcpClient.GetStream();
                byte[] data = new byte[tcpClient.ReceiveBufferSize];
                Int numBytesRead = ns.Read(data, 0, System.Convert.ToInt32(tcpClient.ReceiveBufferSize));
                Console.WriteLine("Received :" + Encoding.ASCII.GetString(data, 0, numBytesRead));
                //---write back to the client---
                ns.Write(data, 0, numBytesRead);
            }
        }
    }
}
 

服务端程序执行下面的任务:

它假设服务器的ip地址是192.168.1.102,在你的终端上测试时,请将这个ip地址替换为你运行这个服务端程序的计算机的ip地址。

它将接收到的所有数据返回给客户端。

一旦接收到数据,服务端不再监听入站数据,如果客户端要再次发生数据,需要重新连接到服务器。

在文本区域中输入一些文字,然后点击Send按钮,如果连接成功,你将会看到Alert视图显示接收到数据。

一个更有趣的例子

在View窗口中添加下面的视图,如图3所示。

标签(Label)

文本区域(Text Field)

圆形按钮(Round Rect Button)

文本视图(Text View)

  

  图 3 视图:增加更多的视图

选择文本视图,按下Command-T将字体大小修改为9,如图4所示。

  

  图 4 修改文本视图的字体大小

在NetworkViewController.h文件中,增加下面的代码:

#import 
@interface NetworkViewController : UIViewController
{
    IBOutlet UITextField *txtMessage;    
    IBOutlet UITextField *txtNickName;
    IBOutlet UITextView  *txtMessages;
}
@property (nonatomic, retain) UITextField *txtMessage;
@property (nonatomic, retain) UITextField *txtNickName;
@property (nonatomic, retain) UITextView *txtMessages;
-(IBAction) btnSend:(id) sender; 
-(IBAction) btnLogin:(id) sender;

@end
 

执行下面的操作

1、按住CTRL键,点击File’s Owner,将其拖到文本区域的顶部,选择txtNickName。

2、按住CTRL键,点击File’s Owner,将其拖到文本视图的顶部,选择txtMessages。

3、按住CTRL键,点击圆形按钮,将其拖到File’s Owner上,选择btnLogin。

在File’s Owner上点击右键,验证它的连接,如图5所示。

  

  图 5 连接:验证连接

在NetworkViewController.m文件中,添加下面的代码:

#import "NetworkViewController.h"
#import "NSStreamAdditions.h"
#import <CFNetwork/CFNetwork.h>

@implementation NetworkViewController

@synthesize txtMessage;
@synthesize txtNickName;
@synthesize txtMessages;

-(IBAction) btnLogin:(id) sender
{
    const uint8_t *str = (uint8_t *) 
    [txtNickName.text cStringUsingEncoding:NSASCIIStringEncoding];
    [self writeToServer:str];    
}

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode 
{
    switch(eventCode) 
   {
       case NSStreamEventHasBytesAvailable:
     {
         if (data == nil) 
         {
              data = [[NSMutableData alloc] init];
         }
         uint8_t buf[1024];
         unsigned int len = 0;
         len = [(NSInputStream *)stream read:buf maxLength:1024];
         if(len)
        {    
            [data appendBytes:(const void *)buf length:len];
            int bytesRead;
            bytesRead += len;
        } 
        else 
       {
              NSLog(@"No data.");
       }
       NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
       NSLog(str);
       NSString *existingMsg = txtMessages.text;
       existingMsg = [existingMsg stringByAppendingString:str];
       txtMessages.text = existingMsg;
       [str release];
       [data release];        
       data = nil;
      } 
      break;
   }
}

- (void)dealloc 
{
    [txtNickName release];
    [txtMessages release];
    [txtMessage release];
    [self disconnect];    
    [iStream release];
    [oStream release];    
    if (readStream) 
    {
        CFRelease(readStream);
     }
     if (writeStream) 
     {
         CFRelease(writeStream);
      }
      [super dealloc];
}

Ok!按Command-R测试这个应用程序,首先,为你自己输入一个昵称,然后点击Login按钮,如图6所示。现在就可以输入消息,点击发送按钮开始聊天了。

  

  图 6 聊天:开始在iPhone上聊天

小结

在这篇文章中,你看到了如何使用TCP/IP与另一台服务器进行通信,知道如何构建与外界通信的应用程序编写方法后,你可以在上面增加更多有趣的功能,iPhone完全可以成为一台mini PC。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值