C/S模型测试及优化

1

1.1代码示例

#include<stdio.h>
#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
	int sockfd=0;
	int ret=0;
	struct sockaddr_in seraddr;

	char tmpbuff[4096]={"I need some offers"};
	int cnt=0;
	ssize_t nsize=0;

	/* 1 创建网络套接字 */
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	/* 2 初始化网络地址结构体 */
	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	/* 3 发送连接请求 */
	ret=connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to connect");
		return -1;
	}

	/* 4 客户端与服务器交互 */
	while(1)
	{
#if 0
		/* 4.1 清空发送缓冲区 */
		memset(tmpbuff,0,sizeof(tmpbuff));
		/* 4.2 拼接数据 */
		sprintf(tmpbuff,"client send:### %d",cnt);
		cnt++;

		/* 4.3 发送数据到网络套接字 */
		nsize=send(sockfd,tmpbuff,strlen(tmpbuff),0);
		if(-1==nsize)
		{
			perror("fail to send");
			return -1;
		}
#endif

#if 1
		/* 清空接收缓冲区 */
		memset(tmpbuff,0,sizeof(tmpbuff));
		/* 接收网络套接字数据 */
		nsize=recv(sockfd,tmpbuff,sizeof(tmpbuff),0);
		if(-1==nsize)
		{
			perror("fail to recv");
			return -1;
		}
#endif
		
		/* 显示接收数据 */
		printf("client recv:### %s\n",tmpbuff);
	}

	/* 5 关闭网络套接字 */
	close(sockfd);

	return 0;
}



int main(void)
{
    int sockfd=0;
    struct sockaddr_in seraddr;
    int ret=0; 

    int confd=0;
    char tmpbuff[4096]={0};
    ssize_t nsize=0;

    /* 1 创建网络套接字 */
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to socket");
        return -1;
    }
    
    /* 2 初始化网络地址结构体 */
    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    /* 3 绑定网络套接字 */
    ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to bind");
        return -1;
    }

    /* 4 监听网络套接字 */
    ret=listen(sockfd,10);
    if(-1==ret)
    {
        perror("fail to listen");
        return -1;
    }

    /* 5 服务器与客户端交互 */
    while(1)
    {
        /* 5.1 创建通信套接字-阻塞等待网络套接字连接请求*/
        confd=accept(sockfd,NULL,NULL);
        if(-1==confd)
        {
            perror("fail to accept");
            return -1;
        }

#if 0
        /* 5.2 清空接收缓冲区 */
        memset(tmpbuff,0,sizeof(tmpbuff));
        /* 5.3 接收通信套接字数据-并做接收完成处理 */
        nsize=recv(confd,tmpbuff,sizeof(tmpbuff),0);
        if(-1==nsize)
        {
            perror("fail to recv");
            return -1;
        }
        else if(0==nsize)
        {
            return 0;
        }
#endif
        
        /* 5.2 清空发送缓冲区 */
        memset(tmpbuff,0,sizeof(tmpbuff));
        /* 5.4 拼接 */
        sprintf(tmpbuff,"server send:### %s",tmpbuff);
        /* 5.5 发送 */
        nsize=send(confd,tmpbuff,strlen(tmpbuff),0);
        if(-1==nsize)
        {
            perror("fail to send");
            return -1;
        }
    }

    /* 6 关闭通信套接字 */
    close(confd);
    /* 7 关闭网络套接字 */
    close(sockfd);

    return 0;
}

1.2运行结果

(1)运行服务器
(2)运行客户端

现象如下:
	客户端发生阻塞

在这里插入图片描述

(1)关闭服务器:
	客户端不阻塞-说明recv函数为非阻塞状态

在这里插入图片描述

总结:
(1)如果将服务器accept函数放在while循环内部:同时运行客户端与服务器,可以发现服务器可以与多个客户端建立连接(解决的问题-服务器并发访问)。
(2)但是只能进行一次数据发送(虽然解决了服务器的并发访问问题,但是对于大量的数据传输却手足无措)
			小意外:当服务器端退出,此时客户端不再阻塞,正常执行函数体。(这就说明了,此时recv函数为非阻塞状态=》那么服务器和客户端同时运行,为什么会发生阻塞?)

补充:
(1)如果将服务器端accept函数放在while循环外部(对比TCP文件传输):
	1)只有一个客户端能够与服务器建立连接并进行数据收发;其它客户端被阻塞,无法与服务器建立连接。(问题:(1)服务器只能与一个客户端建立连接?(2)那么如何解决这个问题?(3)是从服务端解决这个问题,还是从客户端解决这个问题?	先解决问题(3):显然,要解决这个问题肯定是要从服务器端去解决--客户端是向服务器端发起连接请求,那么具体是否能够建立连接,显然是取决于服务器端的意愿和能力;所以,多个客户端向服务器端发起连接请求,能不能实现的关键在于服务器能不能接受多个客户端接入,服务器端是否允许多个客户端接入。	下面再解决问题(2):解决这个问题,其实用一句话来表述就是解决服务器并发访问问题,这个问题后面专项解决,现在只需要明白所面临的问题是服务器并发访问的问题即可。)

