《Linux C/C++服务器开发实践》之第7章 服务器模型设计
《Linux C/C++服务器开发实践》之第7章 服务器模型设计
- 7.1 I/O模型
- 7.1.1 基本概念
- 7.1.2 同步和异步
- 7.1.3 阻塞和非阻塞
- 7.1.4 同步与异步和阻塞与非阻塞的关系
- 7.1.5 采用socket I/O模型的原因
- 7.1.6(同步)阻塞I/O模型
- 7.1.7(同步)非阻塞I/O模型
- 7.1.8(同步)I/O多路复用模型
- 7.1.9(同步)信号驱动式I/O模型
- 7.1.10 异步I/O模型
- 7.1.11 五种I/O模型比较
- 07.udpclient.c
- 07.tcpclient.c
- 7.2 (分时)循环服务器
- 7.2.1 UDP循环服务器
- 07.01.udpserver.c
- 7.2.2 TCP循环服务器
- 07.02.tcpserver.c
- 7.3 多进程并发服务器
- 07.03.tcpforkserver.c
- 7.4 多线程并发服务器
- 07.04.tcpthreadserver.c
- 7.5 I/O多路复用服务器
- 7.5.1 使用场景
- 7.5.2 基于select的服务器
- 07.05.tcpselectserver.c
- 7.5.3 基于poll的服务器
- 07.06.tcppollserver.c
- 7.5.4 基于epoll的服务器
- 07.07.tcpepollserver.c
按使用协议分为TCP服务器和UDP服务器,按处理方式分为循环服务器和并发服务器。
网络服务器的设计模型:
(分时)循环服务器
多进程并发服务器
多线程并发服务器
I/O(Input/Output,输入/输出)复用并发服务器
7.1 I/O模型
7.1.1 基本概念
I/O即数据的读取(接收)和写入(发送)操作,分为内存I/O、网络I/O、磁盘I/O。
进程中的完整I/O分为两个阶段:用户进程空间<–>内核空间、内核空间<–>设备空间(磁盘、网卡等)。
进程无法直接操作I/O设备,通过系统调用请求内核协助完成I/O操作。内核为每个I/O设备维护一个缓冲区。对于输入操作,进程I/O系统调用后,内核先看缓冲区是否有相应数据,无则到设备(比如网卡设备)读取(设备I/O慢,需等待),有则直接复制到用户进程空间。
网络输出操作两阶段:
(1)等待网络数据到达网卡,把数据从网卡读取到内核缓冲区,准备好数据。
(2)从内核缓冲区复制数据到用户进程空间。
网络I/O的本质是socket的读取,对流的操作。一次I/O访问,数据先拷贝到操作系统的内核缓冲区,然后从内核缓冲区拷贝到应用程序的地址空间。
网络I/O模型分为异步I/O(asynchronous I/O)和同步I/O(synchronous I/O),同步I/O包括阻塞I/O(blocking I/O)、非阻塞I/O(non-blocking I/O)、多路复用I/O(multiplexing I/O)和信号驱动式I/O(signal-driven I/O)。
7.1.2 同步和异步
是否等请求出最终结果。异步调用完成后,通过状态、通知、信号和回调来通知调用者。
7.1.3 阻塞和非阻塞
与等待消息通知时的状态(调用线程)有关。非阻塞方式可提高CPU的利用率,但同时增加系统的线程切换。
7.1.4 同步与异步和阻塞与非阻塞的关系
异步肯定是非阻塞的。
同步非阻塞效率低,但高于同步阻塞。
线程五种状态:新建、就绪、运行、阻塞、死亡。
阻塞状态线程放弃CPU的使用,暂停运行,等导致阻塞的原因消除后恢复运行,或者被其他线程中断,推出阻塞状态,抛出InterruptedException。
线程进入阻塞原因:
(1)sleep休眠。
(2)调用I/O阻塞的操作。
(3)试图获取其他线程持有的锁。
(4)等待某个触发条件。
(5)执行wait()方法,等待其他线程执行notify()或者notifyAll()方法。
引起线程阻塞的函数叫阻塞函数。
阻塞函数一定是同步函数,同步函数不一定是阻塞函数。
同步函数做完事情后才返回;阻塞函数也是做完事情后才返回,且会引起线程阻塞。
可能阻塞套接字的socket api分类:
(1)输入操作
recv、recvfrom函数。套接字缓冲区无数据可读,数据到来前阻塞。
(2)输出操作
send、sendto函数。套接字缓冲区无可用空间,线程休眠到有空间。
(3)接受连接
accept函数。无连接请求,会阻塞。
(4)外出连接
connect函数。收到服务器应答前,不会返回。
非阻塞socket在发送缓冲区无足够空间时,会部分拷贝,返回拷贝字节数,将errno置为EWOULDBLOCK。
非阻塞socket在接收缓冲区无数据时,返回-1,将errno置为EWOULDBLOCK。
7.1.5 采用socket I/O模型的原因
同步通信操作会阻塞同一线程的其他操作。
同步通信(阻塞通信)+多线程,可改善同步阻塞线程的情况。可运行线程间上下文切换,浪费CPU时间,效率低。
异步方式更好,但不总能保证收发成功。
7.1.6(同步)阻塞I/O模型
一次读取I/O操作的两个阶段:
(1)等待数据准备好,到达内核缓冲区。
(2)从内核向进程复制数据。
该模型两个阶段都阻塞,但简单、实时性高、响应及时无延时。
7.1.7(同步)非阻塞I/O模型
非阻塞recvform调用后,内核马上返回给进程,若数据未准备好,返回error(EAGAIN或EWOULDBLOCK)。进程返回后,可先处理其他业务逻辑,稍后继续调用recvform。采用轮询方式检查内核数据,直到数据准备好。再拷贝数据到进程,进行数据处理。
第二阶段会阻塞。
该模型能够在等待任务完成的时间里做其他工作,但响应延迟增大,整体数据吞吐量降低,因为轮询。
7.1.8(同步)I/O多路复用模型
单进程同时处理多个网络连接的I/O。应用程序不监视,而内核监视文件描述符。
select,epoll。
系统开销小,不需要创建和维护额外的进程或线程,维护少,节省系统资源,主要应用场景:
(1)同时处理多个监听状态或连接状态的套接字。
(2)同时处理多种协议的套接字。
(3)监听多个端口或处理多种服务。
(4)同时处理用户输入和网络连接。
7.1.9(同步)信号驱动式I/O模型
注册信号处理函数,进程运行不阻塞。数据准备好时,进程收到SIGIO信号,信号处理函数中调用I/O操作。
7.1.10 异步I/O模型
系统调用后不阻塞进程。等数据准备好,内核直接复制数据到进程空间,然后内核通知进程,数据在用户空间,可以处理。
通过信号方式通知,三种情况:
(1)进程进行用户态逻辑,强行打断,调用注册的信号处理函数。
(2)进程在内核态处理,比如同步阻塞读写磁盘,会挂起通知,等内核态事情完成,回到用户态,再触发信号通知。
(3)进程挂起,比如睡眠,唤醒进程,等待CPU调度,触发信号通知。
7.1.11 五种I/O模型比较
前四种同步I/O操作,第二阶段一样:数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。
异步I/O模型在等待和接收数据阶段都是非阻塞的,可以处理其他逻辑,整个I/O操作由内核完成,完成后发送通知。在此期间,用户进程不需要检查I/O操作的状态,也不需要主动拷贝数据。
07.udpclient.c
#include <stdio.h>
#include <winsock.h>// #pragma comment(lib, "wsock32")#define PORT 8888int main()
{WSADATA wsadata;if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0){printf("WSAStartup failed\n");WSACleanup();return -1;}struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ifconfigsaddr.sin_port = htons(PORT);/**** get protocol number from protocol name ****/// struct hostent *phe; // host information// struct servent *pse; // server informationstruct protoent *ppe; // protocol informationif ((ppe = getprotobyname("UDP")) == 0){printf("get protocol information error\n");WSACleanup();return -1;}SOCKET s = socket(PF_INET, SOCK_DGRAM, ppe->p_proto);if (s == INVALID_SOCKET){printf(" creat socket error \n");WSACleanup();return -1;}char wbuf[50] = "hello, server!";// printf("please enter data:");// sscanf_s("%s", wbuf, sizeof(wbuf));int ret = sendto(s, wbuf, strlen(wbuf), 0, (struct sockaddr *)&saddr, sizeof(struct sockaddr));if (ret < 0)perror("sendto failed");char rbuf[100] = {0};struct sockaddr_in raddr; // endpoint IP addressint fromlen = sizeof(struct sockaddr);int len = recvfrom(s, rbuf, sizeof(rbuf), 0, (struct sockaddr *)&raddr, &fromlen);if (len < 0)perror("recvfrom failed");printf("server reply: %s\n", rbuf);closesocket(s);WSACleanup();return 0;
}// gcc 7.udpclient.c -o 7.udpclient.exe -lwsock32 && 7.udpclient.exe
07.tcpclient.c
#include <stdio.h>
#include <winsock2.h>// #pragma comment(lib, "wsock32")#define PORT 8888int main()
{WSADATA wsadata;if (WSAStartup(MAKEWORD(2, 0), &wsadata) != 0){printf("WSAStartup failed\n");WSACleanup();return -1;}struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ifconfigsaddr.sin_port = htons(PORT);SOCKET s = socket(PF_INET, SOCK_STREAM, 0);if (s == INVALID_SOCKET){printf("creat socket error\n");WSACleanup();return -1;}if (connect(s, (struct sockaddr *)&saddr, sizeof(saddr)) == SOCKET_ERROR){printf("connect socket error\n");WSACleanup();return -1;}char wbuf[50] = "hello, server";// printf("please enter data:");// sscanf_s("%s", wbuf, sizeof(wbuf));int len = send(s, wbuf, strlen(wbuf), 0);if (len < 0)perror("send failed");shutdown(s, SD_SEND);char rbuf[100] = {0};len = recv(s, rbuf, sizeof(rbuf), 0);if (len < 0)perror("recv failed");printf("server reply: %s\n", rbuf);closesocket(s);WSACleanup();return 0;
}// gcc 7.tcpclient.c -o 7.tcpclient.exe -lwsock32 && 7.tcpclient.exe
7.2 (分时)循环服务器
串行处理客户端的请求。
7.2.1 UDP循环服务器
socket(...);
bind(...);
while(1)
{recvfrom(...);process(...);sendto(...);
}
07.01.udpserver.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>int main()
{struct sockaddr_in saddr;memset(&saddr, 0, sizeof(struct sockaddr_in));saddr.sin_family = AF_INET;saddr.sin_addr.s_addr = htonl(INADDR_ANY);saddr.sin_port = htons(8888);int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){puts("socket failed");return -1;}char on = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));int val = sizeof(struct sockaddr);int ret = bind(sockfd, (struct sockaddr *)&saddr, val);if (ret < 0){puts("sbind failed");return -1;}struct sockaddr_in raddr;char rbuf[50];char sbuf[100];while (1){puts("waiting data");memset(rbuf, 0, 50);ret = recvfrom(sockfd, rbuf, 50, 0, (struct sockaddr *)&raddr, (socklen_t *)&val);if (ret < 0)perror("recvfrom failed");printf("recv data: %s\n", rbuf);memset(sbuf, 0, 100);sprintf(sbuf, "server has received your data(%s)\n", rbuf);ret = sendto(sockfd, sbuf, strlen(sbuf), 0, (struct sockaddr *)&raddr, sizeof(struct sockaddr));}close(sockfd);return 0;
}
7.2.2 TCP循环服务器
socket(...);
bind(...);
listen(...);
while(1)
{accept(...);process(...);close(...);
}
07.02.tcpserver.c
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>#define PORT 8888int main()
{struct sockaddr_in sin; // endpoint IP addressmemset(&sin, 0, sizeof(sin));sin.sin_family = AF_INET;sin.sin_addr.s_addr = INADDR_ANY;sin.sin_port = htons(PORT);int s = socket(PF_INET, SOCK_STREAM, 0);if (s == -1){printf("creat socket error\n");return -1;}if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) == -1){printf("socket bind error\n");return -1;}if (listen(s, 10) == -1){printf(" socket listen error\n");return -1;}int alen = sizeof(struct sockaddr);struct sockaddr_in fsin;int connum = 0;while (1){puts("waiting client...");int clisock = accept(s, (struct sockaddr *)&fsin, (socklen_t *)&alen);if (clisock == -1){printf("accept failed\n");return -1;}connum++;printf("%d client comes\n", connum);char rbuf[64] = {0};int len = recv(clisock, rbuf, sizeof(rbuf), 0);if (len < 0)perror("recv failed");char buf[128] = {0};sprintf(buf, "Server has received your data(%s).", rbuf);send(clisock, buf, strlen(buf), 0);close(clisock);}close(s);return 0;
}
7.3 多进程并发服务器
客户端有请求时,服务器创建子进程处理,父进程继续等待其他客户端的请求。
#include <sys/types.h>
#include <unistd.h>
pid_t fork();
//成功返回0(子进程)和大于0(父进程中返回子进程ID),错误-1(进程上限或内存不足)
子进程复制父进程资源:进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,Linux内核采取写时拷贝技术(Copy on Write)提高效率。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main()
{pid_t pid = fork();if(pid == -1)//创建子进程失败{perror("cannot fork");return -1;}else if(pid == 0)//子进程{printf("This is child process\n");//getpid()获取自己的进程号printf("Pid is %d, My PID is %d\n", pid, getpid());}else//父进程,pid为子进程ID{printf("This is parent process\n");printf("Pid is %d, My PID is %d\n", pid, getpid());}return 0;
}
int sockfd = socket(...);
bind(...);
listen(...);
while(1)
{int connfd = accept(...);if(fork() == 0)//子进程{close(sockfd);//关闭监听套接字process(...);//具体事件处理close(connfd);//关闭已连接套接字exit(0);//结束子进程}close(connfd);//关闭已连接套接字
}
close(sockfd);
07.03.tcpforkserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// #include <ws2tcpip.h>int main()
{unsigned short port = 8888;struct sockaddr_in my_addr;bzero(&my_addr, sizeof(my_addr));my_addr.sin_family = AF_INET;my_addr.sin_addr.s_addr = htonl(INADDR_ANY);// inet_pton(AF_INET, "127.0.0.1", &my_addr.sin_addr);my_addr.sin_port = htons(port);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket");exit(-1);}char on = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));int err_log = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));if (err_log != 0){perror("binding");close(sockfd);exit(-1);}err_log = listen(sockfd, 10);if (err_log != 0){perror("listen");close(sockfd);exit(-1);}socklen_t cliaddr_len = sizeof(struct sockaddr_in);while (1){puts("Father process is waitting client...");struct sockaddr_in client_addr;int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);if (connfd < 0){perror("accept");close(sockfd);exit(-1);}pid_t pid = fork();if (pid < 0){perror("fork");_exit(-1);}else if (0 == pid){close(sockfd);/*INT WSAAPI inet_pton(INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6PCSTR pszAddrString, //指向以NULL为结尾的字符串指针,该字符串包含要转换为数字的二进制形式的IP地址文本形式。PVOID pAddrBuf//指向存储二进制表达式的缓冲区);*//*PCWSTR WSAAPI InetNtopW(INT Family, //地址家族 IPV4使用AF_INET IPV6使用AF_INET6const VOID *pAddr, //指向网络字节中要转换为字符串的IP地址的指针PWSTR pStringBuf,//指向缓冲区的指针,该缓冲区用于存储IP地址的以NULL终止的字符串表示形式。size_t StringBufSize//输入时,由pStringBuf参数指向的缓冲区的长度(以字符为单位));*/char cli_ip[INET_ADDRSTRLEN] = {0};memset(cli_ip, 0, sizeof(cli_ip));inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);printf("----------------------------------------------\n");printf("client ip=%s, port=%d\n", cli_ip, ntohs(client_addr.sin_port));char recv_buf[1024];int recv_len = 0;while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf) - 1, 0)) > 0){recv_buf[recv_len] = 0;printf("recv_buf: %s\n", recv_buf);send(connfd, recv_buf, recv_len, 0);}printf("client_port %d closed!\n", ntohs(client_addr.sin_port));close(connfd);exit(0);}elseclose(connfd);}close(sockfd);return 0;
}
7.4 多线程并发服务器
进程消耗较大的系统资源。一个进程内的所有线程共享相同的全局内存、全局变量等,注意同步问题。
针对客户端的每个请求,主线程都会创建一个工作者线程,负责和客户端通信。
void *client_fun(void *arg)
{int connfd = *(int *)arg;fun();close(connfd);
}int sockfd = socket(...);
bind(...);
listen(...);
while(1)
{int connfd = accept(...);pthread_t tid;pthread_create(&tid, NULL, (void *)client_fun, (void *)connfd);pthread_deatch(tid);
}
close(sockfd);//关闭监听套接字
07.04.tcpthreadserver.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>void *client_process(void *arg)
{int recv_len;char recv_buf[1024];int connfd = *(int *)arg;while ((recv_len = recv(connfd, recv_buf, sizeof(recv_buf) - 1, 0)) > 0){recv_buf[recv_len] = 0;printf("recv_buf: %s\n", recv_buf);send(connfd, recv_buf, recv_len, 0);}printf("client closed!\n");close(connfd);return NULL;
}int main()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket error");exit(-1);}unsigned short port = 8888;struct sockaddr_in my_addr;bzero(&my_addr, sizeof(my_addr));my_addr.sin_family = AF_INET;my_addr.sin_addr.s_addr = htonl(INADDR_ANY);my_addr.sin_port = htons(port);printf("Binding server to port %d\n", port);char on = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));int err_log = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr));if (err_log != 0){perror("bind");close(sockfd);exit(-1);}err_log = listen(sockfd, 10);if (err_log != 0){perror("listen");close(sockfd);exit(-1);}int connfd;while (1){printf("Waiting client...\n");struct sockaddr_in client_addr;socklen_t cliaddr_len = sizeof(client_addr);connfd = accept(sockfd, (struct sockaddr *)&client_addr, &cliaddr_len);if (connfd < 0){perror("accept this time");continue;}char cli_ip[INET_ADDRSTRLEN] = "";inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);printf("----------------------------------------------\n");printf("client ip=%s, port=%d\n", cli_ip, ntohs(client_addr.sin_port));if (connfd > 0){pthread_t thread_id;pthread_create(&thread_id, NULL, client_process, (void *)&connfd);pthread_detach(thread_id);}}close(sockfd);return 0;
}
7.5 I/O多路复用服务器
select、pselect、poll、epoll等系统调用支持I/O多路复用,通过进程监视多个描述符,描述符就绪(可读写),通知程序进行相应处理。本质上是同步I/O,读写过程是阻塞的,需要读写事件就绪后自己负责读写。异步I/O无需自己负责读写,它会负责把数据从内核拷贝到用户空间。
I/O多路复用的最大优势是系统开销小,无进程/线程的创建和维护。
epoll是Linux特有,select是POSIX规定,一般操作系统均可实现。
7.5.1 使用场景
- 客户端处理多个描述符(交互式输入和网络套接字),必须使用
- 客户端处理多个套接字,很少出现
- TCP服务器处理监听套接字和已连接套接字
- 服务器处理TCP和UDP
- 服务器处理多个服务或多个协议
7.5.2 基于select的服务器
进程调用select(阻塞),内核监视多个socket,任一socket准备好(可读、可写、异常),返回。此时进程执行read、write、exit等。
-
select缺点:
I/O线程不断轮询套接字集合状态,浪费CPU资源。
不适合管理大量客户端连接。
性能低下,需大量查找和拷贝。 -
传递给select函数的参数告诉内核的信息:
需要监视的文件描述符
每个文件描述符的监视状态(读、写、异常)
等待时间(无限长、固定、0) -
select返回后,可获取的内核信息:
准备好的文件描述符个数
文件描述符的具体状态(读、写、异常)
可以调用合适的I/O(read或write),不会被阻塞
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//maxfd,最大文件描述符的值+1
//readfds,套接字读变化
//writefds,套接字写变化
//exceptfds,套接字异常变化
//timeout,等待时间,NULL阻塞,0非阻塞,大于0超时时间
select函数返回时,fd_set结构中填入相应套接字。
readfds数组包含套接字:
- 有数据可读,recv立即读取
- 连接已关闭、重设或终止
- 有请求建立连接的套接字,accept会成功
writefds数组包含套接字:
- 有数据可发出,send立即发送
- connect,已连接成功
exceptfds数组包含套接字:
- connect,已连接失败
- 带外数据可读
struct timeval
{long tv_sect;long tv_usect;
};
非0则等到超时,若成功timeval会被修改为剩余时间。
typedef struct fd_set
{u_int fd_count;socket fd_array[FD_SETSIZE];
} fd_set;
//set集合初始化为空集合
void FD_ZERO(fd_set *set);//套接字fd加入set集合中
void FD_SET(int fd, fd_set *set);//set集合中删除套接字fd
void FD_CLR(int fd, fd_set *set);//检查fd是否为set集合成员
void FD_ISSET(int fd, fd_set *set);
套接字可读写判断步骤:
- 初始化套接字集合,FD_ZERO(&readfds)
- 指定套接字放入集合,FD_SET(s, &readfds)
- 调用select函数,返回所有fd_set集合中变化套接字总个数,并会更新集合中变化套接字状态
- 遍历集合,判断s是否在某个集合内。FD_ISSET(s, &readfds)
- 调用相应socket api函数操作套接字
07.05.tcpselectserver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define MYPORT 8888
#define MAXCLINE 5
#define BUF_SIZE 200int conn_amount = 0;
int fd[MAXCLINE] = {0};void showclient()
{printf("client amount: %d\n", conn_amount);for (int i = 0; i < MAXCLINE; i++)printf("[%d]: %d ", i, fd[i]);printf("\n\n");
}int main()
{int sock_fd;if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){perror("setsockopt");exit(1);}int yes = 1;if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1){perror("setsockopt error \n");exit(1);}struct sockaddr_in server_addr;memset(&server_addr, '\0', sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(MYPORT);if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1){perror("bind error!\n");close(sock_fd);exit(1);}if (listen(sock_fd, MAXCLINE) == -1){perror("listen error!\n");close(sock_fd);exit(1);}printf("listen port %d\n", MYPORT);int maxsock = sock_fd;struct timeval tv = {30, 0};while (1){fd_set fdsr;FD_ZERO(&fdsr);FD_SET(sock_fd, &fdsr); // 监听套接字for (int i = 0; i < MAXCLINE; i++)if (fd[i] != 0)FD_SET(fd[i], &fdsr); // 连接套接字int ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);if (ret < 0){perror("select error!\n");break;}else if (ret == 0){printf("timeout\n");continue;}for (int i = 0; i < conn_amount; i++){if (FD_ISSET(fd[i], &fdsr)){char buf[BUF_SIZE];ret = recv(fd[i], buf, sizeof(buf), 0);if (ret <= 0){printf("client[%d] close\n", i);close(fd[i]);FD_CLR(fd[i], &fdsr);fd[i] = 0;conn_amount--;}else{if (ret < BUF_SIZE){memset(&buf[ret], '\0', 1);ret += 1;}printf("client[%d] send: %s\n", i, buf);send(fd[i], buf, ret, 0);}}}if (FD_ISSET(sock_fd, &fdsr)){struct sockaddr_in client_addr;socklen_t sin_size = sizeof(struct sockaddr_in);int new_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);if (new_fd <= 0){perror("accept error\n");continue;}if (conn_amount < MAXCLINE){for (int i = 0; i < MAXCLINE; i++){if (fd[i] == 0){fd[i] = new_fd;break;}}conn_amount++;printf("new connection client[%d] %s:%d\n", conn_amount, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));if (new_fd > maxsock)maxsock = new_fd;}else{printf("max connections arrive, exit\n");send(new_fd, "bye", 4, 0);close(new_fd);continue;}}showclient();}for (int i = 0; i < MAXCLINE; i++)if (fd[i] != 0)close(fd[i]);close(sock_fd);return 0;
}
7.5.3 基于poll的服务器
poll和select本质一样,管理多个描述符进行轮询,根据描述符状态进行处理,但poll无文件描述符数量的限制(过多性能下降)。相同缺点是大量文件描述符数组整体在用户态和内核的地址空间之间进行复制,无论描述符是否就绪。
poll函数在指定时间内轮询一定数量的文件描述符,测试是否有就绪者,监测多个事件,若无事件发生,进程睡眠,放弃CPU控制权。若监测的任一事件发生,唤醒进程,判断事件,执行相应操作。退出后,struct pollfd变量清零,需重新设置。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//timeout:-1永远等待,0立即返回,大于0,等待毫米数。
//失败返回-1,errno值如下:
//EBADF,结构体中存在无效文件描述符
//EFAULT,fds指针指向的地址超出进程的地址空间
//EINTR,请求的事件之前产生一个信号,调用可以重新发起。
//EINVAL,nfds参数超出PLIMIT_NOFILE值
//ENOMEM,可用内存不足,无法完成请求struct pollfd{int fd;//文件描述符short events;//等待的事件,用户设置,告诉内核我们关注什么short revents;//实际发生的事件,内核调用返回时设置,说明该描述符发生了什么事件
};//POLLIN
//POLLOUT
//POLLERR
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
#include <string.h>
#include <stdio.h>
#include <fcntl.h>int main()
{char *p1 = "This is a c test code";volatile int len = 0;int fp = open("/home/test.txt", O_RDWR|O_CREAT);while(1){int n;if((n=write(fp, pl+len, strlen(pl)-len)) == 0){printf("n = %d\n", n);break;}len += n;}return 0;
}
07.06.tcppollserver.c
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>#include <poll.h>
#include <fcntl.h>
#include <unistd.h>void errExit()
{exit(-1);
}const char resp[] = "HTTP/1.1 200\r\n\
Content-Type: application/json\r\n\
Content-Length: 13\r\n\
Date: Thu, 2 Aug 2021 04:02:00 GMT\r\n\
Keep-Alive: timeout=60\r\n\
Connection: keep-alive\r\n\
\r\n\
[HELLO WORLD]";int main()
{int sd = socket(AF_INET, SOCK_STREAM, 0);if (sd == -1)errExit();fprintf(stderr, "created socket\n");int opt = 1;if (setsockopt(sd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int)) == -1)errExit();fprintf(stderr, "socket opt set\n");const int port = 8888;struct sockaddr_in addr = {0};addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(port);socklen_t addrLen = sizeof(addr);if (bind(sd, (struct sockaddr *)&addr, sizeof(addr)) == -1)errExit();fprintf(stderr, "socket binded\n");if (listen(sd, 1024) == -1)errExit();fprintf(stderr, "socket listen start\n");// number of poll fdsint currentFdNum = 1;struct pollfd *fds = (struct pollfd *)calloc(100, sizeof(struct pollfd));fds[0].fd = sd;fds[0].events = POLLIN;nfds_t nfds = 1;fprintf(stderr, "polling\n");while (1){int timeout = -1;int ret = poll(fds, nfds, timeout);fprintf(stderr, "poll returned with ret value: %d\n", ret);if (ret == -1)errExit();else if (ret == 0)fprintf(stderr, "return no data\n");else{fprintf(stderr, "checking fds\n");if (fds[0].revents & POLLIN){struct sockaddr_in childAddr;socklen_t childAddrLen;int childSd = accept(sd, (struct sockaddr *)&childAddr, &(childAddrLen));if (childSd == -1)errExit();fprintf(stderr, "child got\n");// set non_blockint flags = fcntl(childSd, F_GETFL);if (fcntl(childSd, F_SETFL, flags | O_NONBLOCK) == -1)errExit();fprintf(stderr, "child set nonblock\n");// add child to listfds[currentFdNum].fd = childSd;fds[currentFdNum].events = (POLLIN | POLLRDHUP);nfds++;currentFdNum++;fprintf(stderr, "child: %d pushed to poll list\n", currentFdNum - 1);}// child read & writefor (int i = 1; i < currentFdNum; i++){if (fds[i].revents & (POLLHUP | POLLRDHUP | POLLNVAL)){fprintf(stderr, "child: %d shutdown\n", i);close(fds[i].fd);fds[i].events = 0;fds[i].fd = -1;continue;}// readif (fds[i].revents & POLLIN){char buffer[1024] = {0};while (1){ret = read(fds[i].fd, buffer, 1024);fprintf(stderr, "read on: %d returned with value: %d\n", i, ret);if (ret == 0){fprintf(stderr, "read returned 0(EOF) on: %d, breaking\n", i);break;}else if (ret == -1){const int tmpErrno = errno;if (tmpErrno == EWOULDBLOCK || tmpErrno == EAGAIN){fprintf(stderr, "read would block, stop reading\n");fds[i].events |= POLLOUT;break;}else{errExit();}}}}// writeif (fds[i].revents & POLLOUT){ret = write(fds[i].fd, resp, sizeof(resp));fprintf(stderr, "write on: %d returned with value: %d\n", i, ret);if (ret == -1)errExit();fds[i].events &= !(POLLOUT);}}}}return 0;
}
7.5.4 基于epoll的服务器
epoll只需要监听已经准备好的队列集合中的文件描述符。
select主要缺点:
(1)单个进程监视的文件描述符有上限,通常1024.
(2)内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,巨大开销。
(3)返回整个句柄数组,遍历才能发现发生事件的句柄。
(4)水平触发,已就绪的文件描述符未完成I/O操作,每次调用select都会通知。
poll用链表保存文件描述符,无数量限制。
epoll三大关键要素:mmap、红黑树、链表。mmap将用户空间和内核空间的地址映射到相同物理内存地址,减少用户态和内核态的数据交换。内核可以直接看到epoll监听的句柄,效率高。红黑树存储epoll监听套接字,epoll_ctr在红黑树上插入或删除套接字。添加事件时,会建立与相应设备(网卡)驱动程序的回调关系ep_poll_callback,回调函数ep_poll_callback会将发生的事件放入双向链表rdllist中。epoll_wait时,只检测rdlist中是否存在注册的事件,效率非常高,这里需要将发生了的事件复制到用户态内存中。
红黑树+双链表+回调机制,造就epoll的高效。
select、poll采用轮询遍历,检测就绪事件,LT工作方式。
epoll采用回调检测就绪事件,支持ET高效模式。
epoll的两种工作方式:
- 水平触发(LT),缺省,描述符就绪,内核通知,未处理,下次还通知。
- 边缘触发(ET),只支持非阻塞描述符。需保证缓存区的数据全部读取或写出,下次不会通知。
07.07.tcpepollserver.c
在这里插入代码片
相关文章:
《Linux C/C++服务器开发实践》之第7章 服务器模型设计
《Linux C/C服务器开发实践》之第7章 服务器模型设计 7.1 I/O模型7.1.1 基本概念7.1.2 同步和异步7.1.3 阻塞和非阻塞7.1.4 同步与异步和阻塞与非阻塞的关系7.1.5 采用socket I/O模型的原因7.1.6(同步)阻塞I/O模型7.1.7(同步)非阻…...

