【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 1800|回复: 1

玩转iPhone网络通讯之BSD Socket篇

[复制链接]
发表于 2012-12-28 15:50:24 | 显示全部楼层 |阅读模式

在进行iPhone网络通讯程序的开发中,不可避免的要利用Socket套接字。iPhone提供了Socket网络编程的接口CFSocket,不过笔者更喜欢使用BSD Socket。


iPhone BSD Socket进行编程所需要的头文件基本都位于/Xcode3.1.4/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS3.1.SDK/usr/include/sys下,既然本篇文章作为基础篇,那么笔者就从最基本的知识讲解开始。


首先,Socket是进行程序间通讯(IPC, Internet Process Connection)的BSD方法,这意味着Socket是用来让一个进程和其他的进程互相通讯的,就像我们用电话来和其他人交流一样。


既然说Socket像个电话,那么如果要打电话首先就要安装一部电话,“安装电话”这个动作对BSD Socket来说就是初始化一个Socket,方法如下:


int     socket(int, int, int);


第一个int参数为Socket的地址方式,既然要“安装电话”,那么就要首先确认所要安装的电话是音频的还是脉冲的。而如果要给BSD Socket安装电话,有两种类型可供读者选择:AF_UNIX和AF_INET,它们代表Socket的地址格式。如果选择AF_UNIX,意味着需要为Socket提供一个类似unix路径的名称,这个选项主要用于本地程序之间的socket通讯;本文主要讲解网络通讯,所以需要选择参数AF_INET。


第二个int参数为Socket的类型,“安装电话”需要首先确定是装有线的还是装无线的,安装Socket也一样,在Socket中提供了两种类型:SOCK_STREAM和SOCK_DGRAM。SOCK_STREAM表明数据像字符流一样通过Socket;而SOCK_DGRAM则表明数据以数据报(Datagrams)的形式通过Socket,本文主要讲解SOCK_STREAM,因为它的使用更为广泛。


第三个int参数为所使用的协议,本文里使用0即可。


“安装电话”的代码如下:


    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)

    {

        perror("socket");

        exit(1);

    }


到现在为止,怎么安装电话已经清楚了。因为本文主要演示如何在iPhone上使用BSD Socket获取内容,更多的功能是“打电话”而不是“接电话”,所以下面主要讲解BSD Socket扮演“客户端”角色的操作。


既然要“打电话”,那么首先要有打电话的对象,更确切的说需要一个“电话号码”,BSD Socket中的“电话号码”就是IP地址。更糟糕的情况是,如果只知道联系人的名字而不知道电话号码,那么还需要程序查找相应联系人的电话号码,根据联系人姓名查找电话号码的过程在BSD Socket中叫做DNS解析,代码如下:


- (NSString*)getIpAddressForHost:(NSString*) theHost

{

    struct hostent *host = gethostbyname([theHost UTF8String]);


    if(!host)

    {

        herror("resolv");

        return NULL;

    }


    struct in_addr **list = (struct in_addr **)host->h_addr_list;

    NSString *addressString = [NSString stringWithCString:inet_ntoa(*list[0])];

    return addressString;

}


hostent是个结构体,使用它需要#import <netdb.h>,通过这个方法得到theHost域名的第一个有效的IP地址并返回。


正确的“找到电话号码”后,就需要“拨打电话”了,代码如下:


        their_addr.sin_family = AF_INET;

        their_addr.sin_addr.s_addr = inet_addr([[self getIpAddressForHost:hostName] UTF8String]);

        NSLog(@"getIpAddressForHost :%@",[self getIpAddressForHost:hostName]);


        their_addr.sin_port = htons(80);

        bzero(&(their_addr.sin_zero), 8);


        int conn = connect(sockfd, (struct sockaddr*)&their_addr, sizeof(struct sockaddr));


        NSLog(@"Connect errno is :%d",conn);


笔者最初试图采用NHost进行主机域名的解析,奈何iPhone的这个类为private的,在application的开发中不可使用。