(2)如果将connect函数放在循环外部,将accept函数放在循环外部,此时意味着:客户端与服务器端第一次建立连接,可以发送一次数据;发完数据接着进行第二次,第三次。。。连接请求;此时当然是报错!因为同一个网络套接字只能建立一次连接,不能重复建立连接(这是由TCP通信机制决定的);但是这种方式可以将服务器同时与多个客户端建立连接。。。。。。。。。。。。。。总结一下就是:客户端与服务器端要进行多次请求连接-建立连接;那么每进行一次通信,都必须关闭(close)通信套接字(已经建立三次握手,四次挥手连接的套接字confd-accept)。

在这里插入图片描述

2 多线程解决TCP并发问题

2.1 源码示例

/*************************************************************************
	> File Name: client.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月01日 星期六 10时44分42秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
    int sockfd=0;
    
    struct sockaddr_in seraddr;

    int ret=0;

    ssize_t nsize=0;
    char sendbuffer[4096]={"|c-send|"};
    char recvbuffer[4096]={"|c-recv|"};
	int cnt=100;

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);//本地字节序转换为网络大端字节序
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");//字符串IP转为内存中的IP

    ret=connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to connect");
        return -1;
    }

    while(1)
    {	
        memset(sendbuffer,0,sizeof(sendbuffer));
        sprintf(sendbuffer,"< %d >",--cnt);
        nsize=send(sockfd,sendbuffer,sizeof(sendbuffer),0);
        if(-1==nsize)
        {
            perror("fail to send");
            return -1;
        }
		printf("client send: %s\n",sendbuffer);
		sleep(1);

        memset(recvbuffer,0,sizeof(recvbuffer));
        nsize=recv(sockfd,recvbuffer,sizeof(recvbuffer),0);
        if(-1==nsize)
        {
            perror("fail to recv");
            return -1;
        }
		printf("client recv: %s\n",recvbuffer);
		sleep(1);
    }

    return 0;
}
/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月01日 星期六 14时17分47秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

#include <pthread.h>


void *handle_tcp_client(void *arg);

int main(void)
{
    pthread_t tid=0;
    pthread_attr_t attr;

    int sockfd=0;

    struct sockaddr_in seraddr;

    int ret=0;
    
    int confd=0;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to sockfd");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));//警告: 结构体类型强制转换?
    if(-1==ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret=listen(sockfd,10);
    if(-1==ret)
    {
        perror("fail to listen");
        return -1;
    }

    while(1)
    {
        confd=accept(sockfd,NULL,NULL);
        if(-1==confd)
        {
            perror("fail to accept");
            return -1;
        }
#if 0
        /* <1> 线程回调函数传参-粗心传参*/
        pthread_create(&tid,&attr,handle_tcp_client,&confd);
#endif
#if 1
        /* <1> 线程回调函数传参-细节优化*/
        pthread_create(&tid,&attr,handle_tcp_client,confd);//线程回调函数传参,传的是指针,这里直接传值?
#endif
    }

    close(sockfd);

    return 0;
}

void *handle_tcp_client(void *arg)
{
    ssize_t nsize=0;
    char sendbuffer[4096]={"|s-send|"};
    char recvbuffer[4096]={"|s-recv|"};
#if 0
    /* <1> 线程回调函数传参-粗心传参*/
    int confd=*(int *)arg;
#endif
#if 1
    /* <1> 线程回调函数传参-细节优化*/
    int confd=arg;
#endif
    int cnt=0;

    while(1)
    {
        memset(recvbuffer,0,sizeof(recvbuffer));
        nsize=recv(confd,recvbuffer,sizeof(recvbuffer),0);
        if(-1==nsize)
        {
            perror("fail to recv");
            return NULL;
        }
        else if(0==nsize)
        {
            return NULL;
        }
        printf("server recv: %s\n",recvbuffer);
		sleep(1);

        memset(sendbuffer,0,sizeof(sendbuffer));
        sprintf(sendbuffer,"< %d >",++cnt);
        nsize=send(confd,sendbuffer,strlen(sendbuffer),0);
        if(-1==nsize)
        {
            perror("fail to send");
            return NULL;
        }
        printf("server send: %s\n",sendbuffer);
		sleep(1);
    }

    return NULL;
}

2.2 运行结果

在这里插入图片描述

2.3 分析总结

服务器端:
	两次接收:两个客户端与服务器端建立连接,接受两个客户端分别发送过来的数据;
	两次发送:服务器端与两个客户端建立连接,发送数据时向两个客户端分别发送一次数据。
	
客户端:
	一次发送:向服务器端发送一次数据。
	一次接收:接受服务器端发送过来的数据。

