Non-blocking BSD socket connections

本文探讨了非阻塞BSD套接字连接的处理方式,特别是当使用connect()返回EINPROGRESS或EWOULDBLOCK时,如何通过不同的方法检查连接是否成功或失败,包括使用getsockopt获取so_error、尝试读写操作进行错误滑落、使用getpeername等。

Non-blocking BSD socket connections

Situation: You set up a non-blocking socket and do a connect() that returns -1/EINPROGRESS or -1/EWOULDBLOCK. You select() the socket for writability. This returns as soon as the connection succeeds or fails. (Exception: Under some old versions of Ultrix, select() wouldn't notice failure before the 75-second timeout.)

Question: What do you do after select() returns writability? Did the connection fail? If so, how did it fail?

If the connection failed, the reason is hidden away inside something called so_error in the socket. Modern systems let you see so_error with getsockopt(,,SO_ERROR,,), but this isn't portable---in fact, getsockopt() can crash old systems. A different way to see so_error is through error slippage: any attempt to read or write data on the socket will return -1/so_error.

Sometimes you have data to immediately write to the connection. Then you can just write() the data. If connect() failed, the failure will be reported by write(), usually with the right connect() errno. This is the solution I supplied for IRC in 1990. Unfortunately, on some systems, under some circumstances, write() will substitute EPIPE for the old errno, so you lose information.

Another possibility is read(fd,&ch,0). If connect() succeeded, you get a 0 return value, except under Solaris, where you get -1/EAGAIN. If connect() failed, you should get the right errno through error slippage. Fatal flaw: under Linux, you will always get 0.

Another possibility is read(fd,&ch,1), but this leads to programming difficulties, since a character may in fact have arrived. Everything that reads the socket has to know about your pre-buffered character.

Another possibility is recv(,,,MSG_PEEK). Unfortunately, the recv() code for stream sockets was, historically, completely untested and a massive source of bugs. I'd steer clear of this.

Another possibility is to select() for readability. But this is wrong, because the connect() may have succeeded and data may have arrived before you had time to do a select().

Another possibility is getpeername(). If the socket is connected, getpeername() will return 0. If the socket is not connected, getpeername() will return ENOTCONN, and read(fd,&ch,1) will produce the right errno through error slippage. This is a combination of suggestions from Douglas C. Schmidt and Ken Keys.

Another possibility is a second connect(). This seems to be a strictly worse solution than getpeername(): it doubles network traffic if the connection failed (with some TCP stacks), and doesn't improve reliability. Ken Keys mentions that this works with SOCKS.


<think>我们正在寻找一个适用于阻塞式套接字I/O的C++库或实现方案。根据用户之前的上下文,他们遇到了`httplib`库在编译时出现的`std::function`初始化问题,并且明确表示不需要非阻塞I/O支持。因此,我们专注于阻塞I/O的解决方案。 ### 阻塞式套接字I/O的特点 阻塞式I/O模型下,套接字操作(如`accept`、`connect`、`read`、`write`)会阻塞当前线程直到操作完成。这种模型编程简单,但每个连接需要独立的线程处理,资源消耗较大。 ### 推荐库:`httplib`(继续使用) 尽管用户在使用`httplib`时遇到了编译错误,但该库本身支持阻塞I/O模式,且易于使用。我们已在前一问题中解决了编译问题,因此可以继续使用。`httplib`是一个轻量级的C++11库,支持HTTP服务器和客户端,底层使用阻塞套接字。 **示例:使用`httplib`创建阻塞式HTTP服务器** ```cpp #include <httplib.h> using namespace httplib; int main() { Server svr; // 注册处理函数(阻塞模式,每个请求在独立线程中处理) svr.Get("/", [](const Request& req, Response& res) { res.set_content("Hello, World!", "text/plain"); }); svr.listen("0.0.0.0", 8080); // 阻塞直到服务器停止 } ``` ### 纯套接字编程实现 如果用户希望更底层的控制,可以直接使用操作系统提供的套接字API(如POSIX sockets)。以下是一个简单的阻塞式TCP服务器实现: ```cpp #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <cstring> #include <iostream> int main() { // 创建套接字 int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { std::cerr << "socket failed\n"; return 1; } // 绑定地址和端口 sockaddr_in address{}; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(8080); if (bind(server_fd, (sockaddr*)&address, sizeof(address)) < 0) { std::cerr << "bind failed\n"; close(server_fd); return 1; } // 监听 if (listen(server_fd, 10) < 0) { std::cerr << "listen failed\n"; close(server_fd); return 1; } while (true) { // 接受连接(阻塞) sockaddr_in client_addr{}; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_len); if (client_fd < 0) { std::cerr << "accept failed\n"; continue; } // 读取请求(阻塞) char buffer[1024] = {0}; ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer)); if (bytes_read < 0) { std::cerr << "read failed\n"; close(client_fd); continue; } // 模拟处理请求 const char* response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!"; write(client_fd, response, strlen(response)); // 阻塞写入 close(client_fd); } close(server_fd); return 0; } ``` ### 其他库推荐 1. **Boost.Asio(同步模式)** Boost.Asio虽然以异步I/O闻名,但也支持同步(阻塞)操作。用户可以通过调用同步方法(如`socket.read_some()`、`acceptor.accept()`)实现阻塞式I/O。 ```cpp #include <boost/asio.hpp> using namespace boost::asio; using ip::tcp; int main() { io_service io; tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080)); while (true) { tcp::socket socket(io); acceptor.accept(socket); // 阻塞等待连接 // 阻塞读取 streambuf buf; read_until(socket, buf, "\r\n\r\n"); // 发送响应(阻塞) std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!"; write(socket, buffer(response)); } } ``` 2. **Poco C++ Libraries** Poco提供了`Net`模块,封装了阻塞式套接字操作,适合快速开发网络应用。 ```cpp #include <Poco/Net/TCPServer.h> #include <Poco/Net/TCPServerConnection.h> #include <Poco/Net/TCPServerConnectionFactory.h> #include <Poco/Net/Socket.h> class HelloConnection: public Poco::Net::TCPServerConnection { public: HelloConnection(const StreamSocket& s): TCPServerConnection(s) {} void run() override { StreamSocket& socket = this->socket(); try { char buffer[1024]; socket.receiveBytes(buffer, sizeof(buffer)); // 阻塞读取 const char* response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, World!"; socket.sendBytes(response, strlen(response)); // 阻塞写入 } catch (...) {} } }; int main() { Poco::Net::ServerSocket server_socket(8080); Poco::Net::TCPServer server( new Poco::Net::TCPServerConnectionFactoryImpl<HelloConnection>(), server_socket ); server.start(); while (true) { sleep(1); } } ``` ### 性能优化建议 - **线程池**:为每个连接创建线程开销较大,可使用线程池复用线程资源[^2]。 - **缓冲区复用**:避免频繁分配/释放内存,可预先分配缓冲区池[^3]。 - **大文件传输优化**:使用`sendfile`系统调用(Linux)或`TransmitFile`(Windows)实现零拷贝文件传输[^3]。 --- ### 总结 | 方案 | 适用场景 | 复杂度 | |------|----------|--------| | `httplib` | 快速构建HTTP服务 | ★☆☆ | | 原生套接字 | 底层控制、学习 | ★★★ | | Boost.Asio同步模式 | 跨平台、扩展性强 | ★★☆ | | Poco Net | 企业级应用开发 | ★★☆ | 用户可根据项目需求选择: - 开发效率优先 → `httplib`或Poco - 学习/底层控制 → 原生套接字 - 未来可能扩展异步功能 → Boost.Asio --- §§相关问题§§ 1. 如何在阻塞式I/O模型中设计线程池? 2. 使用`sendfile`系统调用传输文件的具体实现? 3. Boost.Asio同步模式与异步模式的性能对比? 4. 如何避免阻塞式I/O中的线程资源耗尽问题?</think>### C++ 阻塞式套接字 I/O 库推荐与实现方案 针对阻塞式套接字 I/O 需求,以下是经过验证的推荐方案: #### 一、专用库推荐 1. **Boost.Asio(同步模式)** - **优势**:工业级跨平台库,支持同步阻塞操作 - **实现示例**: ```cpp #include <boost/asio.hpp> using namespace boost::asio; void blocking_tcp_client() { io_service io; ip::tcp::socket socket(io); ip::tcp::endpoint endpoint(ip::address::from_string("127.0.0.1"), 8080); socket.connect(endpoint); // 阻塞连接 std::string request = "GET / HTTP/1.1\r\n\r\n"; write(socket, buffer(request)); // 阻塞写入 char response[1024]; size_t len = read(socket, buffer(response)); // 阻塞读取 std::cout << std::string(response, len); } ``` - 通过显式调用同步方法实现阻塞操作[^4] 2. **Poco C++ Libraries** - **优势**:轻量级网络库,原生支持阻塞 I/O - **特性**: ```cpp Poco::Net::SocketAddress address("127.0.0.1", 8080); Poco::Net::StreamSocket socket; socket.connect(address); // 阻塞连接 char buffer[256]; int n = socket.receiveBytes(buffer, sizeof(buffer)); // 阻塞读取 ``` 3. **cpp-netlib(推荐用于 HTTP)** - **优势**:专为 HTTP 设计的阻塞式客户端 - **示例**: ```cpp #include <boost/network/protocol/http/client.hpp> namespace http = boost::network::http; http::client client; http::client::request request("http://example.com"); http::client::response response = client.get(request); // 阻塞调用 std::cout << body(response); ``` #### 二、原生套接字实现 直接使用操作系统 API 的阻塞模式实现: ```cpp #include <sys/socket.h> #include <netinet/in.h> int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in serv_addr{}; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); // 阻塞连接 connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)); // 阻塞发送 const char* msg = "Hello"; send(sockfd, msg, strlen(msg), 0); // 阻塞接收 char buffer[1024]; recv(sockfd, buffer, sizeof(buffer), 0); close(sockfd); } ``` **关键参数**: - `SOCK_STREAM` 指定 TCP 协议 - 默认即为阻塞模式(无需设置 O_NONBLOCK) #### 三、性能优化技巧 1. **缓冲区管理** ```cpp // 重用缓冲区减少内存分配 thread_local static std::vector<char> buffer(4096); recv(sockfd, buffer.data(), buffer.size(), 0); ``` 通过线程局部存储重用缓冲区[^2] 2. **大文件传输优化** ```cpp #include <sys/sendfile.h> // Linux 零拷贝传输 sendfile(out_fd, in_fd, nullptr, file_size); ``` 使用 `sendfile` 系统调用避免数据拷贝[^3] 3. **连接池实现** ```cpp class ConnectionPool { public: Connection get() { if(pool.empty()) create_connection(); return std::move(pool.back()); } private: std::vector<Connection> pool; }; ``` 减少连接建立开销 #### 四、方案对比 | 方案 | 开发效率 | 性能 | 灵活性 | 适用场景 | |------------------|----------|------|--------|------------------| | Boost.Asio 同步 | ★★★★ | ★★★ | ★★★★ | 跨平台通用方案 | | Poco 库 | ★★★★ | ★★★☆ | ★★★☆ | 企业级应用 | | 原生套接字 | ★★ | ★★★★ | ★★★★★ | 极致性能控制 | | cpp-netlib | ★★★★★ | ★★☆ | ★★ | 纯 HTTP 客户端 | > **推荐选择原则**: > - 快速开发:选择 `cpp-netlib` (HTTP) 或 `Poco` > - 高性能需求:原生套接字 + `sendfile` 优化 > - 长期维护:`Boost.Asio` 同步模式 --- ### 阻塞式 I/O 设计要点 1. **线程模型** - 每个连接独立线程:`$$ \text{线程数} = \text{并发连接数} $$` - 线程池优化:`$$ \text{线程数} = \text{CPU核心数} \times 2 $$` 2. **超时控制** ```cpp struct timeval tv{.tv_sec = 5}; // 5秒超时 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); ``` 3. **错误处理** ```cpp if (bytes_received == -1) { if (errno == EAGAIN) {} // 超时 else if (errno == ECONNRESET) {} // 连接重置 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值