62. C语言与SQL Server基础:ODBC/FreeTDS、SQLConnect、SQLExecDirect、SQLFetch
说实话,很多做嵌入式或者Linux后台开发的兄弟,一听到要连SQL Server就头大。为啥?因为微软这套东西在Windows上玩得转,到了Linux上就各种水土不服。但实际项目中,尤其是工业数据采集、设备监控这类场景,后台数据库偏偏就是SQL Server——甲方爸爸说了算嘛。
我早年在一个工厂MES项目里就栽过跟头。设备端是Linux ARM板,数据库是SQL Server 2008 R2。当时年轻,以为装个ODBC驱动就能搞定,结果折腾了三天连不上。后来才发现,Linux上连SQL Server,最靠谱的方案是FreeTDS + unixODBC这套组合拳。今天咱们就把这套路数掰开揉碎了讲清楚。
62.1 环境搭建:FreeTDS + unixODBC
先说说底层逻辑。SQL Server用的是TDS(Tabular Data Stream)协议,而FreeTDS就是一套开源的TDS协议实现。unixODBC则是ODBC标准的Linux实现,它负责管理驱动和数据源。说白了,FreeTDS是翻译官,unixODBC是调度中心。
安装步骤其实不复杂,我习惯用源码编译,因为有些嵌入式系统的包管理器版本太老。但如果你用的是Ubuntu或CentOS,直接apt或yum也行:
# Ubuntu/Debian
sudo apt-get install unixodbc unixodbc-dev freetds-dev freetds-bin tdsodbc
# CentOS/RHEL
sudo yum install unixODBC unixODBC-devel freetds freetds-devel
装完之后,关键一步是配置。你需要告诉unixODBC:FreeTDS驱动在哪儿,以及数据源怎么连。我个人习惯把配置写在/etc/odbcinst.ini和/etc/odbc.ini里:
# /etc/odbcinst.ini
[FreeTDS]
Description = FreeTDS Driver
Driver = /usr/lib/x86_64-linux-gnu/odbc/libtdsodbc.so
Setup = /usr/lib/x86_64-linux-gnu/odbc/libtdsS.so
UsageCount = 1
# /etc/odbc.ini
[MySQLServer]
Description = SQL Server on Linux
Driver = FreeTDS
Server = 192.168.1.100
Port = 1433
Database = TestDB
TDS_Version = 7.4
嗯,这里要注意:TDS_Version别乱填。SQL Server 2008用7.3,2012及以上用7.4。填错了连不上,我当年就卡在这个坑里。
62.2 核心API:SQLConnect、SQLExecDirect、SQLFetch
环境配好了,咱们看看C代码里怎么用。ODBC的流程其实很固定:分配环境句柄 → 分配连接句柄 → 连接数据库 → 执行SQL → 取数据 → 清理。我画了张图,你看一眼就明白了:
看到没?整个流程就是一条直线,没有分支。但实际写代码时,每一步都要检查返回值,否则出错了你都不知道死在哪儿。
62.2.1 SQLConnect:建立连接
这个函数负责真正去连数据库。参数很简单:连接句柄、数据源名、用户名、密码。我习惯这么写:
SQLHENV henv = SQL_NULL_HENV;
SQLHDBC hdbc = SQL_NULL_HDBC;
SQLRETURN ret;
// 分配环境句柄
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("分配环境句柄失败\n");
return -1;
}
// 设置ODBC版本
ret = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
// 分配连接句柄
ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
// 连接数据库
ret = SQLConnect(hdbc, (SQLCHAR*)"MySQLServer", SQL_NTS,
(SQLCHAR*)"sa", SQL_NTS,
(SQLCHAR*)"YourPassword", SQL_NTS);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("连接失败!错误码: %d\n", ret);
// 这里可以调用SQLGetDiagRec获取详细错误
return -1;
}
printf("连接成功!\n");
这里有个细节:SQL_NTS表示字符串以null结尾。如果你传的是固定长度字符串,可以传实际长度。但我建议统一用SQL_NTS,省心。
62.2.2 SQLExecDirect:执行SQL语句
连接建立后,就可以执行SQL了。SQLExecDirect是最直接的方式——传一条SQL字符串进去,它立即执行。适合执行DDL(建表、删表)或者一次性查询。
SQLHSTMT hstmt = SQL_NULL_HSTMT;
SQLCHAR* sql = (SQLCHAR*)"SELECT id, name, age FROM users WHERE age > 18";
// 分配语句句柄
ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
if (ret != SQL_SUCCESS) {
printf("分配语句句柄失败\n");
return -1;
}
// 执行SQL
ret = SQLExecDirect(hstmt, sql, SQL_NTS);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("SQL执行失败\n");
SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
return -1;
}
你想想看,如果SQL语句里包含参数怎么办?比如根据用户输入动态查询。直接用SQLExecDirect拼字符串?那很容易被SQL注入。我建议用SQLPrepare + SQLBindParameter + SQLExecute这套组合,但那是另一个话题了。今天咱们先聚焦基础。
62.2.3 SQLFetch:逐行取数据
SQL执行完了,结果集在服务器端。怎么取到本地?用SQLFetch配合SQLGetData。前者移动游标到下一行,后者读取当前行的某一列。
SQLINTEGER id;
SQLCHAR name[64];
SQLSMALLINT nameLen;
SQLINTEGER age;
SQLLEN idInd, nameInd, ageInd;
// 绑定列(也可以不绑定,直接用SQLGetData)
// 这里演示不绑定的方式,更灵活
while (SQLFetch(hstmt) == SQL_SUCCESS) {
// 取第一列:id (整数)
ret = SQLGetData(hstmt, 1, SQL_C_SLONG, &id, 0, &idInd);
// 取第二列:name (字符串)
ret = SQLGetData(hstmt, 2, SQL_C_CHAR, name, sizeof(name), &nameInd);
// 取第三列:age (整数)
ret = SQLGetData(hstmt, 3, SQL_C_SLONG, &age, 0, &ageInd);
if (idInd == SQL_NULL_DATA || ageInd == SQL_NULL_DATA) {
printf("某列为NULL,跳过\n");
continue;
}
printf("ID: %d, Name: %s, Age: %d\n", id, name, age);
}
// 取完数据后,关闭游标
SQLCloseCursor(hstmt);
这里有个小技巧:SQLGetData的第四个参数是缓冲区,第五个参数是缓冲区大小。对于字符串,一定要给够空间,否则会被截断。我习惯给256字节,除非你确定字段长度。
SQLFetch逐行取。那样网络往返太多,性能很差。我一般用SQLSetStmtAttr设置游标类型为SQL_CURSOR_FORWARD_ONLY,然后一次取一批(比如1000行),用SQLFetchScroll配合SQL_FETCH_NEXT。但那是进阶玩法,今天先不展开。
62.3 完整示例:查询用户表
把上面几段拼起来,就是一个完整的C程序。我贴一个我项目里用过的简化版:
#include <stdio.h>
#include <sql.h>
#include <sqlext.h>
int main() {
SQLHENV henv;
SQLHDBC hdbc;
SQLHSTMT hstmt;
SQLRETURN ret;
// 1. 分配环境
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
// 2. 分配连接
SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
// 3. 连接
ret = SQLConnect(hdbc, (SQLCHAR*)"MySQLServer", SQL_NTS,
(SQLCHAR*)"sa", SQL_NTS,
(SQLCHAR*)"pass123", SQL_NTS);
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) {
printf("连接失败\n");
return 1;
}
// 4. 分配语句句柄并执行
SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
SQLExecDirect(hstmt, (SQLCHAR*)"SELECT id, name FROM users", SQL_NTS);
// 5. 取数据
SQLINTEGER id;
SQLCHAR name[64];
SQLLEN idInd, nameInd;
while (SQLFetch(hstmt) == SQL_SUCCESS) {
SQLGetData(hstmt, 1, SQL_C_SLONG, &id, 0, &idInd);
SQLGetData(hstmt, 2, SQL_C_CHAR, name, sizeof(name), &nameInd);
if (idInd != SQL_NULL_DATA && nameInd != SQL_NULL_DATA) {
printf("%d: %s\n", id, name);
}
}
// 6. 清理
SQLCloseCursor(hstmt);
SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
SQLDisconnect(hdbc);
SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
SQLFreeHandle(SQL_HANDLE_ENV, henv);
return 0;
}
编译的时候记得链接odbc库:
gcc -o sql_test sql_test.c -lodbc
62.4 常见错误与调试
写ODBC程序,90%的时间都在调试连接问题。我总结了几种最常见的错误:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| SQLConnect返回-1 | 数据源名不对,或FreeTDS配置错误 | 用tsql -C检查FreeTDS版本,用tsql -S MySQLServer -U sa -P pass测试直连 |
| SQLExecDirect返回-1 | SQL语法错误,或表不存在 | 把SQL语句打印出来,在SSMS里手动执行一遍 |
| SQLFetch返回100(SQL_NO_DATA) | 结果集为空,或游标已到末尾 | 检查SQL条件,或者先执行SELECT COUNT(*)确认有数据 |
| SQLGetData返回-1 | 列索引错误,或缓冲区太小 | 确认列序号从1开始,不是0;增大缓冲区 |
SQLGetDiagRec可以拿到详细的错误信息,比只看返回值有用得多。比如:
void print_error(SQLSMALLINT handleType, SQLHANDLE handle) {
SQLCHAR sqlState[6], msg[256];
SQLINTEGER nativeError;
SQLSMALLINT msgLen;
SQLGetDiagRec(handleType, handle, 1, sqlState, &nativeError, msg, sizeof(msg), &msgLen);
printf("SQL State: %s, Native Error: %d, Message: %s\n", sqlState, nativeError, msg);
}
好了,今天的内容就到这儿。ODBC这套东西,说白了就是一套标准接口,底层驱动帮你搞定协议转换。你只要记住:环境→连接→语句→取数→释放,这个顺序别搞反了,基本就不会出大问题。至于FreeTDS的配置细节,每个版本略有差异,遇到问题多看看/var/log/freetds.log,里面记录得很清楚。
公众号:蓝海资料掘金营,微信deep3321