SSH穿透ECS访问内网RDS数据库
处于安全考虑,RDS一般只会允许指定的IP进行访问,而我们开发环境的IP往往是动态的,每次IP变动都需要去修改RDS的白名单,为我们的工作带来很大的不便。 那么如何去解决这个问题? 假如我们有一台ESC服务器,E…...

python 有哪些函数
Python内置的函数及其用法。为了方便记忆,已经有很多开发者将这些内置函数进行了如下分类: 数学运算(7个) 类型转换(24个) 序列操作(8个) 对象操作(7个) 反射操作(8个) 变量操作(2个) 交互操作(2个) 文件操作(1个) 编译执行(4个) 装饰器(3个) …...
ubuntu web端远程桌面控制
本方案采用x11vncnovnc来实现x11vnc的安装和配置可以参考UOS搭建VNC及连接教程_uos安装vnc-CSDN博客;并把/lib/systemd/system/x11vnc.service内容修改为如下: [Unit]DescriptionStart x11vnc at startup.Aftermulti-user.target[Service]TypesimpleExecStart/usr/bin/x11vnc …...

PCL 点到三角形的距离(3D)
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 给定三角形ABC和点P,设Q为描述ABC上离P最近的点。求Q的一个方法:如果P在ABC内,那么P的正交投影点就是离P最近的点Q。如果P投影在ABC之外,最近的点则必须位于它的一条边上。在这种情况下,Q可以通过计算线段AB、…...

