MariaDB(MySQL) C API Programming

kafkaaaa·2023년 9월 8일
1

DB

목록 보기
1/1
post-thumbnail

#1. MariaDB 설치

sudo apt install mariadb-server

sudo apt install mariadb-client

sudo mysql_secure_installation

버전, 포트 번호 확인

sudo mysql -u root -p

설치 완료.

포트번호도 확인해보자. 기본 설정은 3306번
show global variables like 'PORT';

설치한DB 버전도 확인해보자.
mysql -V; 또는 mysql -Version;

DB에서 확인하려면
select version();


#2. MySQL C API 개발환경 구축

  • 라이브러리 설치
    sudo apt-get install libmysqlclient-dev

  • 테스트 코드 작성 (Client 버전 확인)

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("MySQL Client Version: %s\n", mysql_get_client_info());
    exit(0);
}
  • 테스트 코드 결과
    • 처음에 확인한 버전은 서버의 버전이라 다른게 정상이다.

🤦‍♂️ 저같은 오류가 나오시는분들...

command-line error: langauge modes specified are incompatible

😥 삽질한 방법 1

gcc -o version version.c $(mysql_config --libs)

또는
gcc -o version version.c -lmysqlclient

-> 이렇게 하면 라이브러리만 찾고 mysql.h 헤더파일은 당연히 어디 있는지 모를 것이다.

😥 삽질한 방법 2

  1. find /usr/ -name 'mysql.h' 헤더파일 위치 찾기.
  1. /usr/include/mysql/mysql.h
  2. vi ~/.bashrc 열고
  3. 아래 내용 추가 하기!

-> LD_LIBRARY_PATH는 응용프로그램이 공유 라이브러리를 사용할때 어느 경로에 있는지 알려주는 환경 변수로
리눅스는 사용자가 만든 공유 라이브러리가 어느 디렉토리에 있는지 모르기 때문에 LD_LIBRARY_PATH에 등록해주어야 합니다.
공유 라이브러리가 아니니까 당연히 안 된다.

😥 삽질한 방법 3

include path에 직접 작성

-> 이 방법은 왜 안되는지 잘 모르겠습니다!

😊 해결한 방법 😊

c99 -I/usr/include/mysql/ -o myfile myfile.c -lmysqlclient

#1. -I/usr/include/mysql/

include 하려는 파일이 표준 경로에 없다면 컴파일러가 찾을 수 있도록
해당 파일의 위치를 넘겨줘야 한다.

find /usr/ -name 'mysql.h' 를 했을 때
/usr/include/mysql/mysql.h 이 경로에 있으므로

다른 경로에 설치된 라이브러리를 사용하려면 -I 옵션을 추가해야 한다.

  • -I 옵션 (대문자 아이) = 헤더 파일이 위치한 directory를 컴파일러에게 알려줌
  • (-I 옵션 생략) = gcc의 기본 include 경로 = /usr/include
  • /usr/include = C에서 범용적으로 필요한 헤더파일들이 저장되어 있음

#2. -lmysqlclient

링크 명령에서 -l 옵션을 사용하여 MySQL 클라이언트를 링크해야 한다.

  • -l 옵션(소문자 엘) = 라이브러리 파일의 이름을 지정해줌.
    • 일반적으로 lib접두사와 .so나 .a 같은 확장자는 제외함
    • -L 옵션 = 라이브러리 파일이 위치한 directory를 지정함

📌 VScode 빨간 메시지 해결(?)

다음과 같이 설정하면 오류를 끌?수 있다.
ctrl+shift+P / Configuration Provider / none

https://github.com/microsoft/vscode-cpptools/issues/9684



#3. MySQL C API 사용해보기

https://zetcode.com/db/mysqlc/
위 페이지의 내용을 순서대로 실습

#3-1. DB 생성

MySQL DB 서버에 접속하고 testdb라는 DB를 만드는 코드

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "%s\n", mysql_error(con));
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "root", "root_passwd", NULL, 0, NULL, 0) == NULL)
  {
      fprintf(stderr, "%s\n", mysql_error(con));
      mysql_close(con);
      exit(1);
  }

  if (mysql_query(con, "CREATE DATABASE testdb"))
  {
      fprintf(stderr, "%s\n", mysql_error(con));
      mysql_close(con);
      exit(1);
  }

  mysql_close(con);
  exit(0);
}

