架构一个 iPhone 聊天应用程序
目前已有 4000 万台 iPhones 在用,您无疑对编写 iOS 应用程序感兴趣。但是从何着手呢?大多数应用程序都会连接网络,那么一个跨越两端的项目(比如说聊天应用程序)又是如何呢?本文将向您介绍如何利用服务器 和客户端组件构建一个聊天应用程序。从本文可以学到编写 iOS 应用程序的整个流程。学完本文之后,我保证您会想要编写一个这样的应用程序。
构建应用程序从架构解决方案开始。图 1 中的架构展示了 iOS 设备(这里是 iPhone)如何通过两个 PHP 页面连接到服务器。
这两个 PHP 页面(add.php 和 messages.php)都连接到数据库,分别用于发布和检索消息。在我提供的代码中,数据库是 MySQL,但是您可以使用 DB2 或者您喜欢的任何其他数据库。
我使用的协议是 XML。add.php 页面返回一个 XML 消息,指出消息发布是否成功。messages.php 页面返回发布到服务器的最新消息。
在您开始之前,我想要介绍一下您将从本文学到的内容。
- 数据库访问。我将向您介绍如何使用 PHP 向数据库添加行和检索行。
- XML 编码。服务器代码演示如何将消息打包成 XML。
- 构建 iOS 界面。我将详细介绍如何为应用程序构建用户界面。
- 查询服务器。Objective-C 代码向 messages.php 页面发出 GET 请求,以得到最新的聊天消息。
- 解析 XML。使用对 iOS 开发人员可用的 XML 解析器,您可以解析从 messages.php 返回的 XML。
- 显示消息。应用程序使用一个定制列表项显示聊天消息;这一方法可以让您了解到如何定制自己的 iOS 应用程序的外观。
- 发布消息。应用程序通过 add.php 将数据发布到服务器,add.php 将指导您完成发布过程。
- 定时器。定时器任务用于周期性地轮询 messages.php,看何时来了新的聊天项目。
对于一个例子来说,这些内容太多了,应该为您开发您想要构建的任何类型的客户端/服务器 iOS 应用程序提供一组适当的工具。
从创建数据库开始。我将我的数据库叫做 "chat",您可以给您的数据库随便取个您喜欢的名字。您只需要确保在 PHP 中更改连接字符串,以匹配数据库的名称。用来为应用程序构建单个表的 SQL 脚本在清单 1 中。
DROP TABLE IF EXISTS chatitems; CREATE TABLE chatitems ( id BIGINT NOT NULL PRIMARY KEY auto_increment, added TIMESTAMP NOT NULL, user VARCHAR(64) NOT NULL, message VARCHAR(255) NOT NULL );
这个简单的单表数据库只有 4 个字段:
- 行的 id,这是一个自动递增的整数
- 添加消息的日期
- 添加消息的用户
- 消息本身的文本
您可以更改这些字段的大小,以适应您的内容。
在生产系统中,您很可能还想要有一个带有姓名和密码字段的用户表,还有一个用户登录界面。对于本例来说,我想要让数据库尽量简单,所以数据库中只有一个表。
您想要构建的第一部分代码是清单 2 中的 add.php 脚本。
<?php header( 'Content-type: text/xml' ); mysql_connect( 'localhost:/tmp/mysql.sock', 'root', '' ); mysql_select_db( 'chat' ); mysql_query( "INSERT INTO chatitems VALUES ( null, null, '". mysql_real_escape_string( $_REQUEST['user'] ). "', '". mysql_real_escape_string( $_REQUEST['message'] ). "')" ); ?> <success />
该脚本连接到数据库,并使用已发布的 user
和 message
字段存储消息。就是在简单的 INSERT 语句中,两个值被转义,以解决任何含义不确定的字符,比如说可能会扰乱 SQL 语法的单引号。
为了测试 add 脚本,您创建一个 test.html 页面,如清单 3 所示,它只是将字段张贴到 add.php 脚本。
<html> <head> <title>Chat Message Test Form</title> </head> <body> <form action="add.php" method="POST"> User: <input name="user" /><br /> Message: <input name="message" /><br /> <input type="submit" /> </form> </body> </html>
这个简单的页面只有一个表单(指向 add.php)和两个文本字段(分别用于用户和消息)。然后还有一个 Submit 按钮,用于执行张贴。
test.html 页面安装好之后,您就可以测试 add.php 脚本了。在浏览器中打开测试页面,结果类似于 图 2,User 字段中显示有值 "jack",Message 字段中有值 "This is a test",下面是一个 Submit Query 按钮。
从这里,您添加一些值并单击 Submit Query 按钮。如果一切正常,您会看到类似于图 3 的画面。
否则,您可能会得到一个 PHP 堆栈跟踪,告诉您数据库连接失败或者 INSERT 语句不工作。
消息添加脚本能够工作,下面应该构建 messages.php 脚本了,它返回消息列表。该脚本展示在清单 4 中。
<?php header( 'Content-type: text/xml' ); mysql_connect( 'localhost:/tmp/mysql.sock', 'root', '' ); mysql_select_db( 'chat' ); if ( $_REQUEST['past'] ) { $result = mysql_query('SELECT * FROM chatitems WHERE id > '. mysql_real_escape_string( $_REQUEST['past'] ). ' ORDER BY added LIMIT 50'); } else { $result = mysql_query('SELECT * FROM chatitems ORDER BY added LIMIT 50' ); } ?> <chat> <?php while ($row = mysql_fetch_assoc($result)) { ?> <message added="<?php echo( $row['added'] ) ?>" id="<?php echo( $row['id'] ) ?>"> <user><?php echo( htmlentities( $row['user'] ) ) ?></user> <text><?php echo( htmlentities( $row['message'] ) ) ?></text> </message> <?php } mysql_free_result($result); ?> </chat>
这个脚本稍微有点复杂。它做的第一件事是完成查询。这里有两种可能:
- 如果提供了
past
参数,那么脚本只返回超过指定 ID 的消息。 - 如果没有指定
past
参数,那么返回所有消息。
使用 past
参数的原因是,您想要客户端是智能的。您想要客户端记住它已经看到过的消息,只寻找那些超过它已经具有的消息。客户端逻辑足够简单,它只保留它找到的最高值 ID,并作为 past
参数发送它。在开始时,它可以发送 0 作为值,相当于根本就不指定任何内容。
脚本的第二部分从查询结果集中检索记录,并将它们编码成 XML。如果这一部分脚本能够工作,那么您在浏览器中打开这一页面时,会看到类似图 4 的效果。
服务器脚本就算完成了。当然,您可以添加您想要的任何逻辑,额外的通道、用户验证和登录,等等。对于这个实验性的聊天应用程序,这个脚本已经工作得很好了。现在您可以构建将会使用这个服务器脚本的 iOS 应用程序了。
iOS IDE 叫做 XCode。如果您还没有这个 IDE,那么需要从 Apple Developer Site(参见 参考资料)下载它。最新生产版本是 XCode 3,我这里的屏幕截图使用的就是这个版本。现在已经有了一个更新的版本,叫做 XCode 4,它在 IDE 中集成了 User Interface 编辑器,但是该版本目前还处于预览模式。
XCode 安装好之后,现在就该使用图 5 所示的 New Project 向导构建应用程序了。
开始最容易的应用程序类型是基于视图的应用程序。这种应用程序允许您在您选择的地方放置控件,并为您完成大多数 UI 设计。选择控件之后,再选择 iPhone 或 iPad。这一选择关系到您将在什么样的设备上进行模拟。您可以编写代码,以便在 iPhone 或 iPad 或者 Apple 即将推出的任何其他 i-设备上运行。
单击 Choose 之后,您会被要求给应用程序命名。我将我的应用程序取名为 “iOSChatClient”,但是您可以随便给自己的应用程序取一个您喜欢的名字。您给应用程序命名之后,XCode IDE 会构建核心应用程序文件。然后,编译并启动它,确保一切正常。
创建应用程序之后,您就可以开发界面了。从视图控制器 XIB 文件开始,该文件位于 Resources 文件夹中。通过双击该文件夹,可以打开 Interface Builder,这是 UI 工具箱。
图 6 展示了我如何布局三个控件。顶部是文本框,用于输入您想要发送的消息。文本框的右边是 Send 按钮。下面是 UITableView 对象,其中展示了所有聊天记录。
我会详细介绍如何在 Interface Builder 中完成这一切,但是我建议您下载项目代码,自己试验一下。尽管放心将该项目用作您自己的应用程序的模板。
用户界面这就完成了。下一个任务是回到 XCode IDE,向视图控制器类定义添加一些成员变量、属性和方法,如清单 5 所示。
清单 5. iOSChatClientViewController.h
#import <UIKit/UIKit.h> @interface iOSChatClientViewController : UIViewController <UITableViewDataSource,UITableViewDelegate> { IBOutlet UITextField *messageText; IBOutlet UIButton *sendButton; IBOutlet UITableView *messageList; NSMutableData *receivedData; NSMutableArray *messages; int lastId; NSTimer *timer; NSXMLParser *chatParser; NSString *msgAdded; NSMutableString *msgUser; NSMutableString *msgText; int msgId; Boolean inText; Boolean inUser; } @property (nonatomic,retain) UITextField *messageText; @property (nonatomic,retain) UIButton *sendButton; @property (nonatomic,retain) UITableView *messageList; - (IBAction)sendClicked:(id)sender; @end
从顶部开始,我向类定义添加了 UITableViewDataSource 和 UITableViewDelegate。该代码用于驱动消息显示。类中有一些方法可以被回调,以便向表视图提供数据和布局信息。
实例变量分为五组。顶部是对各种 UI 元素的对象引用、要发送的消息的文本字段、发送按钮和消息列表。
下面是一些缓冲区,用于存储返回的 XML、消息列表和看到的最新 ID。lastID 从 0 开始,但是被设置为您看到的任何消息的最大 ID 值。它然后作为 past
参数的值被发送回服务器。
定时器每几秒钟触发一次,以查找来自服务器的新消息。最后一部分代码包含解析 XML 所需的所有成员变量。存在很多成员变量,这是因为 XML 解析器是一个基于回调的解析器,这表示它在类中保留有很多状态。
成员变量下面是属性和单击处理程序。它们由 Interface Builder 用来将界面元素连接到这个控制器类。事实上,视图控制器中有了这些元素之后,就可以回到 Interface Builder,使用连接器控件将消息文本、发送按钮和消息列表连接到它们对应的属性,将 Touch Inside
事件连接到 sendClicked
方法。
在这一节,可以开始深入项目正题,实现视图控制器。虽然代码都在一个文件中,但是我把它们分成好几个清单,以便解释每一部分时更简单一点。
第一部分,清单 6,介绍应用程序开始部分和视图控制器的初始化。
清单 6. iOSChatClientViewController.m – 开始
#import "iOSChatClientViewController.h" @implementation iOSChatClientViewController @synthesize messageText, sendButton, messageList; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { lastId = 0; chatParser = NULL; } return self; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { } - (void)dealloc { [super dealloc]; }
这是标准的 iOS 代码。代码中有一些对可变的系统事件(比如说内存警告和存储单元分配)的回调。在生产应用程序中,您想要完美地处理这些事件,但是对于这个示例应用程序来说,我不想让事情过于复杂。
第一个真正的任务是对 messages.php
脚本发出 GET
请求。清单 7 展示了此任务的代码。
清单 7. iOSChatClientViewController.m – 得到消息
- (void)getNewMessages { NSString *url = [NSString stringWithFormat: @"http://localhost/chat/messages.php?past=%ld&t=%ld", lastId, time(0) ]; NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease]; [request setURL:[NSURL URLWithString:url]]; [request setHTTPMethod:@"GET"]; NSURLConnection *conn=[[NSURLConnection alloc] initWithRequest:request delegate:self]; if (conn) { receivedData = [[NSMutableData data] retain]; } else { } } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [receivedData setLength:0]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [receivedData appendData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { if (chatParser) [chatParser release]; if ( messages == nil ) messages = [[NSMutableArray alloc] init]; chatParser = [[NSXMLParser alloc] initWithData:receivedData]; [chatParser setDelegate:self]; [chatParser parse]; [receivedData release]; [messageList reloadData]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector: @selector(timerCallback)]]; [invocation setTarget:self]; [invocation setSelector:@selector(timerCallback)]; timer = [NSTimer scheduledTimerWithTimeInterval:5.0 invocation:invocation repeats:NO]; } - (void)timerCallback { [timer release]; [self getNewMessages]; }
代码开始是 getNewMessages
方法。该方法创建请求,并通过构建一个 NSURLConnection
而开始这个请求。它还创建了用于存储响应数据的数据缓冲区。三个事件处理程序 didReceieveResponse
、didReceiveData
和 connectionDidFinishLoading
都处理加载数据的各个阶段。
connectionDidFinishLoading
方法是最重要的,因为它启动读取数据并挑出消息的 XML 解析器。
这里的最后一个方法是 timerCallback
,由定时器用来启动新消息请求。当定时器超时时,getNewMessages
方法被调用,这将再次启动定时过程,最后将创建一个新的定时器,这个定时器超时时,会再次启动消息检索过程,等等。
下一部分,清单 8,处理 XML 的解析。
清单 8. iOSChatClientViewController.m – 解析消息
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { if ( [elementName isEqualToString:@"message"] ) { msgAdded = [[attributeDict objectForKey:@"added"] retain]; msgId = [[attributeDict objectForKey:@"id"] intValue]; msgUser = [[NSMutableString alloc] init]; msgText = [[NSMutableString alloc] init]; inUser = NO; inText = NO; } if ( [elementName isEqualToString:@"user"] ) { inUser = YES; } if ( [elementName isEqualToString:@"text"] ) { inText = YES; } } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { if ( inUser ) { [msgUser appendString:string]; } if ( inText ) { [msgText appendString:string]; } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { if ( [elementName isEqualToString:@"message"] ) { [messages addObject:[NSDictionary dictionaryWithObjectsAndKeys:msgAdded, @"added",msgUser,@"user",msgText,@"text",nil]]; lastId = msgId; [msgAdded release]; [msgUser release]; [msgText release]; } if ( [elementName isEqualToString:@"user"] ) { inUser = NO; } if ( [elementName isEqualToString:@"text"] ) { inText = NO; } }
了解 SAX 解析的人应该都熟悉这个 XML 解析器。您给它一些 XML,当标签打开或关闭时,当找到文本时,它都会向您的代码发送事件。它是一个基于事件的解析器,而不是基于 DOM 的解析器。事件解析器的优点是,内存占用少。但是缺点是比较难以使用,因为在解析期间,所有的状态都需要存储在主机对象中。
过程开始时,所有成员变量(比如 msgAdded
、msgUser
、inUser
和 inText
)都被初始化为一个空字符串或 false。然后,随着每个标签在didStartElement
方法中完成初始处理,代码查看标签名,并设置适当的 inUser
或 inText
Boolean 值。这里,foundCharacters
方法处理向适当的字符串添加文本数据。didEndElement
方法然后处理标签的结束,即在发现 <message>
结束标签时将已解析的消息添加到消息列表。
现在您需要编写代码来显示消息。代码展示在清单 9 中。
清单 9. iOSChatClientViewController.m – 显示消息
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)myTableView numberOfRowsInSection: (NSInteger)section { return ( messages == nil ) ? 0 : [messages count]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath: (NSIndexPath *)indexPath { return 75; } - (UITableViewCell *)tableView:(UITableView *)myTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = (UITableViewCell *)[self.messageList dequeueReusableCellWithIdentifier:@"ChatListItem"]; if (cell == nil) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ChatListItem" owner:self options:nil]; cell = (UITableViewCell *)[nib objectAtIndex:0]; } NSDictionary *itemAtIndex = (NSDictionary *)[messages objectAtIndex:indexPath.row]; UILabel *textLabel = (UILabel *)[cell viewWithTag:1]; textLabel.text = [itemAtIndex objectForKey:@"text"]; UILabel *userLabel = (UILabel *)[cell viewWithTag:2]; userLabel.text = [itemAtIndex objectForKey:@"user"]; return cell; }
这些就是 UITableViewDataSource
和 UITableViewDelegate
接口定义的所有方法。最重要的一个是 cellForRowAtIndexPath
方法,它为列表项创建一个定制的 UI,并将它的文本字段设置为这个消息的适当文本。
这个定制的列表项定义在新的 ChatListItem.xib
文件中,您需要将这个文件创建在 Resources 文件夹中。在这个文件中,您创建一个新的 UITableViewCell
条目,其中有两个标签,分别标注为 1 和 2。这个文件以及所有其他代码都可从可下载的项目中得到(参见 下载)。
cellForRowAtIndexPath
方法中的代码分配这些 ChatListItem
单元格中的一个,然后将标签的文本字段设置为我们看到的这个消息的文本和用户值。
我知道要考虑的事项太多,但是已经快结束了。您已经完成了启动视图、获得消息 XML、解析消息和显示消息的代码。惟一剩下要做的事情是编写发送消息的代码。
构建此代码的第一件事是为用户名创建一个设置。iOS 应用程序可以定义进入 Settings 控制面板的定制设置。要创建一个设置,您需要使用 New File 向导在 Resources 文件夹中创建一个设置包。然后您使用图 7 中的 settings 编辑器,将它删除成单个设置。
然后您确定,您想要此设置的标题为 User
,并具有键 user_preference
。然后,您就可以为清单 10 中的消息发送代码使用这个首选项来得到用户名了。
清单 10. iOSChatClientViewController.m – 发送消息
- (IBAction)sendClicked:(id)sender { [messageText resignFirstResponder]; if ( [messageText.text length] > 0 ) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *url = [NSString stringWithFormat: @"http://localhost/chat/add.php"]; NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease]; [request setURL:[NSURL URLWithString:url]]; [request setHTTPMethod:@"POST"]; NSMutableData *body = [NSMutableData data]; [body appendData:[[NSString stringWithFormat:@"user=%@&message=%@", [defaults stringForKey:@"user_preference"], messageText.text] dataUsingEncoding:NSUTF8StringEncoding]]; [request setHTTPBody:body]; NSHTTPURLResponse *response = nil; NSError *error = [[[NSError alloc] init] autorelease]; [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; [self getNewMessages]; } messageText.text = @""; } - (void)viewDidLoad { [super viewDidLoad]; messageList.dataSource = self; messageList.delegate = self; [self getNewMessages]; } @end
这是 Send Message 按钮的单击处理程序代码。它创建一个 NSMutableURLRequest
,该请求具有 add.php
脚本的 URL。它然后将消息主体设置为一个字符串,该字符串的用户和消息数据被编码为 POST 格式。它然后使用一个 NSURLConnection
同步地向服务器发送消息数据,并使用 getNewMessages
启动一次消息检索。
该文件底部的 viewDidLoad
方法是视图加载时调用的方法。它开始消息检索过程,并将消息列表与该对象连接,以便消息列表知道从哪里得到数据。
所有这些都编写好之后,现在就该测试应用程序了。首先是在图 8 所示的 Settings 页面中设置用户名。
单击 iOSChatClient 应用程序,会显示图 9 所示的 settings 页面。
然后就像使用手机一样回到应用程序,并像图 10 中一样使用键盘输入一条消息。
然后按下 send 按钮,我们看到消息被发送并发布到服务器,并从 messages.php
返回,就像您可以从图 11 中看到的一样。
您会从代码中看到,send 按钮和消息列表之间没有直接连接。所以消息进入消息列表的惟一方式是,通过服务器成功地将数据插入到数据库中。然后 message.php
代码成功地返回消息列表中的消息用于显示。
这篇文章无疑让您受益匪浅。您在后台对 XML 数据完成了一些数据库操作。构建了一个带有定制用户界面的 iOS 应用程序,它向服务器发送数据,从服务器检索数据。您使用 XML 解析器解析从服务器返回的响应 XML。您还构建了一个定制列表 UI,以便消息看起来更为美观。
下一步该怎么走完全取决于您自己。Apple 已经为您在 iPhone 或 iPad 上实现您的愿景提供了工具。本文给您构建自己的支持网络的应用程序提供了路线图。我鼓励您亲自动手试一试。如果您确实构建了比较酷的应用程序,请告诉我, 我会帮助您将它提交到 App Store。
转载自 IBM Developers
抱歉,暂停评论。