代码之家  ›  专栏  ›  技术社区  ›  Nicholas Mancuso

创建自定义ODBC驱动程序

  •  48
  • Nicholas Mancuso  · 技术社区  · 17 年前

    在我目前的工作中,我们希望实现自己的odbc驱动程序,以允许许多不同的应用程序能够连接到我们自己的应用程序作为数据源。现在,我们正试图权衡根据实现规范开发自己的驱动程序的选择,这是一个巨大的选择, 使用允许程序员“填充”数据特定部分并允许更高层次抽象的SDK。

    还有其他人实现了自定义odbc驱动程序吗?你遇到了什么陷阱?你认为自己做这件事有什么好处?你估计需要多少工时?你使用过SDK吗?如果是,你从这种方法中看到了什么好处/缺点?

    如有任何意见和答案,我们将不胜感激。谢谢!

    编辑: 我们正试图保持用C编写的代码的可移植性。

    5 回复  |  直到 17 年前
        1
  •  28
  •   codeape    4 年前

    另一种选择是:不创建ODBC驱动程序,而是实现一个后端,该后端可以与另一个数据库(例如Postgresql或MySQL)使用的有线协议进行通信。

    然后,您的用户可以下载并使用Postgresql ODBC驱动程序。

    您选择模拟的后端数据库可能最取决于有线协议格式的记录程度。

    两者 Postgres MySQL 对他们的客户端-服务器协议有很好的文档。

    下面是一个简单的Python 2.7服务器后端示例,可以理解Postgresql wire协议的部分内容。示例脚本创建了一个监听端口9876的服务器。我可以使用命令 psql -h localhost -p 9876 连接到服务器。执行的任何查询都将返回一个结果集,其中包含列abc和def以及两行,所有值均为NULL。

    阅读Postgresql文档并使用wireshark等工具来检查真实的协议流量,将使实现与Postgresql兼容的后端变得非常简单。

    import SocketServer
    import struct
    
    def char_to_hex(char):
        retval = hex(ord(char))
        if len(retval) == 4:
            return retval[-2:]
        else:
            assert len(retval) == 3
            return "0" + retval[-1]
    
    def str_to_hex(inputstr):
        return " ".join(char_to_hex(char) for char in inputstr)
    
    class Handler(SocketServer.BaseRequestHandler):
        def handle(self):
            print "handle()"
            self.read_SSLRequest()
            self.send_to_socket("N")
    
            self.read_StartupMessage()
            self.send_AuthenticationClearText()
            self.read_PasswordMessage()
            self.send_AuthenticationOK()
            self.send_ReadyForQuery()
            self.read_Query()
            self.send_queryresult()
    
        def send_queryresult(self):
            fieldnames = ['abc', 'def']
            HEADERFORMAT = "!cih"
            fields = ''.join(self.fieldname_msg(name) for name in fieldnames)
            rdheader = struct.pack(HEADERFORMAT, 'T', struct.calcsize(HEADERFORMAT) - 1 + len(fields), len(fieldnames))
            self.send_to_socket(rdheader + fields)
    
            rows = [[1, 2], [3, 4]]
            DRHEADER = "!cih"
            for row in rows:
                dr_data = struct.pack("!ii", -1, -1)
                dr_header = struct.pack(DRHEADER, 'D', struct.calcsize(DRHEADER) - 1 + len(dr_data), 2)
                self.send_to_socket(dr_header + dr_data)
    
            self.send_CommandComplete()
            self.send_ReadyForQuery()
    
        def send_CommandComplete(self):
            HFMT = "!ci"
            msg = "SELECT 2\x00"
            self.send_to_socket(struct.pack(HFMT, "C", struct.calcsize(HFMT) - 1 + len(msg)) + msg)
    
        def fieldname_msg(self, name):
            tableid = 0
            columnid = 0
            datatypeid = 23
            datatypesize = 4
            typemodifier = -1
            format_code = 0 # 0=text 1=binary
            return name + "\x00" + struct.pack("!ihihih", tableid, columnid, datatypeid, datatypesize, typemodifier, format_code)
    
        def read_socket(self):
            print "Trying recv..."
            data = self.request.recv(1024)
            print "Received {} bytes: {}".format(len(data), repr(data))
            print "Hex: {}".format(str_to_hex(data))
            return data
    
        def send_to_socket(self, data):
            print "Sending {} bytes: {}".format(len(data), repr(data))
            print "Hex: {}".format(str_to_hex(data))
            return self.request.sendall(data)
    
        def read_Query(self):
            data = self.read_socket()
            msgident, msglen = struct.unpack("!ci", data[0:5])
            assert msgident == "Q"
            print data[5:]
    
    
        def send_ReadyForQuery(self):
            self.send_to_socket(struct.pack("!cic", 'Z', 5, 'I'))
    
        def read_PasswordMessage(self):
            data = self.read_socket()
            b, msglen = struct.unpack("!ci", data[0:5])
            assert b == "p"
            print "Password: {}".format(data[5:])
    
    
        def read_SSLRequest(self):
            data = self.read_socket()
            msglen, sslcode = struct.unpack("!ii", data)
            assert msglen == 8
            assert sslcode == 80877103
    
        def read_StartupMessage(self):
            data = self.read_socket()
            msglen, protoversion = struct.unpack("!ii", data[0:8])
            print "msglen: {}, protoversion: {}".format(msglen, protoversion)
            assert msglen == len(data)
            parameters_string = data[8:]
            print parameters_string.split('\x00')
    
        def send_AuthenticationOK(self):
            self.send_to_socket(struct.pack("!cii", 'R', 8, 0))
    
        def send_AuthenticationClearText(self):
            self.send_to_socket(struct.pack("!cii", 'R', 8, 3))
    
    if __name__ == "__main__":
        server = SocketServer.TCPServer(("localhost", 9876), Handler)
        try:
            server.serve_forever()
        except:
            server.shutdown()
    

    命令行psql会话示例:

    [~]
    $ psql -h localhost -p 9876
    Password:
    psql (9.1.6, server 0.0.0)
    WARNING: psql version 9.1, server version 0.0.
             Some psql features might not work.
    Type "help" for help.
    
    codeape=> Select;
     abc | def
    -----+-----
         |
         |
    (2 rows)
    
    codeape=>
    

    一个使用Postgresql协议的ODBC驱动程序也应该可以工作(但我还没有尝试过)。

        2
  •  10
  •   Einstein    17 年前

    ODBC驱动程序非常复杂,编写驱动程序的决定不应掉以轻心。例如,查看现有的开源驱动程序是一种很好的方法,但大多数驱动程序都有你可能不想模仿的缺点:)无论操作系统平台如何,API都是相同的。 MSSQL/Sybase的FreeTDS是我见过的最好的开源ODBC驱动程序实现之一。

    如果你控制了应用程序,你可以在合理的时间内实现可能只是规范的一小部分。在通用环境中使用可能需要付出更多的努力才能正确使用。在我的头脑中,除了简单地实现几十个包装器调用外,你还必须实现:

    • 元数据访问功能
    • ODBC特定查询语法解析
    • SQLSTATE错误消息映射
    • 多字节/字符集编组
    • ODBC版本2,3支持-错误消息/函数映射
    • 光标
    • 用于管理数据源的DM配置UI
        3
  •  9
  •   ConcernedOfTunbridgeWells    17 年前

    我没有,但我曾经面试过一家做过这种事情的公司。他们做了 一种名为AMPS的4GL/DBMS产品,其架构与MUMPS相同,是一种集成了4GL的分层数据库(20世纪70年代出现了一种完整的此类系统)。他们有相当多的遗留代码库,客户希望使用MS Access连接到它。

    采访我的首席开发人员分享了一些关于这方面的战争故事。显然,这样做非常痛苦,不应该掉以轻心。然而,他们确实成功地实现了这一目标。

    一种替代方法是提供数据集市/BI产品(类似于SAP BW),该产品在外部数据库中显示您的应用程序数据,并将其转换为更友好的格式,如星形或雪花模式。

    这将受到不支持实时访问的影响,但可能比ODBC驱动程序更容易实现(更重要的是维护)。如果您的实时访问需求是合理的可预测和有限的,您可能会公开一个web服务API来支持这些需求。

        4
  •  4
  •   Bill Karwin    17 年前

    我还没有实现ODBC驱动程序,只是想提供一个建议,即您可以从开源实现开始,添加自己的自定义项。这可能会让你更快地开始。

    至少有两种选择:

    • unixODBC 根据LGPL许可,这意味着如果你修改代码,你必须将修改开源。

    • iODBC 根据您的选择,在LGPL或新BSD下获得许可。新的BSD允许您进行修改 没有 使您的修改开源。

    但是,与使用与标准ODBC一致的客户端API在UNIX/Linux上运行相比,这些软件包是否在Windows上运行尚不清楚。你没有说明你正在使用哪个平台,所以我不知道这是否与你有关。

        5
  •  2
  •   KylePorter    9 年前

    这篇文章现在有点旧了,但值得一提的是,如果你需要一个ODBC驱动程序,你可以使用这样的SDK: http://www.simba.com/drivers/simba-engine-sdk/ 它解决了其他答案中提出的大多数问题,并为您提供了一个简化得多的接口来实现。

    我碰巧在Simba工作,所以我有点偏见,但使用SDK确实可以很容易地为你想做的任何事情创建ODBC驱动程序。如果你对编码有点精通,你可以在5天内开始工作。

    另一篇文章建议以unixODBC或iODBC作为起点,但这行不通。重要的是要认识到驱动程序管理器(unixODBC、iODBC等)和驱动程序之间的区别。驱动程序管理器充当应用程序和驱动程序之间的中间人,无需直接链接到驱动程序。

    您可以从Postgres或MySQL驱动程序开始,将其分叉以使用您自己的数据库,但这不太可能是一项简单的任务。从头开始创建驱动程序甚至更加困难,并且可能会产生持续的(比预期更高的)维护成本。只要你知道这种方法的成本,它也是可行的。