Networking Tutorial for iOS: How To Create A Socket Based iPhone App and Server

这里写图片描述
许多iOS应用使用HTTP和web server通信,因为这个很简单,方便。
然而,在某些情况,你可能发现需要往网络协议栈下面逛逛,体恤一下民情,使用TCP sockets和你自己的服务端通信。
这么做有好几个优点

  • 你只需要发送需要的数据-使得你的协议简洁有效
  • 你可以在任何时段发送数据给连接的用户,而不需要用户轮询
  • 你可以不依赖web server写socket servers,而且使用你想要的语言
  • 有些时候你必须使用sockets

在这个网络教程里,你会体验到如何创建一个iOS应用和TCP socket server通信,使用的是NSStream和CFStream。你会使用Python构造一个简单的socket server。
这个iOS应用和聊天server会实现聊天功能,你可以实时地在多个设备间聊天。
这个教程假设你已经对Python和iOS编程有基本了解。如果你是Python编程新手,查看这个官方Python教程。如果你是iOS编程新手,查看这个网站的一些iOS tutorials
让我们开始做一些socket编程。

什么是socket?

socket是一个允许你双向传输数据的工具。所以socket有两边,你可以看下面的图。你占领一边,某个人占领另一边-通常是另一个进程,无论在同一个机器或者不同的机器。
这里写图片描述
每一边都是由两个元素结合标识,IP地址和端口号。IP地址标识计算机,端口号标识进程。
存在不同类型的sockets,这些sockets的不同在于传输数据的协议不同。最流行的类型是TCP和UDP。在这篇教程中,我们处理TCP sockets。

客户端和服务端

讨论socket的时候不带上客户端和服务端是不可能的。你可以将这个视作一个餐馆。
这里写图片描述
服务端是一个进程,该进程侦听从客户端来的连接。你可以把这个看作餐馆的服务员,当客户来的时候,服务员招呼他们。
因为存在很多客户同时想要连接或者订单的可能,一个服务端通常开始为每一个连接建立一个子进程。你可以把这个视作大堂经理为每个客户分配一个服务员。
一旦连接建立,客户和服务端可以开始通过他们的socket发送和接收数据。你可以把这个视作客户和服务员之间侃侃而谈。
当客户关闭连接或者服务端停止的时候,这个socket关闭了。接下来企图使用这个socket都会导致错误。你可以将这个视作服务员把无理的客户踢出餐馆。

创作一个TCP服务端:综述

在我们开始构建iOS应用前,我们要开始创造我们的TCP服务端。这个服务端需要

  • 侦听接入连接
  • 追踪连接的用户(由socket和name标识)
  • 派遣事件(一个新的用户加入了聊天)

    为了派遣事件,服务端需要定义协议,这个协议定义了期望的数据格式,可以在客户和服务端之间来往传送。
    对于这个应用,我们准备使用一个非常简单的字符串协议。
    例如,当一个用户加入聊天时,这个iPhone会发送一个特殊的字符串给服务端,类似

ima:cesare

服务端看到 iam 字符串,会将其对待为”用户加入聊天”并且恰当地进行处理。对于其余地命令,例如用户发送消息或者离开聊天,会有类似的相同过程。
你可以使用很多不同的语言构造简单的TCP server。然而,对于这个教程来说,我们准备使用Python语言,因为它广为人知,而且在各个平台都能用。
如果你想,你可以使用内置的Python API制造一个TCP server,但是为了让事情更简单,我们准备使用Twisted功能,这是一个聚焦于网络的Python框架。

什么是Twisted?

Twisted是一个基于事件的引擎,让使用TCP,UDP,SSH,IRC或者FTP构建网络应用变得更加简单。
如果我们要从头开始构建一个TCP,我们需要学习Python类,学习怎么样进行socket和网络通信。Twisted具有很方便的集合管理连接和派遣,我们主要定睛于我们应用的具体事项-广播消息。
Twisted是基于一个所谓reactor pattern的设计模式构造的。这个模式很简单但是很强大-它开始一个循环,等待事件,对事件作出反应。
这里写图片描述
如果你没有使用过Twisted,不要忧虑0我们会在这个网络教程中一步一步引导你使用Twisted。

安装Twisted

如果你工作在MacOSX,已经安装好了Python。打开终端,用‘python -V’查出版本号。Mac OSX 10.6也包含了Twisted,所以你已经预备好了。
如果你的Mac是10.5,从它的官方网站下载和安装Twisted。

一个简单的TCP Server实现

