iOS从零开始学习socket编程——HTTP1.0客户端

在开始socket编程之前,首先需要明确几个概念:
1.网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
2.socket中文名为“套接字”,是基于TCP/IP协议通信机制。
3.客户端的socket连接需要指定主机的ip地址和端口,ip地址类似于家庭地址,用于唯一确认一台主机,而端口类似于门牌号,用于唯一确认主机上的某一个程序。

我们模拟一次HTTP的请求。首先在终端中输入

telnet 202.118.1.7 80

我们会得到这样的提示

Trying 202.118.1.7...
Connected to www.neu.edu.cn.
Escape character is ‘^]‘.

这时候表示已经和服务器建立了socket连接,接下来就是传递参数。
输入:

GET / HTTP/1.0
HOST: www.neu.edu.cn

两次回车后得到这样的数据

HTTP/1.1 200 OK
Date: Thu, 16 Apr 2015 13:58:33 GMT
Content-Type: text/html
Content-Length: 9710
Last-Modified: Thu, 16 Apr 2015 08:51:04 GMT
Connection: close
Vary: Accept-Encoding
ETag: "552f77f8-25ee"
Server: Apache/2.4.12 (FreeBSD)
Accept-Ranges: bytes

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
…………

这表示已经请求成功,其中空白的那一行上面的是请求返回的头部,浏览器需要解析这段数据,对我们有用的是Content-Type字段和空白行下面的内容。Content-Type字段告诉我们接下来的数据应该以何种方式被保存。

HTTP协议是基于TCP/IP协议的,所以显然可以利用socket模拟一次HTTP请求。iOS提供了基于C的socket编程接口CFSocket以及输入输出流CFReadStreamRef、CFWriteStreamRef,但是其实现方法比较复杂,这里我们使用著名的AsyncSocket开源框架进行socket编程。使用AsyncSocket并不影响我们对socket的理解,同时也被QQ等知名软件用来实现即时通讯、文件传输等功能。
AsyncSocket下载地址:https://github.com/robbiehanson/CocoaAsyncSocket
下载后把RunLoop文件夹下的AsyncSocket.h和AsyncSocket.m拷贝到工程文件中即可使用。
需要说明的是,AsyncSocket基于Runloop,可以异步调用方法,并不需要多开一个线程。

我们新建一个SocketDemoViewController类,因为是简单的HTTP客户端,所以所有的操作、视图将会在这个类中实现。在这个简单的客户端中,我们将会模拟一个HTTP的访问,将返回头部显示在屏幕上,并且保存获取到的图片和html文件等。

//SocketDemoViewController.h
#import <UIKit/UIKit.h>
#import "AsyncSocket.h"

#define HOST_IP @"202.118.1.7"
#define HOST_PORT 80
@interface SocketDemoViewController : UIViewController <UITextViewDelegate>

@property (nonatomic, strong) AsyncSocket *client;
@property (nonatomic, strong) UITextView *inputMsg;
@property (nonatomic, strong) UITextView *outputMsg;
@property (nonatomic, strong) NSString *fileName;
@property (nonatomic, strong) NSMutableData *receiveData;
- (int) connectServer: (NSString *) hostIP port:(int) hostPort;
- (void) sendMsg;
- (void) reConnect;

HOST_IP和HOST_PORT我选择了东北大学首页www.neu.edu.cn。
我们创建了一个AsyncSocket类的对象client,两个textview用于显示输入和输出内容。connectServer方法与服务器进行连接,reConnect重新连接,sendMsg方法向服务器发送数据。

接下来是SocketDemoViewController.m的代码,有点长,慢慢分析。

#import "SocketDemoViewController.h"
@implementation SocketDemoViewController
@synthesize inputMsg, outputMsg,fileName,receiveData;
@synthesize client;

- (void)viewDidLoad {
    [super viewDidLoad];
    //UI部分的实现
    inputMsg = [[UITextView alloc] initWithFrame: CGRectMake( 10.0f,  40.0f,  355.0f,  100.0f)];
    inputMsg.delegate = self;
    inputMsg.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview: inputMsg];

    outputMsg = [[UITextView alloc] initWithFrame: CGRectMake( 10.0f,  180.0f,  355.0f,  150.0f)];
    outputMsg.textColor = [UIColor redColor];
    outputMsg.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview: outputMsg];

    UIButton *btnSend = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    btnSend.frame = CGRectMake(150.0f, 350.0f, 75.0f, 30.0f);
    [btnSend setTitle: @"发送" forState: UIControlStateNormal];
    [btnSend addTarget: self action: @selector(sendMsg) forControlEvents: UIControlEventTouchUpInside];
    [self.view addSubview: btnSend];

    UIButton *reConnect = [UIButton buttonWithType: UIButtonTypeRoundedRect];
    reConnect.frame = CGRectMake(150.0f, 400.0f, 75.0f, 30.0f);
    [reConnect setTitle: @"reConnect" forState: UIControlStateNormal];
    [reConnect addTarget: self action: @selector(reConnect) forControlEvents: UIControlEventTouchUpInside];
    [self.view addSubview: reConnect];

    //与服务器进行连接
    [self connectServer:HOST_IP port:HOST_PORT];   
}

