1 局域網羣聊軟件
1.1 問題
UDP協議將獨立的數據包從一臺計算機傳輸到另一臺計算機,可是並不保證接受方可以接收到該數據包,也不保證接收方所接收到的數據和發送方所發送的數據在內容和順序上是徹底一致的。html
UDP廣播就是創建於UDP協議上的數據傳輸,當網絡中的某一臺計算機向交換機或路由發送一個廣播數據時,交換機或路由則會將此廣播數據發送到其節點下的全部接收者。本案例使用第三方Socket編程框架AsyncUdpSocket框架,基於UDP廣播實現一個局域網羣聊軟件,一個基於UD廣播的聊天室程序,不須要任何的服務端程序作數據中轉,如圖-1所示:編程
圖-1數組
1.2 方案
首先建立一個SingleViewApplication應用,導入AsyncUdpSocket框架。在Storyboard中搭建聊天界面,場景左邊拖放一個大的TableView控件用於展現聊天記錄,設置tag值爲0。右邊拖放一個小的TableView控件,用於展現參與羣聊的用戶IP,tag值設置爲1,並將這兩個TableView控件關聯成ViewController的輸出口屬性myChatRecordTV和usersList。服務器
在場景的下方拖放一個Textfield控件用於輸入接受用戶輸入的聊天信息端,該控件上方是一個選擇全部人的按鈕,右邊是一個發送按鈕,將Textfield控件關聯成ViewController的輸出口屬性myTF,將按鈕分別關聯成動做方法sendAll:和send:。網絡
接下來首先實現聊天功能,在ViewController中定義一個屬性AsyncUdpSocket類型的udpSocket。在viewDidLoad方法中建立服務器端udpSocket,將端口號設置爲8000,委託對象設置爲self,並設置廣播屬性。併發
而後再定義兩個NSMutableArray類型的屬性users和chatRecord,分別用於記錄參與聊天用戶的IP和聊天記錄,在viewDidLoad方法中進行初始化。而後實現兩個TableView的協議方法,展現數據。app
接下來定義一個NSString類型的currentHost屬性,該屬性記錄用戶所選擇的聊天的對象,若該屬性爲空則表示聊天對象是全部用戶,而後實現sendAll:方法和send:方法。框架
當用戶點擊輸入框準備輸入的時候會彈出鍵盤,這時候須要將整個聊天界面上移,這裏使用註冊鍵盤通知的方式調整self.view的座標位置。在viewDidLoad方法中註冊鍵盤即將出現和鍵盤即將消失兩個通知,分別實現對應的方法便可。最後在viewWillDisappear:方法中註銷通知。socket
sendAll:方法中直接將屬性currentHost設置爲nil便可,send:方法中將根據是否存在currentHost進行消息發送,若是currentHost存在則將消息發送給currentHost,若是不存在則發送給255.255.255.255,即發送給全員,並更新myChatRecordTV的顯示內容。tcp
最後實現AsyncUdpSocketDelegate的協議方法onUdpSocket:didReceiveData:withTag:fromHost:port:,讀取數據和在線用戶,分別更新顯示聊天記錄內容和用戶列表。
1.3 步驟
實現此案例須要按照以下步驟進行。
步驟一:搭建聊天界面
首先建立一個SingleViewApplication應用,導入AsyncUdpSocket框架。在Storyboard中搭建聊天界面,場景左邊拖放一個大的TableView控件用於展現聊天記錄,設置tag值爲0。右邊拖放一個小的TableView控件,用於展現參與羣聊的用戶IP,tag值設置爲1,並將這兩個TableView控件關聯成ViewController的輸出口屬性myChatRecordTV和usersList,代碼以下所示:
- @interface ViewController ()
- @property (strong, nonatomic) IBOutlet UITableView *myChatRecordTV;
- @property (strong, nonatomic) IBOutlet UITableView *usersList;
- @end
而後在場景的下方拖放一個Textfield控件用於輸入接受用戶輸入的聊天信息端,該控件上方是一個選擇全部人的按鈕,右邊是一個發送按鈕,將Textfield控件關聯成ViewController的輸出口屬性myTF,將按鈕分別關聯成動做方法sendAll:和send:,如圖-2所示:
圖-2
步驟二:實現聊天功能
首先實現聊天功能,在ViewController中定義一個屬性AsyncUdpSocket類型的udpSocket。在viewDidLoad方法中建立服務器端udpSocket,將端口號設置爲8000,委託對象設置爲self,並設置廣播屬性,代碼以下所示:
- self.udpSocket = [[AsyncUdpSocket alloc]initWithDelegate:self];
- [self.udpSocket bindToPort:8000 error:nil];
- [self.udpSocket enableBroadcast:YES error:nil];
- [self.udpSocket joinMulticastGroup:@"192.168.1.104" error:nil];
- [self.udpSocket receiveWithTimeout:-1 tag:0];
再定義兩個NSMutableArray類型的屬性users和chatRecord,分別用於記錄參與聊天用戶的IP和聊天記錄,在viewDidLoad方法中進行初始化,代碼以下所示:
- @property (strong,nonatomic) NSMutableArray *users;
- @property (strong,nonatomic) NSMutableArray *chatRecord;
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.users = [NSMutableArray array];
- self.chatRecord = [NSMutableArray array];
- }
接下來實現兩個TableView的協議方法,展現數據,代碼以下所示:
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
- if (tableView.tag == 0) {
- return self.chatRecord.count;
- }else {
- return self.users.count;
- }
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- static NSString *identifier = @"Cell";
- UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
- if (!cell) {
- cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
- }
- switch (tableView.tag) {
- case 0:
- cell.textLabel.text = self.chatRecord[indexPath.row];
- [cell.textLabel setFont:[UIFont systemFontOfSize:12]];
- break;
- case 1:
- cell.textLabel.text = self.users[indexPath.row];
- [cell.textLabel setFont:[UIFont systemFontOfSize:10]];
- break;
- }
- return cell;
- }
接下來定義一個NSString類型的currentHost屬性,該屬性記錄用戶所選擇的聊天的對象,若該屬性爲空則表示聊天對象是全部用戶。在選擇一個聊天對象時進行賦值,代碼以下所示:
- -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
- self.currentHost = [self.users objectAtIndex:indexPath.row];
- }
而後實現sendAll:方法和send:方法。當用戶點擊輸入框準備輸入的時候會彈出鍵盤,這時候須要將整個聊天界面上移,這裏使用註冊鍵盤通知的方式調整self.view的座標位置。在viewDidLoad方法中註冊鍵盤即將出現和鍵盤即將消失兩個通知,分別實現對應的方法便可,代碼以下所示:
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardShow:) name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardHidden:) name:UIKeyboardWillHideNotification object:nil];
- -(void)keyboardShow:(NSNotification*) notification {
- CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
- NSTimeInterval duration =
- [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationOptions options =
- [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
- duration -= 0.1;
- [UIView animateWithDuration:duration
- delay:0
- options:options
- animations:
- ^{
- self.view.center = CGPointMake(p.x, p.y-keyboardRect.size.height);
- } completion:nil];
- }
- -(void)keyboardHidden:(NSNotification*)notification {
- NSTimeInterval duration =
- [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationOptions options =
- [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
- duration -= 0.1;
- [UIView animateWithDuration:duration
- delay:0
- options:options
- animations:
- ^{
- self.view.center = CGPointMake(p.x, p.y);
- } completion:nil];
- }
最後須要在viewWillDisappear:方法中註銷通知,代碼以下所示:
- -(void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
- }
sendAll:方法中直接將屬性currentHost設置爲nil便可,代碼以下所示:
- - (IBAction)sendAll:(UIButton *)sender {
- self.currentHost = nil;
- }
send:方法中將根據是否存在currentHost進行消息發送,若是currentHost存在則將消息發送給currentHost,若是不存在則發送給255.255.255.255,即發送給全員,並更新myChatRecordTV的顯示內容,代碼以下所示:
- - (IBAction)send:(UIButton *)sender {
- [self.myTF resignFirstResponder];
- if (self.currentHost) {
- NSString *chat = [NSString stringWithFormat:@"我saidto%@:%@",self.currentHost,self.myTF.text];
- NSString *chatSend = [NSString stringWithFormat:@"%@",self.myTF.text];
- [self.udpSocket sendData:[chatSend dataUsingEncoding:NSUTF8StringEncoding] toHost:self.currentHost port:8000 withTimeout:-1 tag:0];
- [self.chatRecord addObject:chat];
- }else {
- [self.udpSocket sendData:[self.myTF.text dataUsingEncoding:NSUTF8StringEncoding] toHost:@"255.255.255.255" port:8000 withTimeout:-1 tag:0];
- NSString *chat = [NSString stringWithFormat:@"我saidToAll:%@",self.myTF.text];
- [self.chatRecord addObject:chat];
- }
- [self.myChatRecordTV reloadData];
- [self.udpSocket receiveWithTimeout:-1 tag:0];
- }
最後實現AsyncUdpSocketDelegate的協議方法onUdpSocket:didReceiveData: withTag:fromHost:port:,讀取數據和在線用戶,分別更新顯示聊天記錄內容和用戶列表,代碼以下所示:
- -(BOOL)onUdpSocket:(AsyncUdpSocket *)sock didReceiveData:(NSData *)data withTag:(long)tag fromHost:(NSString *)host port:(UInt16)port {
- NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- if([str isEqualToString:@"誰在線"]) {
- [self.udpSocket sendData:[@"我在線" dataUsingEncoding:NSUTF8StringEncoding] toHost:@"255.255.255.255" port:8000 withTimeout:-1 tag:0];
- NSString *chat = [NSString stringWithFormat:@"%@:我在線",host];
- [self.chatRecord addObject:chat];
- [self.myChatRecordTV reloadData];
- }else if([str isEqualToString:@"我在線"]) {
- NSLog(@"%@",host);
- [self.users addObject:host];
- [self.usersList reloadData];
- }else {
- NSString *chat = [NSString stringWithFormat:@"%@saidToMe:%@",host,str];
- [self.chatRecord addObject:chat];
- [self.myChatRecordTV reloadData];
- }
- return YES;
- }
1.4 完整代碼
本案例中,ViewController.m文件中的完整代碼以下所示:
- #import "ViewController.h"
- #import "AsyncUdpSocket.h"
- #import "AppDelegate.h"
- @interface ViewController ()<UITableViewDataSource,UITableViewDelegate,UITextFieldDelegate>{
- CGPoint p;
- }
- @property (strong, nonatomic) IBOutlet UITextField *myTF;
- @property (strong, nonatomic) IBOutlet UITableView *myChatRecordTV;
- @property (strong, nonatomic) IBOutlet UITableView *usersList;
- @property (strong,nonatomic) AsyncUdpSocket *udpSocket;
- @property (strong,nonatomic) NSMutableArray *users;
- @property (strong,nonatomic) NSMutableArray *chatRecord;
- @property (nonatomic,retain) NSString *currentHost;
- @end
- @implementation ViewController
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- self.users = [NSMutableArray array];
- self.chatRecord = [NSMutableArray array];
- p = self.view.center;
- self.udpSocket = [[AsyncUdpSocket alloc]initWithDelegate:self];
- [self.udpSocket bindToPort:8000 error:nil];
- [self.udpSocket enableBroadcast:YES error:nil];
- [self.udpSocket joinMulticastGroup:@"192.168.1.104" error:nil];
- [self.udpSocket receiveWithTimeout:-1 tag:0];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardShow:) name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardHidden:) name:UIKeyboardWillHideNotification object:nil];
- }
- -(void)keyboardShow:(NSNotification*) notification {
- CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
- NSTimeInterval duration =
- [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationOptions options =
- [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
- duration -= 0.1;
- [UIView animateWithDuration:duration
- delay:0
- options:options
- animations:
- ^{
- self.view.center = CGPointMake(p.x, p.y-keyboardRect.size.height);
- } completion:nil];
- }
- -(void)keyboardHidden:(NSNotification*)notification {
- NSTimeInterval duration =
- [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
- UIViewAnimationOptions options =
- [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
- duration -= 0.1;
- [UIView animateWithDuration:duration
- delay:0
- options:options
- animations:
- ^{
- self.view.center = CGPointMake(p.x, p.y);
- } completion:nil];
- }
- -(void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
- }
- -(BOOL)onUdpSocket:(AsyncUdpSocket *)sock didReceiveData:(NSData *)data withTag:(long)tag fromHost:(NSString *)host port:(UInt16)port {
- NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
- if([str isEqualToString:@"誰在線"]) {
- [self.udpSocket sendData:[@"我在線" dataUsingEncoding:NSUTF8StringEncoding] toHost:@"255.255.255.255" port:8000 withTimeout:-1 tag:0];
- NSString *chat = [NSString stringWithFormat:@"%@:我在線",host];
- [self.chatRecord addObject:chat];
- [self.myChatRecordTV reloadData];
- }else if([str isEqualToString:@"我在線"]) {
- NSLog(@"%@",host);
- [self.users addObject:host];
- [self.usersList reloadData];
- }else {
- NSString *chat = [NSString stringWithFormat:@"%@saidToMe:%@",host,str];
- [self.chatRecord addObject:chat];
- [self.myChatRecordTV reloadData];
- }
- return YES;
- }
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
- if (tableView.tag == 0) {
- return self.chatRecord.count;
- }else {
- return self.users.count;
- }
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- static NSString *identifier = @"Cell";
- UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
- if (!cell) {
- cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
- }
- switch (tableView.tag) {
- case 0:
- cell.textLabel.text = self.chatRecord[indexPath.row];
- [cell.textLabel setFont:[UIFont systemFontOfSize:12]];
- break;
- case 1:
- cell.textLabel.text = self.users[indexPath.row];
- [cell.textLabel setFont:[UIFont systemFontOfSize:10]];
- break;
- }
- return cell;
- }
- -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
- self.currentHost = [self.users objectAtIndex:indexPath.row];
- }
- -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
- return 25;
- }
- - (IBAction)done:(UITextField *)sender {
- [self.myTF resignFirstResponder];
- }
- - (IBAction)send:(UIButton *)sender {
- [self.myTF resignFirstResponder];
- if (self.currentHost) {
- NSString *chat = [NSString stringWithFormat:@"我saidto%@:%@",self.currentHost,self.myTF.text];
- NSString *chatSend = [NSString stringWithFormat:@"%@",self.myTF.text];
- [self.udpSocket sendData:[chatSend dataUsingEncoding:NSUTF8StringEncoding] toHost:self.currentHost port:8000 withTimeout:-1 tag:0];
- [self.chatRecord addObject:chat];
- }else {
- [self.udpSocket sendData:[self.myTF.text dataUsingEncoding:NSUTF8StringEncoding] toHost:@"255.255.255.255" port:8000 withTimeout:-1 tag:0];
- NSString *chat = [NSString stringWithFormat:@"我saidToAll:%@",self.myTF.text];
- [self.chatRecord addObject:chat];
- }
- [self.myChatRecordTV reloadData];
- [self.udpSocket receiveWithTimeout:-1 tag:0];
- }
- - (IBAction)sendAll:(UIButton *)sender {
- self.currentHost = nil;
- }
- -(BOOL)prefersStatusBarHidden {
- return YES;
- }
- @end
2 基於服務發現的Socket通訊
2.1 問題
Socket須要指定服務器的端口和IP地址,在有些狀況下得到服務器的這些信息是很困難的,蘋果公司提供一種零配置服務發現協議,命名爲Bonjour,使應用在沒必要指定服務器端口和IP地址就能夠動態發現。
蘋果提供的Bonjour編程的相關類主要是兩個,NSNetService和NSNetServiceBrowser,以及和這兩個類配套的協議NSNetServiceDelegate和NSNetServiceBrowserDelegate。本案例經過Bonjour服務發現實現Socket通訊。
2.2 方案
首先建立服務器端應用NetServiceServer,用Xcode建立一個Command Line命令行項目,發現服務並不包含Socket服務器的啓動,所以啓動Socket服務器的代碼須要先編寫,服務器啓動後會得到動態端口,再把這個端口做爲參數傳遞給Bonjour發現服務,發佈成功創建Socket,本案例使用NSStream和CFStream實現服務器代碼。
接下來建立一個NetServiceServer類,幾乎所有的代碼都在該類中實現,在該類中定義兩個私有屬性,一個是NSNetService類型的service,用於發佈Bonjour服務並重寫setter方法進行初始化,另外一個屬性是short類型的port,用於記錄端口號。
其次啓動服務器,將啓動服務器的代碼封裝在setupServer方法,本案例使用NSStream和CFStream來實現服務器的啓動,而後在init方法中調用setupServer方法。
而後發佈服務,將發佈服務的代碼封裝在publishService方法中,並在init方法中調用。而後再實現協議方法netServiceDidPublish:,該方法在服務發佈結束後被調用,能夠經過該方法查看服務是否發佈成功。
最後在main函數中建立NetServiceServer實例對象,並調用CFRunLoopRun()函數,該函數能夠在當前線程啓動一個Runloop循環,使得服務器一直在運行狀態。
接下來建立客戶端應用NetServiceClient,使用Xcode建立一個SingleViewApplication應用,在Storyboard中搭建應用的界面,拖放兩個Button控件和一個Label控件,將Label關聯成TRViewController的輸出口屬性displayLabel,將兩個Button分別關聯成動做方法sendMessage和recvMessage,分別用於發送消息和接受消息。
建立NetServiceClient客戶端類,用於發現Bonjour服務,該類有一個NSMutableArray類型的屬性services用於記錄發現的服務對象,在.h文件中該屬性是隻讀的,在.m文件中該屬性是可讀可寫的。
另外還有一個私用屬性NSNetService類型的service,用於發現解析服務,在init方法中對以上兩個屬性進行初始化併發布服務,最後實現NSNetServiceDelegate協議相關方法。
而後完成ViewController類中的代碼,該類中沒有任何與服務發現相關的代碼,它從NetServiceClient類中得到輸入和輸出流對象,而後進行通訊就能夠了,這裏的讀寫數據流的操做一樣也是使用NSStream和CFStream類來實現。
在ViewController類中定義兩個私有屬性NSInputStream類型的inputStream,以及NSOutputStream類型的outputStream,分別用於記錄輸入流和輸出流,他們分別和服務器中的輸出流CFWriteStreamRef和輸入流CFReadStreamRef對應。
最後實現sendMessage和recvMessage方法,更新displayLabel的顯示。
2.3 步驟
實現此案例須要按照以下步驟進行。
步驟一:建立服務器端應用
首先建立服務器端應用NetServiceServer,用Xcode建立一個Command Line命令行項目,發現服務並不包含Socket服務器的啓動,所以啓動Socket服務器的代碼須要先編寫,服務器啓動後會得到動態端口,再把這個端口做爲參數傳遞給Bonjour發現服務,發佈成功創建Socket,本案例使用NSStream和CFStream實現服務器代碼,所以須要導入頭文件CoreFoundation.h,還須要包含頭文件sys/socket.h和netinet/in.h。
接下來建立一個NetServiceServer類,幾乎所有的代碼都在該類中實現,在該類中定義兩個私有屬性,一個是NSNetService類型的service,用於發佈Bonjour服務並重寫setter方法進行初始化,另外一個屬性是short類型的port,用於記錄端口號,代碼以下所示:
- @interface NetServiceServer () <NSNetServiceDelegate>
- @property (strong,nonatomic)NSNetService *service;
- @property (nonatomic) short port;
- @end
- - (NSNetService *)service
- {
- if (!_service) {
- _service = [[NSNetService alloc]initWithDomain:@"local." type:@"_tarenaipp._tcp." name:@"tarena" port:self.port];
- }
- return _service;
- }
其次啓動服務器,將啓動服務器的代碼封裝在setupServer方法,本案例使用NSStream和CFStream來實現服務器的啓動,代碼以下所示:
- - (void)setupServer
- {
- CFSocketContext CTX = {};
- CFSocketRef serverSocket;
- serverSocket = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, AcceptCallBack, &CTX);
- if(serverSocket == NULL){
- NSLog(@"socket建立失敗");
- return;
- }
- int yes = 1;
- setsockopt(CFSocketGetNative(serverSocket), SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes));
- struct sockaddr_in addr = {};
- addr.sin_family = PF_INET;
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addr.sin_port = 0;
- addr.sin_len = sizeof(addr);
- CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));
- if(CFSocketSetAddress(serverSocket, address) != kCFSocketSuccess){
- NSLog(@"綁定失敗");
- return;
- }
- NSLog(@"綁定成功");
- NSData *socketAddressActualData = (__bridge NSData *)CFSocketCopyAddress(serverSocket);
- struct sockaddr_in socketAddressActual;
- memcpy(&socketAddressActual, [socketAddressActualData bytes], [socketAddressActualData length]);
- self.port = ntohs(socketAddressActual.sin_port);
- NSLog(@"ServerSocket監聽的端口號:%hu\n", self.port);
- CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, serverSocket, 0);
- CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
- CFRelease(sourceRef);
- }
實現Socket的回調函數,並在init方法中調用setupServer方法,代碼以下所示:
- - (instancetype)init
- {
- self = [super init];
- if(self){
- [self setupServer];
- }
- return self;
- }
- #pragma mark - 回調函數
- void AcceptCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
- {
- NSLog(@"....");
- CFReadStreamRef readStream = NULL;
- CFWriteStreamRef writeStream = NULL;
- CFSocketNativeHandle sock = *(CFSocketNativeHandle*)data;
- CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock, &readStream, &writeStream);
- if(!readStream || !writeStream){
- NSLog(@"建立socket的讀寫流失敗.");
- close(sock);
- return;
- }
- CFStreamClientContext streamCTX = {};
- CFReadStreamSetClient(readStream, kCFStreamEventHasBytesAvailable, ReadStreamClientCallBack, &streamCTX);
- CFWriteStreamSetClient(writeStream, kCFStreamEventCanAcceptBytes, WriteStreamClientCallBack, &streamCTX);
- CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- CFReadStreamOpen(readStream);
- CFWriteStreamOpen(writeStream);
- }
- void ReadStreamClientCallBack(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo)
- {
- if(stream){
- UInt8 buf[1024] = {};
- CFReadStreamRead(stream, buf, sizeof(buf));
- NSLog(@"從客戶端讀到數據:%s", buf);
- CFReadStreamClose(stream);
- CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- }
- }
- void WriteStreamClientCallBack(CFWriteStreamRef stream, CFStreamEventType type, void *clientCallBackInfo)
- {
- if(stream){
- UInt8 buf[1024] = "嗨, 您好客戶端, 哈哈哈哈";
- CFWriteStreamWrite(stream, buf, strlen((const char*)buf)+1);
- CFWriteStreamClose(stream);
- CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- }
- }
而後發佈服務,將發佈服務的代碼封裝在publishService方法中,該方法中將服務添加到Runloop循環,並設置委託對象發佈服務,最後在init方法中調用該方法,代碼以下所示:
- - (void)publishService
- {
- [self.service scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- self.service.delegate = self;
- [self.service publish];
- }
- - (instancetype)init
- {
- self = [super init];
- if(self){
- [self setupServer];
- [self publishService];
- }
- return self;
- }
接下來實現協議方法netServiceDidPublish:,該方法在服務發佈結束後被調用,能夠經過該方法查看服務是否發佈成功,代碼以下所示:
- #pragma mark - NSNetServiceDelegate
- - (void)netServiceDidPublish:(NSNetService *)sender
- {
- NSLog(@"服務發佈結束");
- if([sender.name isEqualToString:@"tarena"]){
- }
- }
最後在main函數中建立NetServiceServer實例對象,並調用CFRunLoopRun()函數,該函數能夠在當前線程啓動一個Runloop循環,使得服務器一直在運行狀態,代碼以下所示:
- int main(int argc, const char * argv[])
- {
- @autoreleasepool {
- NetServiceServer *server = [[NetServiceServer alloc]init];
- CFRunLoopRun();
- server = nil;
- }
- return 0;
- }
運行服務器端的應用,能夠看到在控制檯輸出以下結果,如圖-3所示:
圖-3
步驟二:建立客戶端應用
建立客戶端應用NetServiceClient,使用Xcode建立一個SingleViewApplication應用,在Storyboard中搭建應用的界面,拖放兩個Button控件和一個Label控件,將Label關聯成TRViewController的輸出口屬性displayLabel,將兩個Button分別關聯成動做方法sendMessage和recvMessage,分別用於發送消息和接受消息。
完成的Storyboard界面如圖-4所示:
圖-4
接下來建立NetServiceClient客戶端類,用於發現Bonjour服務,該類有一個NSMutableArray類型的屬性services用於記錄發現的服務對象,在.h文件中該屬性是隻讀的,在.m文件中該屬性是可讀可寫的。
另外還有一個私用屬性NSNetService類型的service,用於發現解析服務,代碼以下所示:
- @interface NetServiceClient : NSObject
- @property (strong, nonatomic, readonly) NSMutableArray *services;
- @end
- #import "NetServiceClient.h"
- @interface NetServiceClient () <NSNetServiceDelegate>
- @property (strong, nonatomic, readwrite) NSMutableArray *services;
- @property (strong, nonatomic) NSNetService *service;
- @end
在init方法中對以上兩個屬性進行初始化併發布服務,最後實現NSNetServiceDelegate協議相關方法,代碼以下所示:
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- _services = [[NSMutableArray alloc]init];
- _service = [[NSNetService alloc]initWithDomain:@"local." type:@"_tarenaipp._tcp." name:@"tarena"];
- _service.delegate = self;
- [_service resolveWithTimeout:1.0];
- }
- return self;
- }
- - (void)netServiceDidResolveAddress:(NSNetService *)sender
- {
- NSLog(@"發現Bonjour服務.");
- [self.services addObject:sender];
- }
- - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
- {
- NSLog(@"%@", errorDict);
- }
- @end
而後完成ViewController類中的代碼,該類中沒有任何與服務發現相關的代碼,它從NetServiceClient類中得到輸入和輸出流對象,而後進行通訊就能夠了,所以它有一個NetServiceClient類型的屬性client。
這裏的讀寫數據流的操做一樣也是使用NSStream和CFStream類來實現。在ViewController類中定義兩個私有屬性NSInputStream類型的inputStream,以及NSOutputStream類型的outputStream,分別用於記錄輸入流和輸出流,他們分別和服務器中的輸出流CFWriteStreamRef和輸入流CFReadStreamRef對應,代碼以下所示:
- @interface TRViewController () <NSStreamDelegate>{
- int flag;
- }
- @property (weak, nonatomic) IBOutlet UILabel *displayLabel;
- @property (strong, nonatomic) NetServiceClient *client;
- @property (strong, nonatomic) NSInputStream *inputStream;
- @property (strong, nonatomic) NSOutputStream *outputStream;
- @end
讀寫操做代碼以下所示:
- - (void)openStream
- {
- for (NSNetService *service in self.client.services) {
- if ([@"tarena" isEqualToString:service.name]) {
- if(![service getInputStream:&_inputStream outputStream:&_outputStream]){
- NSLog(@"鏈接服務器失敗");
- return;
- }
- break;
- }
- }
- self.inputStream.delegate = self;
- [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- [self.inputStream open];
- self.outputStream.delegate = self;
- [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- [self.outputStream open];
- }
- #pragma mark - NSStreamDelegate
- - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
- {
- switch (eventCode) {
- case NSStreamEventNone:
- break;
- case NSStreamEventOpenCompleted:
- break;
- case NSStreamEventHasBytesAvailable:
- if(flag==1 && aStream==self.inputStream){
- uint8_t buffer[1024] = {};
- if([self.inputStream hasBytesAvailable]){
- int len = [self.inputStream read:buffer maxLength:sizeof(buffer)];
- if(len>0){
- NSString *string = [NSString stringWithCString:(const char*)buffer encoding:NSUTF8StringEncoding];
- self.displayLabel.text = [@"接收到數據:" stringByAppendingString:string];
- }
- }
- }
- break;
- case NSStreamEventHasSpaceAvailable:
- if(flag==0 && aStream==self.outputStream){
- UInt8 buffer[] = "Hello Server.";
- [self.outputStream write:buffer maxLength:strlen((const char*)buffer)+1];
- [self.outputStream close];
- }
- break;
- default:
- break;
- }
- }
最後實現sendMessage和recvMessage方法,更新displayLabel的顯示,代碼以下所示:
- - (IBAction)sendMessage
- {
- flag = 0;
- [self openStream];
- }
- - (IBAction)recvMessage:(id)sender
- {
- flag = 1;
- [self openStream];
- }
運行服務器端和客戶端程序,結果如圖-5所示:
圖-5
2.4 完整代碼
本案例中,服務器端應用中的NetServiceServer.m文件中的完整代碼以下所示:
- #import "NetServiceServer.h"
- #import <sys/socket.h>
- #import <netinet/in.h>
- #import <string.h>
- @interface NetServiceServer () <NSNetServiceDelegate>
- @property (strong,nonatomic)NSNetService *service;
- @property (nonatomic) short port;
- @end
- @implementation NetServiceServer
- - (instancetype)init
- {
- self = [super init];
- if(self){
- [self setupServer];
- [self publishService];
- }
- return self;
- }
- - (void)setupServer
- {
- CFSocketContext CTX = {};
- CFSocketRef serverSocket;
- serverSocket = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, AcceptCallBack, &CTX);
- if(serverSocket == NULL){
- NSLog(@"socket建立失敗");
- return;
- }
- int yes = 1;
- setsockopt(CFSocketGetNative(serverSocket), SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes));
- struct sockaddr_in addr = {};
- addr.sin_family = PF_INET;
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addr.sin_port = 0;
- addr.sin_len = sizeof(addr);
- CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));
- if(CFSocketSetAddress(serverSocket, address) != kCFSocketSuccess){
- NSLog(@"綁定失敗");
- return;
- }
- NSLog(@"綁定成功");
- NSData *socketAddressActualData = (__bridge NSData *)CFSocketCopyAddress(serverSocket);
- struct sockaddr_in socketAddressActual;
- memcpy(&socketAddressActual, [socketAddressActualData bytes], [socketAddressActualData length]);
- self.port = ntohs(socketAddressActual.sin_port);
- NSLog(@"ServerSocket監聽的端口號:%hu\n", self.port);
- CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, serverSocket, 0);
- CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
- CFRelease(sourceRef);
- }
- - (NSNetService *)service
- {
- if (!_service) {
- _service = [[NSNetService alloc]initWithDomain:@"local." type:@"_tarenaipp._tcp." name:@"tarena" port:self.port];
- }
- return _service;
- }
- - (void)publishService
- {
- [self.service scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
- self.service.delegate = self;
- [self.service publish];
- }
- #pragma mark - NSNetServiceDelegate
- - (void)netServiceDidPublish:(NSNetService *)sender
- {
- NSLog(@"服務發佈結束");
- if([sender.name isEqualToString:@"tarena"]){
- }
- }
- #pragma mark - 回調函數
- void AcceptCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
- {
- NSLog(@"....");
- CFReadStreamRef readStream = NULL;
- CFWriteStreamRef writeStream = NULL;
- CFSocketNativeHandle sock = *(CFSocketNativeHandle*)data;
- CFStreamCreatePairWithSocket(kCFAllocatorDefault, sock, &readStream, &writeStream);
- if(!readStream || !writeStream){
- NSLog(@"建立socket的讀寫流失敗.");
- close(sock);
- return;
- }
- CFStreamClientContext streamCTX = {};
- CFReadStreamSetClient(readStream, kCFStreamEventHasBytesAvailable, ReadStreamClientCallBack, &streamCTX);
- CFWriteStreamSetClient(writeStream, kCFStreamEventCanAcceptBytes, WriteStreamClientCallBack, &streamCTX);
- CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- CFReadStreamOpen(readStream);
- CFWriteStreamOpen(writeStream);
- }
- void ReadStreamClientCallBack(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo)
- {
- if(stream){
- UInt8 buf[1024] = {};
- CFReadStreamRead(stream, buf, sizeof(buf));
- NSLog(@"從客戶端讀到數據:%s", buf);
- CFReadStreamClose(stream);
- CFReadStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- }
- }
- void WriteStreamClientCallBack(CFWriteStreamRef stream, CFStreamEventType type, void *clientCallBackInfo)
- {
- if(stream){
- UInt8 buf[1024] = "嗨, 您好客戶端, 哈哈哈哈";
- CFWriteStreamWrite(stream, buf, strlen((const char*)buf)+1);
- CFWriteStreamClose(stream);
- CFWriteStreamUnscheduleFromRunLoop(stream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
- }
- }
- @end
本案例中,服務器端應用中的main.m文件中的完整代碼以下所示:
- #import <Foundation/Foundation.h>
- #import "NetServiceServer.h"
- int main(int argc, const char * argv[])
- {
- @autoreleasepool {
- NetServiceServer *server = [[NetServiceServer alloc]init];
- CFRunLoopRun();
- server = nil;
- }
- return 0;
- }
本案例中,客戶端應用中的ViewController.m文件中的完整代碼以下所示:
- #import "TRViewController.h"
- #import "NetServiceClient.h"
- @interface TRViewController () <NSStreamDelegate>{
- int flag;
- }
- @property (weak, nonatomic) IBOutlet UILabel *displayLabel;
- @property (strong, nonatomic) NetServiceClient *client;
- @property (strong, nonatomic) NSInputStream *inputStream;
- @property (strong, nonatomic) NSOutputStream *outputStream;
- @end
- @implementation TRViewController
- - (NetServiceClient *)client
- {
- if(!_client)_client = [[NetServiceClient alloc]init];
- return _client;
- }
- - (IBAction)sendMessage
- {
- flag = 0;
- [self openStream];
- }
- - (IBAction)recvMessage:(id)sender
- {
- flag = 1;
- [self openStream];
- }
- - (void)openStream
- {
- for (NSNetService *service in self.client.services) {
- if ([@"tarena" isEqualToString:service.name]) {
- if(![service getInputStream:&_inputStream outputStream:&_outputStream]){
- NSLog(@"鏈接服務器失敗");
- return;
- }
- break;
- }
- }
- self.inputStream.delegate = self;
- [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- [self.inputStream open];
- self.outputStream.delegate = self;
- [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- [self.outputStream open];
- }
- #pragma mark - NSStreamDelegate
- - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
- {
- switch (eventCode) {
- case NSStreamEventNone:
- break;
- case NSStreamEventOpenCompleted:
- break;
- case NSStreamEventHasBytesAvailable:
- if(flag==1 && aStream==self.inputStream){
- uint8_t buffer[1024] = {};
- if([self.inputStream hasBytesAvailable]){
- int len = [self.inputStream read:buffer maxLength:sizeof(buffer)];
- if(len>0){
- NSString *string = [NSString stringWithCString:(const char*)buffer encoding:NSUTF8StringEncoding];
- self.displayLabel.text = [@"接收到數據:" stringByAppendingString:string];
- }
- }
- }
- break;
- case NSStreamEventHasSpaceAvailable:
- if(flag==0 && aStream==self.outputStream){
- UInt8 buffer[] = "Hello Server.";
- [self.outputStream write:buffer maxLength:strlen((const char*)buffer)+1];
- [self.outputStream close];
- }
- break;
- default:
- break;
- }
- }
- @end
本案例中,客戶端應用中的NetServiceClient.h文件中的完整代碼以下所示:
- #import <Foundation/Foundation.h>
- @interface NetServiceClient : NSObject
- @property (strong, nonatomic, readonly) NSMutableArray *services;
- @end
本案例中,客戶端應用中的NetServiceClient.m文件中的完整代碼以下所示:
- #import "NetServiceClient.h"
- @interface NetServiceClient () <NSNetServiceDelegate>
- @property (strong, nonatomic, readwrite) NSMutableArray *services;
- @property (strong, nonatomic) NSNetService *service;
- @end
- @implementation NetServiceClient
- - (instancetype)init
- {
- self = [super init];
- if (self) {
- _services = [[NSMutableArray alloc]init];
- _service = [[NSNetService alloc]initWithDomain:@"local." type:@"_tarenaipp._tcp." name:@"tarena"];
- _service.delegate = self;
- [_service resolveWithTimeout:1.0];
- }
- return self;
- }
- - (void)netServiceDidResolveAddress:(NSNetService *)sender
- {
- NSLog(@"發現Bonjour服務.");
- [self.services addObject:sender];
- }
- - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
- {
- NSLog(@"%@", errorDict);
- }
- @end