我们会将所有的代码放在一个server.py文件中,打开你最喜欢的文本编辑器创建这个文件。
为了使用Twisted特色,我们需要导入一些Python类。最重要的是reactor

from twisted.internet import reactor

然后生成一个Twisted run loop

reactor.run()

这两行已经使我们拥有了应用的主心骨。是不是很简单?
你可以从命令行运行这个文件

python server.py

但是你会看到,目前为止没有任何事发生,它有对程序的控制,但是不具有任何反应的指令。
正如我们之前提到的客户端的任务,它使管理和客户的连接的,类似多个客户进入一个餐馆订餐。
所以我们需要一个机制来创造对象,当新的连接建立时,这些对象处理数据传输。这个连接需要实现一个协议。
我们不久就会看到协议的实现。我们先来看看连接管理。

侦听连接

如果我们想要使用raw Python来处理连接,我们需要为每一个连接生成子进程。
但是Twisted使得这一切变得更容易!我们可以使用内置的Factory方法,这个方法生成一个管理机器,处理和客户建立的每一个连接。让我们更新我们的代码。

from twisted.internet.protocol import Factory
from twisted.internet import reactor

factory = Factory()
reactor.listenTCP(80, factory)
reactor.run()

看到吗?仅仅5行代码,我们已经有了一个reactor模式的实现,有一个相关的factory处理连接。这很棒!我们现在可以专注于服务端的核心逻辑,就是对事件的反应。

定义协议

协议就是服务应用的逻辑。也是我们陈述当一个客户连接的时候,发送消息等等的时候做什么。
我们将这个逻辑放在一个类里面,这扩展了Twisted的Protocol类。
让我们修改我们的server.py

from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor

class IphoneChat(Protocol):
    def connectionMade(self):
        print "a client connected"

factory = Factory()
factory.protocol = IphoneChat
reactor.listenTCP(80, factory)
print "Iphone Chat server started"
reactor.run()

我们创建了一个新的类”IphoneChat”,这个类扩展了”Protocol”,扩展了connectionMade,当有一个连接时,打印这个消息。我们把我们的类分配给factory作为protocol。
我们准备开始运行客户端。
进到server.py文件夹,输入”sudo python server.py”。你应该会看到”Iphone Chat server started”消息。
在这节骨眼上,服务端已经开始了,侦听在我们主机的端口80上。我们选择端口89,因为默认情况下它就打开了,因为它是http连接的标准端口。
我们需要sudo因为在机器上侦听某个端口需要管理者权限。
我们可以使用telnet来测试连接。这是一个装载每一个操作系统上的工具,它也是测试服务端的客户替代品。打开另一个终端输入

telnet localhost 80

如果一切顺利,会显示”a client connected”

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

恭喜,你已经建立了一个连接!如果你喜欢,你可以打开另一个shell,测试服务端的多任务功能。你会看到和上面一样的行为。

追踪客户

服务端的任务之一是追踪连接。每一个客户有一个分配的socket,所以我们需要将这信息存储在数组里面。
将connectionMade修改为下面

def connectionMade(self):
        self.factory.clients.append(self)
        print "clients are ", self.factory.clients

在生成factory后初始化clients数组

factory.clients = []

这样的话,当我们有两个客户连接的时候,服务端会打印出下面信息

clients are  [<__main__.IphoneChat instance at 0x101300830>, <__main__.IphoneChat instance at 0x101300a28>]

这些是我们的协议类的实例。每一个实例有透过连接发送和从客户接收数据的能力。
为了完整,我们也要管理当一个用户断开连接的情况。所以代码如下

from twisted.internet.protocol import Factory, Protocol
from twisted.internet import reactor

class IphoneChat(Protocol):
    def connectionMade(self):
        self.factory.clients.append(self)
        print "clients are ", self.factory.clients

    def connectionLost(self, reason):
        self.factory.clients.remove(self)

factory = Factory()
factory.protocol = IphoneChat
factory.clients = []
reactor.listenTCP(80, factory)
print "Iphone Chat server started"
reactor.run()

对聊天事件作出反应

现在我们允许连接和追踪客户,是时候捣鼓一些有趣的东东了,响应都客户来的聊天命令。
对于这个网络教程,我们会使用非常简单的格式交换数据。我们会使用以冒号分割的字符串。在冒号之前是命令,可以是”iam”或者”msg”。

  • iam消息当某人加入这聊天使用,后面跟着的是这个人的绰号
  • message命令发送一个消息给所有的客户。msg没有必要携带发送者的名字,因为这是由服务端管理的,在self.factory.clients。

    当你制作你自己的app时,你可以制作别的协议,你可以使用JSON,XML,自定制的二进制格式,随你心愿。
    为了接收数据,我们需要重载的回调函数有下面的签名

