很多朋友的公司或家里有一台上网的机器,这些上网的机器有些能够获得公网IP,但是这些IP通常不固定。
大家都想充分利用这些上网设备的网络能力来搭建服务器环境,但由于IP地址老是变化,因此,即使是给这些机器分配了域名,也时常无法访问。于是,很多人想到了动态域名解析,即域名不变,IP地址变化,域名解析记录能够跟随IP地址变化,目前市场上有几种商业的解析方案实现,例如花生壳,更多的就不举例了,避免给他们做免费广告。这些都要收费,而且可能要通过CNAME(将您的域名解析成别人的域名)方式来解决,解析效率略有降低。
好在阿里云的开放精神,他们将域名解析的接口提供了给大家,经过笔者测试非常好用。
本文将实现自己的免费动态域名解析实现分享出来,实现思路如下:
1)首先有一台公网的固定域名的服务器,运行一个助手程序(getipd),来帮助获得动态IP主机的当前IP地址(一般几天变化一次);
2)在动态IP的机器上(或者跨越该路由器的内部网络主机)运行PHP编写的客户端,PHP编写的客户端定期与公网那个运行getipd的服务器通信,一般10秒一次,获得自己的公网IP;
3)客户端程序判断,如果自己的公网IP发生变化,则调用阿里云的接口来更改域名(A记录),阿里云的DNS动态解析真的非常快,一般是实时生效的。
一、PHP客户端的实现所有源代码如下(完整实现 dnsupdater.php,不依赖任何第三方库):
0) {
if (!empty($options['d']))
$show_log = True;
}
if (!function_exists('random_int')) {
//php 5.x compatible
function random_int($min,$max) {
return mt_rand($min,$max);
}
}
/**
* Class AlicloudDNSUpdater
*/
class AlicloudDNSUpdater {
/**
* @var string
*/
public $domainName;
/**
* @var string
*/
public $rR;
/**
* @var string
*/
public $type;
/**
* @var string
*/
public $value;
/**
* @var string
*/
public $accessKeyId;
/**
* @var string
*/
public $accessKeySecret;
/**
* AlicloudUpdateRecord constructor.
*
* @param string $accessKeyId
* @param string $accessKeySecret
*/
function __construct(
$accessKeyId,
$accessKeySecret
) {
$this->accessKeyId = $accessKeyId;
$this->accessKeySecret = $accessKeySecret;
}
/**
* @param string $CanonicalQueryString
* @return string
*/
public function getSignature($CanonicalQueryString)
{
$HTTPMethod = 'GET';
$slash = urlencode('/');
$EncodedCanonicalQueryString = urlencode($CanonicalQueryString);
$StringToSign = "{$HTTPMethod}&{$slash}&{$EncodedCanonicalQueryString}";
$StringToSign = str_replace('%40', '%2540', $StringToSign);
$HMAC = hash_hmac('sha1', $StringToSign, "{$this->accessKeySecret}&", true);
return base64_encode($HMAC);
}
/**
* @return string
*/
public function getDate()
{
$timestamp = date('U');
$date = date('Y-m-d', $timestamp);
$H = date('H', $timestamp);
$i = date('i', $timestamp);
$s = date('s', $timestamp);
return "{$date}T{$H}%3A{$i}%3A{$s}";
}
/**
* @return string
* @throws Exception
*/
public function getRecordId()
{
$queries = [
'AccessKeyId' => $this->accessKeyId,
'Action' => 'DescribeDomainRecords',
'DomainName' => $this->domainName,
'Format' => 'json',
'SignatureMethod' => 'HMAC-SHA1',
'SignatureNonce' => random_int(1000000000, 9999999999),
'SignatureVersion' => '1.0',
'Timestamp' => $this->getDate(),
'Version' => '2015-01-09'
];
$response = $this->doRequest($queries);
if (!isset($response['DomainRecords'])) {
return '';
}
$recordList = $response['DomainRecords']['Record'];
$RR = null;
foreach ($recordList as $key => $record) {
if ($this->rR === $record['RR']) {
$RR = $record;
}
}
if ($RR === null) {
//die('RR ' . $this->rR . ' not found.');
return '';
}
return $RR['RecordId'];
}
/**
* @param string $domainName
*/
public function setDomainName($domainName)
{
$this->domainName = $domainName;
}
/**
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* @param string $rR
*/
public function setRR($rR)
{
$this->rR = $rR;
}
/**
* @param string $recordId
*/
public function setRecordId($recordId)
{
$this->recordId = $recordId;
}
/**
* @param string $type
*/
public function setRecordType($type)
{
$this->type = $type;
}
/**
* @param array $queries
* @return array
*/
public function doRequest($queries)
{
$CanonicalQueryString = '';
$i = 0;
foreach ($queries as $param => $query) {
$CanonicalQueryString .= $i === 0 ? null : '&';
$CanonicalQueryString .= "{$param}={$query}";
$i++;
}
$signature = $this->getSignature($CanonicalQueryString);
$requestUrl = "http://dns.aliyuncs.com/?{$CanonicalQueryString}&Signature=" . urlencode($signature);
$response = file_get_contents($requestUrl, false, stream_context_create([
'http' => [
'ignore_errors' => true
]
]));
return json_decode($response, true);
}
/**
* @return array
* @throws \Exception
*/
public function sendRequest()
{
$RecordId = $this->getRecordId();
if (empty($RecordId)) {
return Array(
'Code'=>'Error',
'Message'=>$this->domainName .' record not found'
);
}
$queries = [
'AccessKeyId' => $this->accessKeyId,
'Action' => 'UpdateDomainRecord',
'Format' => 'json',
'RR' => $this->rR,
'RecordId' => $RecordId,
'SignatureMethod' => 'HMAC-SHA1',
'SignatureNonce' => random_int(1000000000, 9999999999),
'SignatureVersion' => '1.0',
'Timestamp' => $this->getDate(),
'Type' => $this->type,
'Value' => $this->value,
'Version' => '2015-01-09'
];
return $this->doRequest($queries);
}
public function sendAddRequest()
{
$queries = [
'AccessKeyId' => $this->accessKeyId,
'Action' => 'AddDomainRecord',
'Format' => 'json',
'RR' => $this->rR,
'Type' => $this->type,
'Value' => $this->value,
'DomainName' => $this->domainName,
'SignatureMethod' => 'HMAC-SHA1',
'SignatureNonce' => random_int(1000000000, 9999999999),
'SignatureVersion' => '1.0',
'Timestamp' => $this->getDate(),
'Version' => '2015-01-09'
];
return $this->doRequest($queries);
}
}
while(true) {
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$result = @socket_connect($client, $iphelper_addr, $iphelper_port);
if (!$result) {
if ($show_log) {
echo "socket_connect() failed: reason: " . socket_strerror(socket_last_error($client)) . "\n";
}
socket_close($client);
}
else {
$login_info = Array(
'server'=>'server 1',
'time'=>time()
);
socket_write($client, json_encode($login_info));
$response = @socket_read($client, 1024);
if ($response !== False && !empty($response) ) {
$info = json_decode($response,True);
if (is_array($info) && isset($info['ip'])) {
if ($old_gateway_ip != $info['ip']) {
if ($show_log) {
echo date('Y-m-d H:i:s'). " do refresh dns ip :".$info['ip']."\n";
}
$updater = new AlicloudDNSUpdater($AccessKeyId, $AccessKeySecret);
foreach($domain_list as $domain) {
$dotpos = strpos($domain,'.');
if ($dotpos !== False) {
$recoreName = substr($domain,0,$dotpos);
$domainName = substr($domain,$dotpos + 1);
if ($show_log) {
echo 'Update DNS Record:'.$recoreName.'.'.$domainName .' -> '. $info['ip'] ."...\n";
}
$updater->setDomainName($domainName);
$updater->setRecordType('A');
$updater->setRR($recoreName);
$updater->setValue($info['ip']);
$result = $updater->sendRequest();
if ($show_log) {
print_r($result);
echo "\n";
}
}
}
$old_gateway_ip = $info['ip'];
}
else {
if ($show_log) {
echo "IP:{$old_gateway_ip} keep!\n";
}
}
}
}
socket_close($client);
}
Sleep(10);
}
其中:$AccessKeyId ,$AccessKeySecret 是阿里云分配给你的,只要您能够登录阿里云的控制台即可获取。获取位置如下:
公网IP地址获取服务的主机地址 $iphelper_addr 可以修改,为了能够快速测试,可以暂时用 www.iavcast.com 网站提供的,请仅作为临时测试使用,正式使用时请搭建自己的服务器端。
$domain_list 为需要刷新的IP地址列表,请先在阿里云的控制台的域名解析操作页面添加初始化解析记录,例如www.domain.com,live.domain.com 等,添加解析记录时的IP地址可以是任何值,以后dnsupdater.php会修改这个值的。
将dnsupdater.php 下载下来,并设置好必要的$AccessKeyId ,$AccessKeySecret 变量,假设PHP解释器安装在C:\PHP7\php.exe,运行 如下命令即可:
C:\PHP7\php.exe dnsupdater.php
请用php5.6以上运行本客户端。PHP需要开启sockets扩展,即去掉php.ini里的如下行的注释(去掉分号)
extension=php_sockets.dll
如果想让dnsupdater.php 在后台运行,请用RunHiddenConsole.exe,这是一个用于隐藏Windows控制台窗口的助手程序,官方网址是:
https://github.com/wenshui2008/RunHiddenConsole
dnsupdater.php 代码完全可以运行在linux上,在linux系统的shell里输入:
php dnsupdater.php &
可以作为守护进程长期运行。
二、getipd服务器端程序,这是一个用于帮助获取公网IP地址的极其简单的TCP服务器,笔者用C语言与PHP分别实现了一份,用PHP实现的 getip.php(仅仅一个文件)如下,请用PHP解释器执行:
在linux里输入 php getip.php & 即可执行。
用C语言实现的getip.c代码如下,需要编译成可执行程序:
#include
#include
#include
#include
#include
#ifdef WIN32
#include
#include
#else
#include
#include
#include
#include
#define closesocket(s) close(s)
#endif
#define MYPORT 8198 // the port users will be connecting to
#define BACKLOG 5 // how many pending connections queue will hold
#define BUF_SIZE 512
int fd_A[BACKLOG]; // accepted connection fd
int conn_amount = 0; // current connection amount
void showclient()
{
int i;
printf("client amount: %d\n", conn_amount);
for (i = 0; i < BACKLOG; i++) {
printf("[%d]:%d ", i, fd_A[i]);
}
printf("\n\n");
}
const char * g_app_dir = NULL;
const char * g_exe_name = "getipd";
volatile long g_b_exit_server = 0;
int g_web_root_len = 4;
int g_app_dir_len = 0;
#ifdef _WIN32
#define PATH_DEL '\\'
#define PTHREAD_INITIALIZED {0,0}
#else
#define PATH_DEL '/'
#define PTHREAD_INITIALIZED 0
#endif
#undef MAX_PATH
#ifndef MAX_PATH
#define MAX_PATH 1024
#endif
char g_cur_exe_path[MAX_PATH];
void getExePath()
{
char * pch;
#ifdef _WIN32
GetModuleFileNameA(NULL,g_cur_exe_path,ARRAYSIZE(g_cur_exe_path));
pch = strrchr(g_cur_exe_path,'\\');
pch ++ ;
*pch = '\0';
#else
int cnt = readlink("/proc/self/exe", g_cur_exe_path, MAX_PATH);
if (cnt < 0 || cnt >= MAX_PATH) {
strcpy(g_cur_exe_path,"/usr/local/");
}
pch = strrchr(g_cur_exe_path,'/');
if (pch) {
pch ++;
*pch = 0;
}
#endif
g_app_dir = g_cur_exe_path;
g_app_dir_len = strlen(g_app_dir);
}
#ifdef _WIN32
void init_daemon() {};
#else
#ifndef NOFILE
#define NOFILE 3
#endif
void init_daemon()
{
int pid;
int i;
pid=fork();
if(pid<0)
exit(1);
else if(pid>0)
exit(0);
setsid();
pid=fork();
if(pid>0)
exit(0);
else if(pid<0)
exit(1);
for(i=0;i -p -l -t -c \n"
"Parameters:\n"
"\t-p tcp port,default: [%s]\n" DAEMON_HINT,
progname,debug,MYPORT
);
exit(1);
}
int main(int argc,char * argv[])
{
int sock_fd, new_fd; // listen on sock_fd, new connection on new_fd
struct sockaddr_in server_addr; // server address information
struct sockaddr_in client_addr; // connector's address information
socklen_t sin_size;
int yes = 1,b_daemon = 0;
char buf[BUF_SIZE];
int ret;
int i;
fd_set fdsr;
int maxsock,remove_count;
struct timeval tv;
unsigned short port = MYPORT;
#ifdef WIN32
WSADATA wsaData;
WORD wVersionRequested;
wVersionRequested =MAKEWORD( 1, 1 );
ret = WSAStartup( wVersionRequested, &wsaData );
if ( ret != 0 ) {
/* Tell the user that we couldn't find a useable */
/* winsock.dll. */
exit(1);
}
#endif
getExePath();
g_exe_name = strrchr(argv[0],PATH_DEL);
if (g_exe_name) {
g_exe_name ++;
}
else {
g_exe_name = argv[0];
}
/* Parse command line arguments */
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-p") == 0) {
port = atoi(argv[++i]);
}
else if (!strcmp(argv[i],"-d")) {
b_daemon = 1;
break;
}
else if (!strcmp(argv[i],"-?") || !strcmp(argv[i],"-h")) {
Usage();
break;
}
}
if (b_daemon) {
init_daemon();
}
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
server_addr.sin_family = AF_INET; // host byte order
server_addr.sin_port = htons(MYPORT); // short, network byte order
server_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP
memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));
if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(1);
}
if (listen(sock_fd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
printf("listen port %d\n", MYPORT);
conn_amount = 0;
sin_size = sizeof(client_addr);
maxsock = sock_fd;
memset(fd_A,0,sizeof (fd_A));
while (1) {
// timeout setting
tv.tv_sec = 30;
tv.tv_usec = 0;
// initialize file descriptor set
FD_ZERO(&fdsr);
FD_SET(sock_fd, &fdsr);
// add active connection to fd set
for (i = 0; i < BACKLOG; i++) {
if (fd_A[i] != 0) {
FD_SET(fd_A[i], &fdsr);
}
}
ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);
if (ret < 0) {
perror("select");
break;
} else if (ret == 0) {
printf("timeout\n");
continue;
}
remove_count = 0;
// check every fd in the set
for (i = 0; i < conn_amount; i++) {
if (FD_ISSET(fd_A[i], &fdsr)) {
ret = recv(fd_A[i], buf, sizeof(buf), 0);
if (ret <= 0) { // client close
printf("client[%d] close\n", i);
closesocket(fd_A[i]);
FD_CLR(fd_A[i], &fdsr);
fd_A[i] = 0;
remove_count ++;
} else { // receive data
int ret2;
char ipAddr[128];
if (ret < BUF_SIZE)
memset(&buf[ret], '\0', 1);
printf("client[%d] send:%s\n", i, buf);
sin_size = sizeof(client_addr);
ret2 = getpeername(fd_A[i],(struct sockaddr *)&client_addr, &sin_size);
if (ret2 == 0) {
int len = sprintf(buf,"{\"ip\":\"%s\"}", inet_ntop(AF_INET, &client_addr.sin_addr, ipAddr, sizeof(ipAddr)));
send(fd_A[i], buf, len, 0);
remove_count ++;
closesocket(fd_A[i]);
FD_CLR(fd_A[i], &fdsr);
fd_A[i] = 0;
}
}
}
}
//resort the socket
if (remove_count > 0) {
int j=0;
for (i = 0; i < conn_amount; i++) {
if (fd_A[i]) {
fd_A[j] = fd_A[i];
j++;
}
}
for (i = j; i < conn_amount; i++) {
fd_A[i] = 0;
}
conn_amount -= remove_count;
}
// check whether a new connection comes
if (FD_ISSET(sock_fd, &fdsr)) {
new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
if (new_fd <= 0) {
perror("accept");
continue;
}
// add to fd queue
if (conn_amount < BACKLOG) {
char ipAddr[128];
fd_A[conn_amount++] = new_fd;
printf("new connection client[%d] %s:%d\n", conn_amount,
inet_ntop(AF_INET, &client_addr.sin_addr, ipAddr, sizeof(ipAddr)), ntohs(client_addr.sin_port));
if (new_fd > maxsock)
maxsock = new_fd;
}
else {
printf("max connections arrived, exit\n");
send(new_fd, "bye", 3, 0);
closesocket(new_fd);
break;
}
}
showclient();
}
// close other connections
for (i = 0; i < conn_amount; i++) {
if (fd_A[i] != 0) {
closesocket(fd_A[i]);
}
}
#ifdef WIN32
WSACleanup();
#endif
exit(0);
}
getip.c 如果要在Windows下编译请用VC新建一个简单项目,添加此文件即可;
在linux下编译请用:
gcc -O2 -o getipd getip.c
在linux下通过以上命令编译后,输入 ./getipd -d 即以守护进程的方式运行。getipd 用到了8198端口,请注意修改防火墙的规则,打开此端口。
至此,一个属于自己的高效的动态域名解析系统就完成了。
所有代码可以在
https://github.com/wenshui2008/dnsupdater
下载。