如果“电话”能顺利的接通,那么就可以进行“讲话”了,反之则会断开“电话连接”,如果友好的话,最好能给个提示,诸如“您所拨打的电话不在服务区之类”:)


        if(conn != -1)

        {

            NSLog(@"Then the conn is not -1!");


            NSMutableString* httpContent = [self makeHttpHeader:hostName];


            NSLog(@"httpCotent is :%@",httpContent);


            if(contentSended != nil)

                [httpContent appendFormat:contentSended];


            NSLog(@"Sended content is :%@",httpContent);


            NSData *data = [httpContent dataUsingEncoding:NSISOLatin1StringEncoding];

            ssize_t dataSended = send(sockfd, [data bytes], [data length], 0);


            if(dataSended == [data length])

            {

                NSLog(@"Datas have been sended over!");

            }


            printf("send %d bytes to %s\n",dataSended,inet_ntoa(their_addr.sin_addr));


            NSMutableString* readString = [[NSMutableString alloc] init];

            char readBuffer[512];


            int br = 0;

            while((br = recv(sockfd, readBuffer, sizeof(readBuffer), 0)) < sizeof(readBuffer))

            {

                NSLog(@"read datas length is :%d",br);


                [readString appendFormat:[NSString stringWithCString:readBuffer length:br]];


                NSLog(@"Hava received datas is :%@",readString);

            }


            close(sockfd);

        }else {

            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[@"Connection failed to host " stringByAppendingString:hostName] message:@"Please check the hostname in the preferences." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];

            [alert show];

            [alert release];

        }


“讲话”通过send(),“听话”通过recv(),这个两个函数的原型如下:


int send(int sockfd, const void *msg, int len, int flags);

int recv(int sockfd,void *buf,int len,unsigned int flags);


可以看出,这两个函数的参数基本相同。

第一个参数为套接字的句柄。

第二个参数为数据缓冲区。

第三个参数为数据长度。


最后一个参数有点特殊,这个参数是为了让BSD Socket能支持“带外数据”,何谓“带外数据”?顾名思义,就是“带内以外的数据”,而带内数据就是常规的按照Socket字节流顺序进行传递的数据。通常情况下,数据由连接的一端流到接收的一端,并且认为数据的所有字节都是精确排序的,晚写入的字节绝不会早于先写入的字节到达。但是如果我们“挂断了电话”,而接收方还有大量已经被接收的缓冲数据,这些数据还没被程序读取,那么接收方需要在读取这些缓冲的“带内数据”之前先读取一个标识取消的请求,这个请求就可以利用带外请求的方法进行传送。请求带外数据传送需要把标识位置为MSG_OOB,如下:


char buf[64];

int len;   

int s;      

send(s,buf,len,MSG_OOB);


至此,一个完整的“通话过程”已经结束,最后别忘记调用close(sockfd)“挂断电话”。


下面笔者尝试请求www.baidu.com的首页,并把请求的页面内容打印到控制台,所以需要对请求进行封装,以支持HTTP协议。很简单,只需要在请求的内容前面加上相应的HTTP头信息即可,如下:


#define HTTPMETHOD @"GET"

#define HTTPVERSION @"HTTP/1.1"

#define HTTPHOST @"Host"


#define KENTER @"\r\n"

#define KBLANK @" "


- (NSMutableString*) makeHttpHeader:(NSString*) hostName

{

    NSMutableString *header = [[NSMutableString alloc] init];


    [header appendFormat:HTTPMETHOD];

    [header appendFormat:KBLANK];

    [header appendFormat:@"/index.html"];

    [header appendFormat:KBLANK];

    [header appendFormat:HTTPVERSION];

    [header appendFormat:KENTER];


    [header appendFormat:HTTPHOST];

    [header appendFormat:@":"];

    [header appendFormat:hostName];

    [header appendFormat:KENTER];

    [header appendFormat:KENTER];


    return header;

}


在上面的方法中,笔者封装了HTTP头信息,对HTTP不熟悉的同学可以先熟悉熟悉HTTP的格式,请求到的内容打印如下:


[Session started at 2009-11-12 15:40:02 +0800.]
2009-11-12 15:40:04.691 BSDHttpExample[3483:207] getIpAddressForHost :119.75.216.30
2009-11-12 15:40:04.725 BSDHttpExample[3483:207] Connect errno is :0
2009-11-12 15:40:04.727 BSDHttpExample[3483:207] Then the conn is not -1!
2009-11-12 15:40:04.735 BSDHttpExample[3483:207] httpCotent is :GET /index.html HTTP/1.1
Host:www.baidu.com

2009-11-12 15:40:04.736 BSDHttpExample[3483:207] Sended content is :GET /index.html HTTP/1.1
Host:www.baidu.com

