Android NDK层发起 HTTP 请求的问题及解决
事件的起因不说了,总之是需要实现一个 NDK 层的网络请求。为了多端适用,还是选择了 CodeTyphon 作为跨平台方案。关于 CodeTyphon 此处不述,感兴趣的可以直接去其官网查看(传送门)。
CodeTyphon 自带的 fcl-web
库可以直接完成对于 HTTP 请求的支持,虽然我很想这么说... 在实际使用中,的确可以通过引入 fcl-web
来完成跨平台的网络请求,然而在 Android 端实际测试时,却发生了奇怪的错误。
比如说请求我自己的服务器 www.rarnu.com
,会发生以下错误:
Error resolving host www.rarnu.com (-1)
而当我换用 IP 地址来请求时,却是可以成功的。
输入的域名是实际存在的,可以排除掉域名本身的问题。而使用 adb shell
连入设备,并使用 ping
命令访问该域名,也是正常的。
那么问题可能就出在,找不到 nameserver
。我们都知道,在 Linux 下,nameserver
由 resolv.conf
决定,这个文件通常保存在 /etc
下。于是看了一下,Android 里并没有这个文件,应该就是这个原因引起的了,因为读不到 resolv.conf
所以才导致了无法解释域名。接下来就是去找 Android 下,原本该是 resolv.conf
的东西保存在哪里。
不卖关子了,其实 Android 很早就把 resolv.conf 的内容改成了 key-value 的形式,采用 SystemProperties
进行存储,而其关键的 key 是 net.dns1
和 net.dns2
。
尝试使用 adb 连接手机,并对以上两个 key 进行取值:
$ adb shell
$ getprop net.dns1
$ 208.67.222.222
$ getprop net.dns2
$ 208.67.220.220
我的手机上取出来的是 OpenDNS 的值,自己设置过。好了,既然已经知道了 nameserver
的所在,接下去就是修改代码以使程序识别和加载。
在 CodeTyphon 中,有一个基础库文件叫 netdb.pp
,其中包含了 resolveName
方法,其具体代码如下:
function resolveName(hostName : String; var addresses : array of THostAddr) : Integer;
var
i : Integer;
begin
checkResolveFile;
i := 0;
result := 0;
while (result <= 0) and (i <= high(DNSServers)) do begin
result:=resolveNameAt(i, hostName, addresses,0);
Inc(i);
end;
end;
其实这段代码很明确,关键变量是 DNSServers
,打印一下看看是个什么值:
writeLn(Format('DNSServer => %d', [high(DNSServers)]));
程序执行后打出来 -1
,也就是说在 Android 下,由于 DNSServers
变量中没有任何的数据,导致了完全无法解析域名,在其他平台下,在此处打日志均显示 0
,表示在这个数组里有一个下标为 0 的数据。
那事情就变得简单了,我们可以直接去找加载了 DNSServers
的地方,很容易的,找到了 InitResolver
函数,由于该函数比较长,此处只截取加载 DNSServer
的部分:
procedure InitResolver;
begin
... ...
if fileExists(etcPath + resolveFile) then
GetDNsservers(etcPath + resolveFile);
... ...
end;
没有比这更明确的了,就是去找有没有 /etc/resolv.conf
嘛,找到就加载,没找到那就啥都不做了,而刚才说过了 Android 端并没有这么一个文件,于是直接就导致了 nameserver
缺失,间接引起域名无法解析。
好了,那么简易的解决方案也就有了,只需要重建 GetDNsservers
函数,使其能够适应 Android 端的情况即可。
下面给出代码:
function GetDNSServerAndroid(): Integer;
var
L: string;
H : THostAddr;
E : THostEntry;
function CheckDirective(Dir : String) : Boolean;
var
p : Integer;
begin
p := pos(Dir, L);
result := p <> 0;
If result then begin
delete(L, 1, P + length(Dir));
L := trim(L);
end;
end;
begin
result := 0;
L := 'nameserver ' + GetNetDNS();
if StripComment(L) then begin
If CheckDirective('nameserver') then begin
H := HostToNet(StrToHostAddr(L));
If (H.s_bytes[1] <> 0) then begin
setlength(DNSServers, result + 1);
DNSServers[result]:=H;
Inc(result);
end else if FindHostEntryInHostsFile(L, H, E) then begin
setlength(DNSServers, result + 1);
DNSServers[result]:=E.Addr;
Inc(result);
end;
end;
end;
end;
里面还有一个关键代码,是 GetNetDNS
,它用于从 Android 内读取 net.dns1
变量:
function GetNetDNS(): String;
var
outstr: string;
begin
result := '';
if (runCommand('getprop', ['net.dns1'], outstr, [poUsePipes, poWaitOnExit])) then
result := outstr.Trim;
end;
最后,把上面的 InitResolver
改一下,使其可以正常加载工作于 Android 端的这段代码:
procedure InitResolver;
begin
... ...
if fileExists(etcPath + resolveFile) then
GetDNsservers(etcPath + resolveFile);
{$IFDEF ANDROID}
GetDNSServerAndroid();
{$ENDIF}
... ...
end;
编译运行程序,Error resolving host
的问题即得到了解决。