应用知识点: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 创建失败):
- 构造无返回值,无法直接返回错误;
- 用成员变量标记错误状态(sockfd = -1);
- 外部通过判断标记位识别是否失败。
最常用:
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 服务端流程:
socket():创建 TCP 流式套接字;- 填充
sockaddr_in结构体,配置 IP、端口、协议族; bind():将套接字与本地 IP + 端口进行绑定;listen():将套接字设置为被动监听,等待客户端连接。
2.为什么要用 htons、inet_addr?
- 网络通信统一使用大端网络字节序,本机是小端主机字节序;
htons:short 类型端口,主机序转网络序;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 指针?
- 类的成员函数(普通函数)
- 非静态的(没有
static修饰) - 包括:构造函数、析构函数、普通成员函数
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)的核心业务逻辑:
- 让用户输入手机号、用户名、密码(二次确认)
- 做前端校验(非空、密码一致)
- 把注册信息封装成JSON 格式
- 通过Socket 发送给服务器
- 接收服务器返回结果
- 根据返回状态提示注册成功 / 失败
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数组来完成实现