From a7b40144bca8b0fe9180971ece148477e07a225d Mon Sep 17 00:00:00 2001 From: flykhan Date: Sat, 1 Apr 2023 23:52:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E7=BA=BF=E7=A8=8B=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Experiment_2/README.md | 353 +++++++++++++++++++++++++++++++++++ Experiment_2/client.cpp | 48 +++++ Experiment_2/makefile | 35 ++++ Experiment_2/server.cpp | 82 ++++++++ Experiment_2/server.h | 33 ++++ Experiment_2/test_server.cpp | 7 + Experiment_2/test_thread.cpp | 23 +++ 7 files changed, 581 insertions(+) create mode 100644 Experiment_2/README.md create mode 100644 Experiment_2/client.cpp create mode 100644 Experiment_2/makefile create mode 100644 Experiment_2/server.cpp create mode 100644 Experiment_2/server.h create mode 100644 Experiment_2/test_server.cpp create mode 100644 Experiment_2/test_thread.cpp diff --git a/Experiment_2/README.md b/Experiment_2/README.md new file mode 100644 index 0000000..709c882 --- /dev/null +++ b/Experiment_2/README.md @@ -0,0 +1,353 @@ +## 多线程并发服务器 + +### 进程和线程的基本概念 + +#### 进程 + +进程是程序的一次执行过程,是操作系统资源分配的基本单位。 + +比如我们在上一个实验中用 `./server` 命令运行服务器程序,就会产生一个进程,可以使用 `ps -ef|grep ./server` 命令查看相关进程快照,如下: + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011939279.png) + +PID 为 120 即该进程的进程号为 120。 + +#### 线程 + +线程是任务调度和执行的基本单位,一个进程中可以有多个线程独立运行。线程没有自己独立的地址空间,会与其它属于同一进程的线程一起共享进程的资源,但是每个线程也会有自己的独立的栈和一组寄存器。在 Linux 当中,线程的实现比较特别,会把线程当做进程来实现,即将线程视为一个与其它进程共享资源的进程。 + +我们可以使用 ps -T -p 命令来查看一个进程的所有线程,如下( 359 是进程号): + +![图片描述](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011939325.png) + +##### 通俗理解线程进程和协程 + +![image-20230401194319882](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304011943949.png) + +### C++ 11 的 thread 线程库 + +C++ 11 中提供了专门的线程库,可以很方便地进行调用。 + +#### 基本使用 + +需要引入头文件: + +```cpp +#include +``` + +创建一个新线程来执行 run 函数: + +```cpp +thread t(run); //实例化一个线程对象t,让该线程执行run函数,构造对象后线程就开始执行了 +``` + +假如说 run 函数需要传入参数 a 和 b,我们可以这样构造: + +```cpp +thread t(run,a,b); //实例化一个线程对象t,让该线程执行run函数,传入a和b作为run的参数 +``` + +需要注意的是,传入的函数必须是**全局函数或者静态函数**,不能是类的普通成员函数。 + +join 函数会阻塞主线程,直到 join 函数的 thread 对象标识的线程执行完毕为止,join 函数使用方法如下: + +```cpp +t.join(); //调用join后,主线程会一直阻塞,直到子线程的run函数执行完毕 +``` + +但有时候我们需要主线程在继续完成其它的任务,而不是一直等待子线程结束,这时候我们可以使用 detach 函数。 + +detach 函数会让子线程变为分离状态,主线程不会再阻塞等待子线程结束,而是让系统在子线程结束时自动回收资源。使用的方法如下: + +```cpp +t.detach(); +``` + +#### 代码示例 + +接下来用一段代码来示范如何进行简单的多线程编程,我们构建两个线程,同时输出 1-10,如下: + +新建一个文件名为 `test_thread.cpp`: + +```cpp +#include +#include +#include +using namespace std; +void print(){ + for(int i=1;i<=10;i++){ + cout< +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std; +class server{ + private: + int server_port;//服务器端口号 + int server_sockfd;//设为listen状态的套接字描述符 + string server_ip;//服务器ip + vector sock_arr;//保存所有套接字描述符 + public: + server(int port,string ip);//构造函数 + ~server();//析构函数 + void run();//服务器开始服务 + static void RecvMsg(int conn);//子线程工作的静态函数 +}; + +#endif +``` + +接下来我们需要在 `server.cpp` 文件中给出函数具体的定义: + +```cpp +#include "server.h" + +//构造函数 +server::server(int port,string ip):server_port(port),server_ip(ip){} + +//析构函数 +server::~server(){ + for(auto conn:sock_arr) + close(conn); + close(server_sockfd); +} + +//服务器开始服务 +void server::run(){ + //定义sockfd + server_sockfd = socket(AF_INET,SOCK_STREAM, 0); + + //定义sockaddr_in + 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) + { + 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); + if(conn<0) + { + perror("connect");//输出错误原因 + exit(1);//结束程序 + } + cout<<"文件描述符为"< 的探究 + +在 C++ 中,用于包含头文件的指令是 `#include`。在使用 `#include` 指令时,可以使用尖括号 `< >` 或者双引号 `" "` 来引用头文件。 + +当使用尖括号 `< >` 时,编译器会在系统默认的头文件目录中查找该头文件;而当使用双引号 `" "` 时,编译器会先在当前源代码文件所在目录中查找该头文件,如果没有找到再到系统默认的头文件目录中查找。 + +因此,如果要引用系统库中的头文件,应该使用尖括号 `< >`;如果要引用自己编写的头文件,应该使用双引号 `" "`。 + +需要注意的是,如果在使用 `#include` 指令时使用了错误的符号,可能会导致编译器无法正确查找头文件,从而导致编译失败。因此,在编写代码时要注意使用正确的符号来引用头文件。 + +## server代码解释 + +### accept() + +![image-20230401223027473](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012230588.png) + +### 编译警告信息说明 + +![image-20230401234038072](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012340170.png) + +### makefile 编译链接命令解释 + +![image-20230401234152788](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012341878.png) + +![image-20230401234450436](https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202304012344501.png) \ No newline at end of file diff --git a/Experiment_2/client.cpp b/Experiment_2/client.cpp new file mode 100644 index 0000000..f246db2 --- /dev/null +++ b/Experiment_2/client.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +int main() +{ + // 定义客户端 sockfd 套接字描述符 + int sock_cli = socket(AF_INET, SOCK_STREAM, 0); + + // 定义 sockaddr_in + struct sockaddr_in servaddr; + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; // TCP/IP 协议族 + servaddr.sin_port = htons(8023); // 服务器端口 + servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 ip + + // connect 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1 + if (connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) + { + perror("connect"); + exit(1); + } + cout << "连接服务器成功!\n"; + + char sendbuf[100]; // 发送缓冲数组 + // char recvbuf[100]; // 接收缓冲数组 + while (1) + { + memset(sendbuf, 0, sizeof(sendbuf)); + cin >> sendbuf; // 向发送缓冲数组写入数据 + send(sock_cli, sendbuf, sizeof(sendbuf), 0); // 发送数据 + if (strcmp(sendbuf, "exit") == 0) + break; // 如果发送字符为exit,则跳出发送循环 + } + + close(sock_cli); // 关闭发送套接字描述符 + return 0; +} \ No newline at end of file diff --git a/Experiment_2/makefile b/Experiment_2/makefile new file mode 100644 index 0000000..abd9eb8 --- /dev/null +++ b/Experiment_2/makefile @@ -0,0 +1,35 @@ +CXX = g++ +CXXFLAGS = -Wall -Wextra -g + +SRCS_TEST_SERVER = test_server.cpp server.cpp +SRCS_CLIENT = client.cpp + +OBJS_TEST_SERVER = $(SRCS_TEST_SERVER:.cpp=.o) +OBJS_CLIENT = $(SRCS_CLIENT:.cpp=.o) + +TEST_SERVER_TARGET = ./test/test_server +CLIENT_TARGET = ./test/client + +.PHONY: all clean start_server start_client + +all: test_server client + +test_server: $(OBJS_TEST_SERVER) + $(CXX) $(CXXFLAGS) $^ -o $(TEST_SERVER_TARGET) -lpthread + +client: $(OBJS_CLIENT) + $(CXX) $(CXXFLAGS) $^ -o $(CLIENT_TARGET) + +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# clean: +# rm -f $(OBJS_TEST_SERVER) $(OBJS_CLIENT) $(TEST_SERVER_TARGET) $(CLIENT_TARGET) +clean: + rm -rf *.o ./test/* + +start_server: test_server + $(TEST_SERVER_TARGET) + +start_client: client + $(CLIENT_TARGET) diff --git a/Experiment_2/server.cpp b/Experiment_2/server.cpp new file mode 100644 index 0000000..536de28 --- /dev/null +++ b/Experiment_2/server.cpp @@ -0,0 +1,82 @@ +#include "server.h" + +// 构造函数 +server::server(int port, string ip) : server_port(port), server_ip(ip) {} + +// 析构函数 +server::~server() +{ + for (auto conn : sock_arr) + close(conn); + 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); + // 创建线程 + 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; + break; + } + + cout << "收到套接字描述符为" << conn << "发来的信息:" << buffer << endl; + } +} \ No newline at end of file diff --git a/Experiment_2/server.h b/Experiment_2/server.h new file mode 100644 index 0000000..bbca84a --- /dev/null +++ b/Experiment_2/server.h @@ -0,0 +1,33 @@ +#ifndef SERVER_H +#define SERVER_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +class server +{ +private: + int server_port; // 服务器端口号 + int server_sockfd; // 设为 listen 状态的套接字描述符 + string server_ip; // 服务器 ip + vector sock_arr; // 保存所有套接字描述符 +public: + server(int port, string ip); // 构造函数 + ~server(); // 析构函数 + void run(); // 服务器开始服务 + static void RecvMsg(int conn); // 子线程工作的静态函数:用于在子线程中处理客户端的数据收发,conn 参数是与客户端建立连接后返回的新套接字描述符,用于指定当前处理的客户端。 +}; + +#endif \ No newline at end of file diff --git a/Experiment_2/test_server.cpp b/Experiment_2/test_server.cpp new file mode 100644 index 0000000..b556d13 --- /dev/null +++ b/Experiment_2/test_server.cpp @@ -0,0 +1,7 @@ +#include "server.h" + +int main() +{ + server serv(8023, "127.0.0.1"); // 创建实例,传入端口号和 ip 作为构造函数参数 + serv.run(); // 启动服务 +} \ No newline at end of file diff --git a/Experiment_2/test_thread.cpp b/Experiment_2/test_thread.cpp new file mode 100644 index 0000000..0e24cff --- /dev/null +++ b/Experiment_2/test_thread.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +using namespace std; + +void print() +{ + for (int i = 1; i <= 10; i++) + { + cout << i << endl; + sleep(1); // 休眠 1 秒钟 + } +} + +int main(){ + thread t1(print),t2(print); + t1.join(); + t2.join(); + // 也可以使用 detach + // t1.detach(); + // t1.detach(); +} \ No newline at end of file