def dataReceived(self, data):

在这个回调函数中,data是通过socket收到的消息。在这个代码里面,我们可以做下列事情

  1. 将字符串拆开找到命令
  2. 如果是iam,存储客户名字
  3. 在两种情况下,都构造定制消息,例如”Cesare has joined”或者”Ray says hello”
  4. 在两种情况下,广播这个消息到其他客户

    让我们看看如何做到!将下面的代码加到connectionLost方法后面

def dataReceived(self, data):
        a = data.split(':')
        print a
        if len(a) > 1:
            command = a[0]
            content = a[1]

            msg = ""
            if command == "iam":
                self.name = content
                msg = self.name + " has joined"

            elif command == "msg":
                msg = self.name + ": " + content
                print msg

            for c in self.factory.clients:
                c.message(msg)

当我们广播这个消息时,我们呼叫message方法。添加下面的代码

 def message(self, message):
        self.transport.write(message + '\n')

加上’\n’很重要,因为socket看到这个就知道传输结束了。
不管相不相信,到现在,你已经有了一个完整的聊天服务端了。
你可以使用telnet连接到聊天服务端测试它的功能,发送命令例如”iam:cesare”和”msg:hi”。如果你打开多个telnet会话会更有趣。

iPhone客户端

现在服务端已经就绪,我们可以聚焦于客户端。客户端需要管理三个操作

  1. 加入聊天室
  2. 发送消息
  3. 接收消息

    我们会用分层的方式组织视图。会有一个一直可见的主视图。会包含两个子视图,一个为了注册,一个为了聊天,这个视图你可以发送和接收消息。如下图所示
    这里写图片描述
    让我们开始。启动Xcode。工程名字为ChatClient
    这里写图片描述
    让我们开始工作在视图上,生成上面所阐释的结构。
    自动产生的代码包含了通常的应用代理加上一个视图控制器。
    这里写图片描述
    text field用来键入绰号,button用来引发加入聊天。然后将text field和button跟你的视图控制器连接起来。
    textfield作为outlet命名为inputNameField,button作为action命名为joinChat。这个view以outlet命名为joinView。
    此刻如果你编译并且运行你的代码,你应该看到这个view,但它不会做任何别的事情。
    下面我们需要编写代码加入聊天室,这是通过和服务端建立连接达到的。为了完成这个,我们初始化一些变量,但是我们先介绍一点东西。

流编程

我们使用streams在iOS中建立流连接。一个流是对发送接收数据机制的抽象。数据可以包含在不同的地方,例如一个文件,一个C缓冲器或者一个网络连接。一个stream可以有一个相关的代理,允许对特定的事件作出反应,这些事件可以是 “连接打开” “已经收到数据” “连接关闭”等等。
在Cocoa框架里有三个重要的和stream有关的类

  1. NSStream 这是超类,定义了抽象特色例如打开,关闭和代理
  2. NSInputStream NSStream的子类用来读输入
  3. NSOutputStream NSStream的子类,用来写输出

这些类构建在CFStream之上,CFStream是Core Foundation层的成员。如果你感觉自己是一个特别勇敢的人,你可以使用CoreFoundation的类重建这个应用。
但是为了让事情简单点,特别是在这个教程里面。我们准备使用NSStream类,这些类更容易使用。NSStrem类的唯一问题就是不能连接到远程host,而这正是我们在应用中需要的。
不过不要担心-NSStream和CFStream是桥接的,所以从CFStream获得NSStream很容易,只要调用一个魔法函数就可以。
好了,让我们开始。打开ChatClientViewController.h ,添加下面的实例变量

NSInputStream *inputStream;
NSOutputStream *outputStream;

然后打开ChatClientViewController.m,添加下面的方法

- (void)initNetworkCommunication {
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;
    CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"localhost", 80, &readStream, &writeStream);
    inputStream = (NSInputStream *)readStream;
    outputStream = (NSOutputStream *)writeStream;
}

这个有魔力的函数CFStreamCreatePairWithSocketToHost帮助我们绑定两个stream到一个主机和一个端口。一旦你呼叫过这个函数,你可以自由地将CFStreams投掷为NSStreams,这是唯一OC境外的代码。
目前为止我们准备好了这个连接,但是没有建立通信。你必须要在两个streams上呼叫open方法来打开,但是还有一些工作需要先完成。
我们说NSStreams有一个代理,我们在打开连接之前必须设置这个代理,否则我们不会接收任何通知。我们会使用目前的类作为代理,所以将下面的代码放在这个initNetworkCommunication的后面。

