需求
最近做一个需求,接入电信校验手机号码功能电信手机号码校验API。通过与电信工作人员沟通,移动端必须在使用电信蜂窝数据的时候才可以成功获取accessCode,用与本机号码校验。也就是如果在wifi和蜂窝数据同时打开的情况下,使用蜂窝数据做网络请求才能成功。什么鬼???这不是偷偷用用户的数据流量吗?没办法,要实现这个功能,也只能去找对应的解决办法了。
在网上查找资料,受这个切换网卡的启示,尝试了一下通过 getifaddrs() 来获取本机所有地址信息,其中 "pdp_ip0" 的是蜂窝数据的地址。然后 socket 指定这个地址为网卡出口就可以了。
实现
通过socket来实现http请求,参考CocoaAsyncSocket中http demo
- (void)startSocket
{
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
uint16_t port = WWW_PORT;
if (port == 0)
{
#if USE_SECURE_CONNECTION
port = 443; // HTTPS
#else
port = 80; // HTTP
#endif
}
if (![asyncSocket connectToHost:WWW_HOST onPort:port error:&error]){
DDLogError(@"Unable to connect to due to invalid configuration: %@", error);
}else{
DDLogVerbose(@"Connecting to \"%@\" on port %hu...", WWW_HOST, port);
}
...
}
在 startSocket
中 可以看到给 socket 指定了连接的地址和端口,那么既然我们需要指定本地网卡出口,就需要换一个接入的方法, 如下的接口中可以允许我们指定本地 interface, 剩下的就是获取本机ip, 并传入这个方法啦。
- (BOOL)connectToHost:(NSString *)inHost
onPort:(uint16_t)port
viaInterface:(NSString *)inInterface
withTimeout:(NSTimeInterval)timeout
error:(NSError **)errPtr;
获取本机ip
#define IOS_CELLULAR @"pdp_ip0"
#define IOS_WIFI @"en0"
#define IP_ADDR_IPv4 @"ipv4"
#define IP_ADDR_IPv6 @"ipv6"
/**
获取本机ip (必须在有网的情况下才能获取手机的IP地址)
@return str 本机ip 返回蜂窝数据的结果
*/
- (NSString *)getDeviceIPAddress:(BOOL)preferIPv4 {
NSDictionary *addresses = [self getIPAddresses];
NSLog(@"addresses==%@", addresses);
NSString *address = addresses[IOS_CELLULAR @"/" IP_ADDR_IPv4] ?:addresses[IOS_CELLULAR @"/" IP_ADDR_IPv6];
return address ? address : nil;
}
//获取所有相关IP信息
- (NSDictionary *)getIPAddresses
{
NSMutableDictionary *addresses = [NSMutableDictionary dictionaryWithCapacity:8];
// retrieve the current interfaces - returns 0 on success
struct ifaddrs *interfaces;
if(!getifaddrs(&interfaces)) {
// Loop through linked list of interfaces
struct ifaddrs *interface;
for(interface=interfaces; interface; interface=interface->ifa_next) {
if(!(interface->ifa_flags & IFF_UP) /* || (interface->ifa_flags & IFF_LOOPBACK) */ ) {
continue; // deeply nested code harder to read
}
const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr;
char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ];
if(addr && (addr->sin_family== AF_INET || addr->sin_family==AF_INET6)) {
NSString *name = [NSString stringWithUTF8String:interface->ifa_name];
NSString *type;
if(addr->sin_family == AF_INET) {
if(inet_ntop(AF_INET, &addr->sin_addr, addrBuf, INET_ADDRSTRLEN)) {
type = IP_ADDR_IPv4;
}
} else {
const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*)interface->ifa_addr;
if(inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, INET6_ADDRSTRLEN)) {
type = IP_ADDR_IPv6;
}
}
if(type) {
NSString *key = [NSString stringWithFormat:@"%@/%@", name, type];
addresses[key] = [NSString stringWithUTF8String:addrBuf];
}
}
}
// Free memory
freeifaddrs(interfaces);
}
return [addresses count] ? addresses : nil;
}
建立连接之后就按照协议组好 http 请求的包, 发送就可以了。。。
- (void)startSocket
{
asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
NSError *error = nil;
uint16_t port = WWW_PORT;
if (port == 0)
{
#if USE_SECURE_CONNECTION
port = 443; // HTTPS
#else
port = 80; // HTTP
#endif
}
NSString *interface = [self getDeviceIPAddress:YES];
//[asyncSocket connectToHost:WWW_HOST onPort:port error:&error]
if (![asyncSocket connectToHost:WWW_HOST onPort:port viaInterface:interface withTimeout:-1 error:&error])
{
DDLogError(@"Unable to connect to due to invalid configuration: %@", error);
}
else
{
DDLogVerbose(@"Connecting to \"%@\" on port %hu...", WWW_HOST, port);
}
#if USE_SECURE_CONNECTION
#if USE_CFSTREAM_FOR_TLS
{
// Use old-school CFStream style technique
NSDictionary *options = @{
GCDAsyncSocketUseCFStreamForTLS : @(YES),
GCDAsyncSocketSSLPeerName : CERT_HOST
};
DDLogVerbose(@"Requesting StartTLS with options:\n%@", options);
[asyncSocket startTLS:options];
}
#elif MANUALLY_EVALUATE_TRUST
{
// Use socket:didReceiveTrust:completionHandler: delegate method for manual trust evaluation
NSDictionary *options = @{
GCDAsyncSocketManuallyEvaluateTrust : @(YES),
GCDAsyncSocketSSLPeerName : CERT_HOST
};
DDLogVerbose(@"Requesting StartTLS with options:\n%@", options);
[asyncSocket startTLS:options];
}
#else
{
// Use default trust evaluation, and provide basic security parameters
NSDictionary *options = @{
GCDAsyncSocketSSLPeerName : CERT_HOST
};
DDLogVerbose(@"Requesting StartTLS with options:\n%@", options);
[asyncSocket startTLS:options];
}
#endif
#endif
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port
{
DDLogVerbose(@"socket:didConnectToHost:%@ port:%hu", host, port);
NSMutableData *requestData = [self sendData];
//发送数据
[asyncSocket writeData:requestData withTimeout:-1.0 tag:0];
#if READ_HEADER_LINE_BY_LINE
// Now we tell the socket to read the first line of the http response header.
// As per the http protocol, we know each header line is terminated with a CRLF (carriage return, line feed).
[asyncSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1.0 tag:0];
#else
[asyncSocket readDataWithTimeout:-1 tag:0];
#endif
}
- (NSMutableData *)sendData {
NSMutableData *packetData = [[NSMutableData alloc] init];
NSData *crlfData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];//回车换行是http协议中每个字段的分隔符
NSString *requestStrFrmt = @"POST /openapi/networkauth/preGetMobile.do HTTP/1.1\r\nHost: %@\r\n";
NSString *requestStr = [NSString stringWithFormat:requestStrFrmt, WWW_HOST];
DDLogVerbose(@"Sending HTTP Request:\n%@", requestStr);
[packetData appendData:[requestStr dataUsingEncoding:NSUTF8StringEncoding]];//拼接的请求行
[packetData appendData:[@"Content-Type: application/x-www-form-urlencoded; charset=utf-8" dataUsingEncoding:NSUTF8StringEncoding]];//发送数据的格式
[packetData appendData:crlfData];
//组包体
...
[packetData appendData:[[NSString stringWithFormat:@"Content-Length: %ld", bodyStr.length] dataUsingEncoding:NSUTF8StringEncoding]];//说明请求体内容的长度
[packetData appendData:crlfData];
[packetData appendData:[@"Connection: close" dataUsingEncoding:NSUTF8StringEncoding]];
[packetData appendData:crlfData];
[packetData appendData:crlfData];//注意这里请求头拼接完成要加两个回车换行
//以上http头信息就拼接完成,下面继续拼接上body信息
NSString *encodeBodyStr = [NSString stringWithFormat:@"%@\r\n\r\n", bodyStr];//请求体最后也要加上两个回车换行说明数据已经发送完毕
[packetData appendData:[encodeBodyStr dataUsingEncoding:NSUTF8StringEncoding]];
return packetData;
}
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
DDLogVerbose(@"socket:didWriteDataWithTag:");
}
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
DDLogVerbose(@"socket:didReadData:withTag:");
NSString *httpResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
#if READ_HEADER_LINE_BY_LINE
DDLogInfo(@"Line httpResponse: %@", httpResponse);
// As per the http protocol, we know the header is terminated with two CRLF's.
// In other words, an empty line.
if ([data length] == 2) // 2 bytes = CRLF
{
DDLogInfo(@"<done>");
}
else
{
// Read the next line of the header
[asyncSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1.0 tag:0];
}
#else
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//NSLog(@"%@", string);
NSString *testStr = @" HTTP/1.1 302 Found\r\n";
NSRange startCode = [testStr rangeOfString:@"HTTP/"];
NSRange endCode = [testStr rangeOfString:@"\r\n"];
if (endCode.location != NSNotFound && startCode.location != NSNotFound) {
NSString *sub = [testStr substringWithRange:NSMakeRange(startCode.location, endCode.location-startCode.location+1)];//这就是服务器返回的body体里的数据
NSMutableArray *subArr = [[sub componentsSeparatedByString:@" "] mutableCopy];
[subArr removeObject:@""];
if (subArr.count > 2) {
NSString *code = subArr[1];
NSLog(@"code === %@", code);
}
NSLog(@"code str === %@", sub);
}
NSRange start = [string rangeOfString:@"{"];
NSRange end = [string rangeOfString:@"}"];
NSString *sub;
if (end.location != NSNotFound && start.location != NSNotFound) {//如果返回的数据中不包含以上符号,会崩溃
sub = [string substringWithRange:NSMakeRange(start.location, end.location-start.location+1)];//这就是服务器返回的body体里的数据
NSData *subData = [sub dataUsingEncoding:NSUTF8StringEncoding];;
NSDictionary *subDic = [NSJSONSerialization JSONObjectWithData:subData options:0 error:nil];
NSLog(@"result === %@", subDic);
}
DDLogInfo(@"Full HTTP Response:\n%@", httpResponse);
#endif
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
// Since we requested HTTP/1.0, we expect the server to close the connection as soon as it has sent the response.
DDLogVerbose(@"socketDidDisconnect:withError: \"%@\"", err);
}