1 阻塞模式与非阻塞模式的概念
(1)阻塞模式
-
概念: 在阻塞模式下,当套接字执行I/O操作时,如果操作不能立即完成,调用函数会一直等待直到操作完成。在等待期间,执行操作的线程会被阻塞,无法继续执行其他任务。
-
特点:
- 简单直观:对于许多简单的网络应用来说,阻塞模式编程简单直观,易于理解和实现。
- 资源消耗:由于线程在等待 I/O 操作完成时被阻塞,可能会导致资源的浪费,特别是在高并发场景下。
- 效率:在阻塞模式下,线程在等待 I/O 操作完成时会处于空闲状态,这可能会降低程序的执行效率。
- 应用场景: 阻塞模式适用于那些对实时性要求不高,且I/O操作不频繁的场景。
(2)非阻塞模式
-
概念: 非阻塞模式下,套接字在执行 I/O 操作时,无论操作是否完成,调用函数都会立即返回。即使操作没有完成,线程也不会被阻塞,可以继续执行其他任务。
-
特点:
- 高效性:非阻塞模式允许线程在等待 I/O 操作完成的同时执行其他任务,提高了程序的执行效率。
- 编程复杂度:由于非阻塞模式下,函数会在操作未完成时立即返回,因此需要程序员自行处理轮询和重试的逻辑,增加了编程的复杂度。
- 资源消耗:虽然非阻塞模式可以提高线程利用率,但在高并发场景下,频繁的轮询和重试可能会导致额外的CPU资源消耗。
- 应用场景: 非阻塞模式适用于那些对实时性要求高,且需要处理大量并发 I/O 操作的场景,如服务器端的网络编程。
2 阻塞模式套接字编程
2.1 阻塞模式的工作原理
Windows 套接字(Socket)的阻塞模式工作原理主要基于操作系统的 I/O 操作机制。在阻塞模式下,当一个套接字被调用执行 I/O 操作(如发送数据或接收数据)时,如果操作不能立即完成,调用线程将会被挂起,直到操作完成为止。
具体来说,当调用如 send、recv 等函数时,如果此时套接字的缓冲区没有足够的空间来发送数据,或者没有数据可读来接收,那么调用这些函数的线程将会被阻塞。操作系统会将这个线程置于等待状态,并释放CPU控制权给其他线程执行。直到所需的 I/O 操作完成(例如,数据发送完毕或接收到数据),操作系统才会唤醒这个被阻塞的线程,使其继续执行后续的代码。
这种阻塞模式的好处在于编程简单直观,开发者无需关心底层的 I/O 操作细节。然而,它也存在一些缺点。例如,在高并发场景下,大量的线程可能因为等待 I/O 操作而被阻塞,导致 CPU 资源得不到充分利用,进而可能影响到程序的性能。
此外,值得注意的是,并不是所有的 Windows Sockets API 函数在阻塞模式下都会发生阻塞。例如,当以阻塞模式的套接字为参数调用 bind、listen 等函数时,它们会立即返回,因为这些操作并不涉及到实际的 I/O 数据传输。
2.2 阻塞模式下的发送与接收操作
(1)发送操作
在阻塞模式下,当调用发送函数(如 send、WSASend 等)时,线程会等待直到数据完全发送或发生错误。如果套接字的发送缓冲区已满,调用线程将被阻塞,直到有足够的空间可供发送数据。这意味着在发送大量数据或网络状况不佳时,线程可能会被阻塞一段时间。
(2)接收操作
同样地,在阻塞模式下执行接收操作(如 recv、WSARecv 等)时,线程会等待直到有数据可读或发生错误。如果套接字的接收缓冲区为空,调用线程将被阻塞,直到有数据到达。这意味着在没有数据到达或网络延迟较高时,接收操作的线程也会处于阻塞状态。
(3)注意事项
- 线程管理:由于阻塞模式下线程可能会被阻塞,因此合理地管理线程是非常重要的。通常,可以将套接字 I/O 操作放在单独的线程中执行,以避免阻塞主线程。
- 错误处理:在阻塞模式下,如果发送或接收操作失败,会返回错误代码。因此,需要适当地处理这些错误,比如重试发送、关闭套接字或通知用户等。
- 性能考虑:在高并发或需要快速响应的场景中,阻塞模式可能会导致性能问题。此时,可以考虑使用非阻塞模式或异步 I/O 来提高性能。
2.3 阻塞模式套接字编程示例
在 Windows 套接字编程中,阻塞模式是最常见的操作模式,也是默认模式。以下是一个简单的阻塞模式编程示例,包括服务端和客户端的创建、连接和数据发送/接收。
(1)服务端示例
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr, clientAddr;
int addrSize = sizeof(clientAddr);
char buffer[1024];
int result;
// 初始化Winsock库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed: %d\n", WSAGetLastError());
return 1;
}
// 创建套接字
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == INVALID_SOCKET) {
printf("Could not create socket: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 设置服务器地址信息
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(12345);
// 绑定套接字到服务器地址
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Bind failed with error: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}
// 开始监听连接请求
if (listen(serverSocket, 5) == SOCKET_ERROR) {
printf("Listen failed with error: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}
// 接受客户端连接
clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrSize);
if (clientSocket == INVALID_SOCKET) {
printf("Accept failed with error: %d\n", WSAGetLastError());
closesocket(serverSocket);
WSACleanup();
return 1;
}
// 接收数据
result = recv(clientSocket, buffer, sizeof(buffer), 0);
if (result > 0) {
buffer[result] = '\0';
printf("Received: %s\n", buffer);
}
else {
printf("recv failed with error: %d\n", WSAGetLastError());
}
// 发送响应
send(clientSocket, "Hello from server!", strlen("Hello from server!"), 0);
// 关闭套接字和Winsock库
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();
return 0;
}
(2)客户端示例
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET clientSocket;
struct sockaddr_in serverAddr;
char buffer[1024];
int result;
// 初始化Winsock库
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("WSAStartup failed: %d\n", WSAGetLastError());
return 1;
}
// 创建套接字
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("Could not create socket: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 设置服务器地址信息
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 假设服务器运行在本地
serverAddr.sin_port = htons(12345);
// 连接到服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Connect failed with error: %d\n", WSAGetLastError());
closesocket(clientSocket);
WSACleanup();
return 1;
}
// 发送数据
send(clientSocket, "Hello from client!", strlen("Hello from client!"), 0);
// 接收响应
result = recv(clientSocket, buffer, sizeof(buffer), 0);
if (result > 0) {
buffer[result] = '\0';
printf("Received: %s\n", buffer);
}
else {
printf("recv failed with error: %d\n", WSAGetLastError());
}
// 关闭套接字和Winsock库
closesocket(clientSocket);
WSACleanup();
return 0;
}
3 非阻塞模式套接字编程
3.1 非阻塞模式的工作原理
非阻塞模式工作原理主要基于事件驱动和异步通信。与阻塞模式不同,非阻塞模式允许套接字操作(如发送和接收数据)立即返回,而无需等待操作完成。这使得应用程序能够继续执行其他任务,而不是被阻塞在套接字操作上。
在非阻塞模式下,当调用 Windows 套接字API(如 send、recv 等)时,这些函数会立即返回,而不管操作是否完成。如果操作不能立即完成(例如,发送缓冲区已满或没有数据可读),函数将返回一个错误代码(如 WSAEWOULDBLOCK),而不是阻塞调用线程。这样,应用程序就可以继续执行其他任务,而不必等待套接字操作完成。
为了实现非阻塞模式下的通信,应用程序需要采取一种循环检查或事件通知的机制。例如,在接收数据时,应用程序可以不断调用recv函数,直到成功接收到数据或遇到错误为止。同样,在发送数据时,应用程序可能需要检查发送缓冲区的状态,以确保数据可以安全地发送。
此外,非阻塞模式还涉及到一些高级概念,如 I/O 完成端口(IOCP)和事件选择(select)机制。这些机制允许应用程序以更高效的方式处理多个套接字和并发 I/O 操作。例如,IOCP允许应用程序将套接字操作与特定的线程池关联起来,从而实现高效的并发处理。
需要注意的是,非阻塞模式虽然提高了应用程序的响应性和并发性能,但也增加了编程的复杂性。应用程序需要负责处理错误、超时、数据重排等问题,以确保通信的正确性和可靠性。
3.2 将套接字设置为非阻塞模式
在 Windows 平台上,将套接字设置为非阻塞模式涉及到使用 ioctlsocket 函数来修改套接字的行为。以下是如何将已创建的套接字设置为非阻塞模式的步骤:
- 首先,创建一个套接字。
- 然后使用 ioctlsocket 函数和 FIONBIO 标志来设置套接字为非阻塞模式。
以下是一个简单的示例,展示了如何设置套接字为非阻塞模式:
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET socketDescriptor;
u_long nonblocking = 1;
int result;
// 初始化Winsock库
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
return 1;
}
// 创建套接字
socketDescriptor = socket(AF_INET, SOCK_STREAM, 0);
if (socketDescriptor == INVALID_SOCKET) {
printf("Could not create socket: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 设置套接字为非阻塞模式
result = ioctlsocket(socketDescriptor, FIONBIO, &nonblocking);
if (result == SOCKET_ERROR) {
printf("Could not set socket to non-blocking mode: %d\n", WSAGetLastError());
closesocket(socketDescriptor);
WSACleanup();
return 1;
}
// 在这里你可以继续使用socketDescriptor进行连接、发送和接收操作
// ...
// 关闭套接字和清理Winsock库
closesocket(socketDescriptor);
WSACleanup();
return 0;
}
在上面的代码中,FIONBIO 是一个特殊的命令,它告诉 ioctlsocket 函数要修改套接字的阻塞模式。nonblocking变量设置为1,表示我们要将套接字设置为非阻塞模式。如果 ioctlsocket 函数返回 SOCKET_ERROR,则表示设置失败,我们可以检查 WSAGetLastError 函数返回的错误代码来了解失败的原因。
一旦套接字被设置为非阻塞模式,你就可以调用 connect、send、recv 等函数,它们会立即返回,而不会阻塞调用线程。如果操作不能立即完成,它们会返回一个错误代码,通常是 WSAEWOULDBLOCK。
3.3 非阻塞模式下的发送与接收操作
在Windows套接字非阻塞模式下,发送和接收操作将立即返回,无论操作是否完成。如果数据无法立即发送或接收,这些操作会返回一个错误代码,通常是WSAEWOULDBLOCK,表示操作被阻塞了,需要稍后再试。
以下是在非阻塞模式下执行发送和接收操作的基本步骤:
(1)发送数据
SOCKET socketDescriptor; // 假设已经创建了非阻塞套接字
char *sendBuffer = "Hello, server!";
int sendLength = strlen(sendBuffer);
int bytesSent;
// 尝试发送数据
bytesSent = send(socketDescriptor, sendBuffer, sendLength, 0);
if (bytesSent == SOCKET_ERROR) {
int error = WSAGetLastError();
if (error == WSAEWOULDBLOCK) {
// 数据当前无法发送,稍后再试
printf("Data could not be sent immediately, would block.\n");
} else {
// 发送失败,出现其他错误
printf("Send failed with error: %d\n", error);
}
} else {
// 数据已成功发送部分或全部
printf("Sent %d bytes.\n", bytesSent);
}
(2)接收数据
char recvBuffer[1024];
int bytesReceived;
// 尝试接收数据
bytesReceived = recv(socketDescriptor, recvBuffer, sizeof(recvBuffer) - 1, 0);
if (bytesReceived > 0) {
// 成功接收到数据
recvBuffer[bytesReceived] = '\0'; // 确保字符串以null结尾
printf("Received: %s\n", recvBuffer);
} else if (bytesReceived == 0) {
// 连接已关闭,对方发送了EOF(通常是通过shutdown或close实现的)
printf("Connection closed by peer.\n");
} else {
int error = WSAGetLastError();
if (error == WSAEWOULDBLOCK) {
// 当前没有数据可接收,稍后再试
printf("No data to receive, would block.\n");
} else {
// 接收失败,出现其他错误
printf("Recv failed with error: %d\n", error);
}
}
在非阻塞模式下,如果 send 或 recv 返回 WSAEWOULDBLOCK,则应用程序需要稍后再次尝试发送或接收数据。这通常涉及到在应用程序中使用某种形式的循环或事件驱动机制来定期检查套接字的状态。
此外,非阻塞套接字编程通常还需要处理其他情况,例如部分数据发送或接收、错误处理以及超时等。在实际应用中,可能还需要使用更高级的技术,如 I/O 完成端口(IOCP)或事件选择(select)机制,以便更有效地处理多个套接字和并发 I/O 操作。
3.4 非阻塞模式套接字编程示例
(1)服务端示例
#include <stdio.h>
#include <winsock2.h>
#include <mswsock.h> // For ioctlsocket
#include <string>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET listenSocket, clientSocket;
struct sockaddr_in serverAddr;
int result;
char recvBuffer[BUFFER_SIZE];
fd_set readfds;
struct timeval timeout;
int ioctlResult;
u_long iMode = 1; // Enable non-blocking mode
// 初始化Winsock库
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
// 创建套接字
listenSocket = socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET) {
printf("Socket creation failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 将套接字设置为非阻塞模式
ioctlResult = ioctlsocket(listenSocket, FIONBIO, &iMode);
if (ioctlResult == SOCKET_ERROR) {
printf("Failed to set socket to non-blocking mode: %d\n", WSAGetLastError());
closesocket(listenSocket);
WSACleanup();
return 1;
}
// 设置服务器地址信息
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(PORT);
// 绑定套接字到指定地址和端口
result = bind(listenSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (result == SOCKET_ERROR) {
printf("Bind failed with error: %d\n", WSAGetLastError());
closesocket(listenSocket);
WSACleanup();
return 1;
}
// 开始监听连接请求
result = listen(listenSocket, SOMAXCONN);
if (result == SOCKET_ERROR) {
printf("Listen failed with error: %d\n", WSAGetLastError());
closesocket(listenSocket);
WSACleanup();
return 1;
}
printf("Server listening on port %d...\n", PORT);
// 主循环,等待并接受客户端连接
while (1) {
FD_ZERO(&readfds);
FD_SET(listenSocket, &readfds);
timeout.tv_sec = 5; // 设置5秒超时
timeout.tv_usec = 0;
// 使用select等待套接字变得可读
result = select(listenSocket + 1, &readfds, NULL, NULL, &timeout);
if (result == SOCKET_ERROR) {
printf("Select failed with error: %d\n", WSAGetLastError());
break;
}
else if (result == 0) {
// 超时,没有事件发生
printf("Timeout occurred, no incoming connections.\n");
continue;
}
// 检查是否有新的连接请求
if (FD_ISSET(listenSocket, &readfds)) {
clientSocket = accept(listenSocket, NULL, NULL);
if (clientSocket == SOCKET_ERROR) {
if (WSAGetLastError() != WSAEWOULDBLOCK) {
printf("Accept failed with error: %d\n", WSAGetLastError());
}
}
else {
// 设置客户端套接字为非阻塞模式
ioctlResult = ioctlsocket(clientSocket, FIONBIO, &iMode);
if (ioctlResult == SOCKET_ERROR) {
printf("Failed to set client socket to non-blocking mode: %d\n", WSAGetLastError());
closesocket(clientSocket);
continue;
}
// 客户端连接已接受,可以在这里处理客户端请求
printf("New client connected.\n");
// 这里可以添加代码来处理客户端发送的数据
// ...
std::string msg;
std::cin >> msg;
// 关闭客户端套接字
closesocket(clientSocket);
}
}
// 可以在这里添加代码来处理其他套接字(如果有的话)
}
// 关闭监听套接字并清理Winsock
closesocket(listenSocket);
WSACleanup();
return 0;
}
(2)客户端示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <mswsock.h> // For ioctlsocket
#pragma comment(lib, "ws2_32.lib")
#define SERVER_PORT 12345
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
WSADATA wsaData;
SOCKET clientSocket;
struct sockaddr_in serverAddr;
int result;
char sendBuffer[BUFFER_SIZE];
char recvBuffer[BUFFER_SIZE];
u_long iMode = 1; // Enable non-blocking mode
// 初始化Winsock库
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
// 创建套接字
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("Socket creation failed with error: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 将套接字设置为非阻塞模式
result = ioctlsocket(clientSocket, FIONBIO, &iMode);
if (result == SOCKET_ERROR) {
printf("Failed to set socket to non-blocking mode: %d\n", WSAGetLastError());
closesocket(clientSocket);
WSACleanup();
return 1;
}
// 设置服务器地址信息
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
serverAddr.sin_port = htons(SERVER_PORT);
// 连接到服务器
result = connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (result == SOCKET_ERROR) {
int error = WSAGetLastError();
if (error != WSAEWOULDBLOCK) {
printf("Connect failed with error: %d\n", error);
closesocket(clientSocket);
WSACleanup();
return 1;
}
else {
printf("Connect is in progress...\n");
}
}
else {
printf("Connected to server successfully.\n");
}
// 发送数据到服务器
strcpy(sendBuffer, "Hello, server!");
result = send(clientSocket, sendBuffer, strlen(sendBuffer), 0);
if (result == SOCKET_ERROR) {
int error = WSAGetLastError();
if (error != WSAEWOULDBLOCK) {
printf("Send failed with error: %d\n", error);
}
else {
printf("Send would block, retrying later...\n");
}
}
else {
printf("Sent %d bytes to server.\n", result);
}
// 接收来自服务器的数据
result = recv(clientSocket, recvBuffer, BUFFER_SIZE, 0);
if (result > 0) {
printf("Received %d bytes from server: %s\n", result, recvBuffer);
}
else if (result == 0) {
printf("Server closed the connection.\n");
}
else {
int error = WSAGetLastError();
if (error != WSAEWOULDBLOCK) {
printf("Recv failed with error: %d\n", error);
}
else {
printf("No data available, retrying later...\n");
}
}
// 关闭套接字并清理Winsock库
closesocket(clientSocket);
WSACleanup();
return 0;
}
(3)输出
服务端的输出为:
Server listening on port 12345...
New client connected.
客户端的输出为:
Connect is in progress...
Sent 14 bytes to server.
No data available, retrying later...
标签:error,编程,阻塞,模式,result,printf,接字
From: https://blog.csdn.net/h8062651/article/details/137590979