很多朋友的公司或家里有一台上网的机器,这些上网的机器有些能够获得公网IP,但是这些IP通常不固定。
大家都想充分利用这些上网设备的网络能力来搭建服务器环境,但由于IP地址老是变化,因此,即使是给这些机器分配了域名,也时常无法访问。于是,很多人想到了动态域名解析,即域名不变,IP地址变化,域名解析记录能够跟随IP地址变化,目前市场上有几种商业的解析方案实现,例如花生壳,更多的就不举例了,避免给他们做免费广告。这些都要收费,而且可能要通过CNAME(将您的域名解析成别人的域名)方式来解决,解析效率略有降低。
好在阿里云的开放精神,他们将域名解析的接口提供了给大家,经过笔者测试非常好用。
本文将实现自己的免费动态域名解析实现分享出来,实现思路如下:
1)首先有一台公网的固定域名的服务器,运行一个助手程序(getipd),来帮助获得动态IP主机的当前IP地址(一般几天变化一次);
2)在动态IP的机器上(或者跨越该路由器的内部网络主机)运行PHP编写的客户端,PHP编写的客户端定期与公网那个运行getipd的服务器通信,一般10秒一次,获得自己的公网IP;
3)客户端程序判断,如果自己的公网IP发生变化,则调用阿里云的接口来更改域名(A记录),阿里云的DNS动态解析真的非常快,一般是实时生效的。
一、PHP客户端的实现所有源代码如下(完整实现 dnsupdater.php,不依赖任何第三方库):
<?php
date_default_timezone_set('UTC');
set_time_limit(0);
$iphelper_addr = 'www.iavcast.com';
$iphelper_port = '8198';
$AccessKeyId = 'your AccessKeyId';
$AccessKeySecret = 'your AccessKeySecret';
$domain_list = Array('video.yourdomain.com','www.yourdomain.com','video.yourdomain.net','file.yourdomain.net');//
$show_log = False;
$old_gateway_ip = '';
$options = getopt("d:h:");
if (count($options) > 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控制台窗口的助手程序,官方网址是:
GitHub - wenshui2008/RunHiddenConsole: Hide console window for windows programs
dnsupdater.php 代码完全可以运行在linux上,在linux系统的shell里输入:
php dnsupdater.php &
可以作为守护进程长期运行。
二、getipd服务器端程序,这是一个用于帮助获取公网IP地址的极其简单的TCP服务器,笔者用C语言与PHP分别实现了一份,用PHP实现的 getip.php(仅仅一个文件)如下,请用PHP解释器执行:
<?php
date_default_timezone_set('UTC');
$port = 8198;
$sock_srv = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sock_srv, SOL_SOCKET, SO_REUSEADDR, 1);
if (!socket_bind($sock_srv, 0, $port)) {
die('Bind Port '.$port ." Failed\n");
}
if (!socket_listen($sock_srv,5)) {
die('Socket listen Failed On Port '.$port ."\n");
}
$clients = array($sock_srv);
echo "IPHelper Service running on port ".$port ." ...\n";
while(true) {
$readSet = $clients;
$writeSet = null;
$expectSet = null;
$number = socket_select($readSet, $writeSet, $expectSet, 10);
if($number === False) {
echo "Select error:".socket_strerror(socket_last_error()) ."\n";
continue;
}
else if($number === 0) {
//echo "Socket select nothing\n";
continue;
}
//echo count($readSet).':'.count($writeSet).':'.count($expectSet)."\n";
if(in_array($sock_srv, $readSet)) {
$clients[] = $newsock = socket_accept($sock_srv);
if (! @socket_getpeername($newsock, $ip) ) {
echo "socket_getpeername Failed\n";
$ip = '0.0.0.0';
}
echo "New client $ip arrived!\n";
$key = array_search($sock_srv, $readSet);
unset($readSet[$key]);
}
foreach ($readSet as $read_sock) {
$data = @socket_read($read_sock, 1024);
if($data === false || empty($data) ) {
$key = array_search($read_sock, $clients);
if (! @socket_getpeername($clients[$key], $ip)) {
echo "socket_getpeername Failed\n";
$ip = '0.0.0.0';
}
socket_close($read_sock);
unset($clients[$key]);
echo "client $ip disconnected\n";
continue;
}
if (! @socket_getpeername($read_sock, $clientIp)) {
//Never arrive here!
$clientIp = '0.0.0.0';
}
$response = Array();
$response['ip'] = $clientIp;
$data = trim($data);
if(!empty($data)) {
echo 'Client:'.$clientIp ." login-data: ". $data."\n";
}
else {
echo "nothing to read\n";
}
$responseS = json_encode($response,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
socket_write($read_sock, $responseS);
//wait for reponse sending
Sleep(1);
$key = array_search($read_sock, $clients);
socket_close($read_sock);
unset($clients[$key]);
}
}
socket_close($sock_srv);
在linux里输入 php getip.php & 即可执行。
用C语言实现的getip.c代码如下,需要编译成可执行程序:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#ifdef WIN32
#include <WinSock2.h>
#include <WS2tcpip.h>
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#define closesocket(s) close(s)
#endif
#define MAX_WAIT_TIMEOUT 10 // seconds
#define MYPORT 8198 // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
#define BUF_SIZE 512
int fd_A[BACKLOG]; // accepted connection fd
time_t fd_A_time[BACKLOG]; // accepted connection fd time
int conn_amount = 0; // current connection amount
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];
char g_log_file[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);
memcpy(g_log_file, g_app_dir, g_app_dir_len);
strcpy(g_log_file + g_app_dir_len, "getipd.log");
}
#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<NOFILE;i++)
close(i);
chdir(g_cur_exe_path);
umask(0);
return;
}
#endif
#ifndef WIN32
#define DAEMON_HINT "\t-d Running as a daemon process.\n"
#else
#define DAEMON_HINT ""
#endif
void Usage()
{
const char *progname = "getipd";
const char *debug = "";
#ifdef WIN32
debug = "-debug ";
#endif
fprintf(stderr,"Usage:\n%s %s-r <directory> -p <port> -l <logfile> -t <TemplateFileDir> -c <ConfigurationFile> \n"
"Parameters:\n"
"\t-p tcp port,default: [%s]\n" DAEMON_HINT,
progname,debug,MYPORT
);
exit(1);
}
int b_daemon = 0;
int b_listClients = 0;
void showClients()
{
int i;
if (b_daemon || !b_listClients)
return;
printf("client amount: %d\n", conn_amount);
for (i = 0; i < BACKLOG; i++) {
printf("[%d]:%d ", i, fd_A[i]);
}
printf("\n\n");
}
void saveOverflowLog(const char * clientIp)
{
FILE * fp;
fp = fopen(g_log_file, "wb");
if (fp) {
time_t t = time(NULL);
struct tm * local = localtime(&t);
char buf[1024];
int len;
len = sprintf(buf, "[%d-%d-%d %d:%d:%d] client IP:%s\n", local->tm_year+1900, local->tm_mon+1, local->tm_mday,
local->tm_hour, local->tm_min, local->tm_sec,
clientIp);
fwrite(buf,1, len, fp);
fclose(fp);
}
}
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;
char buf[BUF_SIZE];
char ipAddr[128];
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],"-l")) {
b_listClients = 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);
}
if (!b_daemon) {
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 = MAX_WAIT_TIMEOUT;
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) {
if (!b_daemon) {
printf("select %d sockets timeout\n", conn_amount);
}
if (conn_amount == 0)
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
if (!b_daemon) {
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;
if (ret < BUF_SIZE)
memset(&buf[ret], '\0', 1);
if (!b_daemon) {
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) {
if (client_addr.sin_family == AF_INET) {
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;
}
}
else {
printf("getpeername failed: %d\n", ret2);
}
}
}
}
//check client timeout
if (conn_amount > 0) {
time_t now = time(NULL);
for (i = 0; i < conn_amount; i++) {
if (fd_A[i]) {
time_t time_out = now - fd_A_time[i];
if (time_out > MAX_WAIT_TIMEOUT) {
if (!b_daemon) {
struct tm * local = localtime(&fd_A_time[i]);
char time_buf[64];
int len;
len = sprintf(time_buf, "%d-%d-%d %d:%d:%d", local->tm_year+1900, local->tm_mon+1, local->tm_mday,
local->tm_hour, local->tm_min, local->tm_sec);
sin_size = sizeof(client_addr);
getpeername(fd_A[i],(struct sockaddr *)&client_addr, &sin_size);
inet_ntop(AF_INET, &client_addr.sin_addr, ipAddr, sizeof(ipAddr));
printf("client[%d]:%d %s from: %s timeout:%d seconds\n", i, fd_A[i], ipAddr, time_buf, (int) time_out);
}
send(fd_A[i], "timeout", 7, 0);
closesocket(fd_A[i]);
fd_A[i] = 0;
remove_count++;
}
}
}
}
//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];
fd_A_time[j] = fd_A_time[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)) {
char ipAddr[128];
const char * sIpAddr;
new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
if (new_fd <= 0) {
perror("accept");
continue;
}
sIpAddr = inet_ntop(AF_INET, &client_addr.sin_addr, ipAddr, sizeof(ipAddr));
// add to fd queue
if (conn_amount < BACKLOG) {
fd_A_time[conn_amount] = time(NULL);
fd_A[conn_amount++] = new_fd;
if (!b_daemon) {
printf("new connection client[%d] %s:%d\n", conn_amount, sIpAddr, ntohs(client_addr.sin_port));
}
if (new_fd > maxsock)
maxsock = new_fd;
}
else {
saveOverflowLog(sIpAddr);
if (!b_daemon) {
printf("max connections arrived, close the client\n");
}
send(new_fd, "bye", 3, 0);
closesocket(new_fd);
}
}
showClients();
}
// 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端口,请注意修改防火墙的规则,打开此端口。
至此,一个属于自己的高效的动态域名解析系统就完成了。
所有代码可以在
下载。