hf

导航

 

有时候需要在家里连接公司的服务器,但是它的外网IP会经常变换。当然可以用花生壳之类来解决,不过作为程序员,还是尝试用程序的方法解决吧。

接口定义

Windows里在外网IP改变时好像没有现成的事件,所以定义了这样一个接口IPublicIPMonitor:

public interface IPublicIPMonitor
{
    event EventHandler<PublicIpChangedEventArgs> PublicIpChanged;
    void Start();
    void Stop();
}

当监测到IP改变时会触发一个事件PublicIpChanged,有两个方法分别用来开始和停止监测。

事件参数PublicIpChangedEventArgs的定义如下,只有一个叫IP的属性:

[Serializable]
public class PublicIpChangedEventArgs : EventArgs
{
    public string IP { get; private set; }
    public PublicIpChangedEventArgs(string ip)
    {
        IP = ip;
    }
}

还有一个IPublicIPNotifyReceiver的接口用来对IP改变事件进行响应:

public interface IPublicIPNotifyReceiver
{
    void Listen(IPublicIPMonitor monitor);
    void OnPublicIPChanged(PublicIpChangedEventArgs arg);
}

它可以监听某个IPublicIPMonitor并在OnPublicIPChanged里响应。

IPublicIPMonitor实现


实现思路是定时去获取当前IP,如果和之前保存的IP不一样的话则触发PublicIpChanged事件:

public class PublicIPMonitor : IPublicIPMonitor
   { 
       System.Timers.Timer timer;
       string lastIp;
       static Regex rgx = new Regex("((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)", RegexOptions.Compiled);
   
       public event EventHandler<PublicIpChangedEventArgs> PublicIpChanged;
       protected virtual void OnPublicIpChanged(object o, PublicIpChangedEventArgs e)
       {
           if (PublicIpChanged != null) PublicIpChanged(o, e);
       }
       public PublicIPMonitor()
           : this(TimeSpan.FromMinutes(30))
       {
       }
       public PublicIPMonitor(TimeSpan span)
       {
           timer = new System.Timers.Timer(span.TotalMilliseconds);
           timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed); 
       }
       public void Start()
       {
           timer.Enabled = true;
           timer_Elapsed(null, null);
           timer.Start();
       }
       public void Stop()
       {
           timer.Stop();
       }
       void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
       {
           var ip = GetPublicIp(); 
           if (ip == lastIp) { return; }
           lastIp = ip;
           OnPublicIpChanged(this, new PublicIpChangedEventArgs(ip));
       }   
       public static string GetPublicIp()
       {
           try
           {
               string strUrl = "http://www.ip138.com/ip2city.asp"; 
               Uri uri = new Uri(strUrl);
               System.Net.WebRequest wr = System.Net.WebRequest.Create(uri); 
               System.IO.Stream s = wr.GetResponse().GetResponseStream();
               System.IO.StreamReader sr = new System.IO.StreamReader(s, Encoding.ASCII);
               string all = sr.ReadToEnd();  
               var match=rgx.Match(all);
               if(!match.Success)
                   return null;
               var  ip = match.Value;               
               return ip;
           }
           catch (System.Net.WebException)
           {
           }
           return null;
       }        
   }

IPublicIPNotifyReceiver实现

可以用短信、邮件等等需要的方式响应IP改变事件,这里用邮件做例子:

public class EmailNotifyReceiver : PublicIPNotifyReceiver
{ 
    static string receiveEmailAddress;
    public EmailNotifyReceiver() {
        receiveEmailAddress = File.ReadAllText(System.AppDomain.CurrentDomain.BaseDirectory + "\\EmailNotifyReceiverAddress.config");        
    }
    public override void OnPublicIPChanged(PublicIpChangedEventArgs e)
    {
        var tries = 3;
        var success = false;
        while (tries > 0 && !success)
        {
            try
            {
                Send("smtp.qq.com", "***@qq.com", "***",
                       "***@qq.com", receiveEmailAddress, "ip", e.IP);
                success = true;
            }
            catch
            {
                System.Threading.Thread.Sleep(1000 * 60 * 5); 
            } 
        }
    }
 