2009-11-12 15:40:04.736 BSDHttpExample[3483:207] Datas have been sended over!
send 48 bytes to 119.75.216.30
2009-11-12 15:40:04.764 BSDHttpExample[3483:207] read datas length is :363
2009-11-12 15:40:04.765 BSDHttpExample[3483:207] Hava received datas is :HTTP/1.1 200 OK
Date: Thu, 12 Nov 2009 07:40:05 GMT
Server: BWS/1.0
Content-Length: 3520
Content-Type: text/html;charset=gb2312
Cache-Control: private
Expires: Thu, 12 Nov 2009 07:40:05 GMT
Set-cookie: BAIDUID=9B024266ADD3B52AC8367A2BDD1676E5:FG=1; expires=Thu, 12-Nov-39 07:40:05 GMT; path=/; domain=.baidu.com
P3P: CP=" OTI DSP COR IVA OUR IND COM "

2009-11-12 15:40:04.766 BSDHttpExample[3483:207] view has been loaded!


最后为了造福大家,笔者附上完整的代码,头文件如下:


//

//  BSDHttpExampleViewController.h

//  BSDHttpExample

//

//  Created by SUN dfsun2009 on 09-11-12.

//  Copyright __MyCompanyName__ 2009. All rights reserved.

//


#import <UIKit/UIKit.h>


#define MYPORT 4880

#import <stdio.h>

#import <stdlib.h>

#import <unistd.h>

#import <arpa/inet.h>

#import <sys/types.h>

#import <sys/socket.h>

#import <netdb.h>


@interface BSDHttpExampleViewController : UIViewController {

    int sockfd;

    struct sockaddr_in their_addr;

}


@end


实现文件如下:


//

//  BSDHttpExampleViewController.m

//  BSDHttpExample

//

//  Created by sun dfsun2009 on 09-11-12.

//  Copyright __MyCompanyName__ 2009. All rights reserved.

//


#import "BSDHttpExampleViewController.h"


@implementation BSDHttpExampleViewController


#define HTTPMETHOD @"GET"

#define HTTPVERSION @"HTTP/1.1"

#define HTTPHOST @"Host"


#define KENTER @"\r\n"

#define KBLANK @" "


/*

// The designated initializer. Override to perform setup that is required before the view is loaded.

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {

if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {

// Custom initialization

}

return self;

}

*/


/*

// Implement loadView to create a view hierarchy programmatically, without using a nib.

- (void)loadView {

}

*/


void error_handle(char *errorMsg)

{

    fputs(errorMsg, stderr);

    fputc('\n',stderr);

    exit(1);

}


- (NSMutableString*) makeHttpHeader:(NSString*) hostName

{

    NSMutableString *header = [[NSMutableString alloc] init];


    [header appendFormat:HTTPMETHOD];

    [header appendFormat:KBLANK];

    [header appendFormat:@"/index.html"];

    [header appendFormat:KBLANK];

    [header appendFormat:HTTPVERSION];

    [header appendFormat:KENTER];


    [header appendFormat:HTTPHOST];

    [header appendFormat:@":"];

    [header appendFormat:hostName];

    [header appendFormat:KENTER];

    [header appendFormat:KENTER];


    return header;

}


- (NSString*)getIpAddressForHost:(NSString*) theHost

{

    struct hostent *host = gethostbyname([theHost UTF8String]);


    if(!host)

    {

        herror("resolv");

        return NULL;

    }


    struct in_addr **list = (struct in_addr **)host->h_addr_list;

    NSString *addressString = [NSString stringWithCString:inet_ntoa(*list[0])];

    return addressString;

}


- (void)Connect:(NSString *)hostName content:(NSString *)contentSended

{  

    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)


 楼主| 发表于 2012-12-28 17:43:49 | 显示全部楼层
在iphone的平台下,要进行socket开发其实有很多种的方法,开源的库Asyncsocket,官方的CFSocket,还有BSD的socket。
这里要做一个简单的socket普及,这里包含在socket的设置非阻塞喝超时的控制逻辑,心跳包和线程的启动时间同步的控制。

这里都是标准的linux的流程
先创建一个socket

- (int)CSocket
{
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(errno);
}
return sockfd;
}
然后是链接
//////////////////
- (BOOL)ConnectToServer:(NSString*)addr port:(int)port
{
their_addr.sin_family = AF_INET;
their_addr.sin_addr.s_addr = inet_addr([addr UTF8String]);
their_addr.sin_port = htons(port);
bzero(&(their_addr.sin_zero), 8);
int conn = connect(sockfd, (struct sockaddr*)&their_addr, sizeof(struct sockaddr));
NSLog(@"Connect error no is %d:",conn);
return misConnect;
}

