不同操作系统非阻塞Socket事件驱动关键差异
- Linux
- 事件驱动模型:常用epoll,它采用事件通知机制,在内核态维护一个事件表。epoll有两种工作模式:水平触发(LT)和边缘触发(ET)。LT模式下,只要文件描述符还有未处理的事件,epoll_wait就会一直返回;ET模式下,只有在状态发生变化时才触发,适合处理高速流数据,需要应用程序更主动地处理数据。
- 系统调用:通过epoll_create创建epoll实例,epoll_ctl添加、修改或删除监控的文件描述符,epoll_wait等待事件发生。
- Windows
- 事件驱动模型:使用I/O完成端口(IOCP),它是一种异步I/O模型,基于线程池来处理I/O完成通知。每个I/O完成端口可以关联多个Socket句柄,I/O操作完成后,系统将一个完成包投递到相应的I/O完成端口,线程池中的线程从端口中获取完成包进行处理。
- 系统调用:使用CreateIoCompletionPort函数创建I/O完成端口并将Socket句柄与之关联,GetQueuedCompletionStatus函数用于从I/O完成端口获取完成包。
- macOS
- 事件驱动模型:采用kqueue,它是一种高效的事件通知机制,类似于Linux的epoll。kqueue维护一个事件队列,通过kevent系统调用进行事件的注册、修改和获取。它支持水平触发和边缘触发两种模式。
- 系统调用:通过kqueue函数创建kqueue实例,kevent函数用于添加、修改或删除监控事件,kevent函数还用于等待事件发生并获取事件列表。
编写跨平台代码统一处理差异的方法
- 使用跨平台库
- libuv:是一个高性能的事件驱动库,提供了统一的非阻塞I/O和事件驱动抽象层。它支持多种操作系统,包括Linux、Windows和macOS。使用libuv时,开发者通过其提供的API创建Socket、注册事件回调等,libuv内部会根据不同操作系统选择合适的事件驱动模型。例如,在libuv中创建一个TCP服务器:
#include <uv.h>
#include <stdio.h>
void on_new_connection(uv_stream_t* server, int status) {
if (status < 0) {
fprintf(stderr, "New connection error %s\n", uv_strerror(status));
return;
}
uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
uv_tcp_init(uv_default_loop(), client);
if (uv_accept(server, (uv_stream_t*)client) == 0) {
// 处理客户端连接
} else {
uv_close((uv_handle_t*)client, NULL);
}
}
int main() {
uv_loop_t* loop = uv_default_loop();
uv_tcp_t server;
uv_tcp_init(loop, &server);
struct sockaddr_in addr;
uv_ip4_addr("0.0.0.0", 12345, &addr);
uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
int r = uv_listen((uv_stream_t*)&server, 128, on_new_connection);
if (r) {
fprintf(stderr, "Listen error %s\n", uv_strerror(r));
return 1;
}
return uv_run(loop, UV_RUN_DEFAULT);
}
- Boost.Asio:是一个广泛使用的跨平台C++网络编程库,它提供了基于异步操作的Socket编程接口,支持多种操作系统的事件驱动模型。Boost.Asio通过封装不同操作系统的底层实现,为开发者提供了统一的异步I/O操作方式。例如,使用Boost.Asio创建一个简单的TCP客户端:
#include <iostream>
#include <boost/asio.hpp>
int main() {
try {
boost::asio::io_context io_context;
boost::asio::ip::tcp::socket socket(io_context);
boost::asio::ip::tcp::resolver resolver(io_context);
boost::asio::connect(socket, resolver.resolve({"127.0.0.1", "12345"}));
boost::asio::write(socket, boost::asio::buffer("Hello, server!"));
char buffer[1024];
size_t length = socket.read_some(boost::asio::buffer(buffer));
std::cout.write(buffer, length);
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
return 0;
}
- 条件编译
- 通过预处理器指令(如
#ifdef
),根据不同的操作系统定义不同的代码块。例如:
#ifdef _WIN32
// Windows下的事件驱动代码,使用IOCP
#include <windows.h>
#include <io.h>
#include <stdio.h>
// 定义相关的函数和变量处理IOCP
#elif defined(__linux__)
// Linux下的事件驱动代码,使用epoll
#include <sys/epoll.h>
#include <unistd.h>
// 定义相关的函数和变量处理epoll
#elif defined(__APPLE__)
// macOS下的事件驱动代码,使用kqueue
#include <sys/event.h>
#include <unistd.h>
// 定义相关的函数和变量处理kqueue
#endif
- 虽然条件编译可以实现跨平台,但代码维护相对复杂,不同平台的代码逻辑交织在一起,不利于代码的可读性和扩展性。所以,在实际开发中,更推荐使用跨平台库如libuv或Boost.Asio。