    void Send(string smtpHost, string user, string password, string mailFrom, string mailTo, string subject, string body)
    {
        MailAddress from = new MailAddress(mailFrom); //邮件的发件人
        //MailAddress to = new MailAddress(mailTo); //邮件的收件人
 
        MailMessage mail = new MailMessage();
 
        //设置邮件的标题
        mail.Subject = subject;
 
        //设置邮件的发件人
        //Pass:如果不想显示自己的邮箱地址,这里可以填符合mail格式的任意名称,真正发mail的用户不在这里设定,这个仅仅只做显示用
        mail.From = from;
 
        //设置邮件的收件人 
        mail.To.Add(mailTo);
 
        //设置邮件的内容
        mail.Body = body;
        //设置邮件的格式
        mail.BodyEncoding = System.Text.Encoding.UTF8;
        mail.IsBodyHtml = true;
        //设置邮件的发送级别
        mail.Priority = MailPriority.Normal;
 
        mail.DeliveryNotificationOptions = DeliveryNotificationOptions.OnSuccess;
 
        SmtpClient client = new SmtpClient();
        //设置用于 SMTP 事务的主机的名称,填IP地址也可以了
        client.Host = smtpHost;
        //设置用于 SMTP 事务的端口,默认的是 25
        //client.Port = 465;
        //client.EnableSsl = true;
        client.UseDefaultCredentials = false;
        //这里才是真正的邮箱登陆名和密码,比如我的邮箱地址是 hbgx@hotmail, 我的用户名为 hbgx ,我的密码是 xgbh
        client.Credentials = new System.Net.NetworkCredential(user, password);
        client.DeliveryMethod = SmtpDeliveryMethod.Network;
        //都定义完了,正式发送了,很是简单吧!
        //client.Timeout = 30;
        client.Send(mail);
    }
}

在EmailNotifyReceiver的构造函数里,程序从EmailNotifyReceiverAddress.config里读取需要接收通知的邮件地址(用,隔开)。然后在OnPublicIPChanged里尝试给他们发邮件。

程序入口

现在已经可以实现我们需要的功能了,我们可以把IPublicIPMonitor的实现放在一个Windows服务里,然后在其OnStart方法里添加下面代码:

protected override void OnStart(string[] args)
{
    var monitor = new PublicIPMonitor();
    var receiverList = ImplementsLoader.LoadImplementsOf<IPublicIPNotifyReceiver>();
    foreach (var receiver in receiverList)
    {
         receiver.Listen(monitor);
    }
    monitor.Start();
}

ImplementsLoader类

在上面的代码里有这样一个方法ImplementsLoader.LoadImplementsOf<IPublicIPNotifyReceiver>()。它的作用是获取程序所在目录所有的IPublicIPNotifyReceiver的实现。这样当我们添加一个新的接收者的时候不需要改现有代码,只要实现IPublicIPNotifyReceiver接口,然后把这个dll复制到程序目录下就可以了。

public class ImplementsLoader
{
    public static T[] LoadImplementsOf<T>() where T : class
    {
        var files = System.IO.Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory, "*.dll");
        var list = new List<T>();
        foreach (var f in files)
        {
            list.AddRange(LoadFromAssembly<T>(Assembly.LoadFile(f)));
        }
        return list.ToArray();
    }
    static T[] LoadFromAssembly<T>(Assembly ass) where T : class
    {
        return ass.GetTypes().Where(t => !t.IsInterface && !t.IsAbstract && typeof(T).IsAssignableFrom(t)).Select(t => Activator.CreateInstance(t) as T).ToArray();
 
    }
}

一些改进想法

在程序入口里创建IPublicIPMonitor实例的时候,可以用ioc或者读取配置文件等方式。还有EmailNotifyReceiver在代码里写发送者的邮箱密码也不太好,可以放到配置文件里并做加密。。。

posted on 2012-03-30 17:48  hf  阅读(2720)  评论(0)    收藏  举报