您的位置:首页 > 代理IP资讯
发布时间:2020-03-03 16:49:35
电脑改ip地址、动态IP环境下的SQL同步解决方案

  SQL Server数据库复制是通过发布/订阅的机制进行多台服务器之间的数据同步,把它用于数据库的同步备份或异地两个服务器之间保持数据完全一致的需要。这里的同步备份指的是备份服务器与主服务器进行实时数据同步,正常情况下只使用主数据库服务器,备份服务器只在主服务器出现故障时投入使用。


  2问题提出和解决方案


  通常,SQL Server的同步复制功能要求两台数据库服务器都必须通过对方的计算机名称(或者说是SQL服务器的数据库实例名称)来访问对方,如果是在同一个局域网的同一个网段内,比如两者都是192.168.1.X网段的,那么通过计算机名称来实现互相访问是没有问题的。但若是跨越不同网段甚至是跨越互联网来访问,则必须通过IP地址来互访。这个时候,SQL Server提供了一个别名(Alias)机制,即设置一个别名,把对方的计算机名称作为别名,映射到对方的IP地址和端口。这样就实现了跨网段的互访,从而满足SQL Server同步复制功能的要求。不过、这里又提出了一个问题,即要想持续实现互访,则必须保证双方都拥有一个固定的IP地址。在目前的网络中,也只有双方都采用光纤接入方式,才有可能拥有固定的IP地址,而且,这样的接入方式必须在公安部门做备案处理,手续繁琐而且费用很高,对于企业来说,也是一个不小的负担。如何解决这样的问题呢?


  通过研究发现,SQL Server的别名机制设置的别名,是写入在Windows系统的注册表中的,位置为:Windows Registry Editor Version 5.00 x64系统:


  [HKEY_LOCAL_MACHINE\SOFT-


  WARE\Wow6432Node\Microsoft\MSSQLServer\Client\ConnectTo]


  "别名"="DBMSSOCN,192.168.1.8,1433"x32系统


  Windows Registry Editor Version 5.00[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo]


  "别名"="DBMSSOCN,192.168.1.8,1433"


  SQL Server在进行同步复制的计划任务时,从注册表中读取别名设置中的别名,从而取得对方的IP地址和端口号进行远程连接,连接成功后进行数据的同步传输工作。


  有了这个了解,自然可以得出这样的思路:在两台服务器上各自安装一个服务,没有固定IP一段的服务器A定时检测自己的IP地址,如发现IP地址改变了,就把新的IP地址发送给拥有固定IP地址的服务器B,B在接收到A的IP地址信息后,就去修改注册表中A别名对应的IP地址。这样,两者之间就可以保持通信畅通,从而满足了SQL Server同步复制的基础要求了。要实现这样的软件,需要使用如下几项技术来开发软件:


  (1)开发Windows的服务程序。


  (2)获取本机的公网IP地址。


  (3)计算机之间的直接通信。


  (4)发送邮件给管理员,提醒IP地址变更信息。


  (5)修改注册表。


  3开发Windows服务


  使用VS.NET C#开发Windows Service这一块并不复杂,


  但是注意事项太多了,网上资料也很凌乱,偶尔有些细致些的教程也会出现一些有意或无意的疏漏,为此用最朴实的文字,描述最详细的步骤,介绍如何实现一个Windows服务,来实现需要的功能基础模块。


  3.1创建Windows Service项目


  首先,打开VS.NET,在文件菜单下点击新建项目,然后选择C#,再点Windows(图2标识1),在右侧选择Windows服务(图2标识2),然后在下面的名称一栏录入服务的名称(图2标识3),这里以SQLRemoteClient,然后设置好项目保存路径,之后点击“确定”按钮(图2标识4)。


  3.2对Service重命名将Service1重命名为服务名称,这里命名为SqlRemoteClient。


  3.3创建服务安装程序


  双击SrvClient.cs文件(图4标识1),在左侧的设计视图中右键点击,弹出的菜单中点击添加安装程序(图4标识2)。


  之后双击VS创建的ProjectInstaller.Cs打开,可以看到VS已经自动创建了serviceProcessInstaller1和serviceInstaller两个组件(图5标识1所示),右键点击serviceZInstall1,选择属性,在ServiceName一栏中把默认的服务名称修改为服务名称:SrvClient,DisplayName修改为ZiyuSqlClient(这个是在Windows的服务管理器里面看到的名称标识)。(如图5标识2、3)


  设置了服务的属性以后,接下来设置服务的安装权限。右键serviceProcessInsraller1,选择属性,将Account的值改为LocalSystem。如图6所示。


  注意:这里的Account有4个选项。LocaoService:本地服务账户


  NetworkService:网络服务账户


  LocalSystem:本地系统账户User:指定账户这里选择的是LocalSystem,意在给它最高权限,避免安装的时候出现权限不足导致安装失败。实际项目中,要根据服务的性质来决定究竟应该选择哪种账户类型、避免该服务越权造成损失。


  3.4写入服务逻辑代码到此已经完成了服务的设计器上的开发,接下来需要写入服务代码来实现服务所需的功能要求了。右键SrvClient.cs,选择查看代码。然后,写入如下代码:


  using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Diagnostics;


  using System.Linq;using System.ServiceProcess;using System.Text;namespace WindowsServiceTest


  {


  public partial class ServiceTest:ServiceBase


  {


  public ServiceTest()


  {


  InitializeComponent();


  }


  protected override void OnStart(string[]args)


  {


  using(System.IO.StreamWriter sw=new System.


  IO.StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("yyyy-MM-


  dd HH:mm:ss")+"启动服务.");


  }


  }


  protected override void OnStop()


  {


  using(System.IO.StreamWriter sw=new System.IO.


  StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("yyyy-MM-


  dd HH:mm:ss")+"关闭服务.");


  }


  }


  }


  }


  这里只是简单地做一个示例,服务启动的时候在d:\\clog.


  txt文件中写入日志,关闭的时候也写日志。


  3.5创建服务安装脚本在项目中添加2个文件如下:


  3.5.1安装脚本Install.bat


  %SystemRoot%\Microsoft.NET\Framework\v2.0.50727\installutil.exe SqlRemoteClient.exe-安装服务Net Start SrvClient-启动服务


  sc config SrvClient start=auto–设置服务为自动启动


  pause--执行完毕以后暂停,方便观察脚本运行情况


  3.5.2卸载脚本Uninstall.bat


  %SystemRoot%\Microsoft.NET\Framework\v2.0.50727\installutil.exe/u SqlRemoteClient.exe-卸载服务pause--执行完毕以后暂停,方便观察脚本运行情况


  注意:这两个文件必须是ANSI或者UTF-8无BOM格式,且设置其属性为复制到输出目录。编译后,若发现执行异常,则用记事本打开编译后的文件,点击另存为,选择ANSI格式编码,保存一下后再次执行即可。


  到此,Windows服务已开发完毕,编译,然后双击执行Install.bat文件,即可看到安装成功的提示,随后可以看到d:


  \\clog.txt文件中的日志信息了。


  4获取本机的公网IP地址


  要获取本机所在网络的公网IP地址,有一个很简单的办法,通常一些查询IP地址的网站,如www.ip138.com和www.3322.org这样的工具网站,都能直接显示访客的公网IP地址。


  于是很容易就可以写下下面这样的代码来取得公网IP地址了:


  ///<summary>


  ///获取本机所在网络的公网IP地址信息


  ///</summary>private void GetIp()


  {


  string OriginalIp=string.Empty;string myIp=string.Empty;


  myIp=WebHelper.GetWebContent("http://www.


  3322.org/dyndns/getip");if(myIp!=string.Empty&&OriginalIp!=myIp)


  {


  OriginalIp=myIp;if(sMyIP!=myIp)


  {


  sMyIP=myIp;


  if(isDebug)using(System.IO.StreamWriter sw=new


  System.IO.StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("


  yyyy-MM-dd HH:mm:ss得到ip")+sMyIP);//把获取到的ip


  //地址写入到日志文件;


  sw.Close();


  }


  EMail.SendEmail(myIp);//发送邮件给管理员。


  }


  }


  }


  原理:http://www.3322.org/dyndns/getip这样的地址,返回的是一个纯文本形式的IP地址,因此,只要在代码中拿到返回的信息,便得到了电脑所在网络的公网IP地址了。类似这样的网络地址还有很多,有兴趣的朋友们不妨搜索一下。


  5使用Socket协议收发信息


  TCP协议(Transmission Control Protocol,传输控制协议)是TCP/IP体系中面向连接(connection oriented)的传输层(transport layer),TCP协议能够检测和恢复IP层提供的主机到主机的信道中可能发生报文丢失、重复以及其他错误。由于TCP协议是一种面向连接协议:在使用它进行通信之前,两个


  应用程序之间首先要建立一个TCP连接。TCP能够在网络中提供双工和可靠的的服务。


  5.1 TCP概述


  通信双方建立了TCP连接后,双方就可以相互发送数据了。TCP负责把用户数据(字节流)按照一定格式和长度组成多个数据报进行发送,然后在接到数据报之后分解按顺序重新组装和恢复用户数据。利用TCP传输数据时,数据时以字节的形式进行传输。客户端和服务端建立连接后,发送数据方需要先将数据转换为字节流,然后将字节流发送到对方。TCP协议主要有以下特点:


  (1)是面向连接的传输层协议。


  (2)每个TCP连接只能有两个端点,且只能一对一通信。


  (3)通过TCP连接传送数据能够保证报文的完整和准确性。


  (4)数据只能够以字节流的形式传输。


  (5)传输的数据无消息边界。


  5.2在.NET平台TCP应用的工作模式


  在.NET平台下开发TCP应用程序,框架提供两种工作方式,(1)同步工作方式;(2)异步工作方式。这里所说的同步工作方式和异步工作方式和线程间的同步并不是一个概念。线程间的同步指的是不同线程或其他共享资源具有先后关联的关系;而同步TCP和异步TCP指的是TCP编程中采用的两种不同的工作方式,即:从执行到方式、接收或监听语句时,程序是否继续往下执行,继续执行的就是异步TCP,如果程序阻塞那就是同步TCP。


  与同步工作方式和异步工作方式相对应,利用Socket类开发应用.NET框架也提供了相应的编程方式:分别是同步Socket编程和异步Socket编程。为了简化编程的复杂度,.NET将Socket类进行进一步的封装,提供了两个类:TcpClient类和TcpListener类,这两个类也分别提供同步和异步工作方式


  的API。


  通过前面知道:TcpClient类和TcpListener类简化了Socket编程的复杂度,但是要注意:TcpClient和TcpListener这两个类只支持标准协议编程,如果需要编写非标准协议的应用程序,只能使用Socket来实现。


  TcpClient类用于提供本机主机和远程主机的连接信息,而TcpListener类则用于监听客户端的请求(这两个类的更多信息可以参考MSDN类库,文中已经给出连接,这里就不在赘述了)。当Socket通信双发建立了连接后,创建了TcpClient对象,就可以使用该对象的GetStream()方法得到NetworkStream对象,然后再利用网络流对象向远程主机发送或接收流数据。


  5.3解决TCP的无消息边界问题


  网络数据传输是基于流的,在采用TCP通信保证了接收和发送数据顺序和完整性,但是在实际的网络传输过程可能会出现发送方和接收方消息不一致的情况。例如:第一次发送的数据为“123456”,第二次发送的数据为“ABCDEF”,有时候可能会出现这种情况:“123456ABCDEF”同时接收了;或者是先接收“123456ABC”,然后在接收“DEF”等情况。之所以出现这种情况是因为:TCP是一种以字节流形式传输的、无消息边界的协议,由于网络中不确定因数的影响,因此不能够保证每个Send方法发送的数据被对应的Recive读取。所以在实际的Socket应用程序开发是必须要考虑消息边界的问题,否则就有可能出现数据错误等问题。解决TCP消息边界一般使用下面的3种方式,可以根据场景的不同选用不同的方式。


  5.3.1发送固定长度的消息


  这种方式适用于消息长度固定的场景。具体实现时可以使用BinaryReader/BinaryWriter对象每次向网络流,发送/读取一个固定长度的数据即可。例如每次发送一个int类型的32位整数。


  TcpClient client=new TcpClient("www.baidu.com",


  5968);


  NetworkStream m_NetStream=client.GetStream();


  BinaryWriter bw=new BinaryWriter(m_NetStream,


  Encoding.UTF8);bw.Write(99);


  5.3.2将消息长度与消息一起发送


  这种方式一般在每次发送消息的前面用4个字节表面本次消息的长度,然后将包含消息长度的消息发送给对方;对方收到消息后,首先从消息的前4个字节读取消息长度,然后根据消息长度值接收发送方发送的数据。这种方法适用于任何场合,在这里可以利用BinaryReader和BinaryWriter对象来对NetworkStream进行进一步的封装,当使用BinaryWriter对象调用Write(+18重载)方法向网络流写入数据时,该方法会自动计算发出送数据占用的字节数,并使用4(根据发送数据类型)个字节附加到字符串前面;然后另一方使用BinaryReader对象的对应于BinaryWriter对象的Write()方法读取数据时,它会首先读取数据的长度,并自动根据数据前缀读取指定长度的数据。


  5.2.3使用特殊标记分隔消息


  这种方式适用于消息中不包含特殊标记的场合。例如:在每个命令后面添加回车换行(\r\n)符号作为分隔符的场合。如果对于字符串处理,实现这种方法最简便的途径是使用StreamWriter对象和StreamReader对象。发送时使用StreamWriter对象的WriteLine()方法将发送的字符串写入网络流,接收方只需要调用StreamReader对象的ReadLine()方法将以回车换行符作为分隔符的字符串从网络流中读取即可。有了以上这些知识,很容易就能写出下面这样的代码:


  IPAddress ip=IPAddress.Parse(host);


  IPEndPoint ipe=new IPEndPoint(ip,port);


  Socket c=new Socket(AddressFamily.


  InterNetwork,SocketType.Stream,ProtocolType.Tcp);


  //创建一个Socket


  c.Connect(ipe);//连接到服务器


  if(isDebug)using(System.IO.StreamWriter sw=new


  System.IO.StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("


  yyyy-MM-dd HH:mm:ss")+"连接到服务器."+host+"port"


  +port.ToString());


  sw.Close();


  }


  string sendStr=sMyIP;byte[]bs=Encoding.ASCII.GetBytes(sendStr);if(isDebug)using(System.IO.StreamWriter sw=new


  System.IO.StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("


  yyyy-MM-dd HH:mm:ss发送信息")+sendStr);


  sw.Close();


  }


  c.Send(bs,bs.Length,0);//发送测试信息


  string recvStr="";


  byte[]recvBytes=new byte[1024];int bytes;


  bytes=c.Receive(recvBytes,recvBytes.


  Length,0);//从服务器端接受返回信息


  recvStr+=Encoding.ASCII.GetString


  (recvBytes,0,bytes);if(isDebug)using(System.IO.StreamWriter sw=new


  System.IO.StreamWriter("d:\\clog.txt",true))


  {


  sw.WriteLine(DateTime.Now.ToString("


  yyyy-MM-dd HH:mm:ss收到服务器返回信息")+recvStr);


  sw.Close();


  }


  c.Close();


  6 C#发送邮件提醒


  这个算是文中最简单的一个技术点了,网上随便搜一下都可以找到大把的资料,因此这里只简单地给出核心代码。感兴趣的读者可在网上搜索一下。


  SmtpClient client=new SmtpClient();client.Host=GlobalVariables.Config.SmtpHost;


  client.Port=GlobalVariables.Config.SmtpPort;


  //是否使用安全套接字层加密连接client.EnableSsl=false;


  //不使用默认凭证,注意此句必须放在client.Credentials的


  //上面


  client.UseDefaultCredentials=false;


  client.Credentials=new NetworkCredential(GlobalVariables.Config.SmtpLoginUser,GlobalVariables.Config.


  SmtpLoginPwd);


  //邮件通过网络直接发送到服务器


  client.DeliveryMethod=SmtpDeliveryMethod.


  Network;


  try


  {


  client.Send(mail);


  SysLog.WriteEntry("EGetIp","发信成功,本次发信IP为:"+ip,SysLog.LogType.Information,1);


  }


  catch(SmtpException ex)


  {


  SysLog.WriteEntry("EGetIp",ex,4);


  }


  catch(Exception ex)


  {


  SysLog.WriteEntry("EGetIp",ex,5);


  }


  finally


  {


  mail.Dispose();client=null;


  }


  7修改注册表


  Windows操作系统的注册表包含了很多有关计算机运行的配置方式,打开注册表可以看到注册表是按类似于目录的树结构组织的,其中第二级目录包含了5个预定义主键分别是:HKEY_CLASSES_ROOT,HKEY_CURRENT_USER,HKEY_LO


  CAL_MACHINE,HKEY_USERS,HKEY_CURRENT_CONFIG。


  下面来分别解释这5个类的作用


  HKEY_CLASSES_ROOT该主键包含了文件的扩展名和应用程序的关联信息以及Window Shell和OLE用于储存注册表的信息。该主键下的子键决定了在Windows中如何显示该类文件以及他们的图标,该主键是从HKEY_LCCAL_MACHINE\SO


  FTWARE\Classes映射过来的。


  HKEY_CURRENT_USER该主键包含了如用户窗口信息,桌面设置等当前用户的信息。


  HKEY_LOCAL_MACHINE主键包含了计算机软件和硬件的安装和配置信息,该信息可供所有用户使用。


  HKEY_USERS该主键记录了当前用户的设置信息,每次用户登入系统时,就会在该主键下生成一个与用户登入名一样的子键,该子键保存了当前用户的桌面设置、背景位图、快捷键、字体等信息。一般应用程序不直接访问该主键,而是通过


  主键HKEY_CURRENT_USER进行访问。


  HKEY_CURRENT_CONFIG该主键保存了计算机当前硬件


  的配置信息,这些配置可以根据当前所连接的网络类型或硬件驱动软件安装的改变而改变。


  C#也支持对注册表的编辑,.NET框架在Microsoft.Win32名字空间中提供了两个类来操作注册表:Registry和RegistryKey。这两个类都是密封类不允许被继承。下面分别来介绍这两个类。


  Registry类提供了7个公共的静态域,分别代表7个基本主键(其中两个在XP系统中没有,在这就不介绍了)分别


  是:Registry.ClassesRoot,Registry.CurrentUser,Registry.Local Machine Registry.Users,Registry.CurrentConfig。它们分别对应哪几个键一看就会知道。


  RegistryKey类中提供了对注册表操作的方法。要注意的是操作注册表必须符合系统权限,否则将会抛出错误。下面就介绍几个操作注册表常用的方法。创建子键的方法原型为:


  public RegistryKey CreateSubKey(string sunbkey);


  参数sunbkey表示要创建的子键的名称或路径名。创建成


  功返回被创建的子键,否则返回null。打开子键的方法原型为:


  public RegistryKey OpenSubKey(string name);public RegistryKey OpenSubKey(string name,bool


  writable);


  参数name表示要打开的子键名或其路径名,参数writable表示被打开的子键是否允许被修改,第一个方法打开的子键是只读的。Microsoft.Win32类还提供了另一个方法,用于打开远程计算机上的注册表,方法原型为:


  public static RegistryKey OpenRemoteBaseKey


  (RegistryHive hKey,string machineName);


  删除子键的方法原型为:


  public void DeleteKey(string subkey);


  该方法用于删除指定的主键。如果要删除的子键还包含主键则删除失败,并返回一个异常,如果要彻底删除该子键及其目录下的子键可以用方法DeleteSubKeyTree,该方法原型如下:public void DeleteKeyTree(string subkey);读取键值的方法原型如下:


  public object GetValue(string name);public object GetValue(string name,object defaultValue);


  参数name表示键的名称,返回类型是一个object类型,如果指定的键不存在则返回null。如果失败又不希望返回的值是null则可以指定参数defaultValue,指定了参数则在读取失败的情况下返回该参数指定的值。设置键值的方法原型如下:


  public object SetValue(string name,object value);


  在这里,关心的是SQL Server别名(SQLAlias)的注册表信息,通过搜索注册表,SQL Server别名在注册表中的信息如下:注意:32位系统和64位系统的路径有所区别。


  x64


  [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\


  Microsoft\MSSQLServer\Client\ConnectTo]


  "mySQLserver"="DBMSSOCN,192.168.166.6,1588"x32


  [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSSQ


  LServer\Client\ConnectTo]


  "mySQLserver"="DBMSSOCN,192.168.166.6,1588"


  当有固定IP的SQL服务器接收到客户端传来的IP信息的时候,把这个信息写入到上面的键值,这样SQL在同步数据的时候就可以找到客户端SQL的地址来传输数据了。于是有了下面的代码:


  ///<summary>


  ///更改sql别名ip地址


  ///</summary>


  ///<param name="recvStr"></param>private void changeSQLAliasIP(string recvStr)


  {


  RWReg reg=new RWReg("LOCAL_MACHINE");string sOldValue=reg.GetRegVal("SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo","ny


  server","default");


  recvStr="DBMSSOCN,"+recvStr+",1588";if(sOldValue!=recvStr)


  {


  if(reg.SetRegVal("SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo","nyserver",recvStr))


  {


  if(isDebug)using(System.IO.StreamWriter sw1=new


  System.IO.StreamWriter("d:\\Slog.txt",true))


  {


  sw1.WriteLine(DateTime.Now.ToString("


  yyyy-MM-dd HH:mm:ss")+"写入旧设置:{0}","成功"+recvStr);//把客户端传来的信息显示出来


  }


  ChangeReg();


  }


  else


  {


  }


  }


  }


上一篇 下一篇