[TOC]

​ 众所周知,服务端程序Server需要保存用户数据,所以需要进行数据库操作,常见的是使用MySQL数据库连接。MySQL数据库是C/S模型,程序每次操作数据库都需要利用Client组件连接MySQL的服务端。

​ 若每次用户请求,Server都需要新建一个数据库连接,请求结束后释放该数据库连接,那么当用户请求连接过多时,效率就会非常低。所以Server一般采用类似线程池的做法,构建一个数据库连接池SqlConnPool,预先生成一些数据库连接放在那里供用户请求使用。

一、创建MySQL开发环境

1、安装libmysqlclient程序

​ 现在假设读者已经在ubuntu上成功安装了MySQL-server程序。

​ 现在我们想要在C++编写的服务端程序中嵌入MySQL的数据库连接功能,我们需要下载MySQL提供的MySQL C API库,这样就可以在C/C++程序中开发可以连接MySQL-server的程序功能。

​ 执行指令:sudo apt install libmysqlclient-dev,成功安装后将可以使用mysql/mysql.h头文件。

2、在VSCode中配置

安装完后,如果读者使用的是VSCode编辑器,则需要在tasks.json来配置头文件和动态库

"tasks": [
	{
		...,
        "args": [
        	// 新增开始
            "-I",
            "/usr/include/mysql", // 包含头文件目录
            "-L",
            "/usr/lib64/mysql", // 动态库目录
            "-lmysqlclient" // 导入的哪个库
            // 新增结束
        ],
        ...,
	}
]

window10 下tasks.json文件配置类似:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "cppbuild",
      "label": "C/C++: cpp.exe 生成活动文件",
      "command": "L:\\mingw64\\bin\\cpp.exe",
      "args": [
        "-g",
        "${file}",
        "-o",
        "${fileDirname}\\${fileBasenameNoExtension}.exe"
      ],
      "options": {
        "cwd": "${workspaceFolder}"
      },
      "problemMatcher": [
        "$gcc"
      ],
      "group": "build",
      "detail": "编译器: L:\\mingw64\\bin\\cpp.exe"
    }
  ]
}

linux下c_cpp_properties.json文件:

{
  "configurations": [
    {
      "name": "Linux",
      "includePath": [
        "${workspaceFolder}/**",
        "/usr/include/mysql/**" // 只修改这个地方,其他地方默认
      ],
      "defines": [],
      "compilerPath": "/usr/bin/gcc",
      "cStandard": "gnu17",
      "cppStandard": "gnu++14",
      "intelliSenseMode": "linux-gcc-x64"
    }
  ],
  "version": 4
}

window10 下c_cpp_properties.json文件:

{
  "configurations": [
    {
      "name": "Win32",
      "includePath": [
        "${workspaceFolder}/**",
        "F:/MySql/mysql-5.7.24-winx64/include/**"
      ],
      "defines": [
        "_DEBUG",
        "UNICODE",
        "_UNICODE"
      ],
      "compilerPath": "L:\\mingw64\\bin\\gcc.exe",
      "cStandard": "gnu17",
      "cppStandard": "gnu++14",
      "intelliSenseMode": "windows-gcc-x64"
    }
  ],
  "version": 4
}

3、保证MySQL在运行状态

// 启动MySQL
service mysqld start 
// 停止MySQL
service mysqld stop 
// 重启MySQL
service mysqld restart
// 查看MySQL运行状态
systemctl status mysql.service

4、查看和修改MySQL的用户名和密码

第一次安装MySQL后,是如法直接用root用户登录的,会报错Access denied for user 'root'@'localhost'。虽然可以使用sudo来进入,但是此时MySQL配置文件中的默认用户名和密码没变:

// 命令
sudo cat /etc/mysql/debian.cnf

// 配置文件内容
[client]
host     = localhost
user     = debian-sys-maint
password = ***(这里我隐藏了,是一串英文和数字)
socket   = /var/run/mysqld/mysqld.sock
[mysql_upgrade]
host     = localhost
user     = debian-sys-maint
password = ***(这里我隐藏了,是一串英文和数字)
socket   = /var/run/mysqld/mysqld.sock

5、在MySQL中建立数据库

需要先配置好对应的数据库

// 建立一个数据库
create database webserver;

// 切换数据库
USE webserver;
// 创建user表
CREATE TABLE user(
    username char(50) NULL,
    password char(50) NULL
)ENGINE=InnoDB;

// 添加数据
INSERT INTO user(username, password) VALUES('name', 'password');

二、实现连接池功能

​ 首先导入依赖文件#include<mysql/mysql.h>,该文件包含MYSQL类的相关API。

1、初始化连接池和关闭连接池