问题:
	(1)如果连入多个客户端,那么就创建了多个通信套接字(confd);服务器端创建多线程的时候,为回调函数传参问题:
		1)回调函数传参传【&confd】:那么后建立的服务器端与客户端的连接产生的文件描述符(confd)就会将先建立的服务器端与客户端的连接产生的文件描述符(confd)覆盖;这将会导致不同的客户端的不同数据请求得到服务器端的数据响应是一样的(即只有一个客户端可以收到自己想要的数据,而其它的客户端收到的将不是自己请求的数据)。
		2)回调函数传参传【confd】:就可以解决先建立连接产生的文件描述符被覆盖的问题。(同时编译过程中产生2个警告!)
	(2)运行效果:		

在这里插入图片描述

3 IO多路复用解决TCP并发问题

3.1 select

select基本原理是:
	(1)监听所有文件描述符,将处于非ready态的文件描述符从监听集合中清除,只留下ready态的文件描述符在文件描述符集合中。(保留符合)
	(2)查询文件描述符集合中仍然存在的文件描述符,哪个文件描述符有事件发生,则从对应的文件描述符读数据。

3.1.1 源码示例

3.1.1 客户端
/*************************************************************************
	> File Name: client.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月03日 星期一 16时12分48秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
    int sockfd=0;

    struct sockaddr_in seraddr;

    int ret=0;

    ssize_t nsize=0;
    char sendbuffer[4096]={0};
    char recvbuffer[4096]={0};

    int send_cnt=1;
//    int recv_cnt=99;

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    ret=connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to connect");
        return -1;
    }

    while(1)
    {
        memset(sendbuffer,0,sizeof(sendbuffer));
        sprintf(sendbuffer,"|%-3d|",send_cnt++);
        nsize=send(sockfd,sendbuffer,sizeof(sendbuffer),0);
        if(-1==nsize)
        {
            perror("fail to send");
            return -1;
        }
        printf("client send: %s\n",sendbuffer);

        memset(recvbuffer,0,sizeof(recvbuffer));
        nsize=recv(sockfd,recvbuffer,sizeof(recvbuffer),0);
        if(-1==nsize)
        {
            perror("fail to recv");
            return -1;
        }
        printf("client recv: %s\n",recvbuffer);
        
		/* 方便查看运行结果*/
        sleep(3);
    }

    close(sockfd);

    return 0;
}
3.1.2 服务器端
/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月03日 星期一 17时01分50秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
	int sockfd=0;

	struct sockaddr_in seraddr;

	int ret=0;

	int confd=0;
	int maxfd=0;

	ssize_t nsize=0;
	char sendbuffer[4096]={0};
	char recvbuffer[4096]={0};
	
	int send_cnt=99;

	fd_set rdfds;
	fd_set tmpfds;

	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret=listen(sockfd,10);
	if(-1==ret)
	{
		perror("fail to listen");
		return -1;
	}

	/* 文件描述符集操作 */
	FD_ZERO(&rdfds);//初始化文件描述符集合
	FD_SET(sockfd,&rdfds);//添加文件描述符至文件描述符集合

	while(1)
	{
		/* 初始化监听文件描述符集合 */
		tmpfds=rdfds;
		/* 监听文件描述符集合 */
		ret=select(sockfd+1,&tmpfds,NULL,NULL,NULL);
		if(-1==ret)
		{
			perror("fail to select");
			return -1;
		}

		/* 查询文件描述符集合中文件描述符状态*/
		if(FD_ISSET(sockfd,&tmpfds))
		{
			confd=accept(sockfd,NULL,NULL);
			/* 注意: 异常处理(3个操作) */
			if(-1==confd)
			{
				perror("fail to accept");
				FD_CLR(sockfd,&rdfds);//将套接字文件描述符从文件描述符集合中删除
				close(sockfd);//关闭套接字文件
				continue;//既然建立TCP连接失败,跳过TCP收发过程也理所当然
			}

			FD_SET(confd,&rdfds);
//			maxfd = maxfd > confd ? maxfd : confd;
		}



		/* 查询文件描述符集合中文件描述符状态 */
		if(FD_ISSET(sockfd,&tmpfds))
		{

			memset(recvbuffer,0,sizeof(recvbuffer));
			nsize=recv(confd,recvbuffer,sizeof(recvbuffer),0);
			if(-1==nsize)
			{
				perror("fail to recv");
				return -1;
			}
			printf("server recv: %s\n",recvbuffer);

			memset(sendbuffer,0,sizeof(sendbuffer));
			sprintf(sendbuffer,"|%-3d|",send_cnt--);
			nsize=send(confd,sendbuffer,sizeof(sendbuffer),0);
			if(-1==nsize)
			{
				perror("fail to send");
				return -1;
			}
			printf("server send: %s\n",sendbuffer);
		}

		/* 方便查看运行结果*/
		sleep(3);
	}
	
	
	close(confd);
	close(sockfd);
}

运行结果:
在这里插入图片描述