[inputStream setDelegate:self];
[outputStream setDelegate:self];

当然,为了取悦我们的编译器,我们必须标记这个控制器实现了NSStreamDelegate,所以修改这个控制器的接口如下

@interface ChatClientViewController : UIViewController <NSStreamDelegate>

我们的streams必须持续不断地准备好发送和接收数据。我们必须在run loop中调度这个stream来接收事件。如果我们不分配一个run loop,代理会阻塞代码地执行直到没有数据用以读写,这是我们想要避免的。
应用必须要对stream事件作出反应。Run-loop调度允许我们允许我们执行其它的代码,但是确保当stream发生某些事件的时候得到通知。
调度我们的streams让其在run-loop中,添加下面的代码到initNetworkCommunication的末尾

[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

现在我们准备好打开这个连接了!加上下面的代码

[inputStream open];
[outputStream open];

还有一件事情要做,拿掉viewDidLoad方法的注释,呼叫initNetworkCommunication

[self initNetworkCommunication];

目前为止就是这些!编译并且运行这个app,看看你的服务端输出。服务端的shell应该会显示有一个client连接,如下图所示
这里写图片描述

Joining the Chat

现在我们已经连接了,我们准备开始加入聊天!
加入消息的格式是”iam:name”。所以我们需要构建一个字符串,把它写入到outputStream中。
注意我们不能将字符串直接写入到stream中-我们需要将它转换为NSData,所以打开ChatClientViewController.m

 - (IBAction)joinChat:(id)sender {

    NSString *response  = [NSString stringWithFormat:@"iam:%@", inputNameField.text];
    NSData *data = [[NSData alloc] initWithData:[response dataUsingEncoding:NSASCIIStringEncoding]];
    [outputStream write:[data bytes] maxLength:[data length]];

}

不管你相信与否,就是这样-发送消息相当简单,对么?
编译并且运行,在text field中键入名字,然后点击join。切换到你的服务端shell。你会看到一个消息验证你的iam命令,如下面所示
这里写图片描述

生成聊天视图

下面我们要生成一个用户界面来发送和显示聊天消息。
打开 ChatClientViewController.xib,拖拉一个新的UIView,让它成为main UIView的子孙(是带有Join按钮的UIView的兄弟)。改变这两个view的顺序,让新的View是main UIView的第二个子孙(在最上面,现实中也是,后生的孩子比较疼爱)
拽拉一个textfield,button和table view到这个view,按照下图所示匹配排版。
这里写图片描述
下一步你需要将这些UI元素和你的类连接起来。遵循下面的步骤
- textfield 作为inputMessageField
- table view作为tView
- button action sendMessage
- table view配备datasource和delegate
- view作为chatView
- 将这个view放到main uiview的第一个位置,使得joinView成为最先印入眼帘的view。

然后你需要修改ChatClientViewController.h的接口如下所示

@interface ChatClientViewController : UIViewController <NSStreamDelegate, UITableViewDelegate, UITableViewDataSource> {
然后切换到ChatClientViewController.m,添加下面的新方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *CellIdentifier = @"ChatCellIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    return cell;

}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 0;
}

最后,你需要添加一些代码当用户参与到聊天中后,将聊天视图转换到上面。仅仅需要在joinChat的末尾添加这行

[self.view bringSubviewToFront:chatView];

编译并且运行你的代码,键入你的名字,点击join,现在你的新用户界面会显露出来。
这里写图片描述

发送消息

现在剩下的就是我们应用的核心功能,发送和接收消息。
我们会从实现发送消息开始。

 NSString *response  = [NSString stringWithFormat:@"msg:%@", inputMessageField.text];
    NSData *data = [[NSData alloc] initWithData:[response dataUsingEncoding:NSASCIIStringEncoding]];
    [outputStream write:[data bytes] maxLength:[data length]];

这就完了,编译运行你的应用,注册并且发送一个消息。如果你切换到你的服务端shell。你应该会立即看到这个消息。

接收消息

告诉你事实,这个应用也在从服务端接收消息,但是我们没有编写显示它们的代码,让我们来完成这部分。
首先,在ChatClientViewController.h添加一个实例变量

NSMutableArray * messages;

然后,切换到 ChatClientViewController.m,调整下面的变化

// At bottom of viewDidLoad
messages = [[NSMutableArray alloc] init];   

// In dealloc
[messages release];

// At bottom of tableView:cellForRowAtIndexPath:, right before return cell
NSString *s = (NSString *) [messages objectAtIndex:indexPath.row];
cell.textLabel.text = s;

// Replace return 0 in tableView:numberOfRowsInSection: with the following
return messages.count;

这是相当标准的东西-我们只是建立了一个NSMutableArray来包含一系列字符串,然后用我们的table view来显示它们。
现在我们剩下的就是实现NSStream代理。
这个代理包括了stream:handleEvent:让我们的应用能够对streams上发生的活动作出反应。我们已经设置好了代理,所以我们只需要实现下面的方法。校验事件类型。

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
    NSLog(@"stream event %i", streamEvent);
}

NSStream定义的常量如下

typedef enum {
   NSStreamEventNone = 0,
   NSStreamEventOpenCompleted = 1 << 0,
   NSStreamEventHasBytesAvailable = 1 << 1,
   NSStreamEventHasSpaceAvailable = 1 << 2,
   NSStreamEventErrorOccurred = 1 << 3,
   NSStreamEventEndEncountered = 1 << 4
};

在我们这种情形,我们特别感兴趣的是

  • NSStreamEventOpenCompleted 校验连接打开
  • NSStreamEventHasBytesAvailable 接收消息吧,亲
  • NSStreamEventErrorOccurred 在连接的时候校验问题事项
  • NSStreamEventEndEncountered 当服务端关闭的时候,关闭这个stream

    修改这个实现如下面的代码

 - (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {

    switch (streamEvent) {

        case NSStreamEventOpenCompleted:
            NSLog(@"Stream opened");
            break;

        case NSStreamEventHasBytesAvailable:
            break;          

        case NSStreamEventErrorOccurred:
            NSLog(@"Can not connect to the host!");
            break;

        case NSStreamEventEndEncountered:
            break;

        default:
            NSLog(@"Unknown event");
    }

}

这个关键点是NSStreamEventHasBytesAvailable,这里我们要
- 从stream读取bytes
- 将它们收集在buffer
- 将buffer转换为字符串
- 将字符串添加到message数组
- 告诉tableview重新装载
所以让我们开始!

case NSStreamEventHasBytesAvailable:

    if (theStream == inputStream) {

        uint8_t buffer[1024];
        int len;

        while ([inputStream hasBytesAvailable]) {
            len = [inputStream read:buffer maxLength:sizeof(buffer)];
            if (len > 0) {

                NSString *output = [[NSString alloc] initWithBytes:buffer length:len encoding:NSASCIIStringEncoding];

                if (nil != output) {
                    NSLog(@"server said: %@", output);
                }
            }
        }
    }
    break;

首先我们想要确定这个事件是从inputStream来的。然后我们准备了一个buffer(1024对于这个教程来说够用了)
最后我们开始了一个循环来收集stream中的bytes。这个read方法当没有数据可读的时候返回0.所以当结果比0大,我们将buffer转换为字符串并且打印这个结果。
我们做了最困难的部分。现在我们只需要将字符串添加到消息数组然后重载table view。

- (void) messageReceived:(NSString *)message {

    [messages addObject:message];
    [self.tView reloadData];

}

然后在NSLog语句之后加上下面这句

[self messageReceived:output];

我们完成了,让我们运行这个服务端,用iPhone应用连接,然后用telnet连接。你应该可以注册并且发送接收消息。祝贺!
这里写图片描述

调整

有一些可以改进应用的调整。首先,当消息发送后我们可以清空信息框。

inputMessageField.text = @""; // clean the field

第二,当消息的数量增加时,table隐藏了位于键盘下面的消息。为了讷讷狗启用自动滚动,添加下面的代码到messageReceived的底部

NSIndexPath *topIndexPath = 
    [NSIndexPath indexPathForRow:messages.count-1 
                       inSection:0];
[self.tView scrollToRowAtIndexPath:topIndexPath 
                  atScrollPosition:UITableViewScrollPositionMiddle 
                          animated:YES];

对于streams,当服务端关闭连接后,我们应该记住关闭stream,并且将它从run-loop中移除。如下所示

case NSStreamEventEndEncountered:

    [theStream close];
    [theStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

break;

在设备上运行app

如果你想要在你的设备上运行这个应用,如果你在你的本机上运行的你服务端,你可以会遇到路由问题。
如果你遇到了,这里有一些小技巧。首先,记住将localhost字符串换成你的计算机ip。到系统偏爱-网络中可以发现你的当前ip
这里写图片描述
你的设备应该通过无线和你的计算机接到同一个路由器。如果你想要使用3G连接,你需要配置你的路由。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值