Top
簡易的聊天工具
1.1 問題
Socket的英文原義是孔或者插座的意思,通常也稱作套接字,用於描述IP地址和端口,是一個通信鏈的句柄,本案例使用第三方Socket編程框架AsyncSocket框架實現一個簡易的聊天工具,並且能夠進行文件傳輸,由於沒有服務器本案例將服務器端和客戶端寫在一個程序中,如圖-1所示:
圖-1
1.2 方案
首先創建一個SingleViewApplication應用,導入AsyncSocket框架。在Storyboard中搭建聊天界面,上方的Textfield控件用於輸入接受端IP地址,中間TextView控件用於展示聊天記錄,下方的TextView用於接受用戶輸入的聊天內容,右下角有一個發送按鈕。將這三個控件分別關聯成ViewController的輸出口屬性IPTF、chatRecordTV、chatTV。
接下來首先實現聊天功能,在ViewController中定義三個屬性severSocket、clientSocket以及myNewSocket。在viewDidLoad方法中創建服務器端severSocket,將端口號設置爲8000,委託對象設置爲self。將發送按鈕關聯成viewController的動作方法send:,實現send:方法,創建客戶端對象,獲取聊天輸入框的內容轉化成NSData類型的數據發送出去。
然後ViewController遵守AsyncSocketDelegate協議,分別實現關於socket連接,數據傳輸以及數據讀取的協議方法,更新顯示聊天記錄內容。
最後實現傳輸文件功能,傳輸文件時爲了確定所傳輸文件的類型需要拼接一個消息頭,將傳輸文件的類型、名稱和大小保存到消息頭裏,通常傳輸數據的開始的100個字節是消息頭。在sender:方法中增加拼接消息頭的代碼。
接受端在接收到文件數據時首先對消息頭進行解析,獲取到接收文件的類型、大小以及名稱,然後再持續接受文件數據,接受完成後將文件保存到本地路徑。
1.3 步驟
實現此案例需要按照如下步驟進行。
步驟一:搭建聊天界面
首先創建一個SingleViewApplication應用,導入AsyncSocket框架。在Storyboard中搭建聊天界面,上方的Textfield控件用於輸入接受端IP地址,中間TextView控件用於展示聊天記錄,下方的TextView用於接受用戶輸入的聊天內容,如圖-2所示:
圖-2
然後將這三個控件分別關聯成ViewController的輸出口屬性IPTF、chatRecordTV、chatTV,代碼如下所示:
- @interface ViewController ()
- @property (weak, nonatomic) IBOutlet UITextField *IPTF;
- @property (weak, nonatomic) IBOutlet UITextView *chatRecordTV;
- @property (weak, nonatomic) IBOutlet UITextView *chatTV;
- @end
步驟二:實現聊天功能
首先在ViewController中定義三個屬性severSocket、clientSocket以及myNewSocket,代碼如下所示:
- @interface ViewController ()
- @property (nonatomic, strong)AsyncSocket *serverSocket;
- @property (nonatomic, strong)AsyncSocket *clientSocket;
- @property (nonatomic, strong)AsyncSocket *myNewSocket;
- @end
其次在viewDidLoad方法中創建服務器端severSocket,將端口號設置爲8000,委託對象設置爲self,代碼如下所示:
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.serverSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.serverSocket acceptOnPort:8000 error:nil];
- }
將發送按鈕關聯成viewController的動作方法send:,實現send:方法,創建客戶端對象,獲取聊天輸入框的內容轉化成NSData類型的數據發送出去,代碼如下所示:
- - (IBAction)send:(UIButton *)sender {
- [self.chatTV resignFirstResponder];
- //創建Socket客戶端
- self.clientSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.clientSocket connectToHost:self.IPTF.text onPort:8000 withTimeout:-1 error:nil];
- //將聊天輸入框的內容轉化爲NSData
- NSData *data = [self.chatTV.text dataUsingEncoding:NSUTF8StringEncoding];
- //發送數據
- [self.clientSocket writeData:data withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@",self.chatRecordTV.text,self.chatTV.text];
- }
運行程序發現,點擊聊天輸入框彈出的鍵盤會擋住聊天輸入框,因此需要在接受用戶輸入的時候屏幕界面上移,viewController遵守UITextFieldDelegate和UITextViewDelegate協議,當進入輸入狀況的時候屏幕界面上移,代碼如下所示:
- -(BOOL)textViewShouldBeginEditing:(UITextView *)textView {
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = CGPointMake(160, self.view.center.y-200);
- [UIView commitAnimations];
- return YES;
- }
添加一個單擊手勢。當單擊屏幕或者點擊發送按鈕時鍵盤收回,屏幕界面恢復正常位置,代碼如下所示:
- //點擊發送按鈕是
- - (IBAction)send:(UIButton *)sender {
- [self.chatTV resignFirstResponder];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = CGPointMake(160, point.y);
- [UIView commitAnimations];
- //創建Socket客戶端
- self.clientSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.clientSocket connectToHost:self.IPTF.text onPort:8000 withTimeout:-1 error:nil];
- //將聊天輸入框的內容轉化爲NSData
- NSData *data = [self.chatTV.text dataUsingEncoding:NSUTF8StringEncoding];
- //發送數據
- [self.clientSocket writeData:data withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@",self.chatRecordTV.text,self.chatTV.text];
- }
- //單擊屏幕時
- - (IBAction)resign:(UITapGestureRecognizer *)sender {
- if (self.view.center.y!=point.y) {
- [self.chatTV resignFirstResponder];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = point;
- [UIView commitAnimations];
- }
- }
最後ViewController遵守AsyncSocketDelegate協議,分別實現關於socket連接,數據傳輸以及數據讀取的協議方法,代碼如下所示:
- //當Socket接受一個連接的時候被調用
- -(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket{
- //將新接受的socket持有
- self.myNewSocket = newSocket;
- }
- //當Socket連接並準備讀和寫調用
- -(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
- //持續讀取數據
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- //當Socket讀取數據時調用
- -(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
- NSString *chatStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n對方說:%@",self.chatRecordTV.text,chatStr];
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- //持續發送數據
- -(void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag {
- NSLog(@"發送成功");
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
運行程序,聊天效果如圖-3所示:
圖-3
步驟三:實現文件傳輸功能
首先定義三個屬性用於記錄傳輸文件的數據、名稱和大小,代碼如下所示:
- @property (strong,nonatomic) NSMutableData *allData;
- @property (nonatomic, copy)NSString *reciveFileName;
- @property (nonatomic, assign)int reciveFileLength;
文件傳輸需要拼接頭文件,將傳輸文件的類型、大小和名稱放進消息頭裏面,因此在sender:方法中增加拼接消息頭的代碼,本案例以路徑開頭區分是發送文件還是聊天,代碼如下所示:
- - (IBAction)send:(UIButton *)sender {
- [self.chatTV resignFirstResponder];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = CGPointMake(160, point.y);
- [UIView commitAnimations];
- self.clientSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.clientSocket connectToHost:self.IPTF.text onPort:8000 withTimeout:-1 error:nil];
- if (![self.chatTV.text hasPrefix:@"/Users"]) {
- NSData *data = [self.chatTV.text dataUsingEncoding:NSUTF8StringEncoding];
- [self.clientSocket writeData:data withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@",self.chatRecordTV.text,self.chatTV.text];
- }else {
- NSString *filePath = self.chatTV.text;
- NSData *fileData = [NSData dataWithContentsOfFile:filePath];
- //把頭信息添加到文件Data的前面
- NSString *header = [NSString stringWithFormat:@"file&&%@&&%d",[filePath lastPathComponent],fileData.length];
- //把頭字符串轉成data
- NSData *headerData = [header dataUsingEncoding:NSUTF8StringEncoding];
- //把頭替換進100個字節的data裏面
- NSMutableData *sendAllData = [NSMutableData dataWithLength:100];
- [sendAllData replaceBytesInRange:NSMakeRange(0, headerData.length) withBytes:headerData.bytes];
- [sendAllData appendData:fileData];
- NSLog(@"%@",header);
- NSLog(@"SendLength = %d",sendAllData.length);
- [self.clientSocket writeData:sendAllData withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@正在發送",self.chatRecordTV.text,self.chatTV.text];
- }
- }
在讀取數據的方法中首先獲取消息頭信息,如果傳遞過來的是文件則獲取到傳輸文件的類型、大小和名稱,持續讀取文件數據,將文件保存到本地路徑,接受完成更新聊天記錄,代碼如下所示:
- -(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
- if (data.length>=100) {
- NSData *headerData = [data subdataWithRange:NSMakeRange(0, 100)];
- NSString *headerStr = [[NSString alloc]initWithData:headerData encoding:NSUTF8StringEncoding];
- if (headerStr&& [headerStr componentsSeparatedByString:@"&&"].count==3) {
- isFile = YES;
- }
- }
- if (isFile == NO) {
- NSString *chatStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n對方說:%@",self.chatRecordTV.text,chatStr];
- }
- if (isFile==YES) {
- if (data.length>=100) {
- NSData *headerData = [data subdataWithRange:NSMakeRange(0, 100)];
- NSString *headerStr = [[NSString alloc]initWithData:headerData encoding:NSUTF8StringEncoding];
- if (headerStr&& [headerStr componentsSeparatedByString:@"&&"].count==3) {
- NSArray *headers = [headerStr componentsSeparatedByString:@"&&"];
- NSString *type = [headers objectAtIndex:0];
- if ([type isEqualToString:@"file"]) {
- self.reciveFileName = [headers objectAtIndex:1];
- self.reciveFileLength = [[headers objectAtIndex:2] intValue];
- NSData *subFileData = [data subdataWithRange:NSMakeRange(100, data.length-100)];
- if (!self.allData) {
- self.allData = [NSMutableData data];
- }
- [self.allData appendData:subFileData];
- }
- }else {//沒有消息頭的情況下
- [self.allData appendData:data];
- }
- }else {//沒有消息頭的情況下
- [self.allData appendData:data];
- }
- NSLog(@"%d,%d",self.allData.length,self.reciveFileLength);
- if (self.allData.length == self.reciveFileLength ) {
- NSLog(@"接受完成");
- NSString *filePath = [@"/Users/Vivian/Documents" stringByAppendingPathComponent:self.reciveFileName];
- [self.allData writeToFile:filePath atomically:YES];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n對方說:成功接收文件%@",self.chatRecordTV.text,self.reciveFileName];
- self.allData = nil;
- isFile = NO;
- }
- if (isFile == YES) {
- //如果有數據能夠持續接受
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- }
- }
運行程序,傳輸文件效果如圖-4所示:
圖-4
1.4 完整代碼
本案例中,ViewController.m文件中的完整代碼如下所示:
- #import "ViewController.h"
- @interface ViewController () <AsyncSocketDelegate,UITextFieldDelegate,UITextViewDelegate> {
- BOOL isFile;
- CGPoint point;
- }
- @property (weak, nonatomic) IBOutlet UITextField *IPTF;
- @property (weak, nonatomic) IBOutlet UITextView *chatRecordTV;
- @property (weak, nonatomic) IBOutlet UITextView *chatTV;
- @property (nonatomic, strong)AsyncSocket *serverSocket;
- @property (nonatomic, strong)AsyncSocket *clientSocket;
- @property (nonatomic, strong)AsyncSocket *myNewSocket;
- @property (strong,nonatomic) NSMutableData *allData;
- @property (nonatomic, copy)NSString *reciveFileName;
- @property (nonatomic, assign)int reciveFileLength;
- @end
- @implementation ViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.serverSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.serverSocket acceptOnPort:8000 error:nil];
- point = self.view.center;
- self.allData = [NSMutableData data];
- isFile = NO;
- }
- -(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket{
- //將新接受的socket持有
- self.myNewSocket = newSocket;
- }
- -(void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- -(void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
- if (data.length>=100) {
- NSData *headerData = [data subdataWithRange:NSMakeRange(0, 100)];
- NSString *headerStr = [[NSString alloc]initWithData:headerData encoding:NSUTF8StringEncoding];
- if (headerStr&& [headerStr componentsSeparatedByString:@"&&"].count==3) {
- isFile = YES;
- }
- }
- if (isFile == NO) {
- NSString *chatStr = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n對方說:%@",self.chatRecordTV.text,chatStr];
- }
- if (isFile==YES) {
- if (data.length>=100) {
- NSData *headerData = [data subdataWithRange:NSMakeRange(0, 100)];
- NSString *headerStr = [[NSString alloc]initWithData:headerData encoding:NSUTF8StringEncoding];
- if (headerStr&& [headerStr componentsSeparatedByString:@"&&"].count==3) {
- NSArray *headers = [headerStr componentsSeparatedByString:@"&&"];
- NSString *type = [headers objectAtIndex:0];
- if ([type isEqualToString:@"file"]) {
- self.reciveFileName = [headers objectAtIndex:1];
- self.reciveFileLength = [[headers objectAtIndex:2] intValue];
- NSData *subFileData = [data subdataWithRange:NSMakeRange(100, data.length-100)];
- if (!self.allData) {
- self.allData = [NSMutableData data];
- }
- [self.allData appendData:subFileData];
- }
- }else {//沒有消息頭的情況下
- [self.allData appendData:data];
- }
- }else {//沒有消息頭的情況下
- [self.allData appendData:data];
- }
- NSLog(@"%d,%d",self.allData.length,self.reciveFileLength);
- if (self.allData.length == self.reciveFileLength ) {
- NSLog(@"接受完成");
- NSString *filePath = [@"/Users/Vivian/Documents" stringByAppendingPathComponent:self.reciveFileName];
- [self.allData writeToFile:filePath atomically:YES];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n對方說:成功接收文件%@",self.chatRecordTV.text,self.reciveFileName];
- self.allData = nil;
- isFile = NO;
- }
- if (isFile == YES) {
- //如果有數據能夠持續接受
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- }
- }
- -(void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag {
- NSLog(@"發送成功");
- [self.myNewSocket readDataWithTimeout:-1 tag:0];
- }
- -(BOOL)textViewShouldBeginEditing:(UITextView *)textView {
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = CGPointMake(160, self.view.center.y-200);
- [UIView commitAnimations];
- return YES;
- }
- - (IBAction)send:(UIButton *)sender {
- [self.chatTV resignFirstResponder];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = CGPointMake(160, point.y);
- [UIView commitAnimations];
- self.clientSocket = [[AsyncSocket alloc]initWithDelegate:self];
- [self.clientSocket connectToHost:self.IPTF.text onPort:8000 withTimeout:-1 error:nil];
- if (![self.chatTV.text hasPrefix:@"/Users"]) {
- NSData *data = [self.chatTV.text dataUsingEncoding:NSUTF8StringEncoding];
- [self.clientSocket writeData:data withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@",self.chatRecordTV.text,self.chatTV.text];
- }else {
- NSString *filePath = self.chatTV.text;
- NSData *fileData = [NSData dataWithContentsOfFile:filePath];
- //把頭信息添加到文件Data的前面
- NSString *header = [NSString stringWithFormat:@"file&&%@&&%d",[filePath lastPathComponent],fileData.length];
- //把頭字符串轉成data
- NSData *headerData = [header dataUsingEncoding:NSUTF8StringEncoding];
- //把頭替換進100個字節的data裏面
- NSMutableData *sendAllData = [NSMutableData dataWithLength:100];
- [sendAllData replaceBytesInRange:NSMakeRange(0, headerData.length) withBytes:headerData.bytes];
- [sendAllData appendData:fileData];
- NSLog(@"%@",header);
- NSLog(@"SendLength = %d",sendAllData.length);
- [self.clientSocket writeData:sendAllData withTimeout:-1 tag:0];
- self.chatRecordTV.text = [NSString stringWithFormat:@"%@\n我說:%@正在發送",self.chatRecordTV.text,self.chatTV.text];
- }
- }
- - (IBAction)done:(UITextField *)sender {
- [self.IPTF resignFirstResponder];
- }
- - (IBAction)resign:(UITapGestureRecognizer *)sender {
- if (self.view.center.y!=point.y) {
- [self.chatTV resignFirstResponder];
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
- [UIView setAnimationDuration:0.2];
- self.view.center = point;
- [UIView commitAnimations];
- }
- }
- @end