65.9K
CodeProject 正在变化。 阅读更多。
Home

替代 tnsping 工具

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2012年10月10日

CPOL

7分钟阅读

viewsIcon

64910

downloadIcon

1171

Oracle Instant Client 的替代 tnsping 工具。

引言

本项目特别为使用 Oracle 和 Oracle Instant Client 的用户设计。需要一些 Oracle 知识。您需要了解 tnsname、connectstring 等术语的含义。

Oracle Instant Client 非常方便且有用。我非常喜欢它,因为例如,您可以在 64 位服务器上发布 32 位程序,或者将我的程序发布到没有 Oracle 客户端的机器上。与“大型”Oracle 客户端相比,它体积非常小。

对我来说,Instant Client 唯一的缺点是没有 tnsping 工具。在大型公司发布程序时,如果有些用户可以连接到 Oracle 服务器而有些用户不能,那么 tnsping 工具就非常有用。tnsping 非常适合用来追踪错误。

首先,如果您想了解比原始 tnsping 工具更多的信息,请阅读本文档:

http://docs.oracle.com/cd/E11882_01/network.112/e10836/connect.htm

Tnsping 是一个 Oracle 连接测试工具。您可以使用我的程序替代 tnsping 工具。

当然,您可以通过 tnsping 工具来完善您的 instant client。网上有很多关于此的文档。唯一的问题是您必须从完整的 Oracle 客户端复制许多 DLL 文件才能使用 tnsping,这样 Instant Client 就不再“instant”了。因此,编写自己的 tnsping 工具的想法就产生了。

此程序版本可以通过直接的 connectstring、使用 tnsnames.ora 解析以及使用 LDAP 进行 ping。

背景

本文档对以下方面有用:

  • 如何使用 Oracle Instant Client
  • 如何在没有正则表达式的情况下解析文本文件
  • 如何读取 LDAP

原始 tnsping 工具会检查 Oracle 监听器的状态。这是一个类似于 ping 工具的控制台应用程序。它会检查 Oracle 监听器的状态。

在这个程序中,我使用了一个小技巧。tnsping 工具会检查 Oracle 监听器是否正常工作。而我的程序只是模拟它。我尝试使用一个虚拟用户和虚拟密码连接到 Oracle。我将收到一条错误消息。如果错误消息是“ORA-01017”,那么对我们来说就很好:这表示监听器正常,但用户名/密码不正确。没关系:我不想连接,只是想检查监听器。

安装

我提供的源代码将无法直接运行。请下载源代码并按照以下步骤操作:

  • 首先,您需要从这里下载 Oracle Instant Client:

http://www.oracle.com/technetwork/topics/winsoft-085727.html

  • 将 DLL 文件解压缩到我的源代码目录下的 project/tnshrping 目录中(在 tnshrping.sln 文件下一级)。
  • 在 Visual Studio 中,右键单击 tnshrping 项目,然后选择“添加现有项”。将 DLL 文件添加到您的项目(tnshrping 项目)中。
  • 在每个 DLL 的属性中,将“生成操作”设置为“内容”,将“复制到输出目录”设置为“始终复制”。

这些步骤对于编译我的工具是必需的。之后,当您使用 Instant Client 时,只需将 tnshrping.exe 复制到您的 Oracle Instant Client 目录中即可。

使用代码

此程序的流程:

  • 尝试直接连接(参数为 connect string)。
  • 如果成功,则显示“OK”消息并完成。
  • 如果不成功,则读取 sqlnet.ora 文件并设置解析顺序。
  • 尝试使用第一个选项(LDAP 或 TNSNAMES)进行解析并尝试连接。
  • 如果成功,则显示“OK”消息并完成。
  • 如果不成功,则尝试使用第二个选项进行解析。
  • 如果成功,则显示“OK”消息。
  • 如果不成功,则显示错误消息。

sqlnet.oratnsnames.ora 的位置由 tns_admin 环境变量指定。