📝 코드 설명

MYSQL *con = mysql_init(NULL);
  • MySQL 서버를 핸들링할 객체를 메모리에 할당하고 초기화

if (mysql_real_connect(con, "localhost", "root", "root_passwd",
    NULL, 0, NULL, 0) == NULL)
{
    fprintf(stderr, "%s\n", mysql_error(con));
    mysql_close(con);
    exit(1);
}

mysql_connect() 함수 대신 mysql_real_connect를 사용해야 한다.

  • mysql_real_connect( )의 마지막 인자는 Client Flag 인데,
    DB접속 시 각종 옵션을 설정할 수 있다.
  • 실제로 MySQL 서버에 접속하는 함수
    • con = 위에서 생성한 connection handler
    • localhost = host name
    • root = user name
    • password = user password
    • (NULL, 0, NULL, 0) = (db이름, 포트번호, 유닉스 소켓, client 플래그)
    • (DB 생성을 위해서는) superuser 권한 필요


  if (mysql_query(con, "CREATE DATABASE testdb"))
  {
      fprintf(stderr, "%s\n", mysql_error(con));
      mysql_close(con);
      exit(1);
  }

  mysql_close(con);
  • 쿼리문 실행 후 DB연결 해제
    (프로그램이 종료되면서 해제되기는 하지만 명시적으로 해제해주는게 좋다고 합니다)

👀 실행 결과


#3-2. 사용자 생성과 권한부여 & 테이블 생성 & 데이터 삽입

사용자 생성 및 권한 부여

CREATE USER [name]@localhost IDENTIFIED BY '[password]';
GRANT ALL ON testdb.* to [name]@localhost;

테이블 생성 및 데이터 삽입 해보기

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "%s\n", mysql_error(con));
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "DROP TABLE IF EXISTS cars")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "CREATE TABLE cars(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), price INT)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(1,'Audi',52642)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(2,'Mercedes',57127)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(3,'Skoda',9000)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(4,'Volvo',29000)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(5,'Bentley',350000)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(6,'Citroen',21000)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(7,'Hummer',41400)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO cars VALUES(8,'Volkswagen',21600)")) {
      finish_with_error(con);
  }

  mysql_close(con);
  exit(0);
}

📝 코드 설명

  • finish_with_error(con)
    • MySQL 사용 중 발생하는 에러 처리를 별도 함수로 작성
    • 쿼리문에서 에러 발생시 화면에 에러를 표시하고 db접속 해제 및 종료
  • 나머지는 cars라는 테이블을 만들고 mysql_query( )로 샘플 데이터 집어넣기 반복

👀 실행 결과

c99 -I/usr/include/mysql/ -o create_populate create_populate.c -lmysqlclient

./create_populate

sudo mysql -u root -p


#3-3. Retrieve data (데이터 가져오기)

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT * FROM cars"))
  {
      finish_with_error(con);
  }

  MYSQL_RES *result = mysql_store_result(con);

  if (result == NULL)
  {
      finish_with_error(con);
  }

  int num_fields = mysql_num_fields(result);

  MYSQL_ROW row;

  while ((row = mysql_fetch_row(result)))
  {
      for(int i = 0; i < num_fields; i++)
      {
          printf("%s ", row[i] ? row[i] : "NULL");
      }

      printf("\n");
  }

  mysql_free_result(result);
  mysql_close(con);

  exit(0);
}

📝 코드 설명

  • MYSQL_RES *result = mysql_store_result(con);

    • MYSQL_RES는 쿼리 결과를 담는 구조체로 정의되어 있습니다 (mysql.h)

    MYSQL_RES

  • 앞에서 SELECT * FROM Cars 결과를 가져옵니다.

  • MYSQL_ROW 는 테이블의 한 줄(Row)의 데이터를 담고 있습니다.


👀 실행 결과


#3-4. Last Inserted Row ID

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "DROP TABLE IF EXISTS writers"))
  {
      finish_with_error(con);
  }

  char *sql = "CREATE TABLE writers(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))";

  if (mysql_query(con, sql))
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO writers(name) VALUES('Leo Tolstoy')"))
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO writers(name) VALUES('Jack London')"))
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO writers(name) VALUES('Honore de Balzac')"))
  {
      finish_with_error(con);
  }

  int id = mysql_insert_id(con);

  printf("The last inserted row id is: %d\n", id);

  mysql_close(con);
  exit(0);
}