​ 本文要开发的是一个数据库连接池SqlConnPool,池中有多个数据库连接。首先我们要维护三个变量:最大连接数MAX_CONN_,当前可用连接数FREE_CONN_和当前已用连接数USED_CONN_。然后所有的数据库连接要保存在一个容器中,这里使用队列容器,也可以使用链表等其他容器。

int MAX_CONN_;// 最大连接数
int USED_CONN_;//当前已用连接数
int FREE_CONN_;//当前可用连接数
queue<MYSQL *> connQue_;//存储数据库连接的队列容器

(1)相关API介绍

​ 初始化和关闭单个数据库连接的相关API:

​ 首先是初始化和关闭MYSQL C API库的操作:mysql_library_init()和mysql_library_end()。该对API一般放在所有的数据库操作之前喝结束后使用。

​ (1)初始化一个MYSQL连接的实例对象:MYSQL * mysql_init(MYSQL *mysql)

​ 该API的含义:若输入NULL,该函数将分配、初始化、并返回新对象,否则直接初始化对象并返回对象的地址;分配内存不足时,返回NULL。

​ (2)接着建立一个和MySQL-server的连接:MYSQL* mysql_real_connect(MYSQL *mysql,const char *host,const char *user, const char *passwd, const char *db, unsigned int port,const char *unix_socket,unsigned long client_flag)

​ MYSQL *为mysql_init函数返回的指针;host为NULL或 “localhost”,后者链接本地计算机;user是账户名,一般为“root”;passwd为密码,一般为NULL;db是数据库名称,为NULL时使用默认的test数据库;port为端口号;unix_socket为unix连接方式,NULL时表示不使用socket或者管道机制;client_flag一般为0。如果连接成功,返回第一个参数,否则返回NULL。

​ 关闭一个数据库连接的操作:void mysql_close(MYSQL *sock)

(2)初始化连接池

​ 在初始化的时候我们首先建立connSize个数据库连接:

void InitPool(const char* host, int port,
     const char* user,const char* pwd, 
     const char* dbName,
     int connSize = 10) {
    for (int i = 0; i < connSize; i++) {
        MYSQL *sql = nullptr;
        // 初始化一个数据库连接
        sql = mysql_init(sql);
        if (!sql) cout<<"MySql init error!"<<endl;
        // 建立一个数据库连接
        sql = mysql_real_connect(sql, host,user, pwd,dbName, port, nullptr, 0);
        if (!sql) cout<<"MySql Connect error!")<<endl;
        // 保存数据库连接
        connQue_.push(sql);
    }
    MAX_CONN = connSize;
}

(3)关闭连接池

​ 同时,关闭数据库连接池中所有连接的代码如下:

void ClosePool() {
    // 遍历队列
    while(!connQue_.empty()) {
        // 元素出队
        auto item = connQue_.front();
        connQue_.pop();
        // 关闭连接
        mysql_close(item);
    }
    mysql_library_end();//关闭MySQL的使用    
}

2、获取连接和释放连接

​ 接下来介绍对连接池的2种操作(获取和释放),它们本质上就是队列容器的出队和入队。但是由于该方法可能同时被多个线程操作,所以需要用到锁机制。

​ 这里我们使用信号量来控制并发线程的数量不超过连接池的数量,通过互斥锁来控制同一时刻只有一个线程可以进行连接池的获取或释放操作。

​ 初始化信号量和互斥锁:

#include <semaphore.h>//信号量
#include <mutex>//互斥锁
using namespace std;

sem_t semId_;//定义一个信号量变量
sem_init(&semId_, 0, MAX_CONN_);//初始化信号量

mutex mtx_;//互斥量

(1)获取连接

MYSQL* SqlConnPool::GetConn() {
    MYSQL *conn = nullptr;
    if(connQue_.empty()){
        cout<<"SqlConnPool busy!"<<endl;
        return conn;
    }
    // 信号量-1
    sem_wait(&semId_);
    {
        lock_guard<mutex> locker(mtx_);//在该声明周期内上锁
        // 一个连接出队
        conn = connQue_.front();
        connQue_.pop();
    }
    return conn;
}

(2)释放连接

void FreeConn(MYSQL* sql) {
    assert(sql);
    lock_guard<mutex> locker(mtx_);//在该声明周期内上锁
    connQue_.push(sql);//入队
    sem_post(&semId_);//信号量+1
}

三、连接池类

头文件:

/*
 * @Author: MRL Liu
 * @Date: 2022-03-21 22:46:06
 * @Description: SqlConnPool的头文件
 * @LastEditTime: 2022-03-23 17:41:33
 * @FilePath: \WebServer\pool\sqlconnpool.h
 */
