適用於阻塞 Socket,防止 recv/send 無限等待。
int timeoutMs = 1000; // 1 秒
// 接收超時
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
// 發送超時
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO,
reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
行為:超時後 recv/send 返回 SOCKET_ERROR,WSAGetLastError() == WSAETIMEDOUT (Windows) 或 errno == EAGAIN/EWOULDBLOCK (Linux)。
connect 無標準超時選項,需手動實現:
bool connectWithTimeout(SOCKET sock, const sockaddr* addr, int addrLen, int timeoutMs) {
// 1. 設置非阻塞模式
u_long nonBlocking = 1;
ioctlsocket(sock, FIONBIO, &nonBlocking); // Windows
// fcntl(sock, F_SETFL, O_NONBLOCK); // Linux
// 2. 發起連接
int ret = connect(sock, addr, addrLen);
if (ret == 0) { // 立即成功 (本地回環等)
nonBlocking = 0;
ioctlsocket(sock, FIONBIO, &nonBlocking);
return true;
}
// 3. 非阻塞下預期錯誤
int err = WSAGetLastError(); // errno
if (err != WSAEWOULDBLOCK && err != WSAEINPROGRESS && err != EINPROGRESS) {
nonBlocking = 0;
ioctlsocket(sock, FIONBIO, &nonBlocking);
return false; // 真實錯誤
}
// 4. select 監視可寫狀態 (連接完成)
fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(sock, &writeSet);
timeval tv;
tv.tv_sec = timeoutMs / 1000;
tv.tv_usec = (timeoutMs % 1000) * 1000;
ret = select(0, nullptr, &writeSet, nullptr, &tv);
nonBlocking = 0;
ioctlsocket(sock, FIONBIO, &nonBlocking); // 恢復阻塞
if (ret <= 0) return false; // 超時或錯誤
// 5. 獲取連接真實結果 (SO_ERROR)
int sockErr = 0;
int errLen = sizeof(sockErr);
getsockopt(sock, SOL_SOCKET, SO_ERROR,
reinterpret_cast<char*>(&sockErr), &errLen);
return sockErr == 0;
}
多網卡服務器主動連接時,需綁定特定網卡 IP 作為源地址:
bool bindToInterface(SOCKET sock, const char* localIp, uint16_t localPort = 0) {
sockaddr_in localAddr{};
localAddr.sin_family = AF_INET;
localAddr.sin_addr.s_addr = inet_addr(localIp); // 指定網卡 IP
localAddr.sin_port = htons(localPort); // 0 讓系統自動分配端口
int ret = bind(sock, reinterpret_cast<sockaddr*>(&localAddr), sizeof(localAddr));
if (ret == SOCKET_ERROR) {
// WSAEADDRNOTAVAIL: IP 不屬於本機任何網卡
// WSAEADDRINUSE: 端口已被佔用
return false;
}
return true;
}
// 使用示例:雙網卡服務器按業務分流
SOCKET sockBiz = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bindToInterface(sockBiz, "192.168.1.100"); // 業務網卡
connect(sockBiz, &bizServerAddr, sizeof(bizServerAddr));
SOCKET sockMgmt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bindToInterface(sockMgmt, "10.0.0.50"); // 管理網卡
connect(sockMgmt, &mgmtServerAddr, sizeof(mgmtServerAddr));
class Socket {
SOCKET fd_;
public:
Socket() : fd_(socket(AF_INET, SOCK_STREAM, 0)) {}
~Socket() { close(); }
bool setRecvTimeout(int ms) {
return setsockopt(fd_, SOL_SOCKET, SO_RCVTIMEO,
(const char*)&ms, sizeof(ms)) == 0;
}
bool setSendTimeout(int ms) {
return setsockopt(fd_, SOL_SOCKET, SO_SNDTIMEO,
(const char*)&ms, sizeof(ms)) == 0;
}
bool connectWithTimeout(const std::string& ip, uint16_t port, int timeoutMs) {
sockaddr_in addr{}; addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
return connectWithTimeoutImpl(fd_, addr, timeoutMs);
}
bool bindInterface(const std::string& localIp) {
return bindToInterface(fd_, localIp.c_str());
}
private:
// ... 內部實現 ...
};
| 現象 | 可能原因 | 排查步驟 |
|---|---|---|
| connect 超時不生效 | 未設置非阻塞、select 參數錯誤 | 檢查 ioctlsocket/fcntl 返回值、select 第一參數 (Windows 忽略、Linux 為 maxfd+1) |
| recv 超時後再次調用立即返回 | 超時不清除錯誤狀態 | Windows 需無額外處理;Linux 需檢查 errno 重置 |
| bind 失敗 WSAEADDRNOTAVAIL | IP 不屬於本機 | ipconfig / ifconfig 確認 IP 存在、子網掩碼正確 |
| 多網卡仍走默認路由 | bind 後未 connect、或連接本地回環 | 確保 bind 在 connect 之前、目標 IP 非本地 |
| 高併發下文件描述符耗盡 | 未及時 close、未設置 SO_REUSEADDR | RAII 管理 Socket、setsockopt SO_REUSEADDR、調大 ulimit -n |
setsockopt + SO_RCVTIMEO/SO_SNDTIMEO,簡單有效。connect + select 監視可寫 + getsockopt(SO_ERROR) 驗證,跨平台通用模式。bind 指定本地 IP,必須在 connect 之前調用。