一、select多路IO转接
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set); //清空文件描述符集合
void FD_SET(int fd, fd_set *set); //将待监听的文件描述符添加到监听集合中
void FD_CLR(int fd, fd_set *set); //将文件描述符从监听集合中移除
int FD_ISSET(int fd, fd_set *set); //判断文件描述符是否在监听集合中
- nfds:最大描述符+1,告诉内核检测多少个描述符的状态。
中间三个参数分别是监听有读就绪、写就绪、异常就绪文件描述符的集合,都是传入传出参数。 - timeout:定时阻塞监听时间,填NULL将永远等下去至少有一个事件就绪才会返回,设置timeval里的事件均为0则检查完所有描述符后立即返回,若不为0则为设置监听的超时时间,超时时间如果到了还没有事件就绪就返回。
返回值是所有监听的描述符中有事件就绪的描述符个数,返回-1为出错。
二、poll多路IO转接
int poll(struct pollfd * fds, unsigned int nfds, int timeout);
struct pollfd
{
int fd; /* 待监听文件描述符 */
short events; /* 待监听的事件 */
short revents; /* 实际就绪的事件 */
} ;
- fds:监听的pollfd结构体数组,传入传出参数。
事件取值POLLIN、POLLOUT、POLLERR分别代写、读、异常。 - nfds:传入的pollfd结构体数组中需要监听的个数,需要监听多少个文件描述符。
- timeout:超时时长ms。传-1则阻塞等待至少有一个描述符事件就绪才返回;传0表示轮询完pollfd结构体数组立即返回,不阻塞进程;传大于0的指定毫秒数超时时长,超时还未有描述符事件就绪就返回。
返回值是所有监听的描述符中有事件就绪的描述符个数,返回-1为出错。
三、epoll多路IO转接
int epoll_create(int size);
- size为供内核参考的创建监听节点的数量。
返回指向新创建的红黑树根节点fd,失败返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}
struct epoll_event{
uint32_t events;
epoll_data data;
}
- epfd为epoll_create返回值。
- op为EPOLL_CTL_ADD添加新fd监听、EPOLL_CTL_MOD修改监听事件、EPOLL_CTL_DEL将一个fd从监听红黑树删除取消监听。
- fd为被操作的套接字描述符。
- event是epoll_event结构体地址,其events主要有EPOLLIN、EPOLLOUT、EPOLLERR,默认是水平触发,(EPOLLIN|EPOLLET)则表示边缘触发监听读就绪事件,data是一个联合体。
返回0成功,-1失败。
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
- epfd为epoll_create返回值。
- events为传出参数,传出满足监听事件结构体数组。
- maxevents为数组的最大长度。
- timeout超时时长ms。传-1则阻塞等待至少有一个描述符事件就绪才返回;传0表示轮询完fd红黑树立即返回,不阻塞进程;传大于0的指定毫秒数超时时长,超时还未有描述符事件就绪就返回。
返回值是所有监听的描述符中有事件就绪的描述符个数,返回-1为出错。
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
四、epoll的两种触发模式ET、LT
epoll的两种触发模式分别是ET(edge trigger)边缘触发和LT(level triggered)水平触发。
epoll的默认触发模式是LT,select、poll都是LT触发。
4.1 LT
缓冲区只要有数据未读就会导致epoll_wait返回。
上次读数据未读完仍会导致epoll_wait返回。
水平触发模式下阻塞和非阻塞并没有什么区别,因为没有可读时间就绪的话epoll_wait不会返回。
4.2 ET
缓冲区出现新未读数据才会导致epoll_wait返回。
上次读数据未读完不会导致epoll_wait返回。
4.3 非阻塞模式
边缘触发模式下事件就绪只会通知一次,为了保证数据成功被读取或写入,在非阻塞模式下,采用循环的方式进行读写,直到完成或出现异常时退出。
4.4 阻塞模式
如果不采用循环的方式进行读写,就会造成数据读/写不完的情况,因为下一次再调用epoll_wait就不会再通知了,所以职能采用循环的方式进行读写。但是如果尝试采用循环的方式进行读写,则会造成永久阻塞。
造成阻塞的原因只有没有数据可读/可写,在非阻塞模式下出现没有数据可读/可写可以返回相应的错误信息设置errno(EWOULDBLOCK),但是阻塞模式就会进入阻塞状态,而处理的该fd永远也不可能再有可读数据了,所以就被永久阻塞了。
epoll反应堆
采用ET非阻塞轮询,利用epoll_data
联合体中void *ptr
作为回调函数处理accept
、读写事件。
五、select、poll、epoll优缺点对比
优点 | 缺点 | |
---|---|---|
select | 1、跨平台,win、linux等系统都支持。 2、可以等待多个套接字。 | 1、监听上限受文件描述符个数限制,一般每个进程最多允许同时打开1024个文件,所以单个进程最多可监听1024个套接字描述符。 2、每次调用select都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很⼤大。 3、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤大。 4、每次需要重新设定监听fd_set集合。 5、select返回后,需要轮询所有fd来获取就绪的描述符。 |
poll | 1、自带数组结构,输入输出事件分离,不用每次重新设置。 2、套接字文件描述符数量没有上限。 | 1、不能跨平台。 2、和select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。 3、每次调用poll都需要大把大量描述符在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。 |
epoll | 1、不需要轮询,获取事件的时间复杂度是O(1)。 2、套接字文件描述符数量没有上限。 3、不需要每次从用户态拷贝到内核态。 | 1、不能跨平台。 |
六、socket API
6.1 网络字节序和本机字节序
本机字节序:小端字节序,高位存在高地址,低位存在低地址。
本机字节序:大端字节序,高位存在低地址,低位存在高地址。
htonl:通常用来将本机IP转换为网络字节序。
htons:通常用来将本机PORT转换为网络字节序。
ntohl:通常用来将IP从网络字节序转换为本机字节序。
ntohs:通常用来将PORT从网络字节序转换为本机字节序。
6.2 IP地址转换
inet_pton:将本地字节序的string IP转化成网络字节序二进制的IP地址。
inet_ntop:将网络字节序二进制的IP地址转化成本地字节序的string IP。
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
6.3 sockaddr_in结构
struct sockaddr_in{
sa_family_t sin_family; //AF_INET、AF_INET6
in_port_t sin_port; //网络字节序端口号
struct in_addr sin_addr; //网络字节序IP地址
}
struct in_addr{
uint32_t s_addr;
}
struct sockaddr_in addr;
addr.sin_addr.addr = htonl(INADDR_ANY); //INADDR_ANY宏可取出可用的本机字节序IP
6.4 socket函数
int socket(int af, int type, int protocol);
返回一个套接字描述符,失败返回-1。
af(IP协议):AF_INET、AF_INET6、AF_UNIX。
type(数据传输协议):SOCK_STREAM、SOCK_DGRAM。
protocol(代表协议,比如SOCK_STREAM代表协议TCP,SOCK_DGRAM代表协议UDP):默认填0。
6.5 bind函数
int bind(int sockfd, const struct sockaddr, socklen_t addrlen);
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
给socket绑定一个IP和PORT。成功返回0,失败返回-1。
6.6 listen函数
int listen(int sockfd, int backlog);
设置同时可与服务器建立连接的上限数,即同时进行三次握手
的客户端的数量。
6.7 accept函数
int accept(int sockfd, struct sockaddr * addr, socklen_t * addrlen);
阻塞等待客户端建立连接,成功则返回与客户端成功建立通信的套接字描述符。
addr是建立成功的客户端地址。
addrlen是客户端地址的长度。
6.8 connect函数
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
阻塞等待客户端与服务器建立连接,成功则返回0,失败返回-1。
addr是建立成功的客户端地址。
addrlen是客户端地址的长度。
如果客户端不适用bind绑定客户端地址结构,则采用隐式绑定,系统自动绑定。
6.9 setsockopt函数
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt , sizeof(opt));
设置端口复用,允许端口被重复使用,常见的还有设置读写缓冲区大小等。
6.10 fcntl函数
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
常用fcntl(fd, F_SETFL, fcntl(_fd, F_GETFL, 0) | O_NONBLOCK)
来设置非阻塞。
6.11 read
ssize_t read(int fd,void * buf ,size_t count);
读fd套接字描述符数据,数据将读到buf 缓冲区,最多读取count个字节的数据。
返回0代表读到文件末尾,对端套接字关闭。
返回大于0代表读到数据的字节数。
返回-1代表出错,此时errno
如果是EINTR
为异常中断需要重新读,errno
如果是EAGIN
或EWOULDBLOCK
为以非阻塞的方式读数据但是没有数据。其它情况都是发生了错误。
6.12 write
ssize_t write (int fd,const void * buf,size_t count);
向fd套接字描述符写数据,数据将从buf 缓冲区读取count个字节的数据写入fd的读缓冲区。
6.13 TCP通信流程
- socket()创建scoket;
- bind()绑定服务器地址结构;
- listen()设置监听上限;
- accept()阻塞监听客户端连接;
- connect()客户端与服务器连接。