diff --git a/Experiment_5/README.md b/Experiment_5/README.md new file mode 100644 index 0000000..6cb76ee --- /dev/null +++ b/Experiment_5/README.md @@ -0,0 +1,208 @@ +## 添加用户登录功能 + +#### 要求 + +在前面一次实验的服务器客户端代码基础上增添用户登录的功能,在客户端为用户提供登录的选项,客户端将登录信息发送到服务器端,并等待服务器告知登录是否成功。服务器接收客户端的登录信息,并在本地 Mysql 数据库查询该登录信息的账号是否存在,以及密码是否匹配,返回查询结果给客户端。 + +1. 为客户端添加登录功能,让用户输入账号密码,客户端将登录信息发送到服务器端,并等待服务器返回结果。当收到服务器表示登录成功的信息之后,客户端清空终端并输出新的选项(0:退出,1:发起私聊,2:发起群聊)给用户选择。 +2. 为服务器添加登录功能,服务器端接收来自客户端的登录信息,并在本地 Mysql 数据库查询该登录信息的账号是否存在,以及密码是否匹配,返回查询结果给客户端。 +3. 要面向对象编程,进行类封装。 + +#### 实现过程 + +首先修改客户端,当用户在客户端选择“登录”选项后,我们让其输入用户名和密码,然后对其输入的用户名和密码进行格式化,格式化成“loginxxxpass:yyy”的形式(xxx 为具体的用户名,yyy 为具体的密码),然后发送到服务器,服务器会进行相应的处理后返回结果。客户端接收服务器返回的信息,如果为“ok”说明登录成功,否则说明可能是用户名或者密码错误,需要重新输入登录信息。 + +具体实现的时候我们在之前客户端代码的基础上新增 choice==1 的分支,在其中完成登录的业务代码。如果登录成功,我们把 if_login 置为 true,随后清空终端,并输出新的选项信息。 + +修改后 `client.cpp` 的 HandleClient 函数: + +```cpp +void client::HandleClient(int conn){ + int choice; + string name,pass,pass1; + bool if_login=false;//记录是否登录成功 + string login_name;//记录成功登录的用户名 + + 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继续输入你要的选项:"; + } + //登录 + else if(choice==1&&!if_login){ + while(1){ + cout<<"用户名:"; + cin>>name; + cout<<"密码:"; + cin>>pass; + //格式化 + string str="login"+name; + str+="pass:"; + str+=pass; + send(sock,str.c_str(),str.length(),0);//发送登录信息 + char buffer[1000]; + memset(buffer,0,sizeof(buffer)); + recv(sock,buffer,sizeof(buffer),0);//接收响应 + string recv_str(buffer); + //登录成功 + if(recv_str.substr(0,2)=="ok"){ + if_login=true; + login_name=name; + cout<<"登录成功\n\n"; + break; + } + //登录失败 + else + cout<<"密码或用户名错误!\n\n"; + } + } + } + //登录成功 + if(if_login){ + system("clear");//清空终端d + cout<<" 欢迎回来,"<> choice; + if (choice == 0) + break; // 退出 + else if (choice == 3 && if_login) + { + if_login = false; // 退出登录 + system("clear"); // 清屏 + cout << " ---------------------\n"; // 功能选择界面 + cout << "| |\n"; + cout << "| 欢迎来到聊天室~ |\n"; + cout << "| 请输入您的选择: |\n"; + cout << "| 0:退出 |\n"; + cout << "| 1:登录 |\n"; + cout << "| 2:注册 |\n"; + cout << "| |\n"; + cout << " ---------------------\n\n"; + } + // 登录 + else if (choice == 1 && !if_login) + { + while (1) + { + cout << "用户名:"; + cin >> name; + cout << "密码:"; + cin >> password; + // 格式化字符串 + string str = "login:" + name; + str += "password:"; + str += password; + send(sock, str.c_str(), str.length(), 0); // 发送登录信息:用户名和密码:格式为name:xxxpassword:xxx + char buffer[1000]; + memset(buffer, 0, sizeof(buffer)); + recv(sock, buffer, sizeof(buffer), 0); // 接收服务器返回的登录结果 + string recv_str(buffer); + // 登录成功 + if (recv_str.substr(0, 2) == "ok") + { + if_login = true; + login_name = name; + cout << "登录成功!" << endl; + break; + } + // 登录失败 + else if (recv_str.substr(0) == "password_wrong") + cout << "密码错误!\n\n"; + else if (recv_str.substr(0) == "not_have_this_user") + cout << "用户名不存在!\n\n"; + } + } + + // 注册 + 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" + << endl; + } + name = "name:" + name; + password = "password:" + password; + string str = name + password; + send(conn, str.c_str(), str.length(), 0); // 发送注册信息:用户名和密码:格式为name:xxxpassword:xxx + cout << "注册成功!" << endl; + cout << "\n请继续选择:" << endl; + } + + // 登录成功选择功能 + if (if_login) + { + system("clear"); // 清屏 + cout << "欢迎回来," << login_name << "!" << endl; + cout << " ---------------------\n"; // 功能选择界面 + cout << "| |\n"; + cout << "| 请输入您的选择: |\n"; + cout << "| 0:退出系统 |\n"; + cout << "| 1:发起单独聊天 |\n"; + cout << "| 2:发起群聊 |\n"; + cout << "| 3:退出登录 |\n"; + cout << "| |\n"; + cout << " ---------------------\n\n"; + } + } +} + +// 向服务器发送消息 +// 注意:前面不用再加static +void client::SendMsg(int conn) +{ + char sendbuf[100]; // 声明一个大小为100的字符数组sendbuf + while (1) + { + memset(sendbuf, 0, sizeof(sendbuf)); // 每次循环中,它使用memset函数将sendbuf清零 + cin >> 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_5/client.h b/Experiment_5/client.h new file mode 100644 index 0000000..2a1714c --- /dev/null +++ b/Experiment_5/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_5/global.h b/Experiment_5/global.h new file mode 100644 index 0000000..682dde7 --- /dev/null +++ b/Experiment_5/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_5/makefile b/Experiment_5/makefile new file mode 100644 index 0000000..e3f4d99 --- /dev/null +++ b/Experiment_5/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_5/server.cpp b/Experiment_5/server.cpp new file mode 100644 index 0000000..37c999b --- /dev/null +++ b/Experiment_5/server.cpp @@ -0,0 +1,176 @@ +#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; // 用户名和密码 + bool if_login = false; // 记录当前服务对象(当前联机的客户端)是否已经登录 + string login_name; // 记录当前服务对象(当前联机的客户端)的用户名 + + // 连接 MySQL 数据库 + MYSQL *connection = mysql_init(NULL); // 初始化数据库连接变量,返回一个MYSQL*类型的连接句柄,如果返回NULL,说明初始化失败 + mysql_real_connect(connection, + "172.20.193.132", + "test", + "123456", + "ChatProject", + 0, + NULL, + CLIENT_MULTI_STATEMENTS); // 连接数据库:服务器地址,用户名,密码,数据库名,端口号,套接字,连接标志 + + // 注册:name:用户名 password:密码 + 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*类型的,所以要转换一下 + } + + // 登录:name:用户名 password:密码 + else if (str.find("login:") != str.npos) // str.npos 是 string 类的一个静态成员变量,值为 -1,表示查找失败,如果查找成功,返回查找到的第一个字符的位置 + { + int p1 = str.find("login:"), p2 = str.find("password:"); // 查找用户名和密码的起始位置 + name = str.substr(p1 + 6, p2 - 6); // 提取用户名:从第6个字符开始,长度为p2-6 + password = str.substr(p2 + 9, str.length() - p2 - 9); // 提取密码:从第9个字符开始,长度为str.length()-p2-9 + string search = "SELECT * FROM USER WHERE NAME=\""; // 定义 sql语句:查询数据:SELECT * FROM 表名 WHERE 条件; + search += name; + search += "\";"; + cout << "sql 语句为:" << search << endl; // 后台输出 sql 语句 + auto search_res = mysql_query(connection, search.c_str()); // 执行 sql 语句:成功返回 0,失败返回非 0;auto 是 C++11 中的新特性,用于自动推断变量的类型,这里 search_res 的类型为 int + auto result = mysql_store_result(connection); // mysql_store_result() 函数用于获取结果集,返回值为 MYSQL_RES 类型的指针,该指针指向查询结果集,如果查询失败,则返回 NULL。 + int column = mysql_num_fields(result); // 获取结果集的列数:mysql_num_fields() 函数用于获取结果集中字段的个数 + int row = mysql_num_rows(result); // 获取结果集的行数:mysql_num_rows() 函数用于获取结果集中行的个数 + + // 如果数据库中存在该用户 + if (search_res == 0 && row != 0) + { + cout << "查询成功\n" + << endl; + auto info = mysql_fetch_row(result); // 获取结果集中的一行数据:mysql_fetch_row() 函数用于从结果集中获取下一行,返回值为 MYSQL_ROW 类型的指针,该指针指向结果集中的一行数据,如果没有数据了,则返回 NULL。 + cout << "查询到的用户名为:" << info[0] << " 密码为:" << info[1] << endl; // 后台输出查询到的用户名和密码,info[0]是用户名,info[1]是密码 + // 如果密码正确 + if (info[1] == password) + { + cout << "登录密码正确\n\n"; + string str1 = "ok"; + if_login = true; // 标记当前服务对象(当前联机的客户端)已经登录 + login_name = name; // 记录当前服务对象(当前联机的客户端)的用户名 + send(conn, str1.c_str(), str1.length() + 1, 0); // 发送消息给客户端: +1是为了发送结束符 + } + // 如果密码错误 + else + { + cout << "登录密码错误\n\n"; + char str1[100] = "password_wrong"; + send(conn, str1, strlen(str1), 0); // strlen() 不用 +1:strlen() 函数用于计算字符串的长度,不包括字符串结束符,所以不用 +1 + } + } + // 如果数据库不存在该用户 + else + { + cout << "查询失败:数据库中不存在该用户\n\n"; + char str1[100] = "not_have_this_user"; + send(conn, str1, strlen(str1), 0); // strlen 和 .length() 都是用来计算字符串长度的,但是 strlen() 是 C 语言中的函数,而 .length() 是 C++ 中的成员函数,所以 .length() 可以用于 string 类型的变量,而 strlen() 不能用于 string 类型的变量 + } + } +} \ No newline at end of file diff --git a/Experiment_5/server.h b/Experiment_5/server.h new file mode 100644 index 0000000..e7f1889 --- /dev/null +++ b/Experiment_5/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_5/test_client.cpp b/Experiment_5/test_client.cpp new file mode 100644 index 0000000..9bfa8ed --- /dev/null +++ b/Experiment_5/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_5/test_server.cpp b/Experiment_5/test_server.cpp new file mode 100644 index 0000000..d55b306 --- /dev/null +++ b/Experiment_5/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