☰
在线预约01
2026/10/14 6:42:50 网站建设 项目流程

应用知识点:socket tcp libevent json c++ mysql

一、项目环境

在终端中

1.切换管理员模式:sudo su

2.安装:

  • g++:apt install g++
  • libevent:apt install libevent-dev(命令安装)
  • json:apt install libjsoncpp-dev
  • vscode

二、项目框架

服务器端:

需要客户端 :构建libevent(控制整个事件循环):

  • 监听套接字socket_listen:sockfd ;读事件 ; fun()回调函数 : accept
  • 连接套接字socket_con :c ;读事件; func()回调函数:recv(c)//收数据-解析-实现-回复//实现业务功能

三、项目实现

创建项目文件project,内部创建server(服务端)client(客户端)

在ser.h添加所需头文件

实现libevent 监听套接字和连接套接字

1.服务器端:封装监听套接字

class socket_listen { public: // 默认(无参)构造函数 socket_listen() { sockfd = -1;//套接字文件描述符初始置为无效值,标识未创建套接字。 m_port = 6000;//设置默认监听端口。 m_ips = "127.0.0.1"; } // 带参构造函数(初始化列表) socket_listen(string ips, short port) : m_ips(ips), m_port(port) { sockfd = -1; } bool socket_init(); int accept_client(); void Set_base(struct event_base *base) { this->base = base; } int Get_sockfd() const { return sockfd; } struct event_base *Get_base() const { return base; } private: int sockfd; short m_port; string m_ips; struct event_base *base; }

1. 默认构造函数:手动初始化成员变量

2. 带参构造函数

(1).采用初始化列表优先初始化成员:

m_ips(ips):用传入参数ips赋值给成员m_ips

m_port(port):用传入参数port赋值给成员m_port

(2).函数体内统一:

sockfd = -1,保证套接字初始状态合法。

两个构造统一将sockfd初始化为-1,规范化资源初始状态,规避非法文件描述符问题。

1.1面试问题

1.为什么要写两个构造函数?

  • 无参构造:使用默认 IP、端口,快速创建对象;
  • 有参构造:支持自定义 IP、端口,提高灵活性;
  • 本质:构造函数重载,提供多种初始化方式。

2.如果构造函数里写sockfd = socket(...),执行失败怎么办?

答:构造函数没有返回值,不能 return false/error,所以不能靠返回值判断失败。

构造函数执行失败(如 socket 创建失败):

