C语言实现MySQL图片读写

40 阅读8分钟

内容介绍

Github - C语言实现MySQL图片读写

这篇博客的内容是手把手带你实现一个基于C的MySQL数据库连接组件,一般用于节点服务器(NS)中,操作图片的读写。主要思路是先用C语言(mysql.h)搭建起基本数据读写的功能,然后将图片文件转化为二进制数据,并且分段发送,从而实现读写功能。

准备工作
  • UNIX或类UNIX操作系统
  • 配置好MySQL,尤其是和读写权限有关的配置
  • C语言编程工具链gcc等
  • MySQL与C的连接器 --- libmysqlclient-dev

第一部分:二进制数据读写

一、打印

下面这个函数实现了打印我们的需要的表当中的所有数据。


int neil_mysql_select(MYSQL *handle) {
  if (mysql_real_query(handle, SQL_SELECT_TBL_USER, strlen(SQL_SELECT_TBL_USER))) {
    printf("mysql_real_query: %s\n", mysql_error(handle));
  }

  MYSQL_RES *res = mysql_store_result(handle);
  if (res == NULL) {
    printf("mysql_real_query: %s\n", mysql_error(handle));
    return -2;
  }

  int rows = mysql_num_rows(res);
  printf("rows: %d\n", rows);

  int fields = mysql_num_fields(res);
  printf("fields: %d\n",fields);

  //取出并且显示结果
  MYSQL_ROW row;
  while ((row = mysql_fetch_row(res))) {

    int i = 0;
    for (i = 0; i < fields; i++) {
      printf("%s\t", row[i]);
    }
    printf("\n");
  }
  
  
  mysql_free_result(res);

  return 0;

技术阐释: 上面实现的函数中传递了一个特别的参数叫做handle,名字叫做数据库连接句柄。有了它,我们才可以从NS连接到数据库,并且进行我们想要的操作,并且当操作不成功的时候,他可以通过mysql_error(handle)返回错误值。

第一个涉及到的mysql函数就是mysql_real_query(),这个函数可用于实现SQL查询,它接受handle,和我们所要执行的SQl查询语句,还有长度作为参数。之后我们就会看到为了实现SQL还有一个不同的办法, 叫做预处理语句。

第13行和16行的这两个函数分别是为了获取行和列的数目。

代码第21行这个while循环,就是不断地从我们想要的表中读取数据行,并且显示结果。

mysql_free_result(res)用于释放我们查询所占用的内存空间res。比较值的说的是这个res是我们在第7行创建的,用于存储我们查询所得的结果。

二、写入

下面这一个函数实现了二进制数据的写入。


int mysql_write(MYSQL *handle, char *buffer, int length) {

 if(handle == NULL || buffer == NULL || length <= 0) return -1;
 //statement
 MYSQL_STMT *stmt = mysql_stmt_init(handle);
 int ret = mysql_stmt_prepare(stmt, SQL_INSERT_IMG_USER, strlen(SQL_INSERT_IMG_USER));
 if (ret) {
   printf("mysql_stmt_prepar: %s\n", mysql_error(handle));
   return -2;
 }

 MYSQL_BIND param = {0};
 param.buffer_type = MYSQL_TYPE_LONG_BLOB;
 param.buffer = NULL;
 param.is_null = 0;
 param.length = NULL;

 ret = mysql_stmt_bind_param(stmt, &param);
 if (ret) {
   printf("mysql_stmt_bind_param: %s\n", mysql_error(handle));
   return -3;
 }
 //分段将数据发到服务器
 ret = mysql_stmt_send_long_data(stmt, 0, buffer, length);
   if (ret) {      
     printf("mysql_stmt_send_long_data: %s\n", mysql_error(handle));
     return -4;  
   }
 //执行:插入到服务器里
 ret = mysql_stmt_execute(stmt);
 if (ret) {
   printf("mysql_stmt_execute: %s\n", mysql_error(handle));
   return -5;  
 }
 //关闭
 ret = mysql_stmt_close(stmt);
 if (ret) {
   printf("mysql_stmt_close: %s\n", mysql_error(handle));
   return -6;  
 }

 return ret;
}



技术阐释:同样是为了实现SQL语句,与之前说过的mysql_real_query相对,在以上代码的第6行,有一个比较特别的预处理语句句柄的定义---statment,预处理语句使用场景

  • 安全性:有效防止注入攻击
  • 高性能:重复的查询,亦只需要一次编译
  • 二进制数据:更好支持大二进制数据类型BLOB

使用预处理语句可以减少输入的风险,并且减少网络传递负担。并且因为这些优点,预处理语句更加频繁的出现在实际使用当中。

我们之前使用的mysql_real_query实现简单便于调试,比较适合一次性的查询,比如我们之前查完就释放掉了内存空间。

SQL当中实现插入的语句当然是insert,我们在Mysql.c的最开始使用#define定义了它:#define SQL_INSERT_IMG_USER "insert tbl_user(u_name, u_gender, u_img) value('Alice', 'female', ?);",和其他SQL语句一样,我们这样定义是为了方便在下面的代码当中引用。

值得注意的是,对于u_img这个列而言,我们想要插入的数据是图片,可是图片并不是一个普通的字符串数据,图片数据的大小非常大,回想之前我们提到过的办法---将他转化为二进制文件,分段发送,这就是我们上面这个函数的思路。

以上代码的第13~17行定义了一个MYSQL_BIND,这是为了提前告诉服务器所需要传递参数的类型。因为我们在传递参数的时候使用的是SQL_INSERT_IMG_USER "insert tbl_user(u_name, u_gender, u_img) value('Alice', 'female', ?);而最后面这个?是需要bind的内容。13~17行的作用依次为:

MYSQL_BIND参数说明:

  • buffer_type = MYSQL_TYPE_LONG_BLOB - 指定参数为二进制大对象类型
  • buffer = NULL - 不直接提供数据缓冲区,使用分段发送
  • is_null = 0 - 明确表示该参数不是NULL值
  • length = NULL - 长度信息将在mysql_stmt_send_long_data中提供

之后的代码就是将预处理的语句statement执行,即分段发送二进制数据,完成后关闭statement。

三、读出

以下函数实现了二进制数据的读出,整体来看,结构和之前的二进制数据写入差不多,值得注意的是,38行的while(1)是为了处理多行数据,而其中的内层循环是为了从服务器接收分段的数据进入buffer,并且验证数据是否有丢失(MYSQL_DATA_TRUNCATED

//从数据库中读取出来
int mysql_read(MYSQL *handle, char *buffer, int length) {
  if(handle == NULL || buffer == NULL || length <= 0) return -1;
  
  MYSQL_STMT *stmt = mysql_stmt_init(handle);
  int ret = mysql_stmt_prepare(stmt, SQL_SELECT_IMG_USER, strlen(SQL_SELECT_IMG_USER));
  if (ret) {
    printf("mysql_stmt_prepar: %s\n", mysql_error(handle));
    return -2;
  }

  
  MYSQL_BIND result = {0};

  
  result.buffer_type = MYSQL_TYPE_LONG_BLOB;
  unsigned long total_length = 0;
  result.length = &total_length;

  ret = mysql_stmt_bind_result(stmt, &result);
  if (ret) {
    printf("mysql_stmt_bind_result: %s\n", mysql_error(handle));
    return -3;
  }
  
  ret = mysql_stmt_execute(stmt);  
  if (ret) {      
    printf("mysql_stmt_execute: %s\n", mysql_error(handle));
    return -4;  
  }

  ret = mysql_stmt_store_result(stmt);
  if (ret) {      
    printf("mysql_stmt_store_result: %s\n", mysql_error(handle));
    return -5;  
  }
  //多个数据
  while(1) {
    ret = mysql_stmt_fetch(stmt);
    if (ret != 0 && ret != MYSQL_DATA_TRUNCATED) {
      break;
    }
    int start = 0;
    //内层循环
    while(start < (int)total_length) {
      result.buffer = buffer + start; //结果集的buffer和存储图片的buffer是同一个
      result.buffer_length = 1;
      mysql_stmt_fetch_column(stmt, &result, 0, start);
      start += result.buffer_length;      
    } 
  }
  mysql_stmt_close(stmt);
  return total_length;
}

第二部分:图片读写

下面的目标是提供一个接口,用于在主函数main当中调用,可以直接操作我们想要的图片文件。

一、图片写入

int read_image(char *filename, char *buffer) {
  if (filename == NULL || buffer == NULL) return -1;
  
    FILE *fp = fopen(filename, "rb"); //fp指向文件的开头
    if (fp == NULL) {
      printf("fopen failed\n");
      return -2;
    }

    //file size
    //下面这个函数将fp指向末尾
    fseek(fp, 0, SEEK_END);
    int length = ftell(fp);//偏移量
    fseek(fp, 0, SEEK_SET);

    int size = fread(buffer, 1, length, fp);
    if (size != length) {
      printf("fread failed:%d\n", size);
      return -3;
    }

    fclose(fp);

    return size;
}

技术阐释:以上函数的作用就是从filename这个路径中获取我们想要的文件,并将其导入到buffer当中,然后,我们之前实现的二进制数据处理函数就可以顺利地从buffer当中找到需要发送到Mysql服务器的二进制图片数据。

你可能注意到了,从图片格式到二进制数据流这个转换的过程究竟是如何完成的?---答案是fopen,因为我们指定了一个fopen中的第二个参数为“rb”:

  • r代表read only,只读
  • b代表二进制

如果不指定b这个参数,则默认使用文本模式导入,一定要注意的是,图片格式一定不可以用文本模式导入,这会导致一些严重的格式问题,比如说换行符的转换。

文件模式说明:

  • "rb" - 二进制只读模式(图片读取)
  • "wb" - 二进制只写模式,会清空原文件
  • "wb+" - 二进制读写模式,会清空原文件 对于图片保存,使用"wb"更为合适

二、图片读取

几乎和图片写入对称,不多赘述。

int write_image(char *filename, char *buffer, int length) {
  if (filename == NULL || buffer == NULL || length <= 0) return -1;

  FILE *fp = fopen(filename, "wb+"); //fp指向文件的开头
  if (fp == NULL) {
    printf("fopen failed\n");
    return -2;
  }

  int size = fwrite(buffer, 1, length, fp);
  if (size != length) {
    printf("fwrite failed: %d\n", size);
    return -3;
  }

  fclose(fp);
  
  return size;
  
}

三、结果

测试环境:MacOS,如果你使用Linux,会有一些细节上的不同。

Model Name: MacBook Air

      Model Identifier: Mac14,2

      Chip: Apple M2

启动mysql

brew services start mysql

通过ifconfig查看自己的ip地址

en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	...
	inet 192.168.xxx.xxx netmask 0xfffffe00 broadcast 192.168.xxx.xxx
	...
	status: active

通过workbench连接后,我们用sql语句查看需要操作的数据库,发现目前没有内容,均为null

image.png

现在我们希望插入名字为Emacs,性别为unkown,照片Emacs.png如下:

Emacs.png

别忘了在代码的最开始配置好文件路径、需要执行的sql语句内容

编译Mysql.c,注意,这里你需要自己找到你的lmysqlclient在哪,然后替换相应命令,linux应该使用gcc

clang -o Mysql Mysql.c -I/opt/homebrew/opt/mysql-client/include -L/opt/homebrew/opt/mysql-client/lib -lmysqlclient

运行

neil@YuandeMacBook-Air Toys % ./Mysql 

case : mysql --> insert

rows: 1

fields: 4

12 Alice female (null)

case : mysql --> delete

case : mysql --> read image

rows: 1

fields: 4

13 Emacs unkown ?PNG

  

case : mysql --> read mysql and write image

成功之后,打开workbench,select出之前的表,可以看到成功插入了新行,写入功能无问题。

image.png

我们的Mysql.c当中还有从数据库中读取的功能,代码中将读取出的文件保存到了Mysql.c所在的文件目录下,名字为a.png,ls当前目录,发现确实读取出来了:

image.png

用图形界面查看,确认取出的图片无异常。

image.png