20242817-李臻-课下测试:网络编程高级I/O(AI)
20242817-李臻-课下测试:网络编程高级I/O(AI)
一、实验任务
- 在 Ubuntu 或 openEuler 中完成任务(推荐openEuler)
- 借助AI研究Socket 阻塞和非阻塞方式,给出非阻塞Socket的例子,编译与运行过程(4分)
- 借助AI研究Socket与多路复用,给出相关的例子,编译与运行过程(9分)
- 提交代码链接(github 或者 gitee)和 git log 截图(1分)
二、实验过程
任务一:Socket阻塞和非阻塞
Socket 阻塞与非阻塞方式的分析
在网络编程中,Socket支持不同的I/O模式,其中阻塞模式和非阻塞模式是最常用的两种模式。理解这两种模式的区别有助于选择合适的方式。
阻塞模式(Blocking Mode)
- 特点:
- 连接、读、写操作会阻塞,直到操作完成。
- 优点:
- 简单易懂,代码直观。
- 缺点:
- 性能问题,效率低下,不适合多连接处理。
非阻塞模式(Non-Blocking Mode)
- 特点:
- 操作不阻塞,立即返回,若操作未完成返回错误码。
- 优点:
- 高效处理多个连接,灵活性高,节省资源。
- 缺点:
- 编程复杂,需要处理异常,可能需要多线程/多进程。
阻塞模式与非阻塞模式的对比
特性 | 阻塞模式 | 非阻塞模式 |
---|---|---|
默认行为 | 等待操作完成 | 立即返回,可能返回错误码 |
I/O 操作 | 阻塞程序 | 不阻塞,返回错误码 |
适用场景 | 简单应用,单一连接 | 高并发,多连接 |
程序复杂度 | 简单,直观 | 复杂,需要事件轮询 |
性能 | 可能浪费资源 | 提高并发能力,节省资源 |
典型错误处理 | 无需处理特定错误 | 需要处理EAGAIN或EWOULDBLOCK |
多线程/多进程支持 | 不需要 | 经常需要 |
何时选择阻塞模式与非阻塞模式?
-
阻塞模式适合:
- 单线程或低并发应用。
- 网络请求较少,对延迟要求不高的场景。
-
非阻塞模式适合:
- 高并发场景,如Web服务器。
- 需要高效事件驱动处理多个连接时。
任务二:非阻塞Socket实践
在非阻塞模式下,系统调用(如 recv()、send())不会导致进程阻塞等待数据。如果没有数据可读或可写,函数会立即返回,并给出一个特定的错误码(例如,EAGAIN 或 EWOULDBLOCK)。
nonblock_socket.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <errno.h>#define SERVER_PORT 8080
#define SERVER_IP "127.0.0.1"int main() {// 创建一个 Socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("socket");exit(EXIT_FAILURE);}// 设置非阻塞模式int flags = fcntl(sockfd, F_GETFL, 0);if (flags == -1) {perror("fcntl F_GETFL");close(sockfd);exit(EXIT_FAILURE);}flags |= O_NONBLOCK; // 设置非阻塞if (fcntl(sockfd, F_SETFL, flags) == -1) {perror("fcntl F_SETFL");close(sockfd);exit(EXIT_FAILURE);}// 设置服务器地址struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);// 尝试连接服务器int connect_status = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (connect_status == -1 && errno != EINPROGRESS) {perror("connect");close(sockfd);exit(EXIT_FAILURE);}printf("Socket is now in non-blocking mode. Trying to connect...\n");// 等待连接完成fd_set write_fds;FD_ZERO(&write_fds);FD_SET(sockfd, &write_fds);struct timeval timeout;timeout.tv_sec = 5; // 设置超时时间为 5 秒timeout.tv_usec = 0;int select_result = select(sockfd + 1, NULL, &write_fds, NULL, &timeout);if (select_result == -1) {perror("select");close(sockfd);exit(EXIT_FAILURE);} else if (select_result == 0) {printf("Connection timeout.\n");close(sockfd);exit(EXIT_FAILURE);} else if (FD_ISSET(sockfd, &write_fds)) {printf("Connection established.\n");}// 在非阻塞模式下进行读写操作char send_buffer[] = "Hello, server!";int bytes_sent = send(sockfd, send_buffer, strlen(send_buffer), 0);if (bytes_sent == -1 && errno != EAGAIN && errno != EWOULDBLOCK) {perror("send");close(sockfd);exit(EXIT_FAILURE);}// 关闭连接close(sockfd);return 0;
}
运行程序前,确保有一个服务器在 127.0.0.1:8080 上监听。可以使用如下命令启动一个简单的服务器
python3 -m http.server 8080
编译运行刚才的程序
./non_blocking_socket
返回服务器查看结果
- 非阻塞模式设置: 通过 fcntl() 函数来修改 Socket 文件描述符,使其工作在非阻塞模式。
- select() 系统调用: 用来检查 Socket 是否可以写入(连接是否完成)。
任务三:Socket与多路复用
Socket与多路复用分析
多路复用是网络编程中提高效率的关键技术之一,它允许单个线程或进程同时处理多个Socket连接。以下是几种常见的多路复用技术及其分析。
多路复用技术
1. select()
- 特点:
- 检查多个文件描述符(包括Socket),看它们是否有活动的I/O。
- 优点:
- 实现简单,跨平台支持。
- 缺点:
- 可监控的文件描述符数量有限(FD_SETSIZE)。
- 每次调用都需要复制文件描述符集合,效率较低。
2. poll()
- 特点:
- 类似于select,但不使用位图,而是使用链表维护文件描述符。
- 优点:
- 没有文件描述符数量限制。
- 缺点:
- 同样在大量文件描述符时效率较低。
3. epoll()
- 特点:
- Linux特有的高效多路复用接口,只监控活跃的文件描述符。
- 优点:
- 高效,支持大量文件描述符。
- 仅在状态改变时通知,减少不必要的系统调用。
- 缺点:
- 仅限Linux平台。
多路复用技术对比
特性 | select() | poll() | epoll() |
---|---|---|---|
平台支持 | 跨平台 | 跨平台 | Linux专用 |
文件描述符限制 | 有(FD_SETSIZE) | 无 | 无 |
效率 | 低(大量描述符时) | 低(大量描述符时) | 高 |
实现复杂度 | 中等 | 中等 | 高 |
适用场景 | 少量文件描述符 | 大量文件描述符 | 高效处理大量连接 |
何时选择多路复用技术?
-
select()适合:
- 跨平台应用,文件描述符数量较少的场景。
-
poll()适合:
- 需要无文件描述符数量限制的场景。
-
epoll()适合:
- Linux平台,需要高效处理大量并发连接的场景。
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024int main() {int server_fd, new_socket, client_sockets[MAX_CLIENTS] = {0};struct sockaddr_in address;int addrlen = sizeof(address);fd_set readfds;char buffer[BUFFER_SIZE] = {0};// 1. 创建TCP套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 2. 绑定地址和端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 3. 开始监听if (listen(server_fd, 3) < 0) {perror("listen failed");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);// 4. 主循环处理连接while (1) {FD_ZERO(&readfds);FD_SET(server_fd, &readfds);int max_fd = server_fd;// 将客户端套接字加入监控集合for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] > 0) {FD_SET(client_sockets[i], &readfds);if (client_sockets[i] > max_fd)max_fd = client_sockets[i];}}// 5. 使用select监控可读事件if (select(max_fd + 1, &readfds, NULL, NULL, NULL) < 0) {perror("select error");exit(EXIT_FAILURE);}// 6. 处理新连接if (FD_ISSET(server_fd, &readfds)) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept error");continue;}// 将新套接字加入客户端数组for (int i = 0; i < MAX_CLIENTS; i++) {if (client_sockets[i] == 0) {client_sockets[i] = new_socket;printf("New connection: socket fd %d\n", new_socket);break;}}}// 7. 处理客户端数据for (int i = 0; i < MAX_CLIENTS; i++) {int client_fd = client_sockets[i];if (client_fd > 0 && FD_ISSET(client_fd, &readfds)) {int valread = read(client_fd, buffer, BUFFER_SIZE);if (valread == 0) { // 连接关闭getpeername(client_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);printf("Client disconnected\n");close(client_fd);client_sockets[i] = 0;} else { // 回显数据buffer[valread] = '\0';printf("Received: %s\n", buffer);send(client_fd, buffer, valread, 0);}}}}return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUFFER_SIZE 1024int main() {int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("Socket creation error");exit(EXIT_FAILURE);}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {perror("Invalid address");exit(EXIT_FAILURE);}if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {perror("Connection Failed");exit(EXIT_FAILURE);}while (1) {printf("Enter message: ");fgets(buffer, BUFFER_SIZE, stdin);send(sock, buffer, strlen(buffer), 0);read(sock, buffer, BUFFER_SIZE);printf("Server response: %s\n", buffer);}return 0;
}
编译运行
测试流程
- 客户端输入消息后,服务端会显示接收内容并返回响应
- 可同时打开多个客户端终端进行并发测试
- 输入Ctrl+C关闭连接
运行服务器程序
运行客户端1
运行客户端2
运行发现服务器能够接收到客户发送的数据。
任务四:Gitee托管
代码仓库链接:https://gitee.com/li-zhen1215/homework/tree/master/Week9