多路复用I/O
多路I/O复用表示支持多个任务同时对某一进程的I/O进程操作,普通的read/write只能实现同一时间操作一个,无法实现网络通信的并发操作。那么多路复用I/O分为三种机制:select/poll/epoll
多进程多线程的socket模型具有明显缺陷
1.占用内存多 2.进程(线程)切换时间多。3.进程(线程)之间同步麻烦
多路复用的解决理念:
在主控线程中将需要监控的文件描述符保存到文件描述符集中,该文件描述符集为一个位图,我们知道文件描述符正常情况下总是累加上去的,也是一个整数,因此这个整数巧好可以表示该文件描述符在位图的位置(例如位图上的3号位置为1,表示文件描述符等于3有事件发生,否则为空闲。),将服务器和客户端的文件描述符加入到该(文件描述符集)位图中进行监控,若有事件发生则才处理。
API分析:
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval timeout);
nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:监控有读数据到达文件描述符集合,传入传出参数
writefds:监控写数据到达文件描述符集合,传入传出参数
exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout:定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置 timeval,等待固定时间
3.设置 timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; / 秒 /
long tv_usec; / 微妙 */
};
void FD_CLR(int fd, fd_set *set); 把文件描述符集合里fd清0
int FD_ISSET(int fd, fd_set *set); 测试文件描述符集合里fd是否置1
void FD_SET(int fd, fd_set *set); 把文件描述符集合里fd位置1
void FD_ZERO(fd_set *set); 把文件描述符集合里所有位清0
函数2-5均为宏定义函数
通过上述API可知道:
select监控的有三种事件的文件描述符集(读、写、异常)。以及可设置阻塞时间,大大地解决死等的弊端,select无请求后,主控函数将执行后续指令。
但是要注意的是:
select监听的文件描述符集最大支持1024,大小可用宏定义表示FD_SETSIZE( = 1024),解决1024以下客户端时使用select是很合适的,后面会讲解poll,解决1024的限制。
select采用的是轮询模型,数量级在千级还是很适合,但是如果更大会导致每次监控都要从文件描述符集从0到maxfd+1进行遍历,会大大降低服务器响应效率,不应在select上投入更多精力
这是客户端
/*./client serv_ip serv_port */
#include "net.h"
void usage (char *s)
{
printf ("\n%s serv_ip serv_port", s);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
}
int main (int argc, char **argv)
{
int fd = -1;
int port = -1;
struct sockaddr_in sin;
if (argc != 3) {
usage (argv[0]);
exit (1);
}
/* 1. 创建socket fd */
if ((fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
perror ("socket");
exit (1);
}
port = atoi (argv[2]);
if (port < 5000) {
usage (argv[0]);
exit (1);
}
/*2.连接服务器 */
/*2.1 填充struct sockaddr_in结构体变量 */
bzero (&sin, sizeof (sin));
sin.sin_family = AF_INET;
sin.sin_port = htons (port); //网络字节序的端口号
#if 0
sin.sin_addr.s_addr = inet_addr (SERV_IP_ADDR);
#else
if (inet_pton (AF_INET, argv[1], (void *) &sin.sin_addr) != 1) {
perror ("inet_pton");
exit (1);
}
#endif
if (connect (fd, (struct sockaddr *) &sin, sizeof (sin)) < 0) {
perror ("connect");
exit (1);
}
printf ("Client staring...OK!\n");
int ret = -1;
fd_set rset;
int maxfd = -1;
struct timeval tout;
char buf[BUFSIZ];
while (1) {
FD_ZERO (&rset);
FD_SET (0, &rset); //这个是处理键盘输入的进程
FD_SET (fd, &rset);
maxfd = fd;
tout.tv_sec = 5;
tout.tv_usec = 0;
select (maxfd + 1, &rset, NULL, NULL, &tout);
if (FD_ISSET (0, &rset)) { //标准键盘上有输入
//读取键盘输入,发送到网络套接字fd
bzero (buf, BUFSIZ);
do {
ret = read (0, buf, BUFSIZ - 1);
} while (ret < 0 && EINTR == errno);
if (ret < 0) {
perror ("read");
continue;
}
if (!ret)
continue; //服务器关闭
if (write (fd, buf, strlen (buf)) < 0) {
perror ("write() to socket");
continue;
}
if (!strncasecmp (buf, QUIT_STR, strlen (QUIT_STR))) { //用户输入了quit字符
printf ("Client is exiting!\n");
break;
}
}
if (FD_ISSET (fd, &rset)) { //服务器给发送过来了数据
//读取套接字数据,处理
bzero (buf, BUFSIZ);
do {
ret = read (fd, buf, BUFSIZ - 1);
} while (ret < 0 && EINTR == errno);
if (ret < 0) {
perror ("read from socket");
continue;
}
if (!ret)
break; /* 服务器关闭 */
//There is a BUG,FIXME!!
printf ("server said: %s\n", buf);
if ((strlen(buf) > strlen(SERV_RESP_STR))
&& !strncasecmp (buf+strlen(SERV_RESP_STR), QUIT_STR, strlen (QUIT_STR))) { //用户输入了quit字符
printf ("Sender Client is exiting!\n");
break;
}
}
}
/*4.关闭套接字 */
close (fd);
}
参考:
https://blog.csdn.net/weixin_42889383/article/details/102367621?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158434937319724835849784%2522%252C%2522scm%2522%253A%252220140713.130056874…%2522%257D&request_id=158434937319724835849784&biz_id=0&utm_source=distribute.pc_search_result.none-task