一、多线程并发服务器
在 高并发的 TCP 服务器 中,单线程或 fork() 多进程 方式会导致 资源浪费和性能瓶颈。因此,我们可以使用 多线程 来高效处理多个客户端的连接。
承接上文中的多进程并发服务器,代码优化目标:
1.使用 pthread 实现多线程服务器
2.每个客户端连接后,服务器创建一个独立线程进行处理
3.回显(Echo)客户端发送的消息
4.支持多个客户端同时连接
5.主线程负责监听连接,子线程负责处理客户端请求
完整代码:
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // exit()、malloc()、free()
#include <string.h> // 字符串操作
#include <unistd.h> // read(), write(), close()
#include <arpa/inet.h> // sockaddr_in, inet_addr()
#include <sys/socket.h> // 套接字 API
#include <netinet/in.h> // sockaddr_in 结构体
#include <pthread.h> // 线程 API#define PORT 8080 // 服务器监听端口
#define BUFFER_SIZE 1024 // 缓冲区大小
#define MAX_CLIENTS 100 // 最大客户端连接数// **线程处理客户端请求**
void *handle_client(void *arg) {int client_fd = *((int *)arg);free(arg); // 释放动态分配的内存char buffer[BUFFER_SIZE];int bytes_read;printf("✅ 客户端线程启动,处理客户端 %d\n", client_fd);while (1) {memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区bytes_read = read(client_fd, buffer, BUFFER_SIZE);if (bytes_read <= 0) {printf("❌ 客户端 %d 断开连接\n", client_fd);break; // 退出循环,关闭连接}printf("📩 收到客户端 %d 消息: %s\n", client_fd, buffer);// **发送回显消息**write(client_fd, buffer, bytes_read);}// **关闭客户端连接**close(client_fd);printf("关闭客户端 %d 连接\n", client_fd);return NULL;
}int main() {int server_fd, client_fd;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);pthread_t thread_id;// 1️⃣ 创建服务器套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("❌ Socket 创建失败");exit(EXIT_FAILURE);}// 2️⃣ 绑定服务器地址和端口server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("❌ 绑定失败");close(server_fd);exit(EXIT_FAILURE);}// 3️⃣ 监听客户端连接if (listen(server_fd, MAX_CLIENTS) < 0) {perror("❌ 监听失败");close(server_fd);exit(EXIT_FAILURE);}printf("⚡ 多线程 TCP 服务器已启动,监听端口 %d...\n", PORT);while (1) {printf("\n等待客户端连接...\n");// 4️⃣ 接受客户端连接client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);if (client_fd < 0) {perror("❌ 接受客户端连接失败");continue; // 继续等待下一个客户端}printf("✅ 客户端连接成功!IP: %s, 端口: %d\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 5️⃣ 创建线程处理客户端int *new_sock = malloc(sizeof(int)); // 动态分配内存,避免线程冲突*new_sock = client_fd;if (pthread_create(&thread_id, NULL, handle_client, (void *)new_sock) != 0) {perror("❌ 线程创建失败");close(client_fd);free(new_sock);} else {pthread_detach(thread_id); // 让线程自动回收}}// 6️⃣ 关闭服务器(通常不会执行到这里)close(server_fd);return 0;
}
✅ 代码运行步骤
- 编译:(假设文件名为
tcp_server_threads.c
)
gcc tcp_server_threads.c -o tcp_server_threads -pthread
- 运行服务器
./tcp_server_threads
输出示例:
多线程 TCP 服务器已启动,监听端口 8080...
等待客户端连接...
✅ 连接测试
方式 1:使用 telnet
telnet 127.0.0.1 8080
# 输入消息后按 Enter,服务器会返回相同的消息。
方式 2:使用 nc(Netcat)
🔹 启动多个客户端
nc 127.0.0.1 8080
输入内容,服务器会回显,如:
Hello Server
Hello Server # 服务器返回相同内容
详细步骤流程:1. 创建 TCP 套接字 -- socket() -- 创建服务器 socket2. 绑定 IP 和端口 -- bind() -- 监听 8080 端口3. 监听连接 -- listen() -- 允许最多 MAX_CLIENTS 个客户端排队4. 等待客户端连接 -- accept() -- 接受一个客户端连接5. 创建线程 -- pthread_create() -- 让每个客户端由一个线程处理6. 处理客户端请求 -- read() -- 读取客户端发送的数据7. 发送回显数据 -- write() -- 把数据发回客户端8. 关闭连接 -- close() -- 释放资源
该代码是一个基本的 TCP 多线程并发服务器,适用于 中等并发负载, 相比 fork()
,使用 pthread
可以减少资源消耗,提升并发性能。
后续代码可优化
1.使用线程池线程池可以复用线程,避免 pthread_create() 过多消耗资源。参考 pthread pool 机制,预创建固定数量线程,避免频繁创建销毁。2.使用 epoll 结合线程池结合 epoll 监听 accept(),减少 CPU 负担。3.日志管理服务器可以使用 syslog() 或文件写入方式记录 客户端连接信息。4.超时处理服务器可以设置 setsockopt() 限制客户端连接时间:
struct timeval timeout = {5, 0}; // 5 秒超时
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
二、I/O 复用服务器(select / poll)
在 UNIX/Linux
下主要有4种 I/O 模型:
阻塞I/O: 最常用、最简单、效率最低
非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:允许同时对多个I/O进行控制
信号驱动I/O: 一种异步通信模型
阻塞I/O 模式是最普遍使用的 I/O 模式,大部分程序使用的都是阻塞模式的 I/O ;缺省情况下,套接字建立后所处于的模式就是阻塞 I/O 模式。很多读写函数在调用过程中会发生阻塞,例如:读操作中的 read
、recv
、recvfrom
,写操作中的 write
、send
,其他操作:accept
、connect
。
读阻塞:以 read
函数为例:
进程调用 read 函数从套接字上读取数据,当套接字的接收缓冲区中还没有数据可读,函数 read 将发生阻塞。它会一直阻塞下去,等待套接字的接收缓冲区中有数据可读。经过一段时间后,缓冲区内接收到数据,于是内核便去唤醒该进程,通过 read 访问这些数据。但如果在进程阻塞过程中,对方发生故障,那这个进程将永远阻塞下去。
写阻塞:
在写操作时发生阻塞的情况要比读操作少。主要发生在要写入的缓冲区的大小小于要写入的数据量的情况下。这时,写操作不进行任何拷贝工作,将发生阻塞。一旦发送缓冲区内有足够的空间,内核将唤醒进程,将数据从用户缓冲区中拷贝到相应的发送数据缓冲区。UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在发送缓冲区满的情况,在UDP套接字上执行的写操作永远都不会阻塞。
非阻塞模式I/O:
当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。 应用程序不停的 polling
内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。也正因如此,这种模式在使用中不普遍,太浪费资源了。
fcntl()函数一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。可以使用函数fcntl()设置一个套接字的标志为O_NONBLOCK 来实现非阻塞。int fcntl(int fd, int cmd, long arg);int flag;flag = fcntl(sockfd, F_GETFL, 0);flag |= O_NONBLOCK;fcntl(sockfd, F_SETFL, flag);
多路复用I/O
应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的。可是,若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;比较好的方法是使用I/O多路复用。其基本思想是:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行 I/O时函数才返回。
函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
在 高并发的 TCP 服务器 中,传统的 fork()
多进程 或 pthread
多线程 方式容易导致 资源浪费和性能瓶颈。因此,我们才使用 I/O 复用技术(select
/ poll
/ epoll
),使 单线程 就能监听 多个客户端连接,从而提高并发性能。
多路复用select/poll/* According to POSIX.1-2001, POSIX.1-2008 */#include <sys/select.h>/* According to earlier standards */#include <sys/time.h>#include <sys/types.h>#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);/**********************************************************************@brief: 多路复用,将所需要使用的或者需要关注的文件描述符放在一个集合中,当集合中的文件描述符被触发了会去执行相应的任务@nfds: 最大文件描述符 + 1@readfds: 所有要读的文件文件描述符的集合@writefds: 所有要的写文件文件描述符的集合@exceptfds:其他要向我们通知的文件描述符 @timeout: 超时设置. NULL:一直阻塞,直到有文件描述符就绪或出错时间值为0:仅仅检测文件描述符集的状态,然后立即返回时间值不为0:在指定时间内,如果没有事件发生,则超时返回。struct timeval {long tv_sec; /* seconds */long tv_usec; /* microseconds */};@retval: 成功:返回就绪的文件描述符的个数失败:返回-1,并且设置全局错误码为了设置文件描述符我们要使用几个宏:宏的形式:void FD_ZERO(fd_set *fdset) //从fdset中清除所有的文件描述符void FD_SET(int fd,fd_set *fdset) //将fd加入到fdsetvoid FD_CLR(int fd,fd_set *fdset) //将fd从fdset里面清除int FD_ISSET(int fd,fd_set *fdset) //判断fd是否在fdset集合中**********************************************************************/#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);/**********************************************************************@brief: 多路复用,监管文件描述符@fds: 要监管的文件描述符的结构体指针struct pollfd {int fd; /* file descriptor 希望被触发的文件描述符 用户赋值*/short events; /* requested events 希望被触发的事件POLLIN 用户赋值*/short revents; /* returned events 希望被触发的事件发生与否POLLIN 系统赋值*/};The bits that may be set/returned in events and revents are defined in <poll.h>://可在man手册中查询别的events和revents的选值POLLIN There is data to read.POLLPRIThere is some exceptional condition on the file descriptor. Possibilitiesinclude:* There is out-of-band data on a TCP socket (see tcp(7)).* A pseudoterminal master in packet mode has seen a state change on theslave (see ioctl_tty(2)).* A cgroup.events file has been modified (see cgroups(7)).POLLOUTWriting is now possible, though a write larger that the available space in asocket or pipe will still block (unless O_NONBLOCK is set)./**@nfds: 最大文件描述符 + 1@timeout: >0:阻塞对应的时间(毫秒级)=0:不阻塞<0:一直阻塞@retval: >0:集合中已就绪的文件描述符个数=0:集合中没有已就绪的文件描述符-1:poll调用失败,并且设置全局错误码 **********************************************************************/
📌 I/O 复用的三种方式
方法 | 特点 | 适用场景 |
---|---|---|
select() | 需要遍历整个文件描述符集合,最大支持 1024 个连接 | 适用于 少量连接 的情况 |
poll() | 使用链表存储,支持更多连接,但仍然需要遍历整个集合 | 适用于 中等并发 |
epoll() | 事件驱动,只处理活跃的连接,性能远高于 select /poll | 适用于 高并发服务器 |
select() 多路复用服务器
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>#define PORT 8080
#define MAX_CLIENTS 100
#define BUFFER_SIZE 1024int main() {int server_fd, client_fd, max_fd, activity, i;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);char buffer[BUFFER_SIZE];fd_set read_fds, master_fds;// 1️⃣ 创建 TCP 套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("❌ Socket 创建失败");exit(EXIT_FAILURE);}// 2️⃣ 绑定服务器地址和端口server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("❌ 绑定失败");close(server_fd);exit(EXIT_FAILURE);}// 3️⃣ 监听客户端连接if (listen(server_fd, MAX_CLIENTS) < 0) {perror("❌ 监听失败");close(server_fd);exit(EXIT_FAILURE);}printf("`select()` 多路复用服务器已启动,监听端口 %d...\n", PORT);// 4️⃣ 初始化 `select` 的文件描述符集合FD_ZERO(&master_fds);FD_SET(server_fd, &master_fds);max_fd = server_fd;while (1) {read_fds = master_fds; // 每次循环都复制 `master_fds`// 5️⃣ 监听多个文件描述符 `select`activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL);if (activity < 0) {perror("❌ `select` 调用失败");continue;}// 6️⃣ 处理新客户端连接if (FD_ISSET(server_fd, &read_fds)) {client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);if (client_fd < 0) {perror("❌ 客户端连接失败");continue;}printf("新客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));FD_SET(client_fd, &master_fds);if (client_fd > max_fd) {max_fd = client_fd;}}// 7️⃣ 处理已连接的客户端数据for (i = server_fd + 1; i <= max_fd; i++) {if (FD_ISSET(i, &read_fds)) {memset(buffer, 0, BUFFER_SIZE);int bytes_read = read(i, buffer, BUFFER_SIZE);if (bytes_read <= 0) {printf("❌ 客户端断开连接\n");close(i);FD_CLR(i, &master_fds);} else {printf("📩 客户端消息: %s\n", buffer);write(i, buffer, bytes_read); // 回显}}}}close(server_fd);return 0;
}
poll() 多路复用服务器
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8080
#define MAX_CLIENTS 100
#define BUFFER_SIZE 1024int main() {int server_fd, client_fd, i;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);char buffer[BUFFER_SIZE];struct pollfd fds[MAX_CLIENTS];int nfds = 1;// 1️⃣ 创建 TCP 套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("❌ Socket 创建失败");exit(EXIT_FAILURE);}// 2️⃣ 绑定服务器地址和端口server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("❌ 绑定失败");close(server_fd);exit(EXIT_FAILURE);}// 3️⃣ 监听客户端连接if (listen(server_fd, MAX_CLIENTS) < 0) {perror("❌ 监听失败");close(server_fd);exit(EXIT_FAILURE);}printf("`poll()` 多路复用服务器已启动,监听端口 %d...\n", PORT);fds[0].fd = server_fd;fds[0].events = POLLIN;while (1) {// 4️⃣ 监听多个文件描述符 `poll`int activity = poll(fds, nfds, -1);if (activity < 0) {perror("❌ `poll` 调用失败");continue;}// 5️⃣ 处理新客户端连接if (fds[0].revents & POLLIN) {client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);if (client_fd < 0) {perror("❌ 客户端连接失败");continue;}printf("新客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));fds[nfds].fd = client_fd;fds[nfds].events = POLLIN;nfds++;}// 6️⃣ 处理已连接的客户端数据for (i = 1; i < nfds; i++) {if (fds[i].revents & POLLIN) {memset(buffer, 0, BUFFER_SIZE);int bytes_read = read(fds[i].fd, buffer, BUFFER_SIZE);if (bytes_read <= 0) {printf("❌ 客户端断开连接\n");close(fds[i].fd);fds[i] = fds[nfds - 1]; // 移除断开的客户端nfds--;} else {printf("📩 客户端消息: %s\n", buffer);write(fds[i].fd, buffer, bytes_read); // 回显}}}}close(server_fd);return 0;
}
使用 epoll 的高并发服务器(C 语言)
select()
和 poll()
适用于 1000 以内的连接,但随着连接数增加,性能下降。对于高并发服务器,建议使用 epoll()
(Linux) 或 kqueue()
(BSD/macOS)。
epoll
是 Linux 下 高效的 I/O 复用方式,相比 select()
和 poll()
,它支持:
- O(1) 事件触发:只处理活跃的文件描述符,不用遍历整个
fd_set
。 - 支持大规模并发:适用于 上万级别的连接,比
select()
/poll()
性能高很多。 - Edge Trigger (ET) & Level Trigger (LT):支持 边缘触发 和 水平触发,进一步优化性能。
epoll 关键 API
函数 功能
epoll_create1(0) 创建 epoll 实例,返回 epoll_fd
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) 添加监听的 fd
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event) 修改监听的 fd
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, &event) 删除监听的 fd
epoll_wait(epoll_fd, events, MAX_EVENTS, timeout) 等待事件触发
epoll
多路复用服务器(代码实现)(重点)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>#define PORT 8080 // 服务器监听端口
#define MAX_EVENTS 1000 // epoll 最大监听事件数
#define BUFFER_SIZE 1024 // 缓冲区大小// **🔹 设置 fd 为非阻塞模式**
void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {int server_fd, client_fd, epoll_fd, event_count, i;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);struct epoll_event event, events[MAX_EVENTS];char buffer[BUFFER_SIZE];// 1️⃣ 创建 TCP 套接字server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("❌ Socket 创建失败");exit(EXIT_FAILURE);}// 2️⃣ 绑定服务器地址和端口server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("❌ 绑定失败");close(server_fd);exit(EXIT_FAILURE);}// 3️⃣ 开始监听if (listen(server_fd, MAX_EVENTS) < 0) {perror("❌ 监听失败");close(server_fd);exit(EXIT_FAILURE);}printf("⚡ `epoll` 服务器启动,监听端口 %d...\n", PORT);// 4️⃣ 创建 epoll 实例epoll_fd = epoll_create1(0);if (epoll_fd == -1) {perror("❌ epoll_create1 失败");close(server_fd);exit(EXIT_FAILURE);}// 5️⃣ 设置 server_fd 为非阻塞模式,并添加到 epoll 监听set_nonblocking(server_fd);event.events = EPOLLIN; // 监听可读事件(LT 模式)event.data.fd = server_fd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);while (1) {// 6️⃣ 等待事件触发event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);for (i = 0; i < event_count; i++) {if (events[i].data.fd == server_fd) {// 7️⃣ 处理新客户端连接client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);if (client_fd < 0) {perror("❌ 接受客户端连接失败");continue;}printf("✅ 新客户端连接:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));set_nonblocking(client_fd); // 设置非阻塞模式event.events = EPOLLIN | EPOLLET; // 监听可读事件,ET 模式event.data.fd = client_fd;epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);} else {// 8️⃣ 处理客户端数据int client_fd = events[i].data.fd;memset(buffer, 0, BUFFER_SIZE);int bytes_read = read(client_fd, buffer, BUFFER_SIZE);if (bytes_read <= 0) {printf("❌ 客户端断开连接\n");epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);close(client_fd);} else {printf("📩 客户端消息: %s\n", buffer);write(client_fd, buffer, bytes_read); // 回显}}}}close(server_fd);close(epoll_fd);return 0;
}
✅ 代码运行步骤
- 编译(假设文件名为
epoll_server.c
)
gcc epoll_server.c -o epoll_server
- 运行服务器
./epoll_server
输出示例:
`epoll` 服务器启动,监听端口 8080...
✅ 连接测试
📌 使用 nc(Netcat)
nc 127.0.0.1 8080
输入内容,服务器会回显,如:
Hello Server
Hello Server # 服务器返回相同内容
epoll
工作模式
🔹 水平触发(LT,Level Trigger)
- 默认模式,事件未处理时会 持续触发。
- 适用于阻塞 I/O,确保数据不会丢失。
🔹 边缘触发(ET,Edge Trigger)
- 仅在状态变化时触发,不会重复触发。
- 必须使用非阻塞 I/O,否则可能丢失数据。
✅ epoll vs select / poll
特点 | select() | poll() | epoll() |
---|---|---|---|
最大连接数 | 1024(Linux 默认) | 无限制(但扫描所有) | 无限制(事件驱动) |
性能 | O(n) ,遍历 fd_set | O(n) ,遍历 pollfd | O(1) ,只处理活跃连接 |
适用场景 | 少量连接(<1000) | 中等连接数 | 高并发(>10000) |
代码可优化
使用线程池epoll_wait() 只负责监听,线程池 处理数据,提高吞吐量。
使用 EPOLLET(边缘触发)结合 非阻塞 read(),减少 epoll_wait() 触发次数,提高效率。
TCP SO_REUSEADDR避免服务器重启时 bind() 失败:
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
该代码是一个高效的 TCP 并发服务器,使用 epoll 事件驱动,适用于大规模连接。 相比 select()
,epoll
在高并发情况下性能更好,是 Linux 服务器的首选方案! 🎯
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!