《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
在这里插入代码片