  1. 构造无返回值,无法直接返回错误;
  2. 用成员变量标记错误状态(sockfd = -1);
  3. 外部通过判断标记位识别是否失败。

最常用:

socket_listen() { sockfd = -1; // 先标记为无效 // 如果这里写 sockfd = socket(...) // 创建失败 → sockfd 依然是 -1 }

构造失败的处理方案:用成员变量做错误标记:

  • 成功:sockfd = 有效文件描述符(≥0)
  • 失败:sockfd = -1(保持无效)

外部使用时判断:

socket_listen s; if (s.sockfd == -1) { // 构造/初始化失败 }

1.2监听套接字初始化操作

因为m_port和m_ips在构造函数的时候初始化过了,还需初始化sockfd:

完成 socket 的创建、绑定、监听,初始化整个服务端监听流程

bool socket_listen::socket_init() { //创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { return false; } /*AF_INET:使用 IPv4 协议 SOCK_STREAM:使用 TCP 流式套接字 0:默认协议 功能:向系统申请一个套接字,返回文件描述符*/ //配置地址结构体 struct sockaddr_in saddr; //定义 IPv4 地址结构体,用于存放要绑定的 IP + 端口 memset(&saddr, 0, sizeof(saddr)); //将结构体清空为 0,避免随机脏数据。 saddr.sin_family = AF_INET; //指定协议族为 IPv4。 saddr.sin_port = htons(m_port); /*m_port:类成员变量,构造函数传入 / 默认的端口 htons():主机字节序 转 网络字节序(必须转,网络传输统一规则)*/ saddr.sin_addr.s_addr = inet_addr(m_ips.c_str()); /* m_ips:类成员变量,监听 IP m_ips.c_str():string 转 C 风格字符串(inet_addr 要求) inet_addr():点分十进制 IP 转网络字节序 IP */ //绑定bind:绑定固定 IP + 端口 int res = bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr));//把套接字 sockfd 与 IP、端口绑定。 if (-1 == res) { cout << "bind err" << endl; close(sockfd);//关闭已创建的套接字,防止文件描述符泄漏 return false; } //监听listen:开启监听队列,等待连接 res = listen(sockfd, LIS_MAX);//将套接字设为监听模式,等待客户端连接。 //LIS_MAX:最大等待连接的队列长度(宏定义) if (-1 == res) { return false; } return true; }
  • 函数功能:socket_init是socket_listen类的成员函数,完成创建套接字 → 绑定 IP 端口 → 开启监听,是服务端初始化核心函数。
  • 返回值:bool,成功true,失败false,方便外部判断。
  • 失败处理:每一步失败都及时返回,bind失败会关闭sockfd避免资源泄漏。
  • 关键调用
    • socket():创建 TCP 套接字
    • bind():绑定 IP 和端口
    • listen():启动监听
  • 字节序转换
    • htons():端口转网络序
    • inet_addr():IP 转网络序

问题:

1.解释 socket_init 函数整体流程?

四步标准 TCP 服务端流程:

  1. socket():创建 TCP 流式套接字;
  2. 填充sockaddr_in结构体,配置 IP、端口、协议族;
  3. bind():将套接字与本地 IP + 端口进行绑定;
  4. listen():将套接字设置为被动监听,等待客户端连接。

2.为什么要用 htons、inet_addr?

  1. 网络通信统一使用大端网络字节序,本机是小端主机字节序;
  2. htons:short 类型端口,主机序转网络序;
  3. inet_addr:将字符串点分十进制 IP,转为网络字节序整型。

1.3实现libevent

主函数中进行libevent的实现以及回调函数。

主函数:

int main() { // 监听套接字 socket_listen sock_ser; if (!sock_ser.socket_init()) { cout << "socket init err" << endl; exit(1); } // 创建lievent base struct event_base *base = event_init(); if (base == NULL) { cout << "base null" << endl; exit(1); } // 设置socket_listen中的libevent的base sock_ser.Set_base(base); // 添加sockfd到libevent struct event *sock_ev = event_new(base, sock_ser.Get_sockfd(), EV_READ | EV_PERSIST, SOCK_LIS_CALLBACK, &sock_ser);//创建事件 event_add(sock_ev, NULL);//开始监听 // 启动事件循环 event_base_dispatch(base); // select poll epoll // 释放资源 event_free(sock_ev); event_base_free(base); }
  • event_new:创建一个 libevent 事件
  • base:绑定到哪个事件基座
  • sock_ser.Get_sockfd():监听套接字
  • EV_READ:监听读事件(有客户端连接 → 触发读事件)
  • EV_PERSIST:持续监听,事件触发后不删除,继续监听下一个连接
  • SOCK_LIS_CALLBACK:回调函数,事件触发时自动调用
  • &sock_ser:传给回调函数的参数(当前监听对象)

为什么要把监听 socket 交给 libevent?

  • 传统accept()是阻塞的,只能等一个连接
  • libevent 用I/O 多路复用,单线程可以同时监听成千上万个 fd
  • 实现高并发、非阻塞服务端

SOCK_LIS_CALLBACK 是什么?谁调用它?

  • 是回调函数
  • 不是 main 调用,不是你调用
  • 是libevent 事件循环触发时自动调用

1.4处理客户端连接 accept

int socket_listen::accept_client() { int c = accept(sockfd, NULL, NULL); return c; }

1.5监听套接字的回调函数

void SOCK_LIS_CALLBACK(int sockfd, short ev, void *arg) { socket_listen *p = (socket_listen *)arg; if (p == NULL) { return; } if (ev & EV_READ) // 处理读事件 { int c = p->accept_client(); if (c == -1) { return; } cout << "accept : c=" << c << endl; socket_con *q = new socket_con(c); struct event *c_ev = event_new(p->Get_base(), c, EV_READ | EV_PERSIST, SOCK_CON_CALLBACK, q); if (c_ev == NULL) { close(c); delete q; return; } q->Set_ev(c_ev); // 添加到libevent event_add(c_ev, NULL); } }

2.服务器端:封装连接套接字

2.1连接套接字初步形成

class socket_con { public: socket_con(int fd) : c(fd) { c_ev = NULL;//指针给空防止野指针,方便后续指向接受到的事件 } void Set_ev(struct event *ev) { c_ev = ev; } ~socket_con() { event_free(c_ev); close(c); } void Recv_data(); void Send_err();//回复错误信息 void Send_ok();//回复ok void User_Register(); void User_Login(); void User_Show_Ticket(); // 查看预约信息 void User_Subscribe_Ticket(); // 预订 void User_Show_Sub_Ticket(); void User_Cancel_Sub_Ticket(); private: int c; struct event *c_ev; Json::Value val; };

2.2回复正确/错误函数

void Send_err();//回复错误信息

void socket_con::Send_err() { Json::Value res_val; res_val["status"] = "ERR"; send(c, res_val.toStyledString().c_str(), strlen(res_val.toStyledString().c_str()), 0); }

void Send_ok();//回复ok

void socket_con::Send_ok() { Json::Value res_val; res_val["status"] = "OK"; send(c, res_val.toStyledString().c_str(), strlen(res_val.toStyledString().c_str()), 0); }

2.3用户注册User_Register()

void socket_con::User_Register() { string tel, passwd, username; tel = val["user_tel"].asString(); passwd = val["user_passwd"].asString(); username = val["user_name"].asString();//Json转化为字符串 if (tel.empty() || passwd.empty() || username.empty()) { Send_err(); } mysql_client cli; if (!cli.mysql_ConnectServer()) { Send_err(); return; } if (!cli.mysql_Register(tel, passwd, username)) { Send_err(); return; } Send_ok(); return; }

2.2初步Recv_data()

void socket_con::Recv_data() { char buff[256] = {0}; int n = recv(c, buff, 255, 0); if (n <= 0) { cout << "client close" << endl; delete this; return; } //测试 cout<<"recv:"<<buff<<endl; Json::Reader Read; if (!Read.parse(buff, val)) { cout << "Recv_data:解析json失败" << endl; Send_err(); return; } int ops = val["type"].asInt(); // DL = 1, ZC,CKYY,YD,CKYD,QXYD,TC switch (ops) { case DL: User_Login(); break; case ZC: User_Register(); break; case CKYY: User_Show_Ticket(); break; case YD: User_Subscribe_Ticket(); break; case CKYD: User_Show_Sub_Ticket(); break; case QXYD: User_Cancel_Sub_Ticket(); break; default: break; } }

2.3连接套接字的回调函数

3.客户端:封装客户端类

初步:

class socket_client { public: //无参构造函数 socket_client() { sockfd = -1; ips = "127.0.0.1"; port = 6000;// 默认服务器端口 dl_flg = false;//默认未登录 user_op = 0; runing = true; } //有参构造函数 socket_client(string ips, short port) { sockfd = -1; this->ips = ips; this->port = port; dl_flg = false;//默认未登录 user_op = 0; runing = true; } bool Connect_server(); private: string ips; short port; int sockfd; bool dl_flg;//看用户是否登录 string username;//用户名称 string usertel;//用户电话 int user_op;//用户的选择 bool runing; Json::Value m_val; };
  • 存储服务器 IP、端口
  • 存储socket 套接字文件描述符
  • 提供连接服务器的函数接口

this 指针的作用

  • this是 C++ 类自带的隐藏指针,指向当前调用这个函数的对象本身
  • this->ips= 明确指定:赋值给类的成员变量 ips
  • 等号右边的ips= 函数的参数 ips

一句话总结:当参数名和成员变量名重名时,必须用 this-> 明确区分,告诉编译器 “我要给类自己的成员变量赋值”。

谁自带 this 指针?

  1. 类的成员函数(普通函数)
  2. 非静态的(没有static修饰)
  3. 包括:构造函数、析构函数、普通成员函数

3.1连接服务器函数

bool socket_client::Connect_server() { // 创建TCP套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { cout << "create socket err" << endl; return false; } // 定义服务器地址结构体 struct sockaddr_in saddr; // 服务器地址 memset(&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(port);//大小端转换 saddr.sin_addr.s_addr = inet_addr(ips.c_str()); // 发起连接服务器请求 int res = connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)); if (res == -1) { cout << "connect ser err" << endl; return false; } cout << "Connect to Server success" << endl; return true; }

3.2键盘获取数据发送给服务器

void socket_client::Run() { while (runing) { print_info(); switch (user_op) { case DL: User_Login(); break; case ZC: User_Register(); break; case CKYY: User_Show_Ticket(); break; case YD: User_Subscribe_Ticket(); break; case QXYD: User_Cancel_Sub_Ticket(); break; case CKYD: User_Show_Sub_Ticket(); break; case TC: runing = false; break; default: cout << "输入无效" << endl; break; } } }

3.3打印提示信息(界面)

void socket_client::print_info() { if (dl_flg) { cout << "---已登录------用户名:" << username << "-------" << endl; cout << "1: 查看预约 2 : 预订 3: 查看我的预约 4: 取消预约 5 退出" << endl; cout << "-----------------------------" << endl; cout << "请输入选项编号:" << endl; cin >> user_op; user_op += OFFSET;//加偏移量 } else { cout << "---未登录------游客-----------" << endl; cout << "1: 登陆 2 : 注册 3: 退出" << endl; cout << "-----------------------------" << endl; cout << "请输入选项编号:" << endl; cin >> user_op; if (user_op == 3) { user_op = TC; } } }

方便用户选择 且当两个情况的用户选择数字一样不好区分选择了哪个功能,我们选择用枚举列举一下区分。

enum OP_TYPE {DL = 1, ZC,CKYY,YD,CKYD,QXYD,TC};//登录 =1 注册=2 查看预约=3 预定=4 查看预定=5 取消预订=6 退出=7

设置全局偏移量(因为枚举里面对应的数字跟界面上不一致)

const int OFFSET = 2;

3.4 json介绍

JSON是一种纯字符串形式的数据,它本身不提供任何方法(函数),非常适合在网络中进行传输。通常用于在Web客户端(浏览器)与Web服务器端之间传递数据。

JSON 就是用来存数据、传数据的通用文本格式。

c++ 使用 jsoncpp

使用 jsoncpp 库解析 json 格式的数据,需要三个类:

  • Value 类:将 json 支持的数据类型进行了包装,最终得到一个 Value 类型
  • FastWriter 类:将 Value 对象中的数据序列化为字符串
  • Reader 类:反序列化,将 json 字符串解析成 Value 类型:parse来实现反序列化

Value 类成员方法:

string toStyledString () const; //value 类成员方法,可以将该对象序列化为字符串,有换行,适合配置文件

序列化(对象 → 字符串)

Json::Value obj;//定义json对象 obj["name"] = "小明"; obj["age"] = 20; string str = obj.toStyledString(); // 变成字符串

反序列化(字符串 → 对象)

string json_str = R"({"name":"小明","age":20})"; Json::Value obj; Json::Reader reader; reader.parse(json_str, obj); // ✅ 这一行就是 反序列化 // 还原后可以直接用 cout << obj["name"].asString();

parse 干的就是:

把字符串 → 拆明白 → 变成对象

这就叫parse(解析)。

3.5用户注册

代码整体功能

这是C++ 客户端的用户注册功能,属于网络通信项目(Socket+JSON)的核心业务逻辑:

  1. 让用户输入手机号、用户名、密码(二次确认)
  2. 做前端校验(非空、密码一致)
  3. 把注册信息封装成JSON 格式
  4. 通过Socket 发送给服务器
  5. 接收服务器返回结果
  6. 根据返回状态提示注册成功 / 失败
void socket_client::User_Register() { cout << "请输入用户手机号码:" << endl; cin >> usertel; cout << "请输入用户名" << endl; cin >> username; string passwd, tmp; cout << "请输入密码:" << endl; cin >> passwd; cout << "请再次输入密码" << endl; cin >> tmp; if (usertel.empty() || username.empty()) { cout << "手机或用户名不能为空" << endl; return; } // 校验2:两次输入密码必须一致 if (passwd.compare(tmp) != 0) { cout << "密码不一致" << endl; return; } //封装json Json::Value val; val["type"] = ZC; val["user_tel"] = usertel; val["user_name"] = username; val["user_passwd"] = passwd; //socket发送数据给服务器 c_str():转c风格字符串 send(sockfd, val.toStyledString().c_str(), strlen(val.toStyledString().c_str()), 0); //接受服务器响应 char buff[256] = {0}; if (recv(sockfd, buff, 255, 0) <= 0) { cout << "ser close" << endl; return; } val.clear();// 清空原JSON对象 Json::Reader Read; // JSON解析器 if (!Read.parse(buff, val)) { cout << "json 解析失败" << endl; return; } string s = val["status"].asString(); if (s.compare("OK") != 0) { cout << "注册失败" << endl; return; } dl_flg = true; cout << "注册成功" << endl; return; }

. recv 返回值 ≤0 代表什么?

  • =0:服务器正常关闭连接
  • <0:网络异常 / 连接断开
  • 所以要判断,防止程序崩溃

4.服务器端:数据库的连接(登录、注册)

1.数据库的创建:

1.1登录数据库:

新建数据库:

create database+数据库名称

创建成功

select database()//查询当前正在使用的数据库名称,因为此刻我们没有使用所以是NULL

使用该数据库:use +数据库名称

现在加进去了

1.2 创建表

create table +表名()

约束中有一个是自增长:auto_increment

我们需要获取用户的id、电话、姓名、密码、状态。

create table user_info(userid int primary key not null unique auto_increment, tel char(11) not null unique, username varchar(20) not null, passwd varchar(8) not null, status TINYINT not null);

创建成功会显示:

给表中插入一条信息:insert into 表名 values()

注意插入字符串时要加单引号

插入成功之后要查询,使用select* from 表名//查询表中全部信息

删除表:drop table 表名

查询数据库中有几个表:show tables;

MySQL 数据库客户端封装

class mysql_client { public: mysql_client()//构造函数(固定默认值构造) { db_ips = "127.0.0.1"; db_username = "root"; db_dbname = "Project_DB"; db_passwd = "123456"; } // 2. 灵活构造函数(可自定义所有参数) mysql_client(string ips, string username, string dbname, string passwd) { db_ips = ips; db_username = username; db_dbname = dbname; db_passwd = passwd; mysql_init(&mysql_con); // 必须初始化! } ~mysql_client() { mysql_close(&mysql_con); } bool mysql_ConnectServer();//连接数据库 bool mysql_Register(const string &tel, const string &passwd, const string &name); bool mysql_Login(const string &tel, const string &passwd, string &name); bool mysql_Show_Ticket(Json::Value &resval); bool mysql_Subscribe_Ticket(int tk_id, string tel); //查看我的预约,增加函数设计参数 //取消预约,增加函数设计参数 private: bool mysql_user_begin(); bool mysql_user_commit(); bool mysql_user_rollback(); private: MYSQL mysql_con; string db_ips;//db指database数据库 指和数据库相关的变量 指和数据库服务器的ip地址 string db_username;//登陆数据库的用户名 string db_dbname;//要连接的数据库名称 string db_passwd;//登陆数据库里用户密码 };

mysql_ConnectServer 数据库连接函数实现

// mysqlclient bool mysql_client::mysql_ConnectServer() { MYSQL *mysql = mysql_init(&mysql_con); if (mysql == NULL) { return false; } mysql = mysql_real_connect(mysql, db_ips.c_str(), db_username.c_str(), db_passwd.c_str(), db_dbname.c_str(), 3306, NULL, 0); if (mysql == NULL) { cout << "connect db server err" << endl; return false; } return true; }

mysql_client 类 — 用户注册数据库插入函数

bool mysql_client::mysql_Register(const string &tel, const string &passwd, const string &name) { // insert into user_info values(0,'13200000000','小王','123456',1); string sql = string("insert into user_info values(0,'") + tel + string("','") + name + string("','") + passwd + string("',1)"); if (mysql_query(&mysql_con, sql.c_str()) != 0)//语句插入失败 { return false; } return true; }

预约功能

需要同时修改两张表

新建数据库的表

json数组(同时发多条)在客户端给服务器发送时有时候会要求多条,这时候需要用json数组来完成实现

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询