📝 코드 설명

  • mysql_insert_id(con)
    • 마지막 행의 id를 얻어온다.
    • ※ AUTO_INCREMENT 가 정의된 테이블에서만 사용 가능
    • char *sql = "CREATE TABLE writers(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))";

👀 실행 결과


#3-5. Column Header

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT * FROM cars LIMIT 3"))
  {
      finish_with_error(con);
  }

  MYSQL_RES *result = mysql_store_result(con);

  if (result == NULL)
  {
      finish_with_error(con);
  }

  int num_fields = mysql_num_fields(result);

  MYSQL_ROW row;
  MYSQL_FIELD *field;

  while ((row = mysql_fetch_row(result)))
  {
      for(int i = 0; i < num_fields; i++)
      {
          if (i == 0)
          {
             while(field = mysql_fetch_field(result))
             {
                printf("%s ", field->name);
             }

             printf("\n");
          }

          printf("%s  ", row[i] ? row[i] : "NULL");
      }
  }

  printf("\n");

  mysql_free_result(result);
  mysql_close(con);

  exit(0);
}

📝 코드 설명

  • MYSQL_FIELD 구조체
    • 필드 이름, 길이, 타입 등이 저장되어 있다.
    • 필드에 실제 저장된 값은 포함 X
    • 실제 데이터는 MYSQL_ROW 구조체에 저장.

  • (i == 0) = 첫 번째 행 = Column Header

👀 실행 결과



#3-6. Multiple SQL statements in one Query

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  int status = 0;

  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, CLIENT_MULTI_STATEMENTS) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT name FROM cars WHERE id=2;\
      SELECT name FROM cars WHERE id=3;SELECT name FROM cars WHERE id=6"))
  {
      finish_with_error(con);
  }

  do {
      MYSQL_RES *result = mysql_store_result(con);

      if (result == NULL)
      {
          finish_with_error(con);
      }

      MYSQL_ROW row = mysql_fetch_row(result);

      printf("%s\n", row[0]);

      mysql_free_result(result);

      status = mysql_next_result(con);

      if (status > 0) {
          finish_with_error(con);
      }

  } while(status == 0);

  mysql_close(con);
  exit(0);
}

📝 코드 설명

  • ※ 다중 쿼리문을 위해 CLIENT_MULTI_STATEMENTS 값을 세팅해야 한다.
    (이전 예제들에서는 0으로 연결했는데 0=default=disable)

  • 아래 쿼리문에서 \는 개행만을 의미하고, 세미콜론으로 다중 문장을 구분한다.
    • 여러 SQL 쿼리문을 한 번에 실행할 수 있다.

👀 실행 결과



#3-7. 이미지를 DB에 넣기 (BLOB 타입 사용)

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{

  FILE *fp = fopen("test.jpg", "rb");

  if (fp == NULL)
  {
      fprintf(stderr, "cannot open image file\n");
      exit(1);
  }

  fseek(fp, 0, SEEK_END);

  if (ferror(fp)) {

      fprintf(stderr, "fseek() failed\n");
      int r = fclose(fp);

      if (r == EOF) {
          fprintf(stderr, "cannot close file handler\n");
      }

      exit(1);
  }

  int flen = ftell(fp);

  if (flen == -1) {

      perror("error occurred");
      int r = fclose(fp);

      if (r == EOF) {
          fprintf(stderr, "cannot close file handler\n");
      }

      exit(1);
  }

  fseek(fp, 0, SEEK_SET);

  if (ferror(fp)) {

      fprintf(stderr, "fseek() failed\n");
      int r = fclose(fp);

      if (r == EOF) {
          fprintf(stderr, "cannot close file handler\n");
      }

      exit(1);
  }

  char data[flen+1];

  int size = fread(data, 1, flen, fp);

  if (ferror(fp)) {

      fprintf(stderr, "fread() failed\n");
      int r = fclose(fp);

      if (r == EOF) {
          fprintf(stderr, "cannot close file handler\n");
      }

      exit(1);
  }

  int r = fclose(fp);

  if (r == EOF) {
      fprintf(stderr, "cannot close file handler\n");
  }

  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  char chunk[2*size+1];
  mysql_real_escape_string(con, chunk, data, size);

  char *st = "INSERT INTO images(id, data) VALUES(1, '%s')";
  size_t st_len = strlen(st);

  char query[st_len + 2*size+1];
  int len = snprintf(query, st_len + 2*size+1, st, chunk);

  if (mysql_real_query(con, query, len))
  {
      finish_with_error(con);
  }

  mysql_close(con);
  exit(0);
}

