標籤:socket編程 ios http 用戶端 asyncsocke
在開始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.0HOST: www.neu.edu.cn
兩次斷行符號後得到這樣的資料
HTTP/1.1 200 OKDate: Thu, 16 Apr 2015 13:58:33 GMTContent-Type: text/htmlContent-Length: 9710Last-Modified: Thu, 16 Apr 2015 08:51:04 GMTConnection: closeVary: Accept-EncodingETag: "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
iOS從零開始學習socket編程——HTTP1.0用戶端