3.1.3 服务器端改进 1
通过maxfd获取最大文件描述符
/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月03日 星期一 17时01分50秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
	int sockfd=0;

	struct sockaddr_in seraddr;

	int ret=0;

	int confd=0;
	int maxfd=0;

	ssize_t nsize=0;
	char sendbuffer[4096]={0};
	char recvbuffer[4096]={0};
	
	int send_cnt=99;

	fd_set rdfds;
	fd_set tmpfds;

	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret=listen(sockfd,10);
	if(-1==ret)
	{
		perror("fail to listen");
		return -1;
	}

	/* 文件描述符集操作 */
	FD_ZERO(&rdfds);//初始化文件描述符集合
	FD_SET(sockfd,&rdfds);//添加文件描述符至文件描述符集合
    maxfd=sockfd;

	while(1)
	{
		/* 初始化监听文件描述符集合 */
		tmpfds=rdfds;
		/* 监听文件描述符集合 */
		ret=select(maxfd+1,&tmpfds,NULL,NULL,NULL);
		if(-1==ret)
		{
			perror("fail to select");
			return -1;
		}

		/* 查询文件描述符集合中文件描述符状态*/
		if(FD_ISSET(sockfd,&tmpfds))
		{
			confd=accept(sockfd,NULL,NULL);
			/* 注意: 异常处理(3个操作) */
			if(-1==confd)
			{
				perror("fail to accept");
				FD_CLR(sockfd,&rdfds);//将套接字文件描述符从文件描述符集合中删除
				close(sockfd);//关闭套接字文件
				continue;//既然建立TCP连接失败,跳过TCP收发过程也理所当然
			}

			FD_SET(confd,&rdfds);
			maxfd = maxfd > confd ? maxfd : confd;
		}



		/* 查询文件描述符集合中文件描述符状态 */
		if(FD_ISSET(sockfd,&tmpfds))
		{

			memset(recvbuffer,0,sizeof(recvbuffer));
			nsize=recv(confd,recvbuffer,sizeof(recvbuffer),0);
			if(-1==nsize)
			{
				perror("fail to recv");
				return -1;
			}
			printf("server recv: %s\n",recvbuffer);

			memset(sendbuffer,0,sizeof(sendbuffer));
			sprintf(sendbuffer,"|%-3d|",send_cnt--);
			nsize=send(confd,sendbuffer,sizeof(sendbuffer),0);
			if(-1==nsize)
			{
				perror("fail to send");
				return -1;
			}
			printf("server send: %s\n",sendbuffer);
		}

		/* 方便查看运行结果*/
		sleep(3);
	}

	close(confd);
	close(sockfd);

	return 0;
}

运行结果:
在这里插入图片描述
(1)只有一次数据通信
(2)关闭服务器端:客户端自动断开

3.1.4 服务器端改进 2
/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月03日 星期一 17时01分50秒
 ************************************************************************/

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
	int sockfd=0;

	struct sockaddr_in seraddr;

	int ret=0;

	int confd=0;
	int maxfd=0;

	ssize_t nsize=0;
	char sendbuffer[4096]={0};
	char recvbuffer[4096]={0};
	
	int send_cnt=99;

	fd_set rdfds;
	fd_set tmpfds;

    int i=0;

	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret=listen(sockfd,10);
	if(-1==ret)
	{
		perror("fail to listen");
		return -1;
	}

	/* 1 文件描述符集操作 */
	FD_ZERO(&rdfds);//初始化文件描述符集合
	FD_SET(sockfd,&rdfds);//添加文件描述符至文件描述符集合
    maxfd=sockfd;

	while(1)
	{
		/* 2 初始化监听文件描述符集合 */
		tmpfds=rdfds;
		/* 3 监听文件描述符集合 */
		ret=select(maxfd+1,&tmpfds,NULL,NULL,NULL);
		if(-1==ret)
		{
			perror("fail to select");
			return -1;
		}

		/* 4 查询文件描述符集合中sockfd文件描述符状态*/
		if(FD_ISSET(sockfd,&tmpfds))
		{
            /* 建立通信文件描述符 - 监听是否有新的文件描述符加入 */
			confd=accept(sockfd,NULL,NULL);
			/* 注意: 异常处理(3个操作) */
			if(-1==confd)
			{
				perror("fail to accept");
				FD_CLR(sockfd,&rdfds);//将套接字文件描述符从文件描述符集合中删除
				close(sockfd);//关闭套接字文件
				continue;//既然建立TCP连接失败,跳过TCP收发过程也理所当然
			}

			FD_SET(confd,&rdfds);
			maxfd = maxfd > confd ? maxfd : confd;
        }


        for(i=sockfd+1;i<=maxfd;i++)//sockfd是第1个被监听的文件描述符,也是3个标准流除外的最小文件描述符
        {
            /* 查询文件描述符集合中文件描述符状态,并进行IO操作 */
            if(FD_ISSET(i,&tmpfds))
            {
                /* IO操作 */
                memset(recvbuffer,0,sizeof(recvbuffer));
                nsize=recv(i,recvbuffer,sizeof(recvbuffer),0);//每个被监听的文件描述符i
                if(-1==nsize)
                {
                    perror("fail to recv");
                }
                printf("server recv: %s\n",recvbuffer);

                memset(sendbuffer,0,sizeof(sendbuffer));
                sprintf(sendbuffer,"|%-3d|",send_cnt--);
                nsize=send(i,sendbuffer,sizeof(sendbuffer),0);//每个被监听的文件描述符i
                if(-1==nsize)
                {
                    perror("fail to send");
                }
                printf("server send: %s\n",sendbuffer);

                /* IO操作异常处理*/
                if(-1==nsize)
                {
                    fprintf(stderr,"server disconnected!\n");//服务器端异常报错
                    FD_CLR(i,&rdfds);
                    close(i);
                    continue;
                }
                else if(0==nsize)
                {
                    fprintf(stderr,"client disconnected!\n");//客户端异常报错
                    FD_CLR(i,&rdfds);
                    close(i);
                    continue;
                }
            }
        }

        /* 方便查看运行结果*/
        sleep(3);
    }

    close(confd);
	close(sockfd);

	return 0;
}

