一、IO模型

1.分类

在UNIX/Linux下主要有4种I/O 模型:

阻塞I/O:最常用、最简单、效率最低

非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询

I/O 多路复用:允许同时对多个I/O进行控制

信号驱动I/O:一种异步通信模型------底层驱动专栏中详细讲

2.阻塞IO

以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,进程会被挂起,一直阻塞,直到缓冲区中有内容了,内核会唤醒该进程,读完内容后继续向下执行。

写操作也是会阻塞的,当缓冲区满了,就阻塞了,当缓冲区中有足够的空间接收这次写了就能解除阻塞。一般情况下,对于阻塞的问题,考虑的都是读的阻塞。

示例:以写阻塞为例

//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>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 = %d\n", count);}close(fd);return 0;
}//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(){int fd = open("my_fifo", O_RDONLY);char buff[11] = {0};read(fd, buff, 11);while(1);//防止管道破裂close(fd);return 0;
}

3.非阻塞IO

以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,相当于告诉内核,不要将这个进程挂起,而是立即给我返回一个错误。

一般的带有阻塞属性的函数,默认方式都是阻塞IO。对于recv recvfrom 等函数,是可以通过参数来设置成非阻塞的。如:recv 的 MSG_DONTWAIT,recvfrom 的 MSG_DONTWAIT,waitpid 的 WNOHANG等。但是对于 read 等函数,默认方式就是阻塞,如果想使用read实现非阻塞,需要用到 fcntl() 来修改文件描述符的状态。

fcntl函数说明:

int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置或获取文件描述符的状态

#include <unistd.h>
#include <fcntl.h>
参数:
        @fd: 文件描述符
        @cmd: 要控制的指令
                F_GETFL 获取文件描述符的状态
                F_SETFL 设置文件描述符的状态 O_NONBLOCK 非阻塞
        @arg: 可变参
                具体需不需要取决于第二个参数是什么,
                如果第二个参数是 F_GETFL 就不需要
                如果第二个参数是 F_SETFL 就需要
返回值: F_GETFL 返回的就是文件描述符的状态
               F_SETFL 成功返回0 失败返回-1

示例:使用管道时,注意,写端未打开,读端的open会阻塞,需要设置成非阻塞才能读到。

//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>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 = %s\n", buff1);memset(buff1, 0, 128);read(fd2, buff2, 128);printf("buff2 = %s\n", buff2);memset(buff2, 0, 128);read(fd3, buff3, 128);printf("buff3 = %s\n", buff3);memset(buff3, 0, 128);//sleep(1);//为了演示现象用的 防止刷屏}close(fd1);close(fd2);close(fd3);return 0;
}
//写端(三个写端一样的)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>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] = '\0';if(-1 == write(fd, buff, 128)){perror("write error");exit(-1);}memset(buff, 0, 128);}close(fd);return 0;
}

4.IO多路复用

使用阻塞的方式处理多个阻塞函数,相互之间会有影响,有时不可取。如果使用非阻塞,有需要写一个循环轮询每个函数,十分占用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 <sys/select.h>
参数:
        @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 <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>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 <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>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] = '\0';if(-1 == write(fd, buff, 128)){perror("write error");exit(-1);}memset(buff, 0, 128);}close(fd);return 0;
}

二、服务器模型

1.概念

服务器模型主要有两种:

循环服务器:同一时间只能处理一个客户端的请求。

并发服务器:可以同时处理多个客户端的请求。

TCP服务器本身就是一个循环服务器,原因是他有两个阻塞函数,accept 和 recv 他们之间会相互影响。UDP服务器本身就是一个并发服务器,因为他只有一个阻塞函数,recvfrom

2.循环服务器

在上一篇博客(C语言编程实现TCP/UDP/TFTP网络通信)中详细讲解了,这里就不说了。

示例:(跟下面做对照)

//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>#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 <ip> <port>\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.1server_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;
}
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>#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 <ip> <port>\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] = '\0';//清除 \nif(-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;
}

3.并发服务器

实现TCP并发服务器方式:大多数场景下,我们既要保证可靠,又要保证并发,所以就要研究TCP如何实现并发服务器。

方式1:使用多线程实现TCP并发服务器

方式2:使用多进程实现TCP并发服务器

方式3:使用IO多路复用实现TCP并发服务器(最常用)

4.使用多线程实现TCP并发服务器

主线程专门用来接收客户端的连接请求(也就是专门用来处理 accept)

每当有新的客户端连接成功时,就创建一个子线程,在线程处理函数中专门用来和这个客户端通信。

注:多线程的相关知识在IO接口专栏中的  “c语言中的多线程的实现”  博客详细介绍了。

示例:

//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>#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 <ip> <port>\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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>#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 <ip> <port>\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); //关闭父进程的 acceptfdclose(acceptfd);}}close(sockfd);return 0;
}

客户端代码同循环服务器

6.多路IO复用实现TCP并发服务器

将sockfd,和每个客户端的acceptfd 都放到一个表里,传参给select函数

内核帮我们检测哪些文件描述符准备就绪了,select会将准备就绪的文件描述符告诉我们,再根据描述符的不同,分别处理需求即可。

示例:

//服务器端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/select.h>#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 <ip> <port>\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;//更新最大文件描述符//设置超时时间  5sstruct 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_tempreadfds_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 timeout\n");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_fdmax_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 <sys/select.h>
参数:
        @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 <sys/types.h>
#include <sys/socket.h>
参数:
        @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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>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]K\n", count/1024);count = 0;if(-1 == getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF,&count, &len)){perror("getsockopt error");exit(-1);}printf("接收缓冲区大小 [%d]K\n", 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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>#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 tmeout\n");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 <ip> <port>\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;//设置超时时间 5sstruct 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 timeout\n");//自定义的处理方式  将此处的 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;
}
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>#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 <ip> <port>\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] = '\0';//清除 \nif(-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;
}