首先,让我们看看如何检查 Oracle 连接。这是一个非常简单的 Oracle 连接,没有任何特殊之处。我只是尝试使用虚拟用户名和虚拟密码连接到 Oracle。我们只需要 Oracle 服务器的响应并进行解析。Trytoconnect 需要一个 Oracle connect string,并返回关于监听器状态的消息。不需要实际连接,只需解析监听器的错误消息即可。

public static string trytoconnect(string connectstring)
{
  string ret = String.Empty;
 
  //open string with a dummy username and a dummy password
  string openstring = "Data Source=" + connectstring + 
     ";User Id= x_dummy; Password = x_dummy;";

    //return message from oracle connect
    string msg = String.Empty;

    //Ellapsed time in string
    string Ellapsed = String.Empty;

    //Create new oracle connection and safely close the resource
    using (OracleConnection connection = new OracleConnection(openstring))
    {
        //Timestamp for ellapsed time counting
        DateTime stTime = DateTime.Now;
        try
        {
           // Try to connect. If succeed, then close, and create
           // an OK message. x_dummy user and password is improbabile.
            // If connection not successfull, there issued an Oracle
            // exception. We need to parse the exception.
            connection.Open();

            // If succeeded...
            msg = "OK";
            //.. then cloe.
            connection.Close();
   
        }
        // If connection is not succesfull, we get the errormessage.
        catch (Exception ex)
        {
            //We need the first part of message, this is an Oracle message like this: ORA-01017
            msg = ex.Message.Split(':')[0];
        }
        //Timestamp for ellapsed time counting
        DateTime etTime = DateTime.Now;

        //Calculate the ellapsed time
        TimeSpan ts = etTime - stTime;
        //Get the millisecs from ellapsed time
        Ellapsed = ts.Milliseconds.ToString();

    }

    //Parsing the ORA errormessage. If Listener is OK, we will get
    //ORA-01017. It means a bad username and password. Any else
    //response means a kind of listener problem. I tried to parse some of them.
    switch (msg)
    {
        //kind os listener answers 
        case "ORA-12154":
            ret = "No Oracle listener found";
            break;
        case "ORA-12514":
            ret = "No TNS listener found";
            break;
        case "ORA-12541":
            ret = "No listener ";
            break;
        //ORA-01017 is only good response for us. It means
        //that listener is good, but username and passwor is not.
        //This procedure gives back OK message and ellapsed time in this case
        case "ORA-01017":
            ret = String.Format("OK ({0} millisec)", Ellapsed);
            break;
        case "OK":
            ret = String.Format("OK ({0} millisec)", Ellapsed);
            break;
        //In default case this procedure doesn't found listener.
        default:
            ret = "No Oracle listener found";
            break;
    }
    //returns the result of connection                        
    return ret;
}

要从 tnsname 获取连接字符串,有许多方法。此程序可以使用 tnsnames.oraLDAP。解析顺序很重要,它在 sqlnet.ora 文件中设置。此程序在 tns_admin 环境变量指向的文件夹中查找 sqlnet.ora 文件。

程序读取 sqlnet.ora,读取解析顺序(请参见下面的 main 方法),然后尝试从 tnsnames.ora 文件或 LDAP 解析 tnsname。

首先,让我们看看如何解析 tnsnames.oratnsnames.ora 文件包含如下的 tnsnames:

TNSNAME1.DOMAIN.COUNTRY= 
(DESCRIPTION = 
(ADDRESS_LIST = 
(ADDRESS = 
(COMMUNITY = myhost.domain.country)
(PROTOCOL = TCP)
(HOST = myhost.domain.country)
(PORT = 1522) 
)
)
(CONNECT_DATA = 
(SID = MYSID)
)
)

我们必须编写一个方法来解析它。我们向 resolvetns 方法提供一个 tnsname(TNSNAME1.DOMAIN.COUNTRY),然后等待返回 connect string:(DESCRIPTION=....等)。

