《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即可 转换方法: 如果是自己手动上传文件可以手动转换 如果是后端接口调取的地址就需…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility
Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...

成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...