C# wpf 嵌入外部程序
WPF Hwnd窗口互操作系列 第一章 嵌入Hwnd窗口 第二章 嵌入WinForm控件 第三章 嵌入WPF控件 第四章 嵌入外部程序(本章) 第五章 底部嵌入HwndHost 文章目录 WPF Hwnd窗口互操作系列前言一、如何实现?1、定义属性2、进程嵌入(1&…...

【ELK】ELK企业级日志分析系统
搜集日志;日志处理器;索引平台;提供视图化界面;客户端登录 日志收集者:负责监控微服务的日志,并记录 日志存储者:接收日志,写入 日志harbor:负责去连接多个日志收集者&am…...
详细的讲一下java的接口回调
Java的接口回调是一种允许程序在特定事件发生时通知其他对象的机制。这是观察者设计模式的一种实现方式,常用于实现事件监听和异步处理。接口回调允许对象之间进行松耦合的交互:一个对象只知道它可以调用另一个对象的方法,但它不需要知道这个…...

如何将powerpoint(PPT)幻灯片嵌入网页中在线预览、编辑并保存到服务器?
猿大师办公助手不仅可以把微软Office、金山WPS和永中Office的Word文档、Excel表格内嵌到浏览器网页中实现在线预览、编辑保存等操作,还可以把微软Office、金山WPS和永中Office的PPT幻灯片实现网页中在线预览、编辑并保存到服务器。 猿大师办公助手把本机原生Office…...

[Java基础揉碎]日期类
目录 日期类 第一代日期类 第二代日期类 第三代日期类 >前面两代日期类的不足分析 针对以上问题Java在jdk8加入了以下方法 jdk8的时间格式化 时间戳 第三代日期类更多方法 日期类 [知道怎么查,怎么用即可,不用每个方法都背] 第一代日期类 1) Date: …...