这不是一个优雅的方法。使用正则表达式会更优雅。问题是,我不是正则表达式专家,而且 tnsnames.ora 的结构并非那么简单。

我的想法是“规范化” tnsnames.ora 并分离 tnsname 和 connect string。我逐个字符地解析 tnsnames.ora。我区分“括号内”和“括号外”的字符。如果您仔细检查 tnsnames.ora,您会发现 tnsname 和 connectstring 之间的分隔符是“括号外”的等号。所有其他等号都在“括号内”。

我将“括号外”的等号更改为井号(#),然后将 tnsnames.ora 的行按井号分割成 tnsname 和 connectstring。

我们来看看:

private static string resolvetns(string arg)
{
    //return string
    string ret = String.Empty;
    //dummy string for parsing
    string tnsnamesora = String.Empty;
 
    //If tnsnames.ora  exists, the tries to parse
    if (File.Exists(Path.Combine(tns_admin, "tnsnames.ora")))
    {
        //Opens safely tnsnames.ora file resource
        using (var sr = File.OpenText(Path.Combine(tns_admin, "tnsnames.ora")))
        {
            string line;
            //reads tnsnames.ora line by line
            while ((line = sr.ReadLine()) != null)
            {
                //if the line starts with # then ignore
                if (line.StartsWith("#"))
                {
                    continue;
                }

                //if the line doesn'contain any data then ignore
                if (line == "")
                {
                    continue;
                }

                //If lie contain data, then add to tnsnamesora dummy strig, after cutting the spaces.
                tnsnamesora = tnsnamesora + line.Replace(" ", String.Empty).ToUpper();
            }
        }

        //Now tnsnamesora variable contains full tnsnames.ora file,
        //without empty lines, and without spaces.

        //variable for counting brackets. If left bracket found,
        //then increase, if right bracket found then decrease
        int brackets = 0;

        //char array  for converting tnsnamesora
        List<char> tnschars = new List<char>();

        //Iterate on tnsnamesora char by char.
        foreach (char c in tnsnamesora)
        {
            char replacechar;

            //If left bracket found then increase brackets, if right bracket found then decrease
            if (c == '(') brackets++;
            if (c == ')') brackets--;

            //Make difference in text "out of brackets" and "between brackets"
            //If equatation mark is  out of brackets then change it to hash mark. 
            //After then we can split line by hash mark.
            if (brackets == 0 && c == '=')
                replacechar = '#';
                //if the cycle is between brackets, then remain the original char.
            else
                replacechar = c;

            //Add the new char to char array
            tnschars.Add(replacechar);

            //If the cycle is out of brackets, and found the last bracket, then add a newline char.
            if (brackets == 0 && c == ')')
            {
                tnschars.Add(Environment.NewLine[0]);
                tnschars.Add(Environment.NewLine[1]);
            }
        }
        //In this moment the tnsnames.ora looks like this:
        //NAME1.DOMAIN.COUNTRY#(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(COMMUNITY=COMMUNITY)
        //(PROTOCOL=TCP)(HOST=MYDBHOST1.DOMAIN.COUNTRY)(PORT=MYPORT1)))
        //(CONNECT_DATA=(SID=MYSID1)(GLOBAL_NAME=MYGLOBALNAME1.MYDOMAIN.COUNTRY)))
        //NAME2.DOMAIN.COUNTRY#(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(COMMUNITY=COMMUNITY)
        //(PROTOCOL=TCP)(HOST=MYDBHOST2.DOMAIN.COUNTRY)(PORT=MYPORT2)))
        //(CONNECT_DATA=(SID=MYSID2)(GLOBAL_NAME=MYGLOBALNAME2.MYDOMAIN.COUNTRY)))
        // etc....

        //So in fact in this moment the tnsname.ora is "canonized". The splitting char is a hash mark.

        //converts it to a string
        string s = new string(tnschars.ToArray());

        //Puts back to original string. 
        tnsnamesora = s;

        //Creates a dictionary. The key will be the tnsname, the value will be the connectstring.
        Dictionary<string,> tnsdict = new Dictionary<string,>();

        //converts a string to a dictionary with using stringreader resource
        //Using it like the string would be a textfile.
        using (StringReader reader = new StringReader(tnsnamesora))
        {
            //dummy line
            string line;
            //reads tnsnamesora multiline string line by line.
            while ((line = reader.ReadLine()) != null)
            {
                //slits the line by hash mark. Remeber: the only hash mark
                //is the equatation mark "out of brackets"
                string[] tnssplit = line.Split('#');

                //Adds the key to the dictionary, and adds the value
                tnsdict.Add(tnssplit[0], tnssplit[1]);
 
            }
        }

        //Finds the connectstring is dictionary by key.
        //If founds then returns the connectionstring
        if (tnsdict.ContainsKey(arg))
        {
            string value = tnsdict[arg];
            ret = value;
        }
        //If not founds then return an error message.
        else
        {
            ret = "Tns value not found in tnsnames.ora";
        }
    }
    //If tnsnames.ora is not found, then returns error message.
    else
    {
        ret = "Tnsnames.ora not found";

    }
    //Returns connectsring orr error message
    return ret;
}

如果 tns 解析不成功,或者在解析顺序中 LDAP 优先于 tnsnames,那么我们就必须在 LDAP 中查找 tnsname。这并不太复杂,但需要一些 LDAP 知识。我们只需要连接到 LDAP 服务器,并在默认 LDAP 上下文中执行一个简单的搜索命令。此方法会尝试联系 LDAP.ora 文件中找到的所有 LDAP 服务器,直到其中一个响应正常,然后返回结果。

如果您想了解更多关于 LDAP 或 directorysearcher、directory services 的知识,或者想了解如何使用 C# 中的 LDAP 查询,请阅读以下文档:

http://stackoverflow.com/questions/1437250/connecting-to-ldap-from-c-sharp-using-directoryservices

现在,让我们看看如何从 LDAP 解析 tnsname。这是一个简单的 LDAP 查询。此程序会尝试查询 LDAP 服务器,直到 LDAP 服务器响应。

LDAP 服务器之间可能存在差异。在我的工作场所,我必须查询 orclnetdescstring 属性,但其他 LDAP 服务器可能使用其他名称。响应可能是一个字节数组,而在其他 LDAP 服务器中,这可能是一个字符串等。

因此,在使用前,您可能需要根据您的具体情况重写此方法。

让我们来看看代码:

private static string resolveldap(string arg)
{
    //returning answer
    string ret = String.Empty;
    //default admin context i.e "dc=company,dc=country"
    string def_admin_context = String.Empty;

    //Ldap.ora can contain many LDAP servers.
    string[] directoy_servers = null;

    //If LDAP.ora file exists
    if (File.Exists(Path.Combine(tns_admin, "Ldap.ora")))
    {
        //safely opens ldap.ora file
        using (var sr = File.OpenText(Path.Combine(tns_admin, "Ldap.ora")))
        {
            string line;

            //reads ldap.ora line by line
            while ((line = sr.ReadLine()) != null)
            {
                //if line starts with # then ignore
                if (line.StartsWith("#"))
                {
                    continue;
                }
                //if line contains no data then ignore. Maybe not necessary.
                if (line == "")
                {
                    continue;
                }

                //if line starts with DEFAULT_ADMIN_CONTEXT then splits
                //by equatation mark, and parse the second part.
                //The "Split" method does not working,
                //because the line contains many equatation marks.
                if (line.StartsWith("DEFAULT_ADMIN_CONTEXT"))
                {
                    int eqpos = line.IndexOf('=');
                    //get substring fom = mark to the end 
                    def_admin_context = line.Substring(eqpos + 1).Replace("\"", "").Trim();
                }

                //If line starts with DIRECTORY_SERVERS then reads
                //LDAP servesr, splitted by commas, after an equuatation mark.
                if (line.StartsWith("DIRECTORY_SERVERS"))
                {
                    //Split by = mark, get the second part
                    //(means [1]) remove the brackets, and split by commas
                    //the result is a string array, contains LDAP servers
                    directoy_servers = line.Split('=')[1].Replace("(", 
                      "").Replace(")", "").Split(',');
                }
            }
        }

        //Iterate on each LDAP server, and tries to connect
        foreach (string directoy_server in directoy_servers)
        {
            //Tries to connect to LDAP server with using default admin contact
            try
            {
                //LDAP connect
                DirectoryEntry directoryenty = new DirectoryEntry("LDAP://" + 
                  directoy_server + "/" + def_admin_context, null, null, AuthenticationTypes.Anonymous);
                //LDAP searcher, search orclnetservice, searchin for orclnetdescstring property
                DirectorySearcher directorysearcher = new DirectorySearcher(directoryenty, 
                  "(&(objectclass=orclNetService)(cn=" + arg + "))", 
                  new string[] { "orclnetdescstring" }, System.DirectoryServices.SearchScope.Subtree);

                //find only one
                SearchResult searchResult = directorysearcher.FindOne();

                //The result will be a byte array, tries to cast

                byte[] dummy = searchResult.Properties["orclnetdescstring"][0] as byte[];

                //Converts a bytearray to string with default encoding.
                //If casting was not successfull, the result will be null.
                ret = System.Text.Encoding.Default.GetString(dummy);

                //If the connection was successfull, then no necessary to try another LDAP servers.
                break;
            }
            //If the connection to LDAP server not successfull,try to connect to next
            catch
            {
                continue;
            }
        }

        //If casting was not successfull, or not found
        //any tns value, then result is an error message.
        if (ret == "")
        {
            ret = "Tns value not found in LDAP";
        }
    }
    // if LDAP.ora not exists, then returns error message.
    else
    {
        ret = "Ldap.ora not found";
    }
    return ret;
}

最后但同样重要的一点。让我们谈谈这个工具的 main 方法。它很简单。它尝试解析参数。参数可以包含空格,但程序只需要一个参数。因此,首先将它们连接成一个参数。然后 main 方法尝试遵循我之前写过的完整流程。首先,它尝试连接到 Oracle,程序的参数是一个 connectstring。如果成功,则结束。然后它从 tns_admin 环境变量读取 sqlnet.ora、tnsnames.ora 和 ldap.ora 的路径。如果未找到,则结束。如果找到,它会尝试打开 sqlnet.ora(简单的文本文件)并读取 NAMES.DIRECTORY_PATH 行中的搜索顺序。它看起来像这样:

NAMES.DIRECTORY_PATH = (LDAP,TNSNAMES)

在这种情况下,搜索顺序将是 LDAP,然后是 TNSNAMES。

它还从 sqlnet.ora 的此行读取默认区域:

NAME.DEFAULT_ZONE=mydomain.mycountry

如果设置了顺序,main 方法将尝试在此情况下先从 LDAP 解析 tnsname,然后从 TNSNAMES 解析。如果 LDAP 可以返回 connect string,我们尝试使用 trytoconnect 方法连接到 Oracle。如果解析不成功,它会尝试使用 tnsname 并添加默认区域来解析 LDAP:

tnsname.mydomain.mycountry

如果成功,则结束,否则我们尝试使用 tnsnames.ora 进行解析...参见前面,然后连接。然后我们返回一条关于解析尝试的结果消息。

关注点

这是一个非常有用的程序,但它并没有包含很多新的东西。对于遇到此问题的所有人来说,都值得研究。您可以通过此示例学习如何使用 Oracle Instant Client。

历史

这是 1.0 版本。

许可证

此程序拥有 CPOL 许可证,但不包含 Oracle Instant Client。您需要单独下载它,并接受 Oracle 网站上的 Oracle 许可协议。

© . All rights reserved.