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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/823312.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一夜爆红的4款国产软件,却一度被大众误以为是外国人开发

在现今高度信息化的时代&#xff0c;计算机已经深深地渗透到了我们生活的每一个角落。 从日常的办公学习到娱乐休闲&#xff0c;几乎都离不开计算机技术的支持。而在这背后&#xff0c;软件作为计算机的灵魂&#xff0c;其发展历史可谓波澜壮阔。 中国软件产业经过多年的积累和…

node express 请求参数接收方式汇总

express 安装使用 express官网 express 是node.js 中写后端服务比较流行的框架。 安装express npm install -g express安装 express-generator 相当于vue的cli 用来快速生成express项目 npx express-generator生成项目mynode -e是使用ejs模版 express -e mynodeexpress生成器生…

Unity Android 2023 Release-Notes

&#x1f308;Unity Android 2023 Release-Notes 本文信息收集来自自动搜集工具&#x1f448; 版本更新内容2023.2.17Android: Fixed an issue where a black frame flashes when returning to Unity Game Activity from the home screen.(UUM-58966)2023.2.17Android: Fixed …

前端网络---http协议和https协议的区别

http协议和https的区别 1、http是超文本传输协议&#xff0c;信息是明文传输&#xff0c;https则是具有安全性的ssl加密传输协议。 2、http和https使用的端口不一样&#xff0c;http是80&#xff0c;https是443。 3、http的连接很简单&#xff0c;是无状态的&#xff08;可以…

L2-009抢红包

用结构体来存储变量&#xff0c;定义排序规则&#xff0c;对题目所讲的模拟一遍即可。没有什么很深入得内容 #include <bits/stdc.h> using namespace std;typedef struct {int num;double sum;int count; } Node; bool cmp(Node node1, Node node2) {if (node1.sum no…

2024电容笔专业对比评测:西圣、倍思、绿联哪款平替电容笔更好用?

在当今学习和工作环境中&#xff0c;iPad作为一种多功能的学习和生产力工具&#xff0c;受到越来越多人的青睐与需求。然而&#xff0c;要充分发挥iPad的功能&#xff0c;一个优质的电容笔是必不可少的配件之一。电容笔不仅可以帮助用户进行手写笔记、绘画创作&#xff0c;还能…

1373:鱼塘钓鱼(fishing)

【算法分析】 解法1&#xff1a;区间动规 该人只会从编号小的鱼塘走到编号大的鱼塘&#xff0c;不存在往回走的情况&#xff08;从编号大的鱼塘走到编号小的鱼塘&#xff09;。 如果他仅仅往回走但不在任何鱼塘停留&#xff0c;那么这与不往回走钓到的鱼的数量相同&#xff0…

新手做抖音小店,想要快速起店,抓住这两点很关键

大家好&#xff0c;我是电商笨笨熊 抖音小店一定是近几年来爆火的电商项目&#xff0c;凭借着直播电商的方式在短短几年内迅速崛起&#xff0c;成为现在人尽皆知的电商项目。 然而在抖店里&#xff0c;不少进入的玩家都是新手&#xff0c;甚至都是盲目入店&#xff0c;没有任…

【Unity】Feature has expired(H0041)

【背景】 在一台很久不用的电脑上更新了个人License&#xff0c;并导入了云项目&#xff0c;打开时却报错&#xff1a; 【分析】 网上查说要删缓存等等&#xff0c;试过都不行。重装Hub也不行。 这种环境类型的原因很难从信息入手定位错误。 所以我自己检查项目上有什么问题…

MATLAB 浮点数 转化为 定点数

a fi(v,s,w,f) 一个 fi 对象&#xff0c;其值为 v&#xff0c;符号性为 s&#xff0c;字长为 w&#xff0c;小数长度为 f。 AD9361 a fi(0.707,1,12,11)

Qt实现XYModem协议(二)

1 概述 XMODEM协议是一种使用拨号调制解调器的个人计算机通信中广泛使用的异步文件运输协议。这种协议以128字节块的形式传输数据&#xff0c;并且每个块都使用一个校验和过程来进行错误检测。使用循环冗余校验的与XMODEM相应的一种协议称为XMODEM-CRC。还有一种是XMODEM-1K&am…

在列表b是在列表a的首位(末尾)增加了‘x‘元素,要求分别输出列表a(原列表)和列表b

在列表b是在列表a的首位增加了0元素&#xff0c;要求分别输出列表a&#xff08;原列表&#xff09;和列表b 1.创建副本的形式实现 如果你想要在列表 b 中增加元素 0&#xff0c;而不影响原始列表 a&#xff0c;你需要创建 b 的一个副本&#xff0c;而不是让 b 直接指向 a。这…

angular node版本问题导致运行出错时应该怎么处理

如下图所示&#xff1a; 处理方式如下&#xff1a; package.json中start跟build中添加&#xff1a;SET NODE_OPTIONS--openssl-legacy-provider即可

电气中的PT,VT,变压器区别

变压器&#xff08;Transformer&#xff09;和电压互感器&#xff08;Voltage Transformer, VT&#xff0c;也称作电压变压器或电位互感器&#xff09;虽然都是利用电磁感应原理来改变电压的设备&#xff0c;但它们在用途、设计和精度方面有明显的区别&#xff1a; 用途&#x…

还原matlab编辑器窗口和主窗口分开的问题

问题 matlab不知道早点的&#xff0c;点击运行后会弹出新的窗口&#xff0c;咋整都恢复不了 解决方案 首先&#xff0c;在编辑器窗口下&#xff0c;按ctrlshiftD&#xff0c;此时编辑器窗口和主窗口就合并了&#xff0c;问题解决。

TCP的一些功能详述

文章制作不易&#xff0c;望各位大佬多多点赞&#xff0c;球球各位啦&#xff01;&#xff01;&#xff01;&#xff01; 目录 1.TCP的简介 2.TCP协议中部分数据的理解 1.端口号 2.序列号 3.四位首部长度 4.6位保留位 5. 16位校验和 6.数据&#xff08;TCP的载荷&#…

CC254X 8051芯片手册介绍

1 8051CPU 8051是一种8位元的单芯片微控制器&#xff0c;属于MCS-51单芯片的一种&#xff0c;由英特尔(Intel)公司于1981年制造。Intel公司将MCS51的核心技术授权给了很多其它公司&#xff0c;所以有很多公司在做以8051为核心的单片机&#xff0c;如Atmel、飞利浦、深联华等公…

LeetCode 面试经典150题 209.长度最小的子数组

题目&#xff1a; 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0 。 思路&#x…

mybatis的使用技巧7——mysql中in,exists,join的用法和区别

在实际项目开发中&#xff0c;sql查询中的连表查询和子查询用的是最多的&#xff0c;但是很多人对于in&#xff0c;exists&#xff0c;join的用法认识不足&#xff0c;随意运用&#xff0c;这种情况如果在大数据量查询时&#xff0c;会存在很大的隐患。 一.子查询&#xff08;…

【创建型模式】工厂方法模式

一、简单工厂模式 1.1 简单工厂模式概述 简单工厂模式又叫做静态工厂方法模式。 目的&#xff1a;定义一个用于创建对象的接口。实质&#xff1a;由一个工厂类根据传入的参数&#xff0c;动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。 简单工厂模式…