3.1.5 运行结果

在这里插入图片描述
完成TCP并发。

3.1.6 分析总结

(1)

(2)关于select:
		1)select监听文件描述符上限:【1024】。
		2)select需要将数据从内核层向用户层空间拷贝;占用系统资源开销。
		3)select只能工作在水平触发模式(低速模式),无法工作在边沿触发(高速模式)。

3.2 poll

3.2.1 源码示例

(1)原始代码

/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月04日 星期二 21时23分30秒
 ************************************************************************/

#include<stdio.h>

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

#include <poll.h>

int main(void)
{
	int sockfd=0;

	struct sockaddr_in seraddr;

	int ret=0;

	int confd=0;

	ssize_t nsize=0;
	char sendbuffer[4096]={0};
	char recvbuffer[4096]={0};

	int send_cnt=99;

	struct pollfd fds[1024];
    int nready=0;
	int i=0;
    int j=0;

	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret=listen(sockfd,10);
	if(-1==ret)
	{
		perror("fail to listen");
		return -1;
	}

	/* 1.1 初始化文件描述符状态结构体数组 */
	for(i=0;i<1024;++i)
	{
		fds[i].fd=-1;//未使用的文件描述符状态结构体数组元素初始化为-1(作为标志-未使用)
	}
	/* 1.2 初始化文件描述符状态结构体数组 */
	for(i=0;i<1024;++i)
	{
		/* 将文件描述符添加到数组空位置中 */
		if(fds[i].fd==-1)
		{
			fds[i].fd=sockfd;//未使用的文件描述符状态结构体数组元素初始化为-1(作为标志-未使用)
			fds[i].events=POLLIN;

			break;
		}
	}

	while(1)
	{
		/* 2 监听文件描述符状态结构体数组 */
		nready=poll(fds,1024,-1);//数组 - 长度 - 一直等
		if(-1==nready)
		{
			perror("fail to poll");
			return -1;
		}

        /* 3 查询文件描述符状态 - 查询方式: 位掩码 - 置位 */
        for(i=0;i<1024;++i)
        {
            if(fds[i].fd==-1)
            {
                continue;
            }

            if((fds[i].revents & POLLIN) && (fds[i].fd==sockfd))
            {
                confd=accept(sockfd,NULL,NULL);
                if(-1==confd)
                {
                    perror("fail to accept");
                    for(j=0;j<1024;++j)
                    {
                        if(fds[j].fd=sockfd)
                        {
                            fds[j].fd=-1;

                            break;
                        }
                    }
                    close(sockfd);

                    continue;
                }

                /* 将confd添加进来 */
                for(i=0;i<1024;++i)
                {
                    /* 将文件描述符添加到数组空位置中 */
                    if(fds[i].fd==-1)
                    {
                        fds[i].fd=confd;//未使用的文件描述符状态结构体数组元素初始化为-1(作为标志-未使用)
                        fds[i].events=POLLIN;

                        break;
                    }
                }
            }
            else if((fds[i].revents & POLLIN) &&(fds[i].fd!=sockfd))
            {
                memset(recvbuffer,0,sizeof(recvbuffer));
                nsize=recv(fds[i].fd,recvbuffer,sizeof(recvbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to recv");
                }
                printf("server recv: %s\n",recvbuffer);

                memset(sendbuffer,0,sizeof(recvbuffer));
                sprintf(sendbuffer,"|%-3d|",send_cnt--);
                nsize=send(fds[i].fd,sendbuffer,strlen(sendbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to send");
                }
                printf("server send: %s\n",sendbuffer);

                /* IO异常处理 */
                if(-1==nsize)
                {
                    fprintf(stderr,"server disconnected!\n");//服务器端异常报错
                    close(fds[i].fd);
                    /* 从fds集合中删除fd */
                    for(j=0;j<1024;++j)
                    {
                        if(fds[j].fd=fds[i].fd)
                        {
                            fds[j].fd=-1;

                            break;
                        }
                    }
                    continue;
                }
                else if(0==nsize)
                {
                    fprintf(stderr,"client disconnected!\n");//客户端异常报错
                    close(fds[i].fd);
                    /* 从fds集合中删除fd */
                    for(j=0;j<1024;++j)
                    {
                        if(fds[j].fd=fds[i].fd)
                        {
                            fds[j].fd=-1;

                            break;
                        }
                    }
                    continue;
                }
            }
        }

        sleep(3);//方便分析运行结果
    }

    close(sockfd);

    return 0;
}

(2)函数封装

/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月04日 星期二 21时23分30秒
 ************************************************************************/

#include<stdio.h>

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

#include <poll.h>

int fds_init(struct pollfd *fds,int maxlen);
int fd_add(struct pollfd *fds,int maxlen,int fd,short env);
int fd_delete(struct pollfd *fds,int maxlen,int fd);

int main(void)
{
	int sockfd=0;

	struct sockaddr_in seraddr;

	int ret=0;

	int confd=0;

	ssize_t nsize=0;
	char sendbuffer[4096]={0};
	char recvbuffer[4096]={0};

	int send_cnt=99;

	struct pollfd fds[1024];
    int nready=0;
	int i=0;
    int j=0;

	sockfd=socket(AF_INET,SOCK_STREAM,0);
	if(-1==sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	seraddr.sin_family=AF_INET;
	seraddr.sin_port=htons(50000);
	seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

	ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
	if(-1==ret)
	{
		perror("fail to bind");
		return -1;
	}

	ret=listen(sockfd,10);
	if(-1==ret)
	{
		perror("fail to listen");
		return -1;
	}

    fds_init(fds,1024);
    fd_add(fds,1024,sockfd,POLLIN);

	while(1)
	{
		/* 2 监听文件描述符状态结构体数组 */
		nready=poll(fds,1024,-1);//数组 - 长度 - 一直等
		if(-1==nready)
		{
			perror("fail to poll");
			return -1;
		}

        /* 3 查询文件描述符状态 - 查询方式: 位掩码 - 置位 */
        for(i=0;i<1024;++i)
        {
            if(fds[i].fd==-1)
            {
                continue;
            }

            if((fds[i].revents & POLLIN) && (fds[i].fd==sockfd))
            {
                confd=accept(sockfd,NULL,NULL);
                if(-1==confd)
                {
                    perror("fail to accept");
                    fd_delete(fds,1024,sockfd);
                    close(sockfd);

                    continue;
                }
                fd_add(fds,1024,confd,POLLIN);
            }
            else if((fds[i].revents & POLLIN) &&(fds[i].fd!=sockfd))
            {
                memset(recvbuffer,0,sizeof(recvbuffer));
                nsize=recv(fds[i].fd,recvbuffer,sizeof(recvbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to recv");
                }
                printf("server recv: %s\n",recvbuffer);

                memset(sendbuffer,0,sizeof(recvbuffer));
                sprintf(sendbuffer,"|%-3d|",send_cnt--);
                nsize=send(fds[i].fd,sendbuffer,strlen(sendbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to send");
                }
                printf("server send: %s\n",sendbuffer);

                /* IO异常处理 */
                if(-1==nsize)
                {
                    fprintf(stderr,"server disconnected!\n");//服务器端异常报错
                    close(fds[i].fd);
                    fd_delete(fds,1024,fds[i].fd);

                    continue;
                }
                else if(0==nsize)
                {
                    fprintf(stderr,"client disconnected!\n");//客户端异常报错
                    close(fds[i].fd);
                    fd_delete(fds,1024,fds[i].fd);

                    continue;
                }
            }
        }

        sleep(3);//方便分析运行结果
    }

    close(sockfd);

    return 0;
}

int fds_init(struct pollfd *fds,int maxlen)
{
    int i=0;

    for(i=0;i<maxlen;++i)
    {
        fds[i].fd=-1;
    }

    return 0;
}

int fd_add(struct pollfd *fds,int maxlen,int fd,short env)
{
    int i=0;

    for(i=0;i<maxlen;++i)
    {
        if(fds[i].fd==-1)
        {
            fds[i].fd=fd;
            fds[i].events=env;

            break;
        }
    }

    if(i==maxlen)
    {
        return -1;
    }

    return 0;
}

int fd_delete(struct pollfd *fds,int maxlen,int fd)
{
    int i=0;

    for(i=0;i<maxlen;++i)
    {
        if(fds[i].fd==fd)
        {
            fds[i].fd=-1;

            break;
        }
    }
}

3.2.2 运行结果

在这里插入图片描述

3.2.3 分析总结

1.【poll】由内核层向用户层数据空间拷贝的过程中,占用系统资源开销
2.【poll】必须轮询检测生产事件的文件描述符
3.【poll】只能工作在水平触发模式(低速模式),无法工作在边沿触发(高速模式)

3.3 epoll

3.3.1 源码示例

(1)客户端:

/*************************************************************************
	> File Name: client.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月13日 星期四 07时39分26秒
 ************************************************************************/
#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

int main(void)
{
    int sockfd=0;

    struct sockaddr_in seraddr;

    int ret=0;

    ssize_t nsize=0;
    char sendbuffer[4096]={0};
    char recvbuffer[4096]={0};
    int send_cnt=1;

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to socket");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    ret=connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to connect");
        return -1;
    }

    while(1)
    {
        memset(sendbuffer,0,sizeof(sendbuffer));
        sprintf(sendbuffer,"|%-3d|",send_cnt++);
        nsize=send(sockfd,sendbuffer,strlen(sendbuffer),0);
        if(-1==nsize)
        {
            perror("fail to send");
            return -1;
        }
		printf("client send: %s\n",sendbuffer);

        memset(recvbuffer,0,sizeof(recvbuffer));
        nsize=recv(sockfd,recvbuffer,sizeof(recvbuffer),0);
        if(-1==nsize)
        {
            perror("fail to recv");
            return -1;
        }
		printf("client recv: %s\n",recvbuffer);
        
		sleep(3);//便于分析运行结果
    }

    close(sockfd);

    return 0;
}

(2)服务器原始代码:

/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月13日 星期四 08时07分01秒
 ************************************************************************/
#include<stdio.h>

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

#include <sys/epoll.h>

int main(void)
{
    int sockfd=0;

    struct sockaddr_in seraddr;

    int ret=0;

    int confd=0;

    ssize_t nsize=0;
    char sendbuffer[4096]={0};
    char recvbuffer[4096]={0};
    int send_cnt=99;

    int epfd=0;

    int nready=0;

    int i=0;

    struct epoll_event retenv[1024];
    struct epoll_event tmpenv;

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to sockfd");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret=listen(sockfd,10);
    if(-1==ret)
    {
        perror("fail to ret");
        return -1;
    }

    /* 1 创建内核事件表 - 并返回表头 */
    epfd=epoll_create(1024);
    if(-1==epfd)
    {
        perror("fail to epoll_create");
        return -1;
    }

    /* 2 初始化内核epoll事件结构体 */
    tmpenv.events=EPOLLIN;
    tmpenv.data.fd=sockfd;
    ret=epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&tmpenv);
    if(-1==ret)
    {
        perror("fail to epoll_ctl");
        return -1;
    }   

    while(1)
    {
        /* 监听文件描述符状态结构体数组 */
        nready=epoll_wait(epfd,retenv,1024,-1);
        if(-1==nready)
        {
            perror("fail to epoll_wait");
            return -1;
        }

        for(i=0;i<nready;i++)
        {
            if(retenv[i].data.fd==sockfd)
            {
                confd=accept(sockfd,NULL,NULL);
                if(-1==confd)
                {
                    perror("fail to accept");
                    ret=epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
                    if(-1==ret)
                    {
                        perror("fail to epoll_ctl");
                        return -1;
                    }
                    close(sockfd);

                    continue;
                }

                /**/
                #if 0
                fd_add();
                #endif
                tmpenv.events=EPOLLIN;
                tmpenv.data.fd=confd;
                ret=epoll_ctl(epfd,EPOLL_CTL_ADD,confd,&tmpenv);
                if(-1==ret)
                {
                    perror("fail to epoll_ctl");
                    return -1;
                }
            }
            else if(retenv[i].data.fd!=sockfd)
            {
                memset(recvbuffer,0,sizeof(recvbuffer));
                nsize=recv(retenv[i].data.fd,recvbuffer,sizeof(recvbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to recv");
                    return -1;
                }
                printf("server recv: %s\n",recvbuffer);

                memset(sendbuffer,0,sizeof(sendbuffer));
                sprintf(sendbuffer,"|%-3d|",send_cnt--);
                nsize=send(retenv[i].data.fd,sendbuffer,strlen(sendbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to send");
                    return -1;
                }
                printf("server send: %s\n",sendbuffer);


                /* IO异常处理 */
                if(-1==nsize)
                {
                    fprintf(stderr,"server disconnected!\n");//服务器端异常报错
                    ret=epoll_ctl(epfd,EPOLL_CTL_DEL,retenv[i].data.fd,NULL);
                    if(-1==ret)
                    {
                        perror("fail to epoll_ctl");
                        return -1;
                    }
                    close(retenv[i].data.fd);

                    continue;
                }
                else if(0==nsize)
                {
                    fprintf(stderr,"client disconnected!\n");
                    ret=epoll_ctl(epfd,EPOLL_CTL_DEL,retenv[i].data.fd,NULL);
                    if(-1==ret)
                    {
                        perror("fail to epoll_ctl");
                        return -1;
                    }
                    close(retenv[i].data.fd);

                    continue;
                }
            }
        }

        sleep(3);//便于分析运行结果
    }

    close(epfd);
    close(sockfd);

    return 0;
}

(3)服务器函数封装优化:

/*************************************************************************
	> File Name: server.c
	> Author: yas
	> Mail: rage_yas@hotmail.com
	> Created Time: 2024年06月13日 星期四 08时07分01秒
 ************************************************************************/
#include<stdio.h>

#include<stdio.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>

#include <sys/stat.h>
#include <fcntl.h>
    
#include <unistd.h>

#include <string.h>

#include <sys/epoll.h>

int fd_add(int epfd,int fd,uint32_t env);
int fd_delete(int epfd,int fd);

int main(void)
{
    int sockfd=0;

    struct sockaddr_in seraddr;

    int ret=0;

    int confd=0;

    ssize_t nsize=0;
    char sendbuffer[4096]={0};
    char recvbuffer[4096]={0};
    int send_cnt=99;

    int epfd=0;

    int nready=0;

    int i=0;

    struct epoll_event retenv[1024];
    struct epoll_event tmpenv;

    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(-1==sockfd)
    {
        perror("fail to sockfd");
        return -1;
    }

    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(50000);
    seraddr.sin_addr.s_addr=inet_addr("192.168.1.123");

    ret=bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
    if(-1==ret)
    {
        perror("fail to bind");
        return -1;
    }

    ret=listen(sockfd,10);
    if(-1==ret)
    {
        perror("fail to ret");
        return -1;
    }

    /* 1 创建内核事件表 - 并返回表头 */
    epfd=epoll_create(1024);
    if(-1==epfd)
    {
        perror("fail to epoll_create");
        return -1;
    }

    /* 2 初始化内核epoll事件结构体 */
    fd_add(epfd,sockfd,EPOLLIN);

    while(1)
    {
        /* 3 监听文件描述符状态结构体数组 */
        nready=epoll_wait(epfd,retenv,1024,-1);
        if(-1==nready)
        {
            perror("fail to epoll_wait");
            return -1;
        }

        /* 4 轮询处理 */
        for(i=0;i<nready;i++)
        {
            /* 如果sockfd套接字有事件发生, 则建立新的连接(有新的客户端加入) */
            if(retenv[i].data.fd==sockfd)
            {
                confd=accept(sockfd,NULL,NULL);
                /* 连接失败: 将sockfd套接字从事件表中删除 */
                if(-1==confd)
                {
                    perror("fail to accept");
                    fd_delete(epfd,sockfd);
                    close(sockfd);

                    continue;
                }

                /* 连接成功: 将confd通信套接字加入事件表 */
                fd_add(epfd,confd,EPOLLIN);
            }
            /* 如果不是sockfd套接字有事件发生, 则为正在进行通信(数据收发) */
            else if(retenv[i].data.fd!=sockfd)
            {
                memset(recvbuffer,0,sizeof(recvbuffer));
                nsize=recv(retenv[i].data.fd,recvbuffer,sizeof(recvbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to recv");
                    return -1;
                }
                printf("server recv: %s\n",recvbuffer);

                memset(sendbuffer,0,sizeof(sendbuffer));
                sprintf(sendbuffer,"|%-3d|",send_cnt--);
                nsize=send(retenv[i].data.fd,sendbuffer,strlen(sendbuffer),0);
                if(-1==nsize)
                {
                    perror("fail to send");
                    return -1;
                }
                printf("server send: %s\n",sendbuffer);


                /* IO异常处理 */
                if(-1==nsize)
                {
                    fprintf(stderr,"server disconnected!\n");//服务器端异常报错
                    fd_delete(epfd,retenv[i].data.fd);
                    close(retenv[i].data.fd);

                    continue;
                }
                else if(0==nsize)
                {
                    fprintf(stderr,"client disconnected!\n");
                    fd_delete(epfd,retenv[i].data.fd);
                    close(retenv[i].data.fd);

                    continue;
                }
            }
        }

        sleep(3);//便于分析运行结果
    }

    close(epfd);
    close(sockfd);

    return 0;
}

int fd_add(int epfd,int fd,uint32_t env)
{
    struct epoll_event tmpenv;
    int ret=0;

    tmpenv.events=env;
    tmpenv.data.fd=fd;
    ret=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&tmpenv);
    if(-1==ret)
    {
        perror("fail to epoll_ctl");
        return -1;
    }

    return 0;
}

int fd_delete(int epfd,int fd)
{
    int ret=0;

    ret=epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
    if(-1==ret)
    {
        perror("fail to epoll_ctl");
        return -1;
    }

    return 0;
}

3.3.2 运行结果

在这里插入图片描述

3.3.3 分析总结

1.【epoll】没有文件描述符上限限制
2.【epoll】创建内核监听事件表,所以只需要在内核空间完成数据拷贝即可
3.【epoll】会将产生事件的文件描述符对应的事件直接返回
4.【epoll】可以工作在水平触发模式(默认:低速模式),还可以工作在边沿触发模式(高速模式)
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值