4.10作业
//.h文件#ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> //定时器事件类 #include <QTime> //时间类 #include <QString> #include <QPushButton> //按钮类 #include <QLabel> //标签类 #include <QT…...

Hive概述与基本操作
一、Hive基本概念 1.什么是hive? (1)hive是数据仓库建模的工具之一 (2)可以向hive传入一条交互式的sql,在海量数据中查询分析得到结果的平台 2.Hive简介 Hive本质是将SQL转换为MapReduce的任务进行运算,底层由HDFS…...
安装 FFmpeg
安装 FFmpeg 1. Install FFmpeg On Ubuntu2. Install FFmpeg On Ubuntu 16.042.1. First add the repository2.2. Update the newly added repository2.3. Now install the ffmpeg2.4. For opening the ffmpeg for that type ffpmeg on the terminal 3. Uninstall ffmpegRefere…...
18、差分
差分 题目描述 输入一个长度为n的整数序列。 接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。 请你输出进行完所有操作后的序列。 输入格式 第一行包含两个整数n和m。 第二行包含n个整数,表示整…...

13 指针(上)
指针是 C 语言最重要的概念之一,也是最难理解的概念之一。 指针是C语言的精髓,要想掌握C语言就需要深入地了解指针。 指针类型在考研中用得最多的地方,就是和结构体结合起来构造结点(如链表的结点、二叉树的结点等)。 本章专题脉络 1、指针…...

