目录
- 前言
- select的定位与接口分析
- select的特点
- select的缺点
- 基于select的echo服务器代码实现
- telnet测试
前言
对于服务器来讲,需要一刻不停地监听请求,提供服务
我们不希望服务器主程序卡住
服务器监听的本质是:while 1 { 等待数据的到来->数据到来后读取数据 }
由于recv是阻塞等待,所以我们想尽一切办法来处理这个阻塞问题
- 多线程,把每一个链接交给一个新线程处理,新线程recv阻塞等待对应套接字的时候不会干扰到主线程;这时,阻塞等待+数据读取由线程全权承担
- 我们让内核帮助我们检测指定的套接字的状态。一旦套接字有数据就绪,就会把某个数据结构的状态设置为就绪,主程序检测到就绪,就会读取数据。这时,主程序只要不停地去检测一批套接字的状态就行。这时,等待由内核帮我们承担,主程序只要承担数据读取的任务
select的定位与接口分析
select的定位:只负责等待,是就绪事件通知机制,它等待IO就绪;唯一和read,write,recv,send不同的是,select可以等待多个文件描述符
来看select接口:
int select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
);
- timeval结构体:秒+微秒
nullptr:永久等待,直到某个fd就绪才返回 3,0:等待3秒,没有就绪就返回 0,0:没有就绪立马返回 - fd_set:比特位的位置代表文件描述符的编号,比特位的内容代表是否关心
作用:所有关心X事件的文件描述符,都应该添加在这个集合里 输入:用户告诉内核,你要帮我检测一下在这个集合中的fd的X事件 输出:内核告诉用户,你关心的fd,有哪些文件描述符已经就绪了不具有保存sock的功能,只具有通知的能力 - fd_set:位图
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
fd_set的作用是:比特位的位置代表哪一个文件描述符,比特位0/1代表文件描述符是否就绪
- 返回
读就绪代表该文件描述符的底层数据已经就绪 写就绪代表文件描述符可以继续写了 异常就绪代表哪个文件描述符异常了
so
轮询检测有没有关心的fd就绪,覆盖式地写入位图,返回。所以select需要我们花一点点第三方变量把fds保存起来,一般是数组
在我们通知了一轮就绪的sock之后,可能还需要select帮助我们进行监测其他的fd,所以要对fd_set重新设置,故要提前保存
其中,监听套接字只关心读事件
获取新的fd之后我们不读它,因为我们不知道这个新的fd里面有没有读写事件就绪,所以要等下一轮循环
select的特点
- select只能等待确定数量的文件描述符,有上限,可监控的fd的数量取决于fd_set的大小
- 需要第三方数组存储fd
- 每次循环都需要更新nfds的值
select的缺点
- 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小
基于select的echo服务器代码实现
- socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
namespace ns_sock
{
static const int BACKLOG = 5;
enum {
SOCKET_ERROR = 2,
BIND_ERROR,
LISTEN_ERROR,
ACCEPT_ERROR
};
class Sock {
public:
static int Socket() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1) {
std::cerr << "create socket failed" << std::endl;
exit(SOCKET_ERROR);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return sock;
}
static void Bind(int sock, uint16_t port) {
sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) == -1) {
std::cerr << "bind error" << std::endl;
exit(BIND_ERROR);
}
}
static void Listen(int sock) {
if(listen(sock, BACKLOG) == -1) {
std::cerr << "listen failed" << std::endl;
exit(LISTEN_ERROR);
}
}
static void Accept(){}
};
}
- select.hpp
#pragma once
#include "sock.hpp"
#include <sys/select.h>
namespace ns_select_server
{
static const uint16_t g_default_port = 8081;
static const int SELECT_ERROR = 6;
static const int BUFFER_SIZE = 4096;
class SelectServer {
public:
SelectServer(int port = g_default_port):_listen_sock(-1), _port(port) {
_listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(_listen_sock, _port);
ns_sock::Sock::Listen(_listen_sock);
for(int i = 0; i < FD_SETSIZE; i++) {
_fd_array[i] = -1;
}
_fd_array[0] = _listen_sock;
}
void Loop() {
while(true) {
fd_set rfds;
FD_ZERO(&rfds);
int max_fd = -1;
for(int i = 0; i < FD_SETSIZE; i++) {
if(_fd_array[i] != -1) {
FD_SET(_fd_array[i], &rfds);
if(max_fd < _fd_array[i]) {
max_fd = _fd_array[i];
}
}
}
int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
if(n < 0) {
std::cerr << "select error" << std::endl;
exit(SELECT_ERROR);
}
else if(n == 0) {
std::cout << "timeout" << std::endl;
}
else {
for(int i = 0; i < FD_SETSIZE; i++) {
if(_fd_array[i] == -1) {
continue;
}
else {
if(!FD_ISSET(_fd_array[i], &rfds)) {
continue;
}
else {
if(_fd_array[i] == _listen_sock) {
sockaddr_in peer;
bzero(&peer, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
int index = 0;
for(; index < FD_SETSIZE; index++) {
if(_fd_array[index] == -1) {
_fd_array[index] = sock;
break;
}
else {
continue;
}
}
if(index == FD_SETSIZE) {
std::cerr << "select管理的文件描述符超出限制" << std::endl;
}
else {
std::cout << "sock " << sock << " 建立链接" << std::endl;
}
}
else {
Handler(_fd_array[i], i);
}
}
}
}
}
}
}
private:
void Handler(int sock, int i) {
char buffer[BUFFER_SIZE] = {0};
ssize_t size = recv(sock, buffer, sizeof(buffer) - 1, 0);
if(size < 0) {
std::cerr << "recv data error" << std::endl;
}
else if(size == 0) {
std::cout << "sock: " << sock << " closed link" << std::endl;
_fd_array[i] = -1;
close(sock);
}
else {
buffer[size] = '\0';
std::cout << "sock " << sock << "# " << buffer;;
}
}
private:
int _listen_sock;
uint16_t _port;
int _fd_array[FD_SETSIZE];
};
}
- 主程序
#include "select_server.hpp"
int main()
{
ns_select_server::SelectServer *srv = new ns_select_server::SelectServer;
srv->Loop();
return 0;
}
telnet测试
我们使用3台telnet客户端连接select服务器
github仓库地址
|