TCP UDP协议的应用以及高级IO的介绍+
网络通信协议
模型:
- TCP和UDP两个协议都是一对多的网络通信模型
- TCP编程模型
- UDP编程模型
实例:
TCP模型
聊天室的服务器:
有私密消息功能以及列出聊天者的功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_CNT 100
#define MSG_LEN 1024
#define NAME_LEN 48
struct Client{
char name[NAME_LEN];//网名
int fd;//套接字文件描述符
struct sockaddr_in addr;//ip port
pthread_t id;//线程ID
};
//所有客户端数组
struct Client clts[MAX_CNT] = {};
size_t cnt = 0;//目前有cnt个客户端成员
pthread_mutex_t lock;
int sockfd; //服务器的socket套接字 用于接收客户端的连接请求
#define LOG_ERROR(fmt, args...)\
fprintf(stderr, "[ERROR [%s:%d]\n"fmt,__func__,__LINE__,##args);
void handleExit(int sig){
close(sockfd);
int i;
for(i = 0; i < cnt; i++){
pthread_cancel(clts[i].id);
}
for(i = 0; i < cnt; i++){
pthread_join(clts[i].id, NULL);
}
}
int init_server(const char *ip, unsigned short port){
sockfd = socket(AF_INET, SOCK_STREAM, 0);//第一步,建立socket套接字
if(sockfd == -1){
LOG_ERROR("socket:%s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1){//第二步,绑定端口
LOG_ERROR("bind:%s\n", strerror(errno));
return -1;
}
if(listen(sockfd, MAX_CNT) == -1){//第三步,监听使套接字变为被动模式(监听模式),用于接收客户端的连接请求
LOG_ERROR("listen:%s\n", strerror(errno));
return -1;
}
return 0;
}
void broadcast(int fd, const char *msg){
for(int i = 0; i < cnt; i++){
if(fd == clts[i].fd){
continue;}
send(clts[i].fd, msg, strlen(msg)+1,0);
}
}
void delClt(int fd){
pthread_mutex_lock(&lock);
for(int i = 0; i < cnt; i++){
if(clts[i].fd == fd){
clts[i] = clts[cnt-1];
--cnt;
break;
}
}
pthread_mutex_unlock(&lock);
}
void handlePrivateMsg(char *buf, const char *name){
int fd = 0;
sscanf(buf, "%d", &fd);
char msg[MSG_LEN] = {};
while(*buf != ' ' && *buf != '\0') ++buf;
if(*buf == '\0'){
strcpy(msg,name);
strcat(msg, "拍了拍我");
}
else{
strcpy(msg, name);
strcat(msg, " ");
strcat(msg, "四米消息:");
strcat(msg, buf);
}
send(fd, msg, strlen(msg)+1, 0);
}
void handldList(int fd){
char msg[MSG_LEN] = {};
sprintf(msg, "id:name\r\n");
int i = 0;
for(i = 0; i < cnt; ++i){
char buf[128] = {};
if(clts[i].fd == fd){
sprintf(buf, "%d:%s[自己]\r\n", clts[i].fd, clts[i].name);
}
else{
sprintf(buf, "%d:%s\r\n", clts[i].fd, clts[i].name);
}
strcat(msg, buf);
}
if(i == 0){
strcpy(msg, "it's empty!\r\n");
}
send(fd, msg, strlen(msg)+1, 0);
}
void *handleClient(void *arg)
{
struct Client clt = *(struct Client *)arg;
ssize_t rb = recv(clt.fd, clt.name, NAME_LEN, 0);
if(rb <0){
LOG_ERROR("recv:%s\n",strerror(errno));
return NULL;
}
char buf[NAME_LEN] = {};
strcpy(buf, clt.name);
strcat(buf, " ");
strcat(buf, "进入聊天室,真是太帅辣!");
broadcast(clt.fd, buf);
pthread_mutex_lock(&lock);
clts[cnt++] = clt;
pthread_mutex_unlock(&lock);
int len = strlen(clt.name);
for(;;){
strcpy(buf,clt.name);
strcat(buf,":");
rb = recv(clt.fd, buf+len+1, MSG_LEN-len-1, 0);
if(rb < 0){
LOG_ERROR("recv:%s\n",strerror(errno));
delClt(clt.fd);
break;
}
if(rb == 0){
strcpy(buf, clt.name);
strcat(buf, " ");
strcat(buf, "退出了聊天室");
delClt(clt.fd);
broadcast(clt.fd, buf);
break;
}
if(strncmp(buf+len+1, "@", 1) == 0){
handlePrivateMsg(buf+len+2, clt.name);
}
else if(strncmp(buf+len+1, "!list", 5) == 0){
handldList(clt.fd);
}
else{
broadcast(clt.fd, buf);
}
}
return NULL;
}
void server_run(const char *ip, unsigned short port){
//注册一个信号函数 让服务器正常停止
if(signal(SIGINT, handleExit) == SIG_ERR)
{
LOG_ERROR("signal:%s\n",strerror(errno));
return;
}
if(init_server(ip, port) == -1){
return;
}
struct Client clt = {};
socklen_t addrlen = sizeof(clt.addr);
for(;;){
clt.fd = accept(sockfd, (struct sockaddr*)&clt.addr, &addrlen);//第四步,从监听套接字的未完成连接请求队列中取出第一个连接请求,创建一个新的套接字和该连接请求建立连接并返回
if(clt.fd == -1)
{
LOG_ERROR("accept:%s\n",strerror(errno));
exit(-1);
}
errno = pthread_create(&clt.id, NULL, handleClient, &clt);//建立线程处理信号
if(errno != 0){
LOG_ERROR("pthread_create:%s\n", strerror(errno));
}
else{
LOG_ERROR("%s[%hu] client connected!\n",inet_ntoa(clt.addr.sin_addr),ntohs(clt.addr.sin_port));
}
}
}
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法 :%s <ip> <port>\n", argv[0]);
return -1;
}
server_run(argv[1], atoi(argv[2]));
return 0;
}
聊天室客户端
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int sockfd;
int connect_server(const char* ip, unsigned short port){
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int cnt = 0;
while(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1){
perror("connect");
if(errno == ECONNREFUSED || errno == EAGAIN){
++cnt;
if(cnt == 3)
return -1;
}
}
return 0;
}
void *recvData(void *arg){
char buf[1024] = {};
for(;;){
ssize_t rb = recv(sockfd, buf, sizeof(buf), 0);
if(rb <= 0){
break;
}
printf("\r%s\n>",buf);
fflush(stdout);
}
return NULL;
}
void *sendData(void *arg){
char buf[1024] = {};
scanf("%*[^\n]");
scanf("%*c");
for(;;){
printf(">");
fgets(buf, sizeof(buf), stdin);
int len = strlen(buf);
if(buf[len-1] == '\n'){
len--;
buf[len] = '\0';
}
if(len > 0){
ssize_t wb = send(sockfd, buf, len+1, 0);
if(wb <= 0){
perror("send");
break;
}
}
}
return NULL;
}
void handleExit(){
close(sockfd);
exit(0);
}
void client_run(const char *ip, unsigned short port){
if(signal(SIGINT, handleExit) == SIG_ERR){
perror("signal");
return;
}
if(connect_server(ip, port) == -1){
return;
}
char name[48] = {};
printf("input your name\n");
scanf("%s", name);
ssize_t wb = send(sockfd, name, strlen(name)+1, 0);
pthread_t id;
int err = pthread_create(&id, NULL, recvData, NULL);
sendData(NULL);
}
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法:%s <ip> <port>\n", argv[0]);
return -1;
}
client_run(argv[1], atoi(argv[2]));
return 0;
}
UDP模型
服务器
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法; %s <ip> <port>\n", argv[0]);
return -1;
}
printf("1.udp服务器:创建socket套接字...\n");
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("2.upd服务器:绑定到明确的ip和port通信地址上...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1){
perror("bind");
return -1;
}
printf("3.udp服务器:循环接受用户信息...\n");
for(;;){
struct sockaddr_in caddr;
socklen_t addrlen = sizeof(caddr);
char buf[1024] = {};
ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&caddr, &addrlen);
printf("recv:%s(ip:%s port:%hu)\n",buf, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
time_t t = time(NULL);
char *pt = ctime(&t);
ret = sendto(sockfd, pt, strlen(pt)+1, 0, (struct sockaddr*)&caddr, addrlen);
if(ret < 0){
perror("sendto");
return -1;
}
}
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("%s <ip> <port>", argv[0]);
return -1;
}
printf("1.udp客户端:创建socket套接字...\n");
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("2.udp客户端:准备服务器的通信地址...\n");
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(saddr);
printf("3.udp客户端:循环发送数据给服务器...\n");
for(;;){
char buf[1024] = {};
fgets(buf, sizeof(buf), stdin);
if(strncmp(buf, "!quit", 5) == 0){
break;
}
int ret = sendto(sockfd, buf, strlen(buf)+1, 0, (struct sockaddr*)&saddr, addrlen);
if(ret <= 0){
perror("sendto");
break;
}
ret = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("recv%s form server\n", buf);
}
close(sockfd);
return 0;
}
区别:
TCP
-
Transmission Control Protocol 传输控制协议
-
面向连接 (客户端需要调用connect进行连接,三次握手)
-
可靠 数据传输保证数据的完整性和有序性 数据检验、超时自动重传、丢失重传、滑动窗口机制(保证数据收发一致)应答机制
A B C D E —> ABCDE
-
传输效率比较慢
-
安全性要高
UDP
-
User Datagram Protocol 用户数据报文协议
-
不连接 (客户端和服务器不会建立连接)
-
不可靠 数据传输可能导致数据丢失 接收到的数据顺序和发送数据的顺序可能不一致
A B C —> C A
-
传输效率比较高
-
安全性比较低
TCP UDP协议报头
- TCP头部
- 16位源端口 16为目的地址端口
- 点到点一台主机上的进程发送到另一台主机上
- 32位的序列号,TCP数据报的编号, 没法送一个编号自动加一, 32为的确认序列号,每收到一个+1
- 4位首部长度 TCP首部最少20字节,最多60字节 以4字节为单位(TCP首部长度一定是4的整数倍) TCP首部长度 = 首部长度数值X4 字节
- URG、ACK、PSH、RST、SYN、FIN是六个控制位
* URG:紧急标志位(The urgent pointer),说明紧急指针有效。
* ACK:确认标志位(Acknowledgement Number),大多数情况下该标志位是置位的,说明确认序列号有效。该标志在TCP连接的大部分时候都有效。
* PSH:推(PUSH)标志位,该标志置位时,接收端在收到数据后应立即请求将数据递交给应用程序,而不是将它缓冲起来直到缓冲区接收满为止。在处理telnet或rlogin等交互模式的连接时,该标志总是置位的。
* RST:复位标志,用于重置一个已经混乱(可能由于主机崩溃或其他的原因)的连接。该位也可以被用来拒绝一个无效的数据段,或者拒绝一个连接请求。
* SYN:同步标志,说明序列号有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。
* FIN:结束标志,带有该标志置位的数据包用来结束一个TCP会话,但对应端口仍处于开放状态,准备接收后续数据。在TCP四次断开时会使用这个标志位。
- 16位窗口大小 滑动窗口机制 为了限制传输速度
- 16位检验和
- 16位紧急指针 URGUDP报文
- 16位原端口 16位目标地址端口
-
UDP报头
- 16位源端口 16位目的地端口 点到点
- 16位UDP长度
- 16位UDP检验和
TCP | UDP |
---|---|
Transmission Control Protocol 传输控制协议 | User Datagram Protocol 用户数据报文协议 |
面向连接(三次握手四次分手) | 无连接 |
可靠、安全、保证数据有序 | 不可靠、不安全、数据可能丢失、顺序不确定 |
延时重传、丢失重传、应答、检验、滑动窗口 | 没有重传、没有检验、没有应答 |
复杂、传输效率稍低 | 简单、高效、传输速度快 |
适合场合:安全性高、数据量少 | 适合场景:视频传输、数据量大的情况、对数据安全性要求不高 |
SOCK_STREAM | SOCK_DGRAM |
socket/bind/listen/accept/recv/send/connect/close | socket/bind/recvfrom/sendto/close |
- UDP可以实现可靠的数据传输吗?
- 可以
- 怎么实现:在使用udp时,在应用层实现检验、应答、重传等机制
套接字选项
#include <sys/types.h>
#include <sys/socket.h>
//获取套接字选项
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
//设置套接字选项
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
//optlen参数用来说明 optval所指向内存中数据的字节大小 "sizeof(*optval)"
- 可以设置/获取的选项
- 通用选项,工作在套接字类型上
- 在套接字层次管理的选项,但是依赖于下层协议的支持
- 特定用于某协议的选项,某个协议独有
- 参数level标识了应用的协议
- 如果是通用的套接字层次选项 SOL_SOCKET
- 否则level设置成控制这个选项的协议编号
- TCP选项 IPPROTO_TCP
- IP选项 IPPROTO_IP
optname | optval类型 | 描述 |
---|---|---|
SO_ACCEPTCONN | int | 返回信息指示该套接字是否能被监听(仅getsockopt) 是否能调用listen |
SO_BROADCAST | int | 如果*optval非0,广播数据报 |
SO_DEBUG | int | 如果*optval非0,启用网络驱动调试功能 |
SO_DONTROUTE | int | 如果*optval非0,绕过通常路由 |
SO_ERROR | int | 返回挂起的套接字错误并清除(仅getsockopt) |
SO_KEEPALIVE | int | 如果*optval非0,启用周期性keep-alive报文 |
SO_LINGER | struct linger | 当还未发报文而套接字已关闭时,延迟时间 |
SO_OOBINLINE | int | 如果*optval非0,将带外数据放在普通数据中 |
SO_RCVBUF | int | 接收缓冲区的字节长度 |
SO_RCVLOWAT | int | 接收调用中返回的最小数据字节数 |
SO_RCVTIMEO | struct timeval | 套接字接收调用的超时值 |
SO_REUSEADDR | int | 如果*optval非0,重用bind中的地址 |
SO_SNDBUF | int | 发送缓冲区字节长度 |
SO_SNDLOWAT | int | 发送调用中传送的最小数据字节数 |
SO_SNDTIMEO | struct timeval | 套接字发送调用的超时值 |
SO_TYPE | int | 标识套接字类型(仅getsockopt) |
- 做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
# ./server
bind error: Address already in use
-
erver终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态
-
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了
-
端口复用
int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
心跳检测机制
- 方式一:SO_KEEPALIVE 用来检测非正常断开
- 方式二:写一个守护进程,定时发送Heart-Beat包,用于检测对方是否在线
带外数据
-
区别于普通数据,可以优先处理紧急数据
-
当有紧急数据时,内部会为该进程递送一个SIGURG信号,如果需要处理带外数据, 需要实现注册SIGURG信号的处理函数, 直接或者简介去接受带外数据
-
signal(SIGURG,handleurg);
-
建立socket套接字的所有权,以确保信号可以被地送到合适的进程-
-
fcntl(sockfd, F_SETOWN, getpid());
-
recv/send在发送和接收带外数据时, 可以指定flag为MSG_OOB
-
TCP头v不URG的标识,以及一个16位 的紧急指针
- 紧急数据只有一个字节, 只会把tcp首部的紧急指针的前一个字节当作紧急数据
- 如果多次接受到多次紧急数据,会把前面的紧急数据丢弃
-
如果接收端请求读取带外数据(recv指定MSG_OOB),但是没有带外数据,则recv将出错并设置errno位EINVAL
-
在一个接受进程中, 被告知有带外数据的前提下,但是读取带外数据时,带外数据还没有到达,如果使用非阻塞的读取则直接返回-1并设置errno位EOWULDBLOOK
-
如果接收进程已经设置了套接字选项SO_OOBINLINE,则将带外数据作为普通数据读取,此时如果试图用MSG_OOB标识标志读取带外数据,则返回-1,且设置errno位EINVAL
-
带外数据不会受到流量控制,会确保能够正确的发送,在接受时带外数据拥有独立缓冲区,即使接收缓冲区已满,带外缓冲区仍然可以也能正常读取
带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
//返回1标识带外标记
//返回0 标识不是
//返回-1为出错
-
每当收到一个带外数据时,就有一个与之关联的带外标记
-
在从套接字读入期间,接收进程可以通过sockatmark函数确认是否处于带外标记
-
可以通过SO_OOBINLINE这样的方式读取带外数据,
高级IO
阻塞IO/非阻塞IO
同步IO/异步IO
散布读/聚集写
//相当于sendmsg和recvmsg简化版本
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
size_t recvmsg(int sockfd, struct msghdr *msg, int flags);
函数 | 任何描述符 | 仅套接字描述符 | 单个缓冲区 | 分散/集中读写 | 是否可选标志 | 可选对端地址 | 可选控制 信息 |
---|---|---|---|---|---|---|---|
read/write | OK | OK | |||||
readv/writev | OK | OK | |||||
recv/send | OK | OK | OK | ||||
recvfrom/sendto | OK | OK | OK | OK | |||
recvmsg/sendmsg | OK | OK | OK | OK | OK |
多路复用IO
-
在使用线程模型开发服务器时需考虑以下问题:
- 1.调整进程内最大文件描述符上限 头文件中定义的一个宏
- 2.线程如有共享数据,考虑线程同步
- 3.服务于客户端线程退出时,退出处理。(退出值,分离态)
- 4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
-
使用多进程和多线程实现服务器会有瓶颈
- 消耗资源型
- 高并发得不到及时响应
-
多路复用IO
- 开多线程的目的是为了一个线程监视一个客户端(去读取和响应客户端的请求)
- 多路复用IO可以让内核监视所有的客户端(文件描述符),并且通过一定的方式告诉用户,有哪些客户端发来了请求和数据
select/pselect
#include <sys/select.h>
//更早标准的头文件
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
/*
参数:
nfds - 最大文件描述符fd + 1 得到最大文件描述(循环找)
fd_set - 文件描述符集合 文件描述符的集合
readfds - 要监听是否可读的文件描述符集合
writefds - 要监听是否可写的文件描述符集合
exceptfds - 要监听是否异常的文件描述符集合
struct timeval
timeout - 定时阻塞监控时间
1.NULL 一直阻塞,直到监听的文件描述符集合中有对应的事件产生
2.设置timeval为{0,0}阻塞时间为0,调用select时会检查监听的文件描述符集合之后立即返回
3.设置timeval为固定时间,等待固定时间返回
返回值:
返回监听的文件描述符集合中有事件产生的文件描述符的个数
注意: readfds/writefds/exceptfds 既作为输入参数也作为输出参数
readfds 作为参数输入时,是用户关心的文件描述符集合(把要监听可读事件的文件描述符全部添加到这个集合中),在调用select函数时,内核会把没有可读事件的文件描述符从该集合中删除
readfds/writefds/exceptfds作为输出参数,表示的是这些个文件描述符集合中有事件产生的文件描述符
*/
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
//操作文件描述符集合的
//把fd文件描述符从set集合中删除
void FD_CLR(int fd, fd_set *set);
//判断fd文件描述是否在文件描述符集合set中 如果在返回非0
int FD_ISSET(int fd, fd_set *set);
//把文件描述符fd添加到文件描述符集合set中
void FD_SET(int fd, fd_set *set);
//清空文件描述符集合set 初始化
void FD_ZERO(fd_set *set);
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);//在调用pselect函数时,可以屏蔽sigmask信号集中的信号
-
select的编程模型
- 轮询的机制 一直在调用select函数
- 第一步:把关心的文件描述符添加到对应的文件描述符集合中 (循环) 找出最大的文件描述符
- 第二步: 调用select函数
- 需要提前保存文件描述符集合
- select函数在内核运行中,去遍历文件描述符符合,测试每一个文件描述符是否可读、可写、巩异常 (循环0-maxfd) 把没有可读、可写、异常文件描述符从对应的文件描述符集合中删除
- 第三步:遍历测试FD_ISSET关心的文件描述符是否还在可读、可写、异常的文件描述符集合中 (循环)
- 轮询的机制 一直在调用select函数
-
select的缺点
- 随着文件描述符(客户端)的增加,效率急剧下降
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
poll/ppoll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
参数:
fds - 数组首地址
nfds - 数组长度
timeout - 超时等待
-1 :阻塞等待,直到监听的文件描述符的事件有满足条件的
0 :立即返回,会对监听的文件描述符的事件进行一次测试
>0 :阻塞等待timeout毫秒数
返回值:
返回满足监视事件的文件描述符的个数,超时返回0(代表没有满足的监听事件) -1失败
*/
struct pollfd {
int fd; //文件描述符
short events; //监听fd文件描述符的事件(可以有多个事件) 内核并不会修改这个值
short revents; //作为返回用的 所监听fd文件描述符发生发生的事件有哪些 内核只会修改这个值
};
/*
事件取值:(如果对于一个文件描述符想监听多个事件,则按位或)
POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
POLLRDNORM 数据可读
POLLRDBAND 优先级带外数据可读
POLLPRI 高优先级可读数据
POLLOUT 普通或带外数据可写
POLLWRNORM 数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
*/
#define _GNU_SOURCE
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask);
-
编程模型
- 把初始的文件描述符和监听的事件放到struct pollfd数组中
- 轮询的机制
- 调用poll函数 (需要遍历struct pollfd数组)
- 处理文件描述符的事件 循环遍历struct pollfd数组中所有成员,判断其返回事件中revents是否有关心的事件发生,如果则去处理 (有客户端连接 struct pollfd数组中添加成员,如果有退出删除其值)
- 如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0
-
poll相对于select而言,效率提高了
- 复杂的文件描述符集合的操作
- 不需要每次都像select一样把文件文件符重新组装
- poll返回事件和监听事件分开
epoll
- linux下独有的 efficient高效的poll机制
- 非常适用于文件描述符数量巨大且只有少量处于活跃状态的场景
- 高并发首选epoll
#include <sys/epoll.h>
//创建epoll句柄 epoll_create本身创建一个文件描述符
int epoll_create(int size);
/*
在之前 size 表示 能够监听文件描述符的最大个数
现代的linux内核忽略size参数,只要给大于0的数值即可
创建epoll句柄,后续的epoll_ctl和epoll_wait都依赖于这个返回值
本身就是一个文件描述符
*/
//操作事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
op参数:
EPOLL_CTL_ADD 注册事件 只需要注册一次,只要不删除修改,永久有效
注册的事件用红黑树来组织管理的
EPOLL_CTL_MOD 修改注册的事件
EPOLL_CTL_DEL 删除注册的事件
*/
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//等待接收注册事件中满足条件的事件
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
/*
events: 数组首地址 只作为输出参数
maxevents: events最大长度
返回值:
返回往events数组中写入记录的数量
在处理结果时,只需要遍历0-epoll_wait返回值的区间
*/
int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,const sigset_t *sigmask);