AI 对话完善【人工智能】
AI 对话【人工智能】 前言版权开源推荐AI 对话v0版本:基础v1版本:对话数据表tag.jsTagController v2版本:回复中textarea.jsChatController v3版本:流式输出chatLast.jsChatController v4版本:多轮对话QianfanUtilChat…...
利用数组储存表格数据
原理以及普通数组储存表格信息 在介绍数组的时候说过,数组能够用来储存任何同类型的数据,这里的意思就表明只要是同一个类型的数组据就可以储存到一个数组中。那么在表格中同一行的数据是否可以储存到同一个数组中呢?答案自然是可以ÿ…...

[数据概念|数据技术]智能合约如何助力数据资产变现
“ 区块链上数据具有高可信度,智能合约将区块链变得更加智能化,以支持企业场景。” 之前鼹鼠哥已经发表了一篇文章,简单介绍了区块链,那么,智能合约又是什么呢?它又是如何助力数据资产变现的呢?…...
JS中的常见二进制数据格式
格式描述用途示例ArrayBuffer固定长度的二进制数据缓冲区,不直接操作具体的数据,而是通过类型数组或DataView对象来读写用于存储和处理大量的二进制数据,如文件、图像等let buffer new ArrayBuffer(16);TypedArray基于ArrayBuffer对象的视图…...

uniapp开发h5端使用video播放mp4格式视频黑屏,但有音频播放解决方案
mp4格式视频有一些谷歌播放视频黑屏,搜狗浏览器可以正常播放 可能和视频的编码格式有关,谷歌只支持h.264编码格式的视频播放 将mp4编码格式修改为h.264即可 转换方法: 如果是自己手动上传文件可以手动转换 如果是后端接口调取的地址就需…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...

网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...

基于TurtleBot3在Gazebo地图实现机器人远程控制
1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...
JS设计模式(4):观察者模式
JS设计模式(4):观察者模式 一、引入 在开发中,我们经常会遇到这样的场景:一个对象的状态变化需要自动通知其他对象,比如: 电商平台中,商品库存变化时需要通知所有订阅该商品的用户;新闻网站中࿰…...