我正在 Windows 上试验套接字(C++ 程序)。作为练习,我从浏览器发送 HTTP 请求(纯文本),服务器发送响应。我使用
WSAPoll()
来处理多个客户端连接请求和事件。
程序似乎工作正常,但是即使我只有 1 个客户端并发送 1 个请求,服务器指示它收到 2 个客户端连接请求。我在网页的
fetch
命令被触发后收到一个请求(fd = 196),我似乎在发送响应后立即有1(fd = 200) - 当然,fd号会根据它运行的机器。这些只是我运行程序时得到的数字,用于说明目的。 IP 地址相同(客户端/浏览器),但它是一个新的 fd 值。
这是我在控制台中得到的:
$ ./Server
Size of fds before WSAPoll 1
Size of fds before WSAPoll 1
Size of fds before WSAPoll 1
Client IP addr: 127.0.0.1
Size of fds after insert client 2 196
Size of fds before WSAPoll 2
some activity from client 196
Received data from client 196: GET / HTTP/1.1
Host: localhost:27015
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
sec-ch-ua-platform: "Windows"
Accept: */*
Origin: null
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Size of fds before WSAPoll 2
Client IP addr: 127.0.0.1
Size of fds after insert client 3 200
Size of fds before WSAPoll 3
Size of fds before WSAPoll 3
Size of fds before WSAPoll 3
为什么我使用不同的 fd 号码(虽然是相同的 IP 地址)收到多个客户端连接请求,即使我似乎只从浏览器发出一个请求?
这是我用于服务器和 HTML 页面的代码:
// Compile with clang++ -o Server Server1.cpp -lWS2_32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <cstdio>
#include <cassert>
#include <vector>
#include <string>
#pragma include(lib, "Ws2_32.lib")
int main()
{
WSADATA wsadata;
WSAStartup(MAKEWORD(2,2), &wsadata);
SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
assert(listen_socket != INVALID_SOCKET);
#if _WIN32
unsigned long non_blocking = 1;
ioctlsocket(listen_socket, FIONBIO, &non_blocking);
#else
#endif
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(27015);
bind(listen_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(listen_socket, 4);
WSAPOLLFD listen_fd {
.fd = listen_socket,
.events = POLLRDNORM,
};
std::vector<WSAPOLLFD> fds;
fds.push_back(listen_fd);
uint32_t count = 0;
for (;;) {
#if _WIN32
fprintf(stderr, "Size of fds before WSAPoll %llu\n", fds.size());
int result = WSAPoll(fds.data(), fds.size(), 1000); // no timeout, wait indefinitely
if (fds[0].revents & POLLRDNORM) {
// accept new cient connection
sockaddr_in client_addr{};
int client_addr_size = sizeof(client_addr);
SOCKET client_socket = accept(listen_socket, (struct sockaddr*)&client_addr, &client_addr_size);
assert(client_socket != INVALID_SOCKET);
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
fprintf(stderr, "\n\tClient IP addr: %s\n", client_ip);
WSAPOLLFD client_fd {
.fd = client_socket,
.events = POLLRDNORM,
};
fds.push_back(client_fd);
fprintf(stderr, "\tSize of fds after insert client %llu %llu\n\n", fds.size(), client_fd.fd);
}
//for (uint32_t i = 1; i < fds.size(); ++i) {
for (auto iter = fds.begin() + 1; iter != fds.end();) {
//for (size_t i = 1; i < fds.size(); ++i) {
if (iter->revents & (POLLRDNORM | POLLERR | POLLHUP)) {
fprintf(stderr, "some activity from client %lld\n\n", iter->fd);
char buffer[1024];
memset(buffer, 0x0, sizeof(buffer));
int bytes_read = recv(iter->fd, buffer, sizeof(buffer) - 1, 0);
if (bytes_read > 0) {
fprintf(stderr, "Received data from client %lld: %s\n\n", iter->fd, buffer);
std::string msg = "Count = " + std::to_string(count++);
std::string response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: text/plain\r\n";
response += "Access-Control-Allow-Origin: *\r\n";
response += "Content-Length: " + std::to_string(msg.size()) + "\r\n\r\n";
response += msg;
send(iter->fd, response.c_str(), response.size(), 0);
}
else if (bytes_read == 0) {
fprintf(stderr, "Client disconnected %lld\n", iter->fd);
closesocket(iter->fd);
iter = fds.erase(iter);
fprintf(stderr, "Number of clients left: %llu\n", fds.size());
continue;
}
else {
assert(0);
}
}
iter++;
}
#else
int result = poll(&listen_fd, 1, -1); // indefinite timeout
#endif
}
closesocket(listen_socket);
WSACleanup();
return 0;
}
<!DOCTYPE html>
<html>
<head>
<title>ARTS Testing Server</title>
</head>
<body>
<h1>Welcome to My Webpage</h1>
<button onclick="sendRequest()">Send Request</button>
<div id="response"></div>
<script>
function sendRequest() {
console.log("in send request\n");
fetch('http://localhost:27015/')
.then(function(response) {
if (response.ok) {
console.log("OK\n");
return response.text();
} else {
throw new Error("Request failed with status: " + response.status);
}
})
.then (function(responseText) {
console.log("HERE");
document.getElementById("response").innerHTML = responseText;
})
.catch (function(error) {
});
}
</script>
</body>
</html>
HTTP 是无状态的。即使有 keep-alives,浏览器也根本不需要重用连接,否则连接可能已经超时并在请求之间关闭。
所以,如果浏览器将您的 HTML 页面作为远程文件检索,它使用一个连接(当将 HTML 作为本地文件加载时不是这样),并且不能保证
fetch
命令将重用相同的连接,即使它与 HTML 页面来自同一主机/端口。甚至多个fetch
es 也可能使用不同的连接。大多数浏览器将同时维护与服务器的多个连接,以帮助它促进并行加载多个资源(以防 HTML 页面引用其他文件,如图像、脚本等)。
您可以使用浏览器内置的调试工具来查看您的情况是否确实如此。
至于您的实际代码,我发现它有一些问题:
缺乏足够的错误处理。
在评估
WSAPoll()
结构的状态之前,您完全忽略了 WSAPOLLFD
的返回值。返回值告诉您revents
成员是否实际更新。
将新结构推回您的
std::vector
may使您用来循环std::vector
.的迭代器无效
accept()
总是返回阻塞套接字,即使监听套接字是非阻塞的。如果你希望客户端是非阻塞的,你需要明确地设置它。
尝试更像这样的东西:
#include <iostream>
#include <vector>
#include <string>
#if _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma include(lib, "Ws2_32.lib")
typedef SOCKET socket_t;
typedef WSAPOLLFD pollfd_t;
typedef int socklen_t;
static const SOCKET c_INVALID_SOCKET = INVALID_SOCKET;
static const int c_SOCKET_ERROR = SOCKET_ERROR;
static const int c_EWOULDBLOCK = WSAEWOULDBLOCK;
static const int c_EAGAIN = WSAEWOULDBLOCK;
#define LastErrNo WSAGetLastError()
#define Poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout)
#define CloseSocket(fd) closesocket(fd)
bool InitSocketLib() {
WSADATA wsadata;
int errCode = WSAStartup(MAKEWORD(2,2), &wsadata);
return (errCode == 0);
}
void CleanupSocketLib() {
WSACleanup();
}
bool MakeNonBlocking(SOCKET sckt) {
unsigned long non_blocking = 1;
return (ioctlsocket(sckt, FIONBIO, &non_blocking) == 0);
}
#else
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <poll.h>
#include <unistd.h>
typedef int socket_t;
typedef pollfd pollfd_t;
static const int c_INVALID_SOCKET = -1;
static const int c_SOCKET_ERROR = -1;
static const int c_EWOULDBLOCK = EWOULDBLOCK;
static const int c_EAGAIN = EAGAIN;
#define LastErrNo errno
#define Poll(fds, nfds, timeout) poll(fds, nfds, timeout)
#define CloseSocket(fd) close(fd)
bool InitSocketLib() { return true; }
void CleanupSocketLib() { }
bool MakeNonBlocking(int fd) {
int non_blocking = 1;
return (ioctl(fd, FIONBIO, &non_blocking) == 0);
/* alternatively:
int flags = fcntl(fd, F_GETFL, 0);
return (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0);
*/
}
#endif
int main()
{
if (!InitSocketLib()) {
std::cerr << "Unable to initialize socket library, error: " << LastErrNo << '\n';
return -1;
}
socket_t listen_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listen_socket == c_INVALID_SOCKET) {
std::cerr << "Unable to create listening socket, error: " << LastErrNo << '\n';
return -1;
}
if (!MakeNonBlocking(listen_socket)) {
std::cerr << "Unable to set listening socket to non-blocking, error: " << LastErrNo << '\n';
return -1;
}
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(27015);
if (bind(listen_socket, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr)) < 0) {
std::cerr << "Unable to bind listening socket, error: " << LastErrNo << '\n';
return -1;
}
if (listen(listen_socket, 4) < 0) {
std::cerr << "Unable to open listening socket, error: " << LastErrNo << '\n';
return -1;
}
pollfd_t listen_fd {
.fd = listen_socket,
.events = POLLRDNORM,
.revents = 0
};
std::vector<pollfd_t> fds;
fds.push_back(listen_fd);
uint32_t count = 0;
for (;;) {
std::cerr << "Size of fds before poll " << fds.size() << '\n';
int result = Poll(fds.data(), fds.size(), -1); // no timeout, wait indefinitely
if (result < 0) {
std::cerr << "Unable to poll, error: " << LastErrNo << '\n';
return -1;
}
if (result == 0) // timeout
continue;
for (auto iter = fds.begin(); iter != fds.end(); ) {
if (iter->revents & POLLRDNORM) {
if (iter->fd == listen_socket) {
// accept new client connection
sockaddr_in client_addr{};
socklen_t client_addr_size = sizeof(client_addr);
socket_t client_socket = accept(listen_socket, reinterpret_cast<sockaddr*>(&client_addr), &client_addr_size);
if (client_socket == c_INVALID_SOCKET) {
std::cerr << "Unable to accept a client, error: " << LastErrNo << '\n';
}
else if (!MakeNonBlocking(client_socket)) {
std::cerr << "Unable to set client socket to non-blocking, error: " << LastErrNo << '\n';
CloseSocket(client_socket);
}
else {
char client_ip[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
std::cerr << "\n\tClient IP addr: " << client_ip << ", port: " << ntohs(client_addr.sin_port) << '\n';
pollfd_t client_fd {
.fd = client_socket,
.events = POLLRDNORM,
.revents = 0
};
//auto offset = std::distance(fds.begin(), iter);
fds.push_back(client_fd);
iter = fds.begin() /* + offset */;
std::cerr << "\tSize of fds after insert: " << fds.size() << ", client: " << client_socket << "\n\n";
}
}
else {
// read from client connection
std::cerr << "Some activity from client: " << iter->fd << "\n\n";
char buffer[1024];
int bytes_read = recv(iter->fd, buffer, sizeof(buffer), 0);
if (bytes_read > 0) {
std::cerr << "Received data from client: " << iter->fd << ": ";
std::cerr.write(buffer, bytes_read);
std::cerr << "\n\n";
// TODO: parse the request, don't send a reply until the end of the request is actually received...
std::string msg = "Count = " + std::to_string(count++);
std::string response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: text/plain\r\n";
response += "Access-Control-Allow-Origin: *\r\n";
response += "Content-Length: " + std::to_string(msg.size()) + "\r\n\r\n";
response += msg;
// TODO: if send() fails with EWOULDBLOCK/EGAIN,
// cache the unsent data and wait for poll() to
// tell you when it is OK to keep sending it ...
send(iter->fd, response.c_str(), response.size(), 0);
}
else if (bytes_read == 0) {
iter->revents |= POLLHUP;
}
else if ((LastErrNo != c_EWOULDBLOCK) && (LastErrNo != c_EAGAIN)) {
iter->revents |= POLLERR;
}
}
}
if (iter->revents & (POLLERR | POLLHUP)) {
if (iter->fd != listen_socket) {
if (iter->revents & POLLHUP) {
std::cerr << "Client disconnected: " << iter->fd << '\n';
} else {
std::cerr << "Unable to read from client: " << iter->fd << ", error: " << LastErrNo << '\n';
}
socket_t client_socket = iter->fd;
CloseSocket(client_socket);
iter = fds.erase(iter);
std::cerr << "\tSize of fds after remove: " << fds.size() << ", client: " << client_socket << "\n\n";
continue;
}
}
++iter;
}
}
CloseSocket(listen_socket);
CleanupSocketLib();
return 0;
}
话虽如此,请注意WSAPoll 已损坏 开始。考虑 Windows 上的替代设计。您可以只使用
select()
代替,它在 Windows 上没有与在其他平台上相同的限制。或者,您可以改用重叠 I/O 或 I/O 完成端口。