这样子的链接是阻塞的,这样子就比较不好,可以设置成非阻塞的方式来控制超时
/***************************************************/
//在connect之前,设成非阻塞模式
int flags = fcntl(sockfd, F_GETFL,0);
fcntl(sockfd,F_SETFL, flags | O_NONBLOCK);
/***************************************************
//这是另外一种设置成非阻塞的方式
int flags;
if((flags = fcntl(sockfd, F_GETFL)) < 0 )
{
perror("fcntl F_SETFL");
}
flags |= O_NONBLOCK;
if(fcntl(sockfd, F_SETFL,flags) < 0)
{
perror("fcntl");
}
****************************************************/
设置connect后可以设置用select设置超时
/***************************************************/
//设置超时
fd_set fdwrite;
struct timeval tvSelect;

FD_ZERO(&fdwrite);
FD_SET(sockfd, &fdwrite);
tvSelect.tv_sec = 2;
tvSelect.tv_usec = 0;
int retval = select(sockfd + 1,NULL, &fdwrite, NULL, &tvSelect);
if(retval < 0)
{
if ( errno == EINTR )
{
NSLog(@"select error");
}
else
{
NSLog(@"error");
close(sockfd);
}
}
else if(retval == 0)
{
NSLog(@"select timeout........");
}
else if(retval > 0)
{
misConnect = YES;
}
/***************************************************/
//在connect成功之后,设成阻塞模式
flags = fcntl(sockfd, F_GETFL,0);
flags &= ~ O_NONBLOCK;
fcntl(sockfd,F_SETFL, flags);

/***************************************************/
//设置不被SIGPIPE信号中断,物理链路损坏时才不会导致程序直接被Terminate
//在网络异常的时候如果程序收到SIGPIRE是会直接被退出的。
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction( SIGPIPE, &sa, 0 );
/***************************************************/


然后就可以收发数据了
send,write两种方法都可以,你需要自己维护一个队列,控制时间等等
NSString *str = [SendCmdArray objectAtIndex:0];
NSData *data = [str dataUsingEncoding:NSISOLatin1StringEncoding];
// ssize_t datalen = send(sockfd,[data bytes],[data length],0);
ssize_t datalen = write(sockfd, [data bytes], [data length]);
if(datalen == [data length])
{
NSLog(@"Send str:%@",str);
}


如何接收数据,read和recv都可以,这是方法,你需要自己维护一个队列,控制时间等等。
char readBuffer[512] = {0};
NSString* readString = nil;
int br = 0;
while (br = read(sockfd, readBuffer, sizeof(readBuffer)) < sizeof(readBuffer))
// while((br = recv(sockfd, readBuffer, sizeof(readBuffer), 0)) < sizeof(readBuffer))
{
NSLog(@"Received CMD:%s",readBuffer);
readString = [NSString stringWithUTF8String:readBuffer];
memset(readBuffer,0,sizeof(readBuffer));
}
NSLog(@"br is %d,receive exit.",br);


获取时间后就可以进行时间同步了,具体的时间同步协议要根据自己平台来设计
time_t timep;
struct tm *p;
time(&timep);
p = localtime(&timep);
int wday = -1;//return num is (0,6),the weekday range is (1,7)
if(p->tm_wday == 0)
wday = 7;
else
wday = p->tm_wday;
char data[256] = {0};
sprintf(data,"0E4007%02x%02x%02x%02x%02x%02x%02x",(1900+p->tm_year)%100,(1+p->tm_mon),p->tm_mday,p->tm_hour,p->tm_min,p->tm_sec,wday);
NSString *msgtime = [NSString stringWithUTF8String:data];

可以开一个线程来进行收发,处理相关的操作,想要多线程控制需要注意这个socket必须是全局可用的,因为新线程已经不在主循环了
还有如果有界面更新也需要在主线程更新

[NSThread detachNewThreadSelector:@selector(OnNewThread) toTarget:self withObject:nil];

可以用timer做一个心跳包维持通讯

timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(OnHeartBeatTimer:) userInfo:nil repeats:YES];

结束的时候记得关掉定时器和socket
[timer invalidate];
close(sockfd);
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2024-4-29 20:26

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表