poll模型
poll模型和select模型类似,都是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者,需要使用头文件poll.h:
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
在select中使用的是fd_set结构体,而在此处的是pollfd和nfds_t,timeout的作用和select的一样,用于指定poll的超时值:当timeout的值为-1时,poll调用将会永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
poll的返回值也和select一致,表示就绪文件描述符的总数。
pollfd结构体
struct pollfd{
int fd; // 文件描述符
short int events; // 注册的事件
short int revents; // 实际发生的事件,由内核填充
};
- fd指定文件描述符
- events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或
- revents成员由内核修改,以通知应用程序fd上实际发生了哪些事件
poll支持以下事件类型:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
POLLRNDORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读 | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作。它由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
在表中提到了很多事件,但是Linux中没有完全支持它们。
通常,应用程序需要根据recv调用的返回值来区分socket上接受到的是有效数据还是对方关闭连接的请求,并做相应的处理。
不过,自Linux内核2.6.17开始,GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求后触发。这为我们区分recv接受到的数据是有效数据还是对方关闭连接的请求提供了一种更简单的方式。
但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。
nfds_t的定义
typedef unsigned long int nfds_t;
该参数用来指定被监听事件集合fds的大小。
一个简单的poll服务器
这个服务器是个简单的echo服务器:
#include <iostream>
#include <vector>
#include <sys/socket.h>
#include <poll.h>
#include <algorithm>
#include <arpa/inet.h>
#include <assert.h>
#include <vector>
#include <unistd.h>
using namespace std;
int main(int argc, char* argv[]){
if(argc != 3){
cerr << "格式为 ip port" << endl;
return 1;
}
int serverSocket, clientSocket;
/*
这个头文件在in.h中(实际上我们调用的是inet/in.h
而inet/in.h被包含在头文件arpa/inet。h中了
*/
struct sockaddr_in serverAddr{}, clientAddr{};
socklen_t clientAddrLen;
// 创建监听套接字
serverSocket = socket(AF_INET, SOCK_STREAM, 0);
assert(serverSocket != -1);
serverAddr.sin_family = AF_INET;
/*
htons的英文意思是host to network shrot
用于将16位无符号短整型(port)的主机字节序转换为网络字节序
*/
serverAddr.sin_port = htons(stoi(argv[2]));
/*
在调用 inet_pton 函数时,需要将 &(address.sin_addr) 作为参数传递,而不是 &(address.sin_addr.s_addr)
struct sockaddr_in 结构体中的 sin_addr 字段是一个 struct in_addr 类型的结构体
它包含了 IP 地址的二进制表示形式。in_addr 结构体中的 s_addr 字段实际上就是一个无符号整数类型(uint32_t)
用来存储 IP 地址的二进制形式。
*/
inet_pton(AF_INET, argv[1], &(serverAddr.sin_addr));
// 绑定套接字到地址和端口
int ret = bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
assert(ret != -1);
ret = listen(serverSocket, 5);
assert(ret != -1);
// 监听的文件描述符队列
/*
创建一个pollfd类型的空向量
并且将之前创建的serverSocket加入到其中进行监听
POLLIN是我们所监视的事件类型,这点笔记中有写
为什么需要监视serverSocket?
答:这是为了实现服务器的异步IO,通过监视serverSocket及时检测到下面两种情况:
1. 当有新的客户端连接请求到达时,我们希望能够立即进行处理。
通过监视serverSocket上的POLLIN事件,可以检测到是否有客户端尝试建立连接
2. 当serverSocket上出现其他错误情况(如连接断开或发生错误)时,我们也希望能及时进行处理
通过监视serverSocket上的激长时间,如POLLHUP或POLLERR,可以检测到这些错误情况
*/
vector<pollfd> fds;
fds.push_back({serverSocket, POLLIN});
while(true){
int numRead = poll(fds.data(), fds.size(), -1);
if(numRead < 0){
cerr << "poll error";
return 1;
}
//
for(auto &fd : fds){
/*
确保有新的连接
但是不是很理解为什么是一直监听serverSocket
*/
if(fd.fd == serverSocket && fd.revents & POLLIN){
// 有新连接
clientAddrLen = sizeof(clientAddr);
clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
if(clientSocket < 0){
cerr << "Failed to accepted connection" << endl;
return 1;
}
else{
cout << "New connection from:" << inet_ntoa(clientAddr.sin_addr) << endl;
fds.push_back({clientSocket, POLLIN});
}
}else if(fd.revents & POLLIN){
// 需要读取信息
// 此时的不是监听socket,而是客户端的连接socket
char buffer[1024];
ssize_t bytesRead = recv(fd.fd, buffer, sizeof(buffer), 0);
if(bytesRead <= 0){
if(bytesRead < 0){
cerr << "Error reading from client." << endl;
}
else{
cout << "Connection clost by client." << endl;
}
close(fd.fd);
// 这个remove_if()不是很熟
fds.erase(remove_if(fds.begin(), fds.end(),
[&](const pollfd& pfd){ return pfd.fd == fd.fd; }),
fds.end());
}else{
// 处理数据
cout << "Received data: " << string(buffer, bytesRead) << endl;
// 将收到的数据回发给客户端
send(fd.fd, buffer, bytesRead, 0);
}
}
}
}
// 关闭监听套接字
close(serverSocket);
return 0;
}
总结
学到这里我就发现:想要真正去理解Linux网络编程,还是要懂Linux内核,比如:套接字和文件描述符的联系?要去了解下select和poll的工作原理,这样才能理解程序为何这么编写。