《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 8888

int 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"); // ifconfig
	saddr.sin_port = htons(PORT);

	/**** get protocol number  from protocol name  ****/
	// struct hostent *phe; // host information
	// struct servent *pse; // server information
	struct protoent *ppe; // protocol information
	if ((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 address
	int 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 8888

int 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"); // ifconfig
	saddr.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 8888

int main()
{
	struct sockaddr_in sin; // endpoint IP address
	memset(&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_INET6
			  PCSTR pszAddrString, //指向以NULL为结尾的字符串指针,该字符串包含要转换为数字的二进制形式的IP地址文本形式。
			  PVOID pAddrBuf//指向存储二进制表达式的缓冲区
			);
			*/
			/*
			PCWSTR WSAAPI InetNtopW(
				INT        Family,  //地址家族  IPV4使用AF_INET  IPV6使用AF_INET6
				const 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);
		}
		else
			close(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 200

int 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 fds
	int 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_block
				int flags = fcntl(childSd, F_GETFL);
				if (fcntl(childSd, F_SETFL, flags | O_NONBLOCK) == -1)
					errExit();
				fprintf(stderr, "child set nonblock\n");

				// add child to list
				fds[currentFdNum].fd = childSd;
				fds[currentFdNum].events = (POLLIN | POLLRDHUP);
				nfds++;
				currentFdNum++;
				fprintf(stderr, "child: %d pushed to poll list\n", currentFdNum - 1);
			}

			// child read & write
			for (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;
				}

				//  read
				if (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();
							}
						}
					}
				}

				// write
				if (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.mfbz.cn/a/551897.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Matlab拟合常见错误解决 |分段微分方程组拟合【源码+教程】

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《复杂函数拟合案例分享》本专栏旨在提供 1.以案例的形式讲解各类复杂函数拟合的程序实现方法&#xff0c;并提供所有案例完整源码&#xff1b;2.…

我们一起看看《看漫画学C++》中如何介绍的字符串的用法

C中的字符串使用的是 std::string 类型&#xff0c;它是C标准库中提供的字符串类&#xff0c;提供了丰富的字符串操作方法。下面是关于C字符串的一些常用用法&#xff1a; 字符串拼接 字符串查找 字符串追加 购书地址&#xff1a;https://item.jd.com/14418856.html

邮件过滤是什么?怎么设置邮件过滤?

现在我们每天都要收发很多电子邮件。有的是朋友发来的问候&#xff0c;有的是工作伙伴的沟通&#xff0c;还有的可能是那些我们不想要的广告或垃圾邮件。这么多邮件&#xff0c;怎么看过来呀&#xff1f;其实&#xff0c;有一个好工具叫“邮件过滤”&#xff0c;它就像你的私人…

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

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

最最普通程序员,如何利用工资攒够彩礼,成为人生赢家

今天我们不讲如何提升你的专业技能去涨工资&#xff0c;不讲面试技巧如何跳槽涨工资&#xff0c;不讲如何干兼职赚人生第一桶金&#xff0c;就讲一个最最普通的程序员&#xff0c;如何在工作几年后&#xff0c;可以攒够彩礼钱&#xff0c;婚礼酒席钱&#xff0c;在自己人生大事…

gcc原理和使用

gcc gcc是什么 GCC&#xff0c;全称 GNU Compiler Collection&#xff08;GNU 编译器套件&#xff09;&#xff0c;是一套功能强大的编程语言编译器&#xff0c;由自由软件基金会&#xff08;Free Software Foundation, FSF&#xff09;作为GNU项目的一部分开发和维护。它最初…

CMEF | 澳鹏Appen精彩亮相第89届中国国际医疗器械博览会

4月14日&#xff0c;为期四天的第89届中国国际医疗器械博览会&#xff08;CMEF&#xff09;盛大收官。如今&#xff0c;人们的健康需求在人口老龄化等一系列因素的影响下持续增长&#xff0c;这意味着卫生系统也面对着更多具有复杂健康需求的患者。信息化、数字化、智能化已经成…

Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码

Java语言开发的AI智慧导诊系统源码springbootredis 3D互联网智导诊系统源码 智慧导诊解决盲目就诊问题&#xff0c;减轻分诊工作压力。降低挂错号比例&#xff0c;优化就诊流程&#xff0c;有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位&#xff0c;了解对应…

1.总结串口的发送和接收功能使用到的函数2.总结DMA的作用,和DMA+空闲中断的使用方式3.使用PWM+ADC光敏电阻完成光控灯的实验

1.总结串口的发送和接收功能使用到的函数 串口发送函数&#xff1a;HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout) UART_HandleTypeDef *huart&#xff1a;指定要使用的串口 const uint8_t *pData&…

计算机中的小数表示

文章目录 前言整数表示的缺陷定点小数定点小数加法乘法运算 浮点数IEEE754浮点数标准移码阶码的移码表示 IEEE754中的特殊点两个0非规格化数字正常浮点数无穷大NaN 浮点数简单举例浮点数一些其余特性浮点数计算不符合结合律浮点数舍入规则浮点数与整数之间的相互转换 总结 前言…

条形码与二维码的优缺点分析

条形码和二维码在我们的日常生活和工业应用中无处不在&#xff0c;广泛应用于各类商品、物流、防伪等各个领域&#xff0c;它们可以帮助我们快速识别产品、跟踪货物、管理库存、验证身份、获取信息等。条形码和二维码凭借着便捷、低成本、精准等独特的优势&#xff0c;应用领域…

(C语言入门)复合类型、内存管理

目录 复合类型&#xff08;自定义类型&#xff09; 概述&#xff1a; 结构体变量的定义和初始化&#xff1a; 结构体成员的使用&#xff1a; 结构体做函数参数&#xff1a; 结构体值传参&#xff1a; 结构体地址传参&#xff1a; 共用体&#xff08;联合体&#xff09;&…

前端开发攻略---实现与ChatGPT同款光标闪烁打字效果。

1、演示 2、实现代码 <!DOCTYPE html> <html lang"ch-ZN"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name"viewport" content"widt…

基于R语言实现的beta二项回归模型【理解与实现】

本实验&#xff0c;创建一组使用二项分布模拟的数据&#xff08;不带额外的随机性&#xff09;&#xff0c;和另一组使用Beta二项分布模拟的数据&#xff08;引入了随机成功概率 p&#xff0c;从而增加了数据的离散性。 现在假设我们站在上帝视角&#xff0c;有两组不知道分布…

网工交换基础——MUX VLAN

前言&#xff1a; MUX VLAN&#xff08;Multiplex VLAN&#xff0c;多复用VLAN&#xff09;提供了一种通过VLAN进行网络资源控制的机制。例如&#xff0c;在企业网络中&#xff0c;企业员工和企业客户可以访问企业的服务器。对于企业来说&#xff0c;希望企业内部员工之…

谷粒商城part3——快速开发篇

这里是过来人的学习建议&#xff1a; 1、如有条件电脑内存至少16G起步&#xff0c;条件进一步加个屏幕&#xff0c;条件更进一步租一台至少4G内存的X86架构云服务器&#xff0c;所有部署的东西全扔云服务器上 2、P16&#xff0c;P17没法搭起来的建议照着rerenfast的github上的教…

Python革命:如何利用AI数据分析引领人工智能的未来

在人工智能迅速发展的今天&#xff0c;Python语言已经成为了推动AI领域发展的一大利器。作为一种高级编程语言&#xff0c;Python以其简洁的语法和强大的功能&#xff0c;为AI数据分析提供了强有力的支持&#xff0c;帮助开启了人工智能的新时代。 Python的核心优势 Python的最…

FreeRTOS学习 -- 中断配置

一、什么是中断 中断时微控制器一个很常见的特性&#xff0c;中断是由硬件产生&#xff0c;当中断产生以后CPU就会中断当前的流程而去处理中断服务&#xff0c;Cortex-M内核的MCU提供了一个用于中断管理的嵌套向量中断控制器&#xff08;NVIC&#xff09;。 二、中断优先级分…

区块链安全应用----压力测试

通过Caliper进行压力测试程序 1.环境配置 第一步. 配置基本环境 部署Caliper的计算机需要有外网权限&#xff1b;操作系统版本需要满足以下要求&#xff1a;Ubuntu > 16.04、CentOS > 7或MacOS > 10.14&#xff1b;部署Caliper的计算机需要安装有以下软件&#xff…

作业4.17

1.总结串口的发送和接收功能使用到的函数 发送&#xff1a; HAL_StatusTypeDef HAL_UART_Transmit( UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout ) 接受&#xff1a; HAL_StatusTypeDef HAL_UART_Receive_IT( UART_HandleTypeDef *…