From 3c88d332171011a3ae6b1d943cf2a5412ad50e6b Mon Sep 17 00:00:00 2001 From: flykhan Date: Sat, 8 Apr 2023 14:39:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Experiment_4/README.md | 301 +++++++++++++++++++++++++++++++++++ Experiment_4/client.cpp | 109 +++++++++++++ Experiment_4/client.h | 21 +++ Experiment_4/global.h | 21 +++ Experiment_4/makefile | 36 +++++ Experiment_4/server.cpp | 125 +++++++++++++++ Experiment_4/server.h | 21 +++ Experiment_4/test_client.cpp | 7 + Experiment_4/test_server.cpp | 7 + 9 files changed, 648 insertions(+) create mode 100644 Experiment_4/README.md create mode 100644 Experiment_4/client.cpp create mode 100644 Experiment_4/client.h create mode 100644 Experiment_4/global.h create mode 100644 Experiment_4/makefile create mode 100644 Experiment_4/server.cpp create mode 100644 Experiment_4/server.h create mode 100644 Experiment_4/test_client.cpp create mode 100644 Experiment_4/test_server.cpp diff --git a/Experiment_4/README.md b/Experiment_4/README.md new file mode 100644 index 0000000..5adc663 --- /dev/null +++ b/Experiment_4/README.md @@ -0,0 +1,301 @@ +## 添加用户注册功能 + +#### 要求 + +在之前实验的服务器客户端代码中增添用户注册的功能,在客户端为用户提供注册的选项,并将用户注册的账号、密码发送到服务器端,并保存在服务器的 MySQL 数据库中。 + +1. 在服务器本地 Mysql 中创建新数据库 ChatProject,库中有一张表叫 USER,该表中有账号 NAME 和密码 PASSWORD 两项属性。 +2. 为客户端添加注册功能,让用户注册账号密码,客户端将注册信息发送到服务器端,注意:当用户注册时应输入两次密码,如果密码不一致需要重新输入。 +3. 服务器端接收来自客户端的注册信息,将用户的账号、密码写入本地 Mysql 数据库。 +4. 要面向对象编程,进行类封装。 + +#### 具体实现 + +首先在 MySQL 控制台创建数据库 ChatProject,如下: + +```sql +create database ChatProject; +``` + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304081434336.png) + +接下来先用 use 命令切换到新建的数据库。 + +```sql +use ChatProject; +``` + +然后新建一张表格叫 USER 用来保存账号信息,表中有账号 NAME 和密码 PASSWORD 两项属性,都为 VARCHAR 可变长度字符串类型,且将账号 NAME 设为 PRIMARY KEY 主键,主键不允许重复保证了账号的唯一性,而且主键能自动建立索引加快查询速度。 + +```sql +CREATE TABLE USER( + NAME VARCHAR(20) PRIMARY KEY, + PASSWORD VARCHAR(20) +); +``` + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304081434324.png) + +建好表之后我们可以查看当前数据库中所有的表格。 + +```sql +show tables; +``` + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304081434320.jpeg) + +最后输入 `exit` 即可离开 MySQL 控制台回到终端。 + +创建好了表格之后,我们就可以开始为 Client 类和 Server 类添加注册账号的代码了。 + +首先修改 `client.h` 头文件,添加一个函数 HandleClient,该函数将在与服务器连接建立之后开始工作,与用户进行交互并处理各项事务。 + +```cpp +#ifndef CLIENT_H +#define CLIENT_H + +#include "global.h" + +class client{ + private: + int server_port; + string server_ip; + int sock; + public: + client(int port,string ip); + ~client(); + void run(); + static void SendMsg(int conn); + static void RecvMsg(int conn); + void HandleClient(int conn); +}; +#endif +``` + +然后修改 `client.cpp` 文件中的 run 函数,连接完服务器之后就调用 HandleClient,并将文件描述符作为参数传入,如下: + +```cpp +void client::run(){ + + //定义sockfd + sock = socket(AF_INET,SOCK_STREAM, 0); + + //定义sockaddr_in + struct sockaddr_in servaddr; + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(server_port); //服务器端口 + servaddr.sin_addr.s_addr = inet_addr(server_ip.c_str()); //服务器ip + + //连接服务器,成功返回0,错误返回-1 + if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) + { + perror("connect"); + exit(1); + } + cout<<"连接服务器成功\n"; + + HandleClient(sock); + + return; +} +``` + +接下来设计 `client.cpp` 文件中的 HandleClient 函数,开头先打印一段信息指示用户操作,然后开始处理事务,如果用户输入 2 就进入注册模块,注册时输入两次密码,密码不一致就要重新输入。最后将注册的账号和密码格式化成“name:xxxpass:yyy”(xxx 代表用户名,yyy 代表密码)发送到服务器端,格式化之后可以方便服务器提取出用户名和密码。 + +```cpp +void client::HandleClient(int conn){ + int choice; + string name,pass,pass1; + //bool if_login=false;//记录是否登录成功 + + cout<<" ------------------\n"; + cout<<"| |\n"; + cout<<"| 请输入你要的选项:|\n"; + cout<<"| 0:退出 |\n"; + cout<<"| 1:登录 |\n"; + cout<<"| 2:注册 |\n"; + cout<<"| |\n"; + cout<<" ------------------ \n\n"; + + //开始处理各种事务 + while(1){ + //if(if_login) + // break; + cin>>choice; + if(choice==0) + break; + //注册 + else if(choice==2){ + cout<<"注册的用户名:"; + cin>>name; + while(1){ + cout<<"密码:"; + cin>>pass; + cout<<"确认密码:"; + cin>>pass1; + if(pass==pass1) + break; + else + cout<<"两次密码不一致!\n\n"; + } + name="name:"+name; + pass="pass:"+pass; + string str=name+pass; + send(conn,str.c_str(),str.length(),0); + cout<<"注册成功!\n"; + cout<<"\n继续输入你要的选项:"; + } + } +} +``` + +接下来开始设计服务器端,因为服务器要连接 MySQL,所以我们先在 `global.h` 中添加 mysql 头文件,如下: + +```cpp +#ifndef _GLOBAL_H +#define _GLOBAL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include //新添加 +using namespace std; + +#endif +``` + +然后编写 `server.h`,添加静态函数 HandleRequest 的定义。 + +server.h: + +```cpp +#ifndef SERVER_H +#define SERVER_H + +#include "global.h" + +class server{ + private: + int server_port; + int server_sockfd; + string server_ip; + static vector sock_arr; + public: + server(int port,string ip); + ~server(); + void run(); + static void RecvMsg(int conn); + static void HandleRequest(int conn,string str); +}; +#endif +``` + +修改 `server.cpp` 中 RecvMsg 的代码,让其接收到信息之后调用 HandleRequest 函数,并传入文件描述符和接收到的字符串作为参数。 + +```cpp +void server::RecvMsg(int conn){ + //接收缓冲区 + char buffer[1000]; + //不断接收数据 + while(1) + { + memset(buffer,0,sizeof(buffer)); + int len = recv(conn, buffer, sizeof(buffer),0); + //客户端发送exit或者异常结束时,退出 + if(strcmp(buffer,"exit")==0 || len<=0){ + close(conn); + sock_arr[conn]=false; + break; + } + cout<<"收到套接字描述符为"< use ChatProject; +> select * from USER; +``` + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304081434326.png) + +可见成功注册。 + +最后提交 git: + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304081434410.png) \ No newline at end of file diff --git a/Experiment_4/client.cpp b/Experiment_4/client.cpp new file mode 100644 index 0000000..1367275 --- /dev/null +++ b/Experiment_4/client.cpp @@ -0,0 +1,109 @@ +#include "client.h" + +client::client(int port, string ip) : server_port(port), server_ip(ip) {} // 初始化服务器 ip 和端口号 +client::~client() +{ + close(sock); // 关闭套接字描述符 +} + +void client::run() // run 函数负责建立与服务器的连接并且启动发送线程和接收线程 +{ + // 定义 sockfd 套接字描述符 + sock = socket(AF_INET, SOCK_STREAM, 0); + + // 定义套接字信息 + struct sockaddr_in servaddr; // 定义套接字地址结构 + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(server_port); // 服务器端口 + servaddr.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 服务器 ip + + // 连接服务器,成功返回 0,错误返回 -1 + if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) + { + perror("connect"); + exit(1); + } + cout << "连接服务器成功\n"; + + HandleClient(sock); // 处理客户端请求 + + return; +} + +void client::HandleClient(int conn) +{ + int choice; // 用户选项 + string name, password, password_confirm; // 用户名,密码,确认密码 + + cout << " --------------------\n"; // 功能选择界面 + cout << "| |\n"; + cout << "| 请输入您的选择: |\n"; + cout << "| 0:退出 |\n"; + cout << "| 1:登录 |\n"; + cout << "| 2:注册 |\n"; + cout << "| |\n"; + cout << " --------------------\n\n"; + + while (1) + { + cin >> choice; + if (choice == 0) + break; // 退出 + + // 注册 + else if(choice ==2){ + cout<<"请输入注册用户名:"; + cin>>name; + while(1){//循环输入密码,直到两次密码一致 + cout<<"密码:"; + cin>>password; + cout<<"确认密码:"; + cin>>password_confirm; + if(password == password_confirm) + break;//两次密码一致,跳出循环 + else + cout<<"两次密码不一致,请重新输入\n\n"<> sendbuf; // 从cin输入流中读取用户输入的信息,将其存放到sendbuf中。 + int ret = send(conn, sendbuf, strlen(sendbuf), 0); // send函数将sendbuf中的内容发送给服务器,其中conn参数表示连接套接字。如果发送成功,send函数返回发送的字节数 + if (strcmp(sendbuf, "exit") == 0 || ret <= 0) // ret <= 0表示发送失败 + break; + } +} + +// 从服务器接收消息 +void client::RecvMsg(int conn) +{ + // 接收缓冲区 + char buffer[1000]; + // 不断接收数据 + while (1) + { + memset(buffer, 0, sizeof(buffer)); + int len = recv(conn, buffer, sizeof(buffer), 0); + // recv 返回值小于等于 0,退出 + if (len <= 0) + break; + cout << "收到服务器发来的信息:" << buffer << endl; + } +} \ No newline at end of file diff --git a/Experiment_4/client.h b/Experiment_4/client.h new file mode 100644 index 0000000..2a1714c --- /dev/null +++ b/Experiment_4/client.h @@ -0,0 +1,21 @@ +#ifndef CLIENT_H +#define CLIENT_H + +#include "global.h" + +class client +{ +private: + int server_port; // 服务器端口号 + string server_ip; // 服务器 ip + int sock; // 与服务器建立连接的套接字描述符 +public: + client(int port, string ip); + ~client(); + void run(); // 启动客户端服务 + static void SendMsg(int conn); // 发送线程, static 修饰的函数是类的静态成员函数,不需要创建对象就可以直接调用:所有对象共享一个函数 + static void RecvMsg(int conn); // 接收线程 + void HandleClient(int conn); // 处理客户端请求,在与服务器连接建立之后开始工作,与用户进行交互并处理各项事务 +}; + +#endif \ No newline at end of file diff --git a/Experiment_4/global.h b/Experiment_4/global.h new file mode 100644 index 0000000..682dde7 --- /dev/null +++ b/Experiment_4/global.h @@ -0,0 +1,21 @@ +#ifndef _GLOBAL_H +#define _GLOBAL_H + +#include //基本系统数据类型 +#include //socket:套接字函数和结构,bind,listen,accept,connect,send,recv,sendto,recvfrom,shutdown +#include //printf:标准输入输出函数 +#include //sockaddr_in:套接字地址结构,AF_INET:地址族,htons:主机字节顺序转网络字节顺序,INADDR_ANY:任意地址 +#include //inet_addr:将点分十进制ip地址转换为网络字节顺序的二进制ip地址 +#include //close:关闭文件描述符 +#include //bzero:将字符串清零 +#include //exit(0);:正常退出 +#include //文件控制定义:open,read,write,close +#include //共享内存:shmget,shmat,shmdt,shmctl +#include //cout cin:标准输入输出 +#include //thread 头文件:用于创建线程 +#include //vector容器:动态数组,可以动态增加和删除元素 +#include // mysql 头文件, 需要安装 mysql-devel:用于连接 mysql 数据库 + +using namespace std; // 使用标准命名空间 + +#endif \ No newline at end of file diff --git a/Experiment_4/makefile b/Experiment_4/makefile new file mode 100644 index 0000000..e3f4d99 --- /dev/null +++ b/Experiment_4/makefile @@ -0,0 +1,36 @@ +CXX = g++ +CXXFLAGS = -Wall -Wextra -g + +SRCS_TEST_SERVER = test_server.cpp server.cpp +SRCS_TEST_CLIENT = test_client.cpp client.cpp + +OBJS_TEST_SERVER = $(SRCS_TEST_SERVER:.cpp=.o) +OBJS_TEST_CLIENT = $(SRCS_TEST_CLIENT:.cpp=.o) + +TEST_SERVER_TARGET = ./test/test_server +TEST_CLIENT_TARGET = ./test/test_client + +.PHONY: all clean start_server start_client + +all: test_server test_client + +test_server: $(OBJS_TEST_SERVER) + $(CXX) $(CXXFLAGS) $^ -o $(TEST_SERVER_TARGET) -lpthread -lmysqlclient + +test_client: $(OBJS_TEST_CLIENT) + $(CXX) $(CXXFLAGS) $^ -o $(TEST_CLIENT_TARGET) -lpthread + +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJS_TEST_SERVER) $(OBJS_TEST_CLIENT) $(TEST_SERVER_TARGET) $(TEST_CLIENT_TARGET) + +# clean: +# rm -rf *.o ./test/* + +start_server: test_server + $(TEST_SERVER_TARGET) + +start_client: test_client + $(TEST_CLIENT_TARGET) diff --git a/Experiment_4/server.cpp b/Experiment_4/server.cpp new file mode 100644 index 0000000..4949a23 --- /dev/null +++ b/Experiment_4/server.cpp @@ -0,0 +1,125 @@ +#include "server.h" + +// 在定义时,sock_arr 被初始化为一个大小为 1000 的容器,并使用 false 作为默认元素值(表示未建立连接)。这意味着,当程序开始运行时,sock_arr 中包含了 1000 个初始值为 false 的元素,可以用于记录当前所有连接套接字的状态。 +// 这个静态成员变量的作用可能是用于记录服务器上所有连接套接字的状态。具体来说,当一个新的连接套接字建立时,程序会向 sock_arr 中添加一个新的 bool 元素,表示该套接字处于“已连接”状态。当该套接字关闭时,程序会将相应的 bool 元素设置为 false,从而更新套接字的状态。 +vector server::sock_arr(1000, false); + +// 构造函数 +server::server(int port, string ip) : server_port(port), server_ip(ip) {} + +// 析构函数 +server::~server() +{ + for (int i = 0; i < sock_arr.size(); i++) // 找到处于连接状态的客户端套接字并关闭它们 + { + if (sock_arr[i]) + close(i); + } + close(server_sockfd); // 关闭服务器套接字 +} + +// 服务器开始服务 +void server::run() +{ + // 定义 sockfd + server_sockfd = socket(AF_INET, SOCK_STREAM, 0); + + // 定义地址信息 + struct sockaddr_in server_sockaddr; // 套接字地址结构 + server_sockaddr.sin_family = AF_INET; // TCP/IP 协议族 + server_sockaddr.sin_port = htons(server_port); // server_port 端口号 + server_sockaddr.sin_addr.s_addr = inet_addr(server_ip.c_str()); // ip 地址,127.0.0.1 是环回地址,相当于本机 ip + + // bind,成功返回 0,出错返回 -1 + if (bind(server_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)) == -1) // 服务端套接字绑定 ip 地址和 port 端口号 + { + perror("bind"); // 输出错误原因 + exit(1); // 结束程序 + } + + // listen,成功返回 0,出错返回 -1 + if (listen(server_sockfd, 20) == -1) + { + perror("listen"); // 输出错误原因 + exit(1); // 结束程序 + } + + // 客户端套接字 + struct sockaddr_in client_addr; + socklen_t length = sizeof(client_addr); + + // 不断取出新连接并创建子线程为其服务 + while (1) + { + int conn = accept(server_sockfd, (struct sockaddr *)&client_addr, &length); // server_sockfd 是服务器端监听套接字的文件描述符,client_addr 是一个结构体指针,存储了客户端的地址信息(IP 地址和端口号),length 是客户端地址结构体的长度。 + if (conn < 0) + { + perror("connect"); // 输出错误原因 + exit(1); // 结束程序 + } + + cout << "文件描述符为" << conn << "的客户端成功连接\n"; + sock_arr.push_back(conn); // 当一个新的连接套接字建立时,程序会将其添加到 sock_arr 中。这样做可以方便地管理多个连接套接字,并在需要时对它们进行遍历、筛选等操作。 + // 创建线程 + thread t(server::RecvMsg, conn); + t.detach(); // 置为分离状态,不能用 join ,join 会导致主线程阻塞 + } +} + +// 定义子线程工作的静态函数:用于在子线程中处理客户端的数据收发,conn 参数是与客户端建立连接后返回的新套接字描述符,用于指定当前处理的客户端。 +// 注意:头文件 server.h 中已经定义过静态函数时,在另一个文件或同一个文件中包含该头文件并使用该静态函数时,不需要再次添加 static 关键字。如果重复添加 static 关键字,则会导致编译错误。 +void server::RecvMsg(int conn) +{ + // 接收缓冲区 + char buffer[1000]; + // 不断接收数据 + while (1) + { + memset(buffer, 0, sizeof(buffer)); // 在网络编程中,由于发送和接收的数据可能会出现不完整的情况,因此需要先将接收缓冲区清空,以避免旧数据对新数据造成干扰。在这段代码中,使用 memset() 函数将 buffer 清空,以确保该缓冲区为空,可以安全地存储新的接收数据。 + int len = recv(conn, buffer, sizeof(buffer), 0); + // 客户端发送 exit 或者异常结束时,退出 + if (strcmp(buffer, "exit") == 0 || len <= 0) + { + cout << "套接字描述符为" << conn << "的客户端断开了连接" << endl; + sock_arr[conn] = false; // 将 sock_arr[conn] 的值设置为 false,表示这个套接字已经不再处于连接状态了 + break; + } + + cout << "收到套接字描述符为" << conn << "发来的信息:" << buffer << endl; + + string str(buffer); // 将 buffer 转换为 string 类型 + HandleRequest(conn, str); // 处理请求 + } +} + +void server::HandleRequest(int conn, string str) +{ + char buffer[1000]; + string name, password; // 用户名和密码 + + // 连接 MySQL 数据库 + MYSQL *connection = mysql_init(NULL); // 初始化数据库连接变量,返回一个MYSQL*类型的连接句柄,如果返回NULL,说明初始化失败 + mysql_real_connect(connection, + "172.22.169.114", + "test", + "123456", + "ChatProject", + 0, + NULL, + CLIENT_MULTI_STATEMENTS); // 连接数据库:服务器地址,用户名,密码,数据库名,端口号,套接字,连接标志 + + if (str.find("name:") != str.npos) + { + int p1 = str.find("name:"), p2 = str.find("password:"); // 查找用户名和密码的起始位置 + name = str.substr(p1 + 5, p2 - 5); // 提取用户名:从第5个字符开始,长度为p2-5 + password = str.substr(p2 + 9, str.length() - p2 - 9); // 提取密码:从第9个字符开始,长度为str.length()-p2-9 + string search = "INSERT INTO USER VALUES(\""; // 定义 sql语句:插入数据:INSERT INTO 表名 VALUES(值1,值2,值3,...); + search += name; + search += "\",\""; + search += password; + search += "\");"; + cout << "sql语句为:" << search << endl + << endl; // 两个endl是为了换行 + mysql_query(connection, search.c_str()); // 执行sql语句:成功返回0,失败返回非0,参数1:连接句柄,参数2:sql语句;.c_str()将string转换为char* 类型:为什么要转换为char*类型呢?因为mysql_query()函数的第二个参数是char*类型的,所以要转换一下 + } +} \ No newline at end of file diff --git a/Experiment_4/server.h b/Experiment_4/server.h new file mode 100644 index 0000000..e7f1889 --- /dev/null +++ b/Experiment_4/server.h @@ -0,0 +1,21 @@ +#ifndef SERVER_H +#define SERVER_H + +#include "global.h" + +class server +{ +private: + int server_port; // 服务器端口号 + int server_sockfd; // 设为 listen 状态的套接字描述符 + string server_ip; // 服务器 ip + static vector sock_arr; // 当一个新的连接套接字建立时,程序会向sock_arr中添加一个新的bool元素,表示该套接字处于“已连接”状态。当该套接字关闭时,程序会将相应的bool元素设置为false,从而更新套接字的状 +public: + server(int port, string ip); // 构造函数 + ~server(); // 析构函数 + void run(); // 服务器开始服务 + static void RecvMsg(int conn); // 子线程工作的静态函数:用于在子线程中处理客户端的数据收发,conn 参数是与客户端建立连接后返回的新套接字描述符,用于指定当前处理的客户端。 + static void HandleRequest(int conn, string str); // 处理客户端请求: 用于处理客户端的请求,conn 参数是与客户端建立连接后返回的新套接字描述符,用于指定当前处理的客户端。str 参数是客户端发送的请求字符串。 +}; + +#endif \ No newline at end of file diff --git a/Experiment_4/test_client.cpp b/Experiment_4/test_client.cpp new file mode 100644 index 0000000..9bfa8ed --- /dev/null +++ b/Experiment_4/test_client.cpp @@ -0,0 +1,7 @@ +#include "client.h" + +int main() +{ + client clnt(8023, "127.0.0.1"); // 创建了一个名为 clnt 的客户端对象,该对象的构造函数参数分别为服务器的端口号 8023 和 IP 地址 "127.0.0.1",表示要连接的服务器的地址和端口 + clnt.run(); // 调用了 clnt 对象的 run 函数,开始执行客户端程序 +} \ No newline at end of file diff --git a/Experiment_4/test_server.cpp b/Experiment_4/test_server.cpp new file mode 100644 index 0000000..d55b306 --- /dev/null +++ b/Experiment_4/test_server.cpp @@ -0,0 +1,7 @@ +#include "server.h" + +int main() +{ + server serv(8023, "127.0.0.1"); // 创建了一个名为serv的服务器对象,该对象的构造函数参数分别为服务器的端口号8023和IP地址"127.0.0.1",表示要监听的服务器的地址和端口 + serv.run(); // 调用了serv对象的run函数,开始执行服务器程序并等待客户端连接 +} \ No newline at end of file