#ifndef MRL_SQL_CONN_POOL_H_
#define MRL_SQL_CONN_POOL_H_

#include <mysql/mysql.h>//MSQL数据库
#include <queue>//队列容器
#include <mutex>//互斥锁
#include <semaphore.h>//信号量
#include <thread>
#include<assert.h>
#include <string>
//#include "../log/log.h"
#include<iostream>
using namespace std;

// MySQL数据库连接池类
class SqlConnPool
{
private:
    int MAX_CONN_;// 连接池最大连接数
    int USED_CONN_;//连接池已用连接数
    int FREE_CONN_;//连接池空闲连接数
    queue<MYSQL *> connQue_;// MySQl连接的队列容器
    sem_t  semId_;//信号量
    mutex mtx_;//互斥量
private:
    SqlConnPool();// 构造函数
    ~SqlConnPool();// 析构函数
public:
    static SqlConnPool *Instance();// 单例函数
    // 初始连接池
    void InitPool(const char* host, int port,
              const char* user,const char* pwd, 
              const char* dbName, int connSize);
    // 关闭连接池
    void ClosePool();
    // 获取一个连接
    MYSQL *GetConn();
    // 释放一个连接
    void FreeConn(MYSQL * conn);
    // 获取空闲的连接数
    int GetFreeConnCount();
    

};
#endif //MRL_SQL_CONN_POOL_H_

源代码:

/*
 * @Author: MRL Liu
 * @Date: 2022-03-21 22:46:32
 * @Description: SqlConnPool的实现
 * @LastEditTime: 2022-03-23 17:47:37
 * @FilePath: \WebServer\pool\sqlconnpool.cpp
 */
#include"sqlconnpool.h"
#include <semaphore.h>//信号量
using namespace std;


// 构造函数
SqlConnPool::SqlConnPool(){
    USED_CONN_ = 0;
    FREE_CONN_ = 0;
}
// 析构函数
SqlConnPool::~SqlConnPool(){
    ClosePool();
}

// 单例函数
SqlConnPool* SqlConnPool::Instance() {
    static SqlConnPool connPool;
    return &connPool;
}
// 初始连接池
void SqlConnPool::InitPool(const char* host, int port,
            const char* user,const char* pwd, const char* dbName,
            int connSize = 10) {
    assert(connSize > 0);
    MAX_CONN_ = connSize;
    // 初始化若干个连接
    for (int i = 0; i < MAX_CONN_; i++) {
        MYSQL *conn = nullptr;
        // 初始化连接
        conn = mysql_init(conn);
        if (!conn) {
            cout<<"MySql初始化出错"<<endl;
            //LOG_ERROR("MySql init error!");
            assert(conn);
        }
        // 建立数据库连接
        conn = mysql_real_connect(conn, host, user, pwd,dbName, port, NULL, 0);
        if (!conn) {
            cout<<"MySql连接出错"<<endl;
            //LOG_ERROR("MySql Connect error!");
        }
        // 添加到容器
        connQue_.push(conn);//添加到队列
    }
    // 初始化信号量
    sem_init(&semId_, 0, MAX_CONN_);
    cout<<"成功初始化连接池"<<endl;
}
// 关闭连接池
void SqlConnPool::ClosePool() {
    lock_guard<mutex> locker(mtx_);//在声明周期内对互斥量上锁
    // 遍历队列,关闭所有连接
    while(!connQue_.empty()) {
        // 元素出队
        auto item = connQue_.front();
        connQue_.pop();
        // 关闭连接
        mysql_close(item);
    }
    mysql_library_end();  // 关闭MySQL API库
    cout<<"成功关闭连接池"<<endl;
}
// 获取一个连接
MYSQL* SqlConnPool::GetConn() {
    MYSQL *conn = nullptr;
    if(connQue_.empty()){
        cout<<"SqlConnPool busy!"<<endl;
        return conn;
    }
    // 具备信号量的线程才能执行
    sem_wait(&semId_);
    {
        lock_guard<mutex> locker(mtx_);//在声明周期内对互斥量上锁
        //一个连接出队
        conn = connQue_.front();
        connQue_.pop();
    }
    return conn;
}
// 释放一个连接
void SqlConnPool::FreeConn(MYSQL* sql) {
    assert(sql);
    lock_guard<mutex> locker(mtx_);//在声明周期内对互斥量上锁
    connQue_.push(sql);//入队
    sem_post(&semId_);//释放一个信号量
}

// 获取空闲的连接数量
int SqlConnPool::GetFreeConnCount() {
    lock_guard<mutex> locker(mtx_);//在声明周期内对互斥量上锁
    return connQue_.size();//此时连接池的数量
}