基於 MySQL C API (libmysqlclient) 開發高可靠客戶端,需規避經典陷阱。本文結合官方文檔與社區血淚經驗,梳理十大核心錯誤及修正模式。
錯誤:直接遍歷 MYSQL_ROW 做數值運算。
風險:列類型不匹配(如 VARCHAR 存數字)、NULL 值導致崩潰、字符集轉換異常。
正確模式:
MYSQL_RES *res = mysql_store_result(conn);
MYSQL_FIELD *field = mysql_fetch_field_direct(res, col_idx);
/* 1. 存在性檢查 */
if (col_idx >= mysql_num_fields(res)) { fprintf(stderr, "Column index OOB\n"); return; }
/* 2. 類型檢查:確保數值類型 */
if (!(field->type == MYSQL_TYPE_LONG || field->type == MYSQL_TYPE_LONGLONG ||
field->type == MYSQL_TYPE_DOUBLE || field->type == MYSQL_TYPE_DECIMAL)) {
fprintf(stderr, "Column %d not numeric (type=%d)\n", col_idx, field->type);
return;
}
/* 3. 統計計算 */
mysql_data_seek(res, 0); // 重置指針,支持多次遍歷
while ((row = mysql_fetch_row(res))) {
if (row[col_idx]) {
double val = strtod(row[col_idx], NULL);
sum += val; cnt++;
} else null_cnt++;
}
錯誤:sprintf(sql, "SELECT * FROM t WHERE name='%s'", user_input);
風險:單引號截斷、二進制數據不可打印、編碼不一致。
正確模式:預處理語句
MYSQL_STMT *stmt = mysql_stmt_init(conn);
mysql_stmt_prepare(stmt, "SELECT * FROM t WHERE name=?", strlen("SELECT * FROM t WHERE name=?"));
MYSQL_BIND bind[1] = {0};
bind[0].buffer_type = MYSQL_TYPE_VAR_STRING;
bind[0].buffer = (void*)user_input;
bind[0].buffer_length = strlen(user_input);
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
兼容舊代碼:必須用 mysql_real_escape_string()(注意:需已建立連接的 MYSQL* 上下文)
char *to = malloc(from_len * 2 + 1);
mysql_real_escape_string(conn, to, from, from_len);
// 使用 to 構建 SQL
free(to);
存入:
FILE *fp = fopen("image.png", "rb");
fseek(fp, 0, SEEK_END); long fsize = ftell(fp); fseek(fp, 0, SEEK_SET);
char *buf = malloc(fsize); fread(buf, 1, fsize, fp); fclose(fp);
MYSQL_STMT *stmt = mysql_stmt_init(conn);
mysql_stmt_prepare(stmt, "INSERT INTO images(data) VALUES(?)", ...);
MYSQL_BIND bind[1] = {0};
bind[0].buffer_type = MYSQL_TYPE_BLOB;
bind[0].buffer = buf;
bind[0].buffer_length = fsize;
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
free(buf);
讀出:
MYSQL_BIND bind[1] = {0};
bind[0].buffer_type = MYSQL_TYPE_BLOB;
bind[0].buffer = malloc(max_size);
bind[0].buffer_length = max_size;
unsigned long length;
bind[0].length = &length;
mysql_stmt_bind_result(stmt, bind);
mysql_stmt_fetch(stmt);
// 使用 bind[0].buffer 前 length 字節
fwrite(bind[0].buffer, 1, length, stdout);
關鍵:mysql_fetch_lengths() 獲取真實長度,**不可**按 C 字符串 (以 \0 結尾) 處理。
// 等價於 DESCRIBE tbl
mysql_query(conn, "SHOW COLUMNS FROM tbl_name");
// 或
mysql_query(conn, "SHOW FIELDS FROM tbl_name LIKE 'col_name'"); // 單列
MYSQL_RES *res = mysql_store_result(conn);
while ((row = mysql_fetch_row(res))) {
// row[0]=Field, [1]=Type, [2]=Null, [3]=Key, [4]=Default, [5]=Extra
}
錯誤 1:傳遞未初始化指針
MYSQL *conn; // 未初始化,棧上隨機值
mysql_init(conn); // 傳遞野指針 → 內存踐踏
錯誤 2:全局靜態指針未賦值
MYSQL *conn; // 全局默認 NULL
mysql_init(conn); // 參數為 NULL → 內部分配新結構
// 但 conn 仍為 NULL,後續傳遞 conn 給 mysql_real_connect 導致崩潰
正確唯一模式:
MYSQL *conn = mysql_init(NULL); // 顯式傳 NULL,獲取返回指針
if (!conn) { /* 分配失敗 */ }
if (!mysql_real_connect(conn, host, user, pass, db, port, unix_socket, flags)) {
fprintf(stderr, "Connect failed: %s\n", mysql_error(conn));
mysql_close(conn);
return;
}
錯誤:
MYSQL_RES *res = mysql_store_result(conn);
while ((row = mysql_fetch_row(res))) { ... } // res 可能為 NULL
修正:
MYSQL_RES *res = mysql_store_result(conn);
if (!res) {
if (mysql_field_count(conn) == 0) { /* 非查詢語句,受影響行數 */ }
else { fprintf(stderr, "Store result failed: %s\n", mysql_error(conn)); }
return;
}
while ((row = mysql_fetch_row(res))) { ... }
mysql_free_result(res);
現象:printf("%s", row[i]) 某些 libc 輸出 (null),某些直接 Segfault。
統一防護:
while ((row = mysql_fetch_row(res))) {
for (i = 0; i < num_fields; i++) {
printf("%s\t", row[i] ? row[i] : "NULL");
}
puts("");
}
或提前從 MYSQL_FIELD 獲取 IS_NOT_NULL() 信息。
錯誤:
char *to; // 未分配
mysql_real_escape_string(conn, to, from, len); // 踐踏隨機內存
規範:目標緩衝區 ≥ from_len * 2 + 1,調用者負責分配/釋放。
mysql_autocommit(conn, 0); // 關閉自動提交
mysql_query(conn, "INSERT ...");
mysql_query(conn, "UPDATE ...");
if (all_ok) mysql_commit(conn);
else mysql_rollback(conn);
mysql_autocommit(conn, 1); // 恢復
注意:DDL (CREATE/ALTER/DROP) 隱式提交,不可回滾。
// 正確順序
mysql_free_result(res);
mysql_stmt_close(stmt);
mysql_close(conn); // 最後關閉連接
長連接池:
- 定期 mysql_ping() 檢測存活
- 空閒超時前主動 mysql_close() 重建
- 多線程環境:**連接不可跨線程共享**,每線程獨享或使用線程池庫
| # | 檢查點 | 驗證命令/工具 |
|---|---|---|
| 1 | 元數據驗證後再計算 | Code Review |
| 2 | 全參數化查詢 / 逃逸函數 | sqlmap 掃描、靜態分析 |
| 3 | BLOB 讀寫用長度而非 \0 | Valgrind、ASan |
| 4 | SHOW COLUMNS 獲取結構 | 單元測試 |
| 5 | mysql_init(NULL) 接收返回值 | 編譯警告 -Wall |
| 6 | mysql_store_result 非 NULL 判斷 | 覆蓋率測試 |
| 7 | row[i] 為 NULL 保護 | Fuzz 測試 |
| 8 | escape 目標緩衝區預分配 | 靜態分析 |
| 9 | 事務邊界明確、DDL 隱式提交 | 壓測驗證 |
| 10 | 資源逆序釋放、連接不跨線程 | Helgrind、TSan |