//链接server
- (int) connectServer: (NSString *) hostIP port:(int) hostPort{
    if (client == nil)
    {
        client = [[AsyncSocket alloc] initWithDelegate:self];//初始化client,记得设置代理
        NSError *err = nil;
        if (![client connectToHost:hostIP onPort:hostPort error:&err])
        {
            //连接失败
            return 2;
        }
        else
        {
            NSLog(@"连接成功");
            return 1;
        }
    }
    else
    {
        [client readDataWithTimeout:-1 tag:0];
        return 0;
    }

}

- (void) reConnect{
    int stat = [self connectServer:HOST_IP port:HOST_PORT];
}

- (void)sendMsg{
    //把inputMsg的内容发送给服务器
    NSString *inputMsgStr = self.inputMsg.text;
    NSData *data = [inputMsgStr dataUsingEncoding:NSUTF8StringEncoding];
    [client writeData:data withTimeout:-1 tag:0];
}

#pragma mark -
#pragma mark close Keyboard
- (void)textFieldDidEndEditing:(UITextField *)textField
{
    [inputMsg resignFirstResponder];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [inputMsg resignFirstResponder];
    return  YES;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [inputMsg resignFirstResponder];
}

#pragma mark socket delegate

- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port{
    //成功连接到服务器执行的回调函数
    [client readDataWithTimeout:-1 tag:0];
}

- (void)onSocketDidDisconnect:(AsyncSocket *)sock
{
    client = nil;//置空client
}

- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    //收到数据的处理
    if (receiveData == nil) {
        receiveData = [[NSMutableData alloc] init];
    }
    [receiveData appendData:data];
    NSString* aStr = [[NSString alloc] initWithData:receiveData encoding:NSUTF8StringEncoding];

    NSRange endRange = [aStr rangeOfString:@"\r"];
    if (endRange.location != NSNotFound) {
        NSRange range = [aStr rangeOfString:@"\r\n\r\n"];
        if (range.location != NSNotFound) {
            NSString *requestHeader = [aStr substringToIndex:(unsigned long)range.location];
            self.outputMsg.text = requestHeader;
        }
        else{
            self.outputMsg.text = aStr;
        }

        //获取真正的请求到的数据
        NSData *contentData = [self getContentDataWithData:data];
        NSRange htmlRange = [aStr rangeOfString:@"text/html"];
        if (htmlRange.location != NSNotFound) {
            NSArray *paths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
            NSString *documentsDirectory =[paths objectAtIndex:0];
            NSString *ducumentPlistPath = [documentsDirectory stringByAppendingPathComponent:@"get.html"];//plist文件位置
            [[NSFileManager defaultManager] createFileAtPath:ducumentPlistPath contents:contentData attributes:nil];
        }
        UIImageView *imgv = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 50, 50)];
        imgv.image = [UIImage imageWithData:contentData];
        if (imgv.image != nil) {
            [self.view addSubview:imgv];
            UIImageWriteToSavedPhotosAlbum(imgv.image, self, @selector(imageSavedToPhotosAlbum:didFinishSavingWithError:contextInfo:), nil);
        }
        receiveData = nil;
    }
    [client readDataWithTimeout:-1 tag:0];
}

- (NSData *)getContentDataWithData:(NSData *)data{
    //将数据块和返回头部区分开来,返回NSData类型的请求到的数据
    NSString *toSearch = @"\r\n\r\n";
    NSData *target = [toSearch dataUsingEncoding:NSUTF8StringEncoding];
    NSRange doubleChangeLine = [data rangeOfData:target options:0 range:NSMakeRange(0, [data length])];
    if (doubleChangeLine.location != NSNotFound) {
        NSData *content = [data subdataWithRange:NSMakeRange(doubleChangeLine.location + 4, [data length] - doubleChangeLine.location - 4)];
        return content;
    }
    else{
        return nil;
    }
}

- (void)imageSavedToPhotosAlbum:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{
    //把image保存到本地相册中
    if (!error) {
        NSLog(@"成功保存到相册");
    }
}

@end

运行程序后,只要在输入框中,输入之前命令行中的那一段代码(记得要换行,参见截图中光标的位置),即可模拟HTTP请求。
技术分享技术分享
在onSocket:(AsyncSocket )sock didReadData:(NSData )data withTag:(long)tag方法中,我们根据Content-Type字段来选择处理数据的方式,并且将HTML文件保存在沙盒documents目录下的get.html方法中。讲可能存在的图片保存在本地相册中。
对于详细的didReadData解析,参见上一篇文章:《AsyncSocket didReadData函数详解》
http://blog.csdn.net/abc649395594/article/details/45046871

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。