在UNIX/Linux下主要有4种I/O 模型:
阻塞I/O:最常用、最简单、效率最低
非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
I/O 多路复用:允许同时对多个I/O进行控制
信号驱动I/O:一种异步通信模型------底层驱动专栏中详细讲
2.阻塞IO以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,进程会被挂起,一直阻塞,直到缓冲区中有内容了,内核会唤醒该进程,读完内容后继续向下执行。
写操作也是会阻塞的,当缓冲区满了,就阻塞了,当缓冲区中有足够的空间接收这次写了就能解除阻塞。一般情况下,对于阻塞的问题,考虑的都是读的阻塞。
示例:以写阻塞为例
//写端 #include3.非阻塞IO#include #include #include #include int main(){ int fd = open("my_fifo", O_WRONLY); if(-1 == fd){ perror("open error"); exit(-1); } int count = 0; while(1){ if(-1 == write(fd, "hello world", 11)){ perror("write error"); exit(-1); } count++; printf("count = %dn", count); } close(fd); return 0; } //读端 #include #include #include #include int main(){ int fd = open("my_fifo", O_RDONLY); char buff[11] = {0}; read(fd, buff, 11); while(1);//防止管道破裂 close(fd); return 0; }
以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,相当于告诉内核,不要将这个进程挂起,而是立即给我返回一个错误。
一般的带有阻塞属性的函数,默认方式都是阻塞IO。对于recv recvfrom 等函数,是可以通过参数来设置成非阻塞的。如:recv 的 MSG_DONTWAIT,recvfrom 的 MSG_DONTWAIT,waitpid 的 WNOHANG等。但是对于 read 等函数,默认方式就是阻塞,如果想使用read实现非阻塞,需要用到 fcntl() 来修改文件描述符的状态。
fcntl函数说明:
int fcntl(int fd, int cmd, ... );
功能:设置或获取文件描述符的状态
#include
#include
参数:
@fd: 文件描述符
@cmd: 要控制的指令
F_GETFL 获取文件描述符的状态
F_SETFL 设置文件描述符的状态 O_NONBLOCK 非阻塞
@arg: 可变参
具体需不需要取决于第二个参数是什么,
如果第二个参数是 F_GETFL 就不需要
如果第二个参数是 F_SETFL 就需要
返回值: F_GETFL 返回的就是文件描述符的状态
F_SETFL 成功返回0 失败返回-1
示例:使用管道时,注意,写端未打开,读端的open会阻塞,需要设置成非阻塞才能读到。
//读端 #include#include #include #include #include #include #include #include #include int main(){ int fd1 = open("fifo1", O_RDONLY); if(-1 == fd1){ perror("open error"); exit(-1); } int fd2 = open("fifo2", O_RDONLY); if(-1 == fd2){ perror("open error"); exit(-1); } int fd3 = open("fifo3", O_RDONLY); if(-1 == fd3){ perror("open error"); exit(-1); } //将文件描述符 fd1 fd2 fd3 设置成非阻塞 int flag = fcntl(fd1, F_GETFL); flag |= O_NONBLOCK; fcntl(fd1, F_SETFL, flag); flag = fcntl(fd2, F_GETFL); flag |= O_NONBLOCK; fcntl(fd2, F_SETFL, flag); flag = fcntl(fd3, F_GETFL); flag |= O_NONBLOCK; fcntl(fd3, F_SETFL, flag); char buff1[128] = {0}; char buff2[128] = {0}; char buff3[128] = {0}; while(1){ read(fd1, buff1, 128); printf("buff1 = %sn", buff1); memset(buff1, 0, 128); read(fd2, buff2, 128); printf("buff2 = %sn", buff2); memset(buff2, 0, 128); read(fd3, buff3, 128); printf("buff3 = %sn", buff3); memset(buff3, 0, 128); //sleep(1);//为了演示现象用的 防止刷屏 } close(fd1); close(fd2); close(fd3); return 0; }
//写端(三个写端一样的) #include4.IO多路复用#include #include #include #include #include int main(){ int fd = open("fifo1", O_WRONLY); if(-1 == fd){ perror("open error"); exit(-1); } char buff[128] = {0}; while(1){ fgets(buff, 128, stdin); buff[strlen(buff)-1] = ' '; if(-1 == write(fd, buff, 128)){ perror("write error"); exit(-1); } memset(buff, 0, 128); } close(fd); return 0; }
使用阻塞的方式处理多个阻塞函数,相互之间会有影响,有时不可取。如果使用非阻塞,有需要写一个循环轮询每个函数,十分占用CPU,也不可取。使用多进程、多线程也可以解决这个问题,但是要考虑资源的回收及安全问题,比较麻烦。比较好的一种方式,是使用 IO 多路复用。
IO多路复用的基本思想:
先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
select函数说明:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:IO多路复用
#include
参数:
@nfds: 最大的文件描述符+1
@readfds: 要监控的读文件描述符集合 我们一般考虑读
@writefds: 要监控的写文件描述符集合
@exceptfds: 要监控的异常文件描述符集合
@timeout: 超时时间
有值:阻塞的时间,超时后 select会立即返回
0: 非阻塞
NULL:永久阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1
注:FD_SETSIZE:select 能监视的最大的文件描述符个数是1024
文件描述符相关函数:
void FD_CLR(int fd, fd_set *set); //在集合中删除一个文件描述符
int FD_ISSET(int fd, fd_set *set); //判断文件描述符是否在集合中
void FD_SET(int fd, fd_set *set); //向集合中添加一个文件描述符
void FD_ZERO(fd_set *set); //将集合清空
示例:
//读端 #include#include #include #include #include #include #include #include #include #include int main(){ int fd1 = open("fifo1", O_RDONLY); int fd2 = open("fifo2", O_RDONLY); int fd3 = open("fifo3", O_RDONLY); int max_fd = 0;//保存最大的文件描述符 //构建要监视的文件描述符集合 fd_set readfds;//保存初始的 fd_set readfds_temp;//给select用的 FD_ZERO(&readfds);//清空 FD_ZERO(&readfds_temp);//清空 //将要监视的文件描述符添加进集合 FD_SET(fd1, &readfds); max_fd = (max_fd>fd1?max_fd:fd1); FD_SET(fd2, &readfds); max_fd = (max_fd>fd2?max_fd:fd2); FD_SET(fd3, &readfds); max_fd = (max_fd>fd3?max_fd:fd3); char buff1[128] = {0}; char buff2[128] = {0}; char buff3[128] = {0}; while(1){ //注意:每次select返回都会将没有准备好的文件描述符在表中擦除 //所以每次要重新将文件描述符添加到集合中 readfds_temp = readfds; if(-1 == select(max_fd+1, &readfds_temp, NULL, NULL, NULL)){ perror("select error"); exit(-1); } if(FD_ISSET(fd1, &readfds_temp)){ read(fd1, buff1, 128); printf("buff1 = [%s]n", buff1); memset(buff1, 0, 128); } if(FD_ISSET(fd2, &readfds_temp)){ read(fd2, buff2, 128); printf("buff2 = [%s]n", buff2); memset(buff2, 0, 128); } if(FD_ISSET(fd3, &readfds_temp)){ read(fd3, buff3, 128); printf("buff3 = [%s]n", buff3); memset(buff3, 0, 128); } } close(fd1); close(fd2); close(fd3); return 0; }
//写端 #include二、服务器模型 1.概念#include #include #include #include #include int main(){ int fd = open("fifo1", O_WRONLY); if(-1 == fd){ perror("open error"); exit(-1); } char buff[128] = {0}; while(1){ fgets(buff, 128, stdin); buff[strlen(buff)-1] = ' '; if(-1 == write(fd, buff, 128)){ perror("write error"); exit(-1); } memset(buff, 0, 128); } close(fd); return 0; }
服务器模型主要有两种:
循环服务器:同一时间只能处理一个客户端的请求。
并发服务器:可以同时处理多个客户端的请求。
TCP服务器本身就是一个循环服务器,原因是他有两个阻塞函数,accept 和 recv 他们之间会相互影响。UDP服务器本身就是一个并发服务器,因为他只有一个阻塞函数,recvfrom
2.循环服务器在上一篇博客(C语言编程实现TCP/UDP/TFTP网络通信)中详细讲解了,这里就不说了。
示例:(跟下面做对照)
//服务器端 #include#include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; //网络字节序的端口号,可以是 8888 9999 6789 等都可以 server_addr.sin_port = htons(atoi(argv[2])); //IP地址 //不能随便填,可以填自己主机的IP地址 //如果只是在本地测试,也可以填 127.0.0.1 server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&client_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); char buff[128] = {0}; int acceptfd = 0; int bytes = 0; while(1){ //5.阻塞等待客户端连接 acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen); if(-1 == acceptfd){ ERRLOG("accept error"); } printf("客户端 %s:%d 连接到服务器了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); while(1){ //6.与客户端通信 if(0 > (bytes = recv(acceptfd, buff, 128, 0))){ ERRLOG("recv error"); }if(bytes == 0){ printf("客户端 %s:%d 断开了连接n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 %s:%d 退出了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; } printf("%s-%d:[%s]n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff); //组装应答 strcat(buff, "--server"); if(-1 == send(acceptfd, buff, 128, 0)){ ERRLOG("send error"); } } } //7.关闭套接字 close(acceptfd); } close(sockfd); return 0; }
//客户端 #include3.并发服务器#include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.与服务器建立连接 if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("connect error"); } //4.与服务器通信 char buff[128] = {0}; while(1){ fgets(buff, 128, stdin); buff[strlen(buff)-1] = ' ';//清除 n if(-1 == send(sockfd, buff, 128, 0)){ ERRLOG("send error"); } if(0 == strcmp(buff, "quit")){ break; } if(-1 == recv(sockfd, buff, 128, 0)){ ERRLOG("recv error"); } printf("收到回复:[%s]n", buff); } //5.关闭套接字 close(sockfd); return 0; }
实现TCP并发服务器方式:大多数场景下,我们既要保证可靠,又要保证并发,所以就要研究TCP如何实现并发服务器。
方式1:使用多线程实现TCP并发服务器
方式2:使用多进程实现TCP并发服务器
方式3:使用IO多路复用实现TCP并发服务器(最常用)
4.使用多线程实现TCP并发服务器主线程专门用来接收客户端的连接请求(也就是专门用来处理 accept)
每当有新的客户端连接成功时,就创建一个子线程,在线程处理函数中专门用来和这个客户端通信。
注:多线程的相关知识在IO接口专栏中的 “c语言中的多线程的实现” 博客详细介绍了。
示例:
//服务器端 #include#include #include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) typedef struct MSG{ int acceptfd; struct sockaddr_in client_addr; }msg_t; void *deal_recv_send(void *arg){ msg_t msg = *(msg_t *)arg; printf("客户端 %s:%d 连接到服务器了n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); int bytes = 0; char buff[128] = {0}; while(1){ //6.与客户端通信 if(0 > (bytes = recv(msg.acceptfd, buff, 128, 0))){ ERRLOG("recv error"); }else if(bytes == 0){ printf("客户端 %s:%d 断开了连接n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); break; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 %s:%d 退出了n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); break; } printf("%s-%d:[%s]n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port), buff); //组装应答 strcat(buff, "--server"); if(-1 == send(msg.acceptfd, buff, 128, 0)){ ERRLOG("send error"); } } } //7.关闭套接字 close(msg.acceptfd); } int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&client_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); int acceptfd = 0; pthread_t tid = 0; msg_t client_msg; while(1){ //5.阻塞等待客户端连接 acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen); if(-1 == acceptfd){ ERRLOG("accept error"); } //将客户端的套接字和客户端的网络信息结构体传给线程处理函数 client_msg.acceptfd = acceptfd; client_msg.client_addr = client_addr; //创建线程单独处理和客户端的通信 if(0 != pthread_create(&tid, NULL, deal_recv_send, &client_msg)){ ERRLOG("pthread_create error"); } //设置线程分离属性 if(0!=pthread_detach(tid)){ ERRLOG("pthread_detach error"); } } close(sockfd); return 0; }
客户端代码同循环服务器
5.使用多进程实现TCP并发服务器父进程专门用来接收客户端的连接请求(也就是专门用来处理 accept)
每当有新的客户端连接成功时,就创建一个子进程,在子进程中专门用来和这个客户端通信。
注:多进程的相关知识在IO接口专栏中的 “c语言中的多进程的实现” 博客详细介绍了。
示例:
//服务器端 #include#include #include #include #include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) //自定义的信号处理函数 void deal_child(int x){ wait();//阻塞 //waitpid(-1, NULL, W_NOHONG);//非阻塞 //使用阻塞好一些,如果使用非阻塞,子进程发射出退出信号后,再退出 //有可能导致父进程没有回收到资源,还是会有僵尸进程产生 } int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&client_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); int acceptfd = 0; pthread_t tid = 0; pid_t pid = 0; while(1){ //5.阻塞等待客户端连接 acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen); if(-1 == acceptfd){ ERRLOG("accept error"); } //创建子进程 单独处理和该客户端的通信 if(-1 == (pid = fork())){ ERRLOG("fork error"); }else if(pid == 0){ //子进程的逻辑 printf("客户端 %s:%d 连接到服务器了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); int bytes = 0; char buff[128] = {0}; while(1){ //6.与客户端通信 if(0 > (bytes = recv(acceptfd, buff, 128, 0))){ ERRLOG("recv error"); }else if(bytes == 0){ printf("客户端 %s:%d 断开了连接n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 %s:%d 退出了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; } printf("%s-%d:[%s]n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff); //组装应答 strcat(buff, "--server"); if(-1 == send(acceptfd, buff, 128, 0)){ ERRLOG("send error"); } } } //关闭套接字 close(acceptfd); //子进程退出前 给父进程发射 SIGUSR1 信号 kill(getppid(), SIGUSR1); exit(0); }else if(pid >0 ){ //父进程的逻辑 //父进程需要回收子进程的资源,防止僵尸进程 //方式1:wait 但是wait本身也是阻塞,不推荐 //方式2:waitpid 的 W_NOHONG 非阻塞,需要轮询,也不推荐 //方式3:父进程退出了子进程资源就回收了 但是服务器程序一般不会退出 //方式4:使用信号的方式处理比较好: //子进程退出时,给父进程发一个信号 SIGCHLD 或者使用 SIGUSR1 也行 //父进程就干自己的活(等待客户端连接) //什么时候收到了子进程退出的信号,然后再去回收子进程的资源 //捕获子进程的退出发射的信号 signal(SIGUSR1, deal_child); //关闭父进程的 acceptfd close(acceptfd); } } close(sockfd); return 0; }
客户端代码同循环服务器
6.多路IO复用实现TCP并发服务器将sockfd,和每个客户端的acceptfd 都放到一个表里,传参给select函数
内核帮我们检测哪些文件描述符准备就绪了,select会将准备就绪的文件描述符告诉我们,再根据描述符的不同,分别处理需求即可。
示例:
//服务器端 #include#include #include #include #include #include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&server_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); int max_fd = 0; //构建文件描述符表 fd_set readfds;//是我们自己填充的,相当于备份 fd_set readfds_temp;//是给 select用的 因为每次擦除 FD_ZERO(&readfds); FD_ZERO(&readfds_temp); //将sockfd 添加进集合 FD_SET(sockfd, &readfds); max_fd = max_fd>sockfd?max_fd:sockfd;//更新最大文件描述符 //设置超时时间 5s struct timeval tm; memset(&tm, 0, sizeof(tm)); tm.tv_sec = 5; tm.tv_usec = 0; int ret = 0; int acceptfd = 0; int i = 0; int bytes = 0; char buff[128] = {0}; while(1){ //每次重置readfds_temp readfds_temp = readfds; //每次重置超时时间 tm.tv_sec = 5; tm.tv_usec = 0; if(-1 == (ret = select(max_fd+1, &readfds_temp, NULL, NULL, &tm))){ ERRLOG("select error"); }else if(ret == 0){ printf("select timeoutn"); continue; }else if(ret > 0){ //判断条件的 ret != 0 是表示:如果n个就绪,只处理n个即可,后面的就不用管了 for(i = 3; i < max_fd+1 & ret != 0; i++){ if(FD_ISSET(i, &readfds_temp)){ if(i == sockfd){ //说明有新的客户端连接 if(-1 == (acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen))){ ERRLOG("accept error"); } printf("客户端 [%d] 连接到服务器n", acceptfd); //连接成功了 将新的客户端的文件描述符加入到表中 FD_SET(acceptfd, &readfds); //更新max_fd max_fd = max_fd>acceptfd?max_fd:acceptfd; }else{ //6.与客户端通信 if(0 > (bytes = recv(i, buff, 128, 0))){ ERRLOG("recv error"); }else if(bytes == 0){ printf("客户端 [%d] 断开了连接n", i); //将文件描述符在表中删除 FD_CLR(i, &readfds); //关闭对应的文件描述符 close(i); continue; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 [%d] 退出了n", i); //将文件描述符在表中删除 FD_CLR(i, &readfds); //关闭对应的文件描述符 close(i); continue; } printf("客户端 [%d] 发来消息:[%s]n", i, buff); //组装应答 strcat(buff, "--server"); if(-1 == send(i, buff, 128, 0)){ ERRLOG("send error"); } } } ret--; } } } } close(sockfd); return 0; }
客户端代码同循环服务器
三、网络超时检测 1.概念阻塞IO:当程序运行到IO函数时,如果缓冲区中有内容,则程序正常运行,如果没有内容,程序就会阻塞,直到有内容了再继续运行。
非阻塞:当程序运行到IO函数时,如果缓冲区中有内容,则程序正常运行,如果没有内容,程序不会阻塞,而是立刻返回错误。
超时检测:是介于阻塞和非阻塞之间的,可以设定一个时间,在这个时间范围内,如果缓冲区没有内容,就阻塞,如果到了设定的时间,缓冲区中还没有内容,就会变成非阻塞,立刻返回错误。
2.实现超时检测的方式方式1:select 函数实现超时检测(poll 和 epoll_wait 也可以)
方式2:可以使用 setsockopt 函数设置超时检测
方式3:可以使用alarm信号 实现超时检测
3.使用select实现超时检测select函数补充:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:最后一个参数,就是要设置的超时时间
#include
参数:
@nfds: 最大的文件描述符+1
@readfds: 要监控的读文件描述符集合 我们一般考虑读
@writefds: 要监控的写文件描述符集合
@exceptfds: 要监控的异常文件描述符集合
@timeout: 超时时间
struct timeval:阻塞一定时间
struct timeval {
long tv_sec;
long tv_usec;
};
NULL:永久阻塞
0:非阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1
示例:见上面的 多路IO复用实现TCP并发服务器 的例子
4.使用setsockopt实现超时检测①getsockopt()函数
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
功能:获取套接字的选项
#include
#include
参数:
@sockfd:要操作的套接字
@level:
socket级别:SOL_SOCKET
tcp级别:IPPROTO_TCP
ip级别:IPPROTO_IP
@ optname:
socket级别:
SO_BROADCAST 是否允许发送广播
SO_RCVBUF 接收缓冲区的大小:单位字节
SO_REUSEADDR 设置端口复用
SO_SNDBUF 发送缓冲区的大小:单位字节
SO_RCVTIMEO 接收超时时间
SO_SNDTIMEO 发送超时时间
超时时间 optval参数 使用 struct timeval 结构体
超时会返回-1 并且错误码会被设置成 EAGAIN
@optval:socket级别,除非另有说明,否则是一个int *指针
@optlen:optval 大小
返回值: 成功返回0,失败返回-1,置位错误码
示例:使用getsockopt函数获取发送和接收缓冲区的大小
#include#include #include #include #include #include #include #include #include int main(){ //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ perror("socket error"); exit(-1); } int count = 0; int len = sizeof(count); if(-1 == getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF,&count, &len)){ perror("getsockopt error"); exit(-1); } printf("发送缓冲区大小 [%d]Kn", count/1024); count = 0; if(-1 == getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF,&count, &len)){ perror("getsockopt error"); exit(-1); } printf("接收缓冲区大小 [%d]Kn", count/1024); return 0; }
执行结果:发送缓冲区大小:16K 接收缓冲区大小:128K
②setsockopt函数说明:用法和 getsockopt 函数基本一样,只不过一个是获取一个是设置
示例:使用setsockopt设置端口复用
int sockfd = socket(); int on = 1;//设置端口复用时 optval是一个整数布尔型值 0 假 非0真 setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bind(); //端口复用要加在socket函数之后,bind之前
③超时检测代码实现:
//服务器端 #include#include #include #include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) typedef struct MSG{ int acceptfd; struct sockaddr_in client_addr; }msg_t; void *deal_recv_send(void *arg){ msg_t msg = *(msg_t *)arg; printf("客户端 %s:%d 连接到服务器了n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); int bytes = 0; char buff[128] = {0}; while(1){ //6.与客户端通信 //由已经设置过超时检测的sockfd产生的acceptfd会继承 sockfd 的超时属性 //如果不想改 直接使用即可 //如果想要重新设置,再次调用 setsockopt 即可 if(0 > (bytes = recv(msg.acceptfd, buff, 128, 0))){ if(errno == EAGAIN){ printf("recv tmeoutn"); break;//直接关闭客户端的套接字 } ERRLOG("recv error"); }else if(bytes == 0){ printf("客户端 %s:%d 断开了连接n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); break; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 %s:%d 退出了n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port)); break; } printf("%s-%d:[%s]n", inet_ntoa(msg.client_addr.sin_addr), ntohs(msg.client_addr.sin_port), buff); //组装应答 strcat(buff, "--sever"); if(-1 == send(msg.acceptfd, buff, 128, 0)){ ERRLOG("send error"); } } } //7.关闭套接字 close(msg.acceptfd); } int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&server_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); int acceptfd = 0; pthread_t tid = 0; msg_t client_msg; //设置超时时间 5s struct timeval tm; memset(&tm, 0, sizeof(tm)); tm.tv_sec = 5; tm.tv_usec = 0; if(-1 == setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tm, sizeof(tm))){ ERRLOG("setsockopt error"); } while(1){ //5.阻塞等待客户端连接 acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen); if(-1 == acceptfd){ if(errno == EAGAIN){ printf("accept timeoutn");//自定义的处理方式 将此处的 printf替换掉即可 continue; } ERRLOG("accept error"); } //将客户端的套接字和客户端的网络信息结构体传给线程处理函数 client_msg.acceptfd = acceptfd; client_msg.client_addr = client_addr; //创建线程单独处理和客户端的通信 if(0 != pthread_create(&tid, NULL, deal_recv_send, &client_msg)){ ERRLOG("pthread_create error"); } //设置线程分离属性 if(0!=pthread_detach(tid)){ ERRLOG("pthread_detach error"); } } close(sockfd); return 0; }
//客户端 #include5.使用 alarm 闹钟实现超时检测#include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.与服务器建立连接 if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("connect error"); } //4.与服务器通信 int bytes = 0; char buff[128] = {0}; while(1){ fgets(buff, 128, stdin); buff[strlen(buff)-1] = ' ';//清除 n if(-1 == send(sockfd, buff, 128, 0)){ ERRLOG("send error"); } if(0 == strcmp(buff, "quit")){ break; } if(-1 == (bytes = recv(sockfd, buff, 128, 0))){ ERRLOG("recv error"); }else if(bytes == 0){ //如果对端已经关闭了套接字 //第二次给对端发消息时 会出现 SIGPIPE 导致进程结束 printf("由于你长时间没有说话,已经被踢出聊天了n"); break; } printf("收到回复:[%s]n", buff); } //5.关闭套接字 close(sockfd); return 0; }
信号的自重启属性:使用alarm函数可以设置一个超时时间,一旦时间到达了,就会给进程发一个SIGALRM信号,进程对SIGALRM默认的处理方式是终止。对于服务器程序而言,不能因为超时就终止,所以需要对SIGALRM信号做一个捕捉。如果将信号的处理方式设置成捕捉,当信号产生时,就会去调用信号处理函数,当信号处理函数执行完毕后,程序会回到产生信号时的状态继续向下运行,这种属性称为信号的自重启属性。
如果想要使用alarm实现超时检测,就要关闭信号的自重启属性(sigaction函数)。关闭之后,信号处理函数执行完,会立即返回错误 EINTR,而不是重新启动原进程。
进程对信号默认的处理方式:
方式1:终止进程
方式2:终止进程
方式3:忽略
方式4:让停止的进程继续运行
人为对信号的处理方式:
方式1:忽略
方式2:默认
方式3:捕捉
sigaction函数说明:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或者改变信号的行为
#include
参数:
@signum : 要处理行为的信号的编号,除了 SIGKILL 和 SIGSTOP
@act : 新的行为 (在获取行为时,可以置NULL)
@oldact : 旧的行为 (在设置行为时,可以置NULL)
struct sigaction {
void (*sa_handler)(int);//信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理函数 两个不要同时设置
sigset_t sa_mask;//关于阻塞的掩码 我们用不到
int sa_flags;//信号的行为
SA_RESTART 信号自重启属性
void (*sa_restorer)(void);//一般不用于应用程序
}
返回值: 成功返回0,失败返回-1,置位错误码
示例:使用sigaction关闭 SIGALRM 信号的自重启属性,并实现超时检测
//服务器端 #include#include #include #include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) //自定义的信号处理函数 void my_signal(int x){ //什么都不用做 printf("my_signaln"); } int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //取消SIGALRM信号的自重启属性 struct sigaction oldact; memset(&oldact, 0, sizeof(oldact)); //获取旧的行为 if(-1 == sigaction(SIGALRM, NULL, &oldact)){ ERRLOG("sigaction error"); } //设置信号处理函数 oldact.sa_handler = my_signal; //取消自重启属性 oldact.sa_flags &= ~SA_RESTART; //设置新的行为 if(-1 == sigaction(SIGALRM, &oldact, NULL)){ ERRLOG("sigaction error"); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } //创建服务器网络信息结构体 struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; //网络字节序的端口号,可以是 8888 9999 6789 等都可以 server_addr.sin_port = htons(atoi(argv[2])); //IP地址 //不能随便填,可以填自己主机的IP地址 //如果只是在本地测试,也可以填 127.0.0.1 server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.将套接字和网络信息结构体进行绑定 if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("bind error"); } //4.将服务器的套接字设置成被动监听状态 if(-1 == listen(sockfd, 5)){ ERRLOG("listen error"); } //定义一个结构体,保存客户端的信息 struct sockaddr_in client_addr; memset(&server_addr, 0, sizeof(client_addr));//清空 socklen_t clientaddrlen = sizeof(client_addr); char buff[128] = {0}; int acceptfd = 0; int bytes = 0; while(1){ alarm(5);//设置超时时间5s //5.阻塞等待客户端连接 acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddrlen); if(-1 == acceptfd){ if(errno == EINTR){ printf("accept timeoutn"); continue; } ERRLOG("accept error"); } printf("客户端 %s:%d 连接到服务器了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); while(1){ alarm(5); //6.与客户端通信 if(0 > (bytes = recv(acceptfd, buff, 128, 0))){ if(errno == EINTR){ printf("recv timeoutn"); break; } printf("errno = %dn", errno); ERRLOG("recv error"); }else if(bytes == 0){ printf("客户端 %s:%d 断开了连接n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; }else{ if(0 == strcmp(buff, "quit")){ printf("客户端 %s:%d 退出了n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); break; } printf("%s-%d:[%s]n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buff); //组装应答 strcat(buff, "--sever"); if(-1 == send(acceptfd, buff, 128, 0)){ ERRLOG("send error"); } } } //7.关闭套接字 close(acceptfd); } close(sockfd); return 0; }
//客户端 #include#include #include #include #include #include #include #include #include #define ERRLOG(errmsg) do{ perror(errmsg); printf("%s-%s(%d)n", __FILE__, __func__, __LINE__); exit(-1); }while(0) int main(int argc, char *argv[]){ if(3!=argc){ printf("Usage : %s n", argv[0]); exit(-1); } //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sockfd){ ERRLOG("socket error"); } struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr));//清空 //2.填充服务器网络信息结构体 server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); server_addr.sin_addr.s_addr = inet_addr(argv[1]); socklen_t addrlen = sizeof(server_addr); //3.与服务器建立连接 if(-1 == connect(sockfd, (struct sockaddr *)&server_addr, addrlen)){ ERRLOG("connect error"); } //4.与服务器通信 char buff[128] = {0}; int bytes = 0; while(1){ fgets(buff, 128, stdin); buff[strlen(buff)-1] = ' ';//清除 n if(-1 == send(sockfd, buff, 128, 0)){ ERRLOG("send error"); } if(0 == strcmp(buff, "quit")){ break; } if(-1 == (bytes = recv(sockfd, buff, 128, 0))){ ERRLOG("recv error"); }else if(0 == bytes){ printf("由于你长时间没有说话,已经被踢掉了n"); break; } printf("收到回复:[%s]n", buff); } //5.关闭套接字 close(sockfd); return 0; }