5.使用 alarm 闹钟实现超时检测

信号的自重启属性:使用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 <signal.h>
参数:
        @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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>#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_signal\n");
}int main(int argc, char *argv[]){if(3!=argc){printf("Usage : %s <ip> <port>\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.1server_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 timeout\n");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 timeout\n");break;}printf("errno = %d\n", 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 <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>#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 <ip> <port>\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] = '\0';//清除 \nif(-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;
}

C语言中IO模型实现并发服务器相关推荐

  1. Linux网络编程服务器模型选择之IO复用循环并发服务器

    在前面我们介绍了循环服务器,并发服务器模型.简单的循环服务器每次只能处理一个请求,即处理的请求是串行的,效率过低:并发服务器可以通过创建多个进程或者是线程来并发的处理多个请求.但是当客户端增加时,就需 ...

  2. 单片机c语言中io取反,单片机小白学步(21) IO口:基本的LED和按键操作

    原标题:单片机小白学步(21) IO口:基本的LED和按键操作 本篇我们先介绍最基础的几个IO口操作.学完本篇,大家要能自己独立完成下面几个实验. 1.点亮 2.一个按键控制LED,按下灯亮,释放灯灭 ...

  3. Unix C语言编写基于IO多路复用的小型并发服务器

    背景介绍 如果服务器要同时处理网络上的套接字连接请求和本地的标准输入命令请求,那么如果我们使用accept来接受连接请求,则无法处理标准输入请求;类似地,如果在read中等待一个输入请求,则无法处理网 ...

  4. Redis的IO模型以及客户端与服务端设计

    文章目录 IO模型--事件驱动 文件事件(通常是与客户端的交互) 文件事件的处理器 时间事件(服务器的自身触发的一些维护操作) 分类 底层实现 时间事件应用实例:serverCron函数 事件的调度与 ...

  5. 基于Linux平台的TCP通信并发服务器---在线英语词典项目

    文章目录 前言 一.什么是并发服务器 二.服务器的实现 三.客户端的实现 四.代码测试结果 五.代码测试注意 总结 前言 本文是我在IO进线程.网络编程学习阶段的练习项目.项目基于linux平台,利用 ...

  6. Go语言中的goroutine

    目录 Go并发优势 goroutine goroutine定义 线程和协程的区别 创建goroutine runtime包 Gosched() Goexit() GOMAXPROCS() Go并发优势 ...

  7. C++项目实战-高并发服务器详析

    本节中的socket一套函数用的是之前封装过了的,进行异常处理(可自取) 链接: C++项目实战-socket编程_干饭小白的博客-CSDN博客 目录 BIO模型 NIO模型 多进程并发服务器 多线程 ...

  8. 并发模型与IO模型梳理

    并发模型 常见的并发模型一般包括3类,基于线程与锁的内存共享模型,actor模型和CSP模型,其中尤以线程与锁的共享内存模型最为常见.由于go语言的兴起,CSP模型也越来越受关注.基于锁的共享内存模型 ...

  9. python编辑程序模型_python并发编程之IO模型

    了解新知识之前需要知道的一些知识 同步(synchronous):一个进程在执行某个任务时,另外一个进程必须等待其执行完毕,才能继续执行 #所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调 ...

  10. Java并发编程学习-日记1、常见的IO模型、NIO、OIO

    常见的IO模型: 1.同步阻塞IO(Blocking IO): 在Java中,默认创建的socket都是阻塞.同步IO,是一种用户空间与内核空间的IO发起方式.同步IO是指用户空间的线程是主动发起IO ...

最新文章

  1. 018_rate评分
  2. worpress安装mysql连不上_wordpress安装时连接不上MySQL8.0(已解决)Windows
  3. keras从入门到放弃(二十)LSTM处理 电影评价预测
  4. webconfig中注册HttpHandler报错:检测到在集成的托管管道模式下不适用的 ASP.NET 设置。...
  5. 22 款设计和可视化神经网络的工具
  6. Celery多个定时任务使用RabbitMQ,Queue冲突解决
  7. 2015-2016-1学期 《信息安全系统设计基础》课程总结
  8. Ubuntu 16.04 安装 MPI(Message Passing Interface)
  9. 十八年开发经验分享(二)问题解决篇(上)
  10. 计算机无法读取移动光驱,外置光驱无法读取光盘怎么解决
  11. 世界所有国家的信息(2)
  12. 常见电商模式B2B、B2C、C2B、C2C、O2O
  13. BVH with SAH (Bounding Volume Hierarchy with Surface Area Heuristic)
  14. git报错:fatal: 无法为 ‘https‘ 找到远程助手
  15. 屏蔽拼多多广告信息的方法
  16. 处理win10系统自动休眠bug
  17. 联想开机启动项按哪个_联想启动u盘按什么键_联想笔记本按哪个键进入u盘启动-win7之家...
  18. 基于链表和禁忌搜索启发式算法实现非一刀切二维矩形排样算法
  19. 这几种神级性能优化手段,你用过几个?
  20. jsp70860基于web的大学教室管理系统

热门文章

  1. selenium调用javascript代码的方法
  2. 伺服驱动器PID调节
  3. 什么是美国能源之星计划?
  4. 超声波加湿器原理与检修
  5. 成立1年的水母智能,AI智能设计生意做得如何?
  6. Ubuntu下shift键失灵解决办法
  7. 手动修改dns服务器设置,【当贝市场】怎样手动设置DNS服务器?教程如下
  8. MLX90614各类型芯片总结
  9. 优秀java程序员必须具备的技术技能
  10. 2012考研数学二第(18)题——多元函数积分学:二重积分求面积+画曲线:心形线