※ 예제 실행을 하려면 test.jpg 파일이 같은 디렉토리에 있어야 하고,

CREATE TABLE images(id INT PRIMARY KEY, data MEDIUMBLOB);

위와 같이 images 테이블을 생성해야 한다.


📝 코드 설명

  • 이미지는 Binary Data이며 MySQL에서는 BLOB(Binary Large Object) 라는 특수한 데이터 타입을 사용한다.

  • 위 예제에서는 MEDIUMBLOB 타입으로 최대 16MB 크기의 이미지까지 저장할 수 있다.

  • fseekftell 함수를 통해 이미지 파일의 크기를 알아낸다.
  • mysqL_real_escape_string(con, chunk, data, size) :
    • 백슬래시 (\)를 추가해서 이스케이프 시퀀스를 만들어줌
    • SQL Injection 공격 예방
  • snpritnf : 설정한 format으로 buffer에 출력하는 함수인데,
    sprintf와 다른 점은 string의 길이를 인자로 넘겨서
    길이를 초과하는 부분은 자르게 되고 따라서 buffer overflow를 막을 수 있다.

  • mysql_real_query() ?
    • mysql_query() 는 NULL로 끝나는 문자열 쿼리를 실행하는데,
      이 함수는 Binary Data를 포함하는 쿼리의 경우 사용할 수 없다.
      -> Binary Data에는 '\0'문자가 포함될 수 있는데 이것을 문자열의 끝으로 해석해버리기 때문.

👀 실행 결과

c99 -I/usr/include/mysql -o insert_image insert_image.c -lmysqlclient

엄청난 양의 바이너리가 DB에 저장된다.



#3-8. 이미지 추출

3-7에서 DB에 넣은 이미지(test.jpg)를 test2.jpg로 출력해보자.

#include <mysql.h>
#include <stdio.h>
#include <stdlib.h>

void finish_with_error(MYSQL *con)
{
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv)
{
  FILE *fp = fopen("test2.jpg", "wb");

  if (fp == NULL)
  {
      fprintf(stderr, "cannot open image file\n");
      exit(1);
  }

  MYSQL *con = mysql_init(NULL);

  if (con == NULL)
  {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, "localhost", "abcde", "abcde",
          "testdb", 0, NULL, 0) == NULL)
  {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT data FROM images WHERE id=1"))
  {
      finish_with_error(con);
  }

  MYSQL_RES *result = mysql_store_result(con);

  if (result == NULL)
  {
      finish_with_error(con);
  }

  MYSQL_ROW row = mysql_fetch_row(result);
  unsigned long *lengths = mysql_fetch_lengths(result);

  if (lengths == NULL) {
      finish_with_error(con);
  }

  fwrite(row[0], lengths[0], 1, fp);

  if (ferror(fp))
  {
      fprintf(stderr, "fwrite() failed\n");
      mysql_free_result(result);
      mysql_close(con);

      exit(1);
  }

  int r = fclose(fp);

  if (r == EOF) {
      fprintf(stderr, "cannot close file handler\n");
  }

  mysql_free_result(result);
  mysql_close(con);

  exit(0);
}

📝 코드 설명

  • MYSQL_ROW row = mysql_fetch_row(result)
    • row는 이미지 binary data를 가지고 있다.
  • unsigned long *lengths = mysql_fetch_lengths(result);
    • 이미지 binary data의 길이를 알아낸다.

👀 실행 결과

귀여운 펭귄 이미지가 복사되었다 😊



Ref

https://zetcode.com/db/mysqlc/
https://stackoverflow.com/questions/14604228/mysql-h-file-cant-be-found
https://blogger.pe.kr/885/
https://it.gwangtori.com/42
https://blog.naver.com/PostView.nhn?blogId=xenostream&logNo=120197609525
https://devanix.tistory.com/215
http://www.innodbcluster.com/?depth=23080401
http://www.innodbcluster.com/?depth=230806
https://dev.mysql.com/doc/c-api/8.0/en/mysql-real-connect.html

profile
일모도원

0개의 댓글