1 局域网群聊软件
1.1 问题
UDP协议将独立的数据包从一台计算机传输到另外一台计算机,但是并不保证接受方能够接收到该数据包,也不保证接收方所接收到的数据和发送方所发送的数据在内容和顺序上是完全一致的。
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的协议方法,展示数据。
接下来定义一个NSString类型的currentHost属性,该属性记录用户所选择的聊天的对象,若该属性为空则表示聊天对象是所有用户,然后实现sendAll:方法和send:方法。
当用户点击输入框准备输入的时候会弹出键盘,这时候需要将整个聊天界面上移,这里使用注册键盘通知的方式调整self.view的坐标位置。在viewDidLoad方法中注册键盘即将出现和键盘即将消失两个通知,分别实现对应的方法即可。最后在viewWillDisappear:方法中注销通知。
sendAll:方法中直接将属性currentHost设置为nil即可,send:方法中将根据是否存在currentHost进行消息发送,如果currentHost存在则将消息发送给currentHost,如果不存在则发送给255.255.255.255,即发送给全员,并更新myChatRecordTV的显示内容。
最后实现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];
- //当键盘即将出现的时候self.view根据键盘的高度上移
- -(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];
- }
- //当键盘即将消失的时候self.view恢复到原来的位置
- -(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];
- //记录self.view的中心位置
- 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];
- }
- //当键盘即将出现的时候self.view根据键盘的高度上移
- -(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];
- }
- //当键盘即将消失的时候self.view恢复到原来的位置
- -(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
- //重写setter方法初始化
- - (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 = {};
- //创建Socket,其实就是指针
- CFSocketRef serverSocket;
- //设置回调函数
- serverSocket = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, AcceptCallBack, &CTX);
- if(serverSocket == NULL){
- NSLog(@"socket创建失败");
- return;
- }
- //设置一些socket的属性
- //布尔值类型, 可以重复使用一个已经使用过的地址和端口
- int yes = 1;
- setsockopt(CFSocketGetNative(serverSocket), SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes));
- //设置地址
- struct sockaddr_in addr = {};
- //设置IPv4
- addr.sin_family = PF_INET;
- //内核分配,本机地址,htonl函数将无符号短整型数转换成网络字节序
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- //端口号设置为0
- addr.sin_port = 0;
- addr.sin_len = sizeof(addr);
- //将struct sockaddr_in ==> CFDataRef,从指定字节缓冲区复试一个不可变的CFData对象
- CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));
- //设置Socket
- if(CFSocketSetAddress(serverSocket, address) != kCFSocketSuccess){
- NSLog(@"绑定失败");
- return;
- }
- NSLog(@"绑定成功");
- //在Bonjour广播时需要port
- 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);
- //创建Run Loop Socket源
- CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, serverSocket, 0);
- //将socket源加入到Run Loop中
- CFRunLoopAddSource(CFRunLoopGetCurrent(), sourceRef, kCFRunLoopCommonModes);
- CFRelease(sourceRef);
- }
实现Socket的回调函数,并在init方法中调用setupServer方法,代码如下所示:
- //在init方法中调用setupServer
- - (instancetype)init
- {
- self = [super init];
- if(self){
- [self setupServer];
- }
- return self;
- }
- #pragma mark - 回调函数
- //CFSocket回调函数, 有客户端连接上来时调用
- void AcceptCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
- {
- NSLog(@"....");
- CFReadStreamRef readStream = NULL;
- CFWriteStreamRef writeStream = NULL;
- //如果回调类型是kCFSocketAcceptCallBack, data就是CFSocketNativeHandle类型的指针,指向生成的socket
- CFSocketNativeHandle sock = *(CFSocketNativeHandle*)data;
- //创建读写socket流 readStream, writeStream
- 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
- {
- //添加服务到当前的Run Loop
- [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"]){
- // [sender getInputStream:<#(out NSInputStream *__strong *)#> outputStream:<#(out NSOutputStream *__strong *)#>];
- }
- }
最后在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,用于发现解析服务,代码如下所示:
- //NetServiceClient.h
- @interface NetServiceClient : NSObject
- //发现的所有service
- @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
在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;
- }
- //NSNetServiceDelegate方法,解析成功调用以下方法
- - (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>{
- //进行读写操作的标记 flag==0 写 flag==1 读
- 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
- {
- //进行读写操作 flag==0 写 flag==1 读
- 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 = {};
- //创建Socket,其实就是指针
- CFSocketRef serverSocket;
- //设置回调函数
- serverSocket = CFSocketCreate(NULL, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack, AcceptCallBack, &CTX);
- if(serverSocket == NULL){
- NSLog(@"socket创建失败");
- return;
- }
- //设置一些socket的属性
- //布尔值类型, 可以重复使用一个已经使用过的地址和端口
- int yes = 1;
- setsockopt(CFSocketGetNative(serverSocket), SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes));
- //设置地址
- struct sockaddr_in addr = {};
- //设置IPv4
- addr.sin_family = PF_INET;
- //内核分配,本机地址,htonl函数将无符号短整型数转换成网络字节序
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- //端口号设置为0
- addr.sin_port = 0;
- addr.sin_len = sizeof(addr);
- //将struct sockaddr_in ==> CFDataRef,从指定字节缓冲区复试一个不可变的CFData对象
- CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8*)&addr, sizeof(addr));
- //设置Socket
- if(CFSocketSetAddress(serverSocket, address) != kCFSocketSuccess){
- NSLog(@"绑定失败");
- return;
- }
- NSLog(@"绑定成功");
- //在Bonjour广播时需要port
- 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);
- //创建Run Loop Socket源
- CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, serverSocket, 0);
- //将socket源加入到Run Loop中
- 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
- {
- //添加服务到当前的Run Loop
- [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"]){
- // [sender getInputStream:<#(out NSInputStream *__strong *)#> outputStream:<#(out NSOutputStream *__strong *)#>];
- }
- }
- #pragma mark - 回调函数
- //CFSocket回调函数, 有客户端连接上来时调用
- void AcceptCallBack(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
- {
- NSLog(@"....");
- CFReadStreamRef readStream = NULL;
- CFWriteStreamRef writeStream = NULL;
- //如果回调类型是kCFSocketAcceptCallBack, data就是CFSocketNativeHandle类型的指针,指向生成的socket
- CFSocketNativeHandle sock = *(CFSocketNativeHandle*)data;
- //创建读写socket流 readStream, writeStream
- 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>{
- //进行读写操作的标记 flag==0 写 flag==1 读
- 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
- {
- //进行读写操作 flag==0 写 flag==1 读
- 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
- //发现的所有service
- @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;
- }
- //NSNetServiceDelegate
- - (void)netServiceDidResolveAddress:(NSNetService *)sender
- {
- NSLog(@"发现Bonjour服务.");
- [self.services addObject:sender];
- }
- //错误处理
- - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict
- {
- NSLog(@"%@", errorDict);
- }
- @end