Linux网络编程:多路I/O转接服务器(select poll epoll)

文章目录:

一:select

1.基础API

select函数

思路分析

select优缺点

2.server.c

3.client.c

二:poll

1.基础API

poll函数

poll优缺点

read函数返回值

[突破1024 文件描述符限制](#突破1024 文件描述符限制)

2.server.c

3.client.c

三:epoll

1.基础API

[epoll_create创建 epoll_ctl操作 epoll_wait阻塞](#epoll_create创建 epoll_ctl操作 epoll_wait阻塞)

epoll实现多路IO转接思路

epoll优缺点

ctags使用

2.server.c

3.client.c

[4.事件模型(epoll 事件触发模型ET和LT)](#4.事件模型(epoll 事件触发模型ET和LT))

[4.1 server.c](#4.1 server.c)

[4.2 client.c](#4.2 client.c)

[5.epoll 反应堆模型](#5.epoll 反应堆模型)


select、poll以及epoll都是系统内核来对网络通信中的通信套接字(文件描述符)来进行监视
能够在与服务器连接的大量客户端中识别出与服务器请求了数据交换的客户端,并把它们所对应的套接字通过函数返回,交给服务器
此时服务器只需要和请求了数据交换的客户端进行通信即可,而其它的套接字则不做任何处理

因此,比起服务器自身每次去轮询查询并处理每个套接字的效率要高很多

一:select

1.基础API

select函数

原理:  借助内核, select 来监听, 客户端连接、数据通信事件


//将给定的套接字fd从位图set中清除出去
    void FD_CLR(int fd,fd_set* set);			
        FD_CLR(4, &rset);                    将一个文件描述符从监听集合中 移除

//检查给定的套接字fd是否在位图里面,返回值 在1 不在0
    int FD_ISSET(int fd,fd_set* set);	
        FD_ISSET(4,&rset);                   判断一个文件描述符是否在监听集合中

//将给定的套接字fd设置到位图set中		
    void FD_SET(int fd,fd_set* set);            将待监听的文件描述符,添加到监听集合中	
        FD_SET(3, &rset);	
        FD_SET(5, &rset);	
        FD_SET(6, &rset);

//将整个位图set置零		
    void FD_ZERO(fd_set* set);					
        fd_set rset;                            清空一个文件描述符集
		FD_ZERO(&rset);


//select 是一个系统调用,用于监控多个文件描述符(sockets, files等)的 I/O 活动
//它等待某个文件描述符集变为可读、可写或出现异常,然后返回	
    int select(int nfds, 
               fd_set *readfds, 
               fd_set *writefds,
               fd_set *exceptfds, 
               struct timeval *timeout);

		nfds     :监听 所有文件描述符中,最大文件描述符+1
		readfds  :读   文件描述符监听集合。	传入、传出参数
		writefds :写   文件描述符监听集合。	传入、传出参数		NULL
		exceptfds:异常 文件描述符监听集合	传入、传出参数		NULL

		timeout: 	
                > 0 : 设置监听超时时长
				NULL:阻塞监听
				0   :非阻塞监听,轮询

		返回值:
			> 0:所有监听集合(3个)中, 满足对应事件的总数
			  0:没有满足监听条件的文件描述符
			 -1:errno

思路分析

	int maxfd = 0;
	lfd = socket() ;			    创建套接字
	maxfd = lfd;                   备份
	bind();					        绑定地址结构
	listen();				        设置监听上限

	fd_set rset, allset;			创建r读监听集合
	FD_ZERO(&allset);				将r读监听集合清空
	FD_SET(lfd, &allset);			将 lfd 添加至读集合中
        lfd文件描述符在监听期间没有满足读事件发生,select返回的时候rset不会在集合中

	while(1) {
		rset = allset;			                                保存监听集合
		ret  = select(lfd+1, &rset, NULL, NULL, NULL);		监听文件描述符集合对应事件
		if(ret > 0) {							                有监听的描述符满足对应事件
            //处理连接:一次监听		
			if (FD_ISSET(lfd, &rset)) {				            1 在集合中,0不在
				cfd = accept();				                建立连接,返回用于通信的文件描述符
				maxfd = cfd;
				FD_SET(cfd, &allset);				            添加到监听通信描述符集合中
			}
            //处理通信:剩下的
			for (i = lfd+1; i <= 最大文件描述符; i++){
                //嵌套
				    FD_ISSET(i, &rset)				            有read、write事件
				read()
				小 -- 大
				write();
			}	
		}
	}

select优缺点

当你只需要监听几个指定的套接字时, 需要对整个1024的数组进行轮询, 效率降低

缺点:监听上限受文件描述符限制。 最大1024
      检测满足条件的fd,自己添加业务逻辑提高小,提高了编码难度

      如果监听的文件描述符比较散乱、而且数量不多,效率会变低


优点:	跨平台win、linux、macOS、Unix、类Unix、mips

2.server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>

#include "wrap.h"

#define SERV_PORT 6666


void FD_CLR(int fd,fd_set* set);			//将给定的套接字fd从位图set中清除出去
int FD_ISSET(int fd,fd_set* set);			//检查给定的套接字fd是否在位图里面,返回0或1
void FD_SET(int fd,fd_set* set);			//将给定的套接字fd设置到位图set中
void FD_ZERO(fd_set* set);					//将整个位图set置零


int main(int argc, char *argv[]){
	int i, j, n, maxi;
	
		/*数组:将需要轮询的客户端套接字放入数组client[FD_SETSIZE],防止遍历1024个文件描述符  FD_SETSIZE默认为1024*/
		int nready, client[FD_SETSIZE];		
		
		int listenFd, connectFd, maxFd, socketFd;
		
		char buf[BUFSIZ], str[INET_ADDRSTRLEN];					//#define INET_ADDRSTRLEN 16

		struct sockaddr_in serverAddr, clientAddr;
		
		socklen_t clientAddrLen;
		
		fd_set rset, allset;                            		//rset读事件文件描述符集合,allset用来暂存
	
	
	/*得到监听套接字*/
		listenFd = Socket(AF_INET, SOCK_STREAM, 0);
			/*定义两个集合,将listenFd放入allset集合当中*/
		fd_set rset, allset;
		FD_ZERO(&allset);										//将整个位图set置零
		//将给定的套接字fd设置到位图set中
		FD_SET(listenFd, &allset);								//将connectFd加入集合:构造select监控文件描述符集
	
	
	/*设置地址端口复用*/
		int opt = 1;
		setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
	
	
	/*填写服务器地址结构*/
		bzero(&serverAddr, sizeof(serverAddr));
		
		serverAddr.sin_family = AF_INET;
		serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
		serverAddr.sin_port = htons(SERVER_PORT);
	
	
	/*绑定服务器地址结构*/
		Bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
		Listen(listenFd, 128);
	
	
	/*将listenFd设置为数组中最大的Fd*/
		maxFd = listenFd;										//起初 listenfd 即为最大文件描述符
		maxi = -1;												//将来用作client[]的下标, 初始值指向0个元素之前下标位置
		
	
	/*数组:初始化自己的数组为-1*/
		for (i = 0; i < FD_SETSIZE; ++i)
			client[i] = -1;

	while (1){
		/*把allset给rest,让他去用*/
		rset = allset;											//备份:每次循环时都从新设置select监控信号集
		nready = select(maxFd + 1, &rset, NULL, NULL, NULL);	//使用select监听文件描述符集合对应事件

		if (nready == -1)										//出错返回
			perr_exit("select error");

		/*listen满足监听的事件:如果有了新的连接请求,得到connectFd,并将其放入自定义数组中*/
			if (FD_ISSET(listenFd, &rset)){						//检查给定的套接字fd是否在位图里面,返回0或1
				clientAddrLen = sizeof(clientAddr);
				
				//建立链接,不会阻塞
				connectFd = Accept(listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
				
				printf(
					"Recived from %s at PORT %d\n", 
					inet_ntop(AF_INET, 
					&(clientAddr.sin_addr.s_addr), 
					str, 
					sizeof(str)), 
					ntohs(clientAddr.sin_port));

				for (i = 0; i < FD_SETSIZE; ++i)
					if (client[i] < 0){							//找client[]中没有使用的位置
						client[i] = connectFd;					//保存accept返回的文件描述符到client[]里	
						break;
					}
					
				/*自定义数组满了:达到select能监控的文件个数上限 1024 */
					if(i==FD_SETSIZE){
						fputs("Too many clients\n",stderr);
						exit(1);
					}
				
				/*connectFd加入监听集合:向监控文件描述符集合allset添加新的文件描述符connectFd*/
					FD_SET(connectFd, &allset);					//将给定的套接字fd设置到位图set中

				/*更新最大的Fd*/
					if (maxFd < connectFd)
						maxFd = connectFd;
					
				/*更新循环上限*/
					if(i>maxi)
						maxi=i;									//保证maxi存的总是client[]最后一个元素下标
					
				/*select返回1,说明只有建立连接请求,没有数据传送请求,跳出while循环剩余部分(下面的for循环轮询过程)*/
				//如果只有listen事件,只需建立连接即可,无需数据传输,跳出循环剩余部分
					if (--nready == 0)
						continue;
			}
		/*检测哪个clients 有数据就绪:select返回不是1,说明有connectFd有数据传输请求,遍历自定义数组*/
		//否则,说明有数据传输需求
			for (i = 0; i <= maxi; ++i){
				if((socketFd=client[i])<0)
					continue;
					
				/*遍历检查*/
				if (FD_ISSET(socketFd, &rset)){					//检查给定的套接字fd是否在位图里面,返回0或1
					/*read返回0说明传输结束,关闭连接:当client关闭链接时,服务器端也关闭对应链接*/
					if ((n=read(socketFd,buf,sizeof(buf)))==0){
						close(socketFd);
						//将给定的套接字fd从位图set中清除出去
						FD_CLR(socketFd, &allset);				//解除select对此文件描述符的监控
						client[i]=-1;
					}else if(n>0){
						for (j = 0; j < n; ++j)
							buf[j] = toupper(buf[j]);
						write(socketFd, buf, n);
						write(STDOUT_FILENO, buf, n);
					}
					
					/*不懂:需要处理的个数减1?*/
					if(--nready==0)
						break;									//跳出for, 但还在while中
				}
			}
	}
	close(listenFd);
	return 0;
}

3.client.c

/* client.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, n;

    if (argc != 2) {
        printf("Enter: ./client server_IP\n");
        exit(1);
    }

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("------------connect ok----------------\n");

    while (fgets(buf, MAXLINE, stdin) != NULL) {
        Write(sockfd, buf, strlen(buf));
        n = Read(sockfd, buf, MAXLINE);
        if (n == 0) {
            printf("the other side has been closed.\n");
            break;
        }
        else
            Write(STDOUT_FILENO, buf, n);
    }
    Close(sockfd);

    return 0;
}

二:poll

这个函数是一个半成品,用的很少

1.基础API

poll函数

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
		fds:监听的文件描述符,传入传出【数组】
			struct pollfd {			
				  int fd      :待监听的文件描述符				
				  short events:待监听的文件描述符对应的监听事件
						  取值:POLLIN、POLLOUT、POLLERR
				 short revnets:
                            传入时,给0
                            如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
			}

		nfds: 监听数组的,实际有效监听个数

		timeout:  
             > 0:超时时长。单位:毫秒
			  -1:阻塞等待
			   0:不阻塞

		返回值:返回满足对应监听事件的文件描述符 总个数

poll优缺点

优点:
		自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离
		拓展 监听上限。 超出 1024限制


缺点:
		不能跨平台。 Linux
		无法直接定位满足监听事件的文件描述符, 编码难度较大

read函数返回值

	> 0: 实际读到的字节数

	=0: socket中,表示对端关闭。close()

	-1:	
        如果 errno == EINTR                    被异常终端                         需要重启
		如果 errno == EAGIN 或 EWOULDBLOCK     以非阻塞方式读数据,但是没有数据    需要,再次读
		如果 errno == ECONNRESET               说明连接被 重置                    需要 close(),移除监听队列
		错误

突破1024 文件描述符限制

cat /proc/sys/fs/file-max     ------> 当前计算机所能打开的最大文件个数。 受硬件影响

ulimit -a 	                  ------> 当前用户下的进程,默认打开文件描述符个数。  缺省为 1024

修改:
    打开 sudo vi /etc/security/limits.conf, 写入:
        * soft nofile 65536			    --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
        * hard nofile 100000			--> 命令修改上限

2.server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024



int main(int argc,char* argv[]){
	int ret=0;
	/*poll函数返回值*/
	int nready=0;
	int i,j,maxi;
	int connectFd,listenFd,socketFd;
	ssize_t n;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	socklen_t clientLen;
	
	
	
	/*创建结构体数组*/	
		struct pollfd client[OPEN_MAX];
	
	/*创建客户端地址结构和服务器地址结构*/
		struct sockaddr_in clientAddr,serverAddr;
	
	/*得到监听套接字listenFd*/
		listenFd=Socket(AF_INET,SOCK_STREAM,0);
	
	
	
	/*设置地址可复用*/
		int opt=0;
		ret=setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
		if(ret==-1)
			perr_exit("setsockopt error");
		
	/*向服务器地址结构填入内容*/
		bzero(&serverAddr,sizeof(serverAddr));
		serverAddr.sin_family=AF_INET;
		serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
		serverAddr.sin_port=htons(SERVER_PORT);
	
	/*绑定服务器地址结构到监听套接字,并设置监听上限*/
		Bind(listenFd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
		Listen(listenFd,128);
	
	/*初始化第一个pollfd为监听套接字*/
		client[0].fd=listenFd;					//listenfd监听普通读事件 
		client[0].events=POLLIN;				//事件已经准备好被读取或处理
	
	/*将pollfd数组的余下内容的fd文件描述符属性置为-1*/
		for(i=1;i<OPEN_MAX;++i)
			client[i].fd=-1;					//用-1初始化client[]里剩下元素
			
		maxi=0;									//client[]数组有效元素中最大元素下标
	
		while(1){
		/*nready是有多少套接字有POLLIN请求*/
			nready=poll(client,maxi+1,-1);		//阻塞
			if(nready==-1)
				perr_exit("poll error");
				
			
		/*如果listenFd的revents有POLLIN请求,则调用Accept函数得到connectFd*/
			if(client[0].revents&POLLIN){		//有客户端链接请求
				clientLen=sizeof(clientAddr);
				connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientLen);
				
				/*打印客户端地址结构信息*/
					printf("Received from %s at PORT %d\n",
							inet_ntop(AF_INET,&(clientAddr.sin_addr.s_addr),str,sizeof(str)),
							ntohs(clientAddr.sin_port));
					
				/*将创建出来的connectFd加入到pollfd数组中*/
					for(i=1;i<OPEN_MAX;++i)
						if(client[i].fd<0){
							//找到client[]中空闲的位置,存放accept返回的connfd 
							client[i].fd=connectFd;			
							break;
						}

					if(i==OPEN_MAX)
						perr_exit("Too many clients,I'm going to die...");
						
				/*当没有错误时,将对应的events设置为POLLIN*/
					client[i].events=POLLIN;	//设置刚刚返回的connfd,监控读事件

					if(i>maxi)						
						maxi=i;					//更新client[]中最大元素下标
					if(--nready<=0)
						continue;				//没有更多就绪事件时,继续回到poll阻塞
			}
		
		
		/*开始从1遍历pollfd数组*/
			for(i=1;i<=maxi;++i){				//检测client[] 
				/*到结尾了或者有异常*/
					if((socketFd=client[i].fd)<0)
						continue;
						
				/*第i个客户端有连接请求,进行处理	read*/
				if(client[i].revents&POLLIN){
					if((n=read(socketFd,buf,sizeof(buf)))<0){
						/*出错时进一步判断errno*/
							if(errno=ECONNRESET){
								printf("client[%d] aborted connection\n",i);
								close(socketFd);
								client[i].fd=-1;
							}else
								perr_exit("read error");
					}else if(n==0){
						/*read返回0,说明读到了结尾,关闭连接*/
							printf("client[%d] closed connection\n",i);
							close(socketFd);
							client[i].fd=-1;
					}else{
						/*数据处理*/
							for(j=0;j<n;++j)
								buf[j]=toupper(buf[j]);
							Writen(STDOUT_FILENO,buf,n);
							Writen(socketFd,buf,n);
					}
					if(--nready==0)
						break;
				}
			}
	}
	return 0;
}

3.client.c

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);

	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	while (fgets(buf, MAXLINE, stdin) != NULL) {
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, MAXLINE);
		if (n == 0)
			printf("the other side has been closed.\n");
		else
			Write(STDOUT_FILENO, buf, n);
	}
	Close(sockfd);
	return 0;
}

三:epoll

epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率:都连接但不发送数据

1.基础API

红黑树

lfd数据连接


cfd数据通信

epoll_create创建 epoll_ctl操作 epoll_wait阻塞

int epoll_create(int size);						                                    创建一棵监听红黑树
		size:创建的红黑树的监听节点数量(仅供内核参考)
		返回值:
            成功:指向新创建的红黑树的根节点的 fd
			失败: -1 errno


int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	                 操作控制监听红黑树
		epfd:epoll_create 函数的返回值 epfd

		op  :对该监听红黑数所做的操作
			EPOLL_CTL_ADD 添加fd到 监听红黑树
			EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件
			EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)

		fd:待监听的fd			

		event:本质struct epoll_event 结构体 地址
			成员 events:EPOLLIN / EPOLLOUT / EPOLLERR				
                EPOLLIN :	表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
		        EPOLLOUT:	表示对应的文件描述符可以写
		        EPOLLPRI:	表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
		        EPOLLERR:	表示对应的文件描述符发生错误
		        EPOLLHUP:	表示对应的文件描述符被挂断;
		        EPOLLET: 	将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
		        EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
                
			成员 typedef union epoll_data: 联合体(共用体)
				int fd;	      对应监听事件的 fd
				void *ptr; 
				uint32_t u32;
				uint64_t u64;		

		返回值:成功 0; 失败: -1 errno


int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 	   阻塞监听
		epfd:epoll_create 函数的返回值 epfd
		events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体
		maxevents:数组 元素的总个数 1024(不是字节数)				
			       struct epoll_event evnets[1024]
		timeout:
			-1: 阻塞------------通过等待某些特定条件出现来实现的,而在等待的过程中,程序的其他部分都会被暂停执行
			 0:不阻塞
			>0: 超时时间 (毫秒)

		read返回值:
			> 0: 满足监听的 总个数,可以用作循环上限
			  0:没有fd满足监听事件
			 -1:失败,errno

epoll实现多路IO转接思路

lfd = socket();			                    监听连接事件lfd
bind();
listen();


int epfd = epoll_create(1024);				    epfd, 监听红黑树的树根

    struct epoll_event tep, ep[1024];			tep, 用来设置单个fd属性, ep是epoll_wait() 传出的满足监听事件的数组
    tep.events = EPOLLIN;					    初始化  lfd的监听属性_文件描述符可以读
    tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep);		将 lfd 添加到监听红黑树上


while (1) {
	ret = epoll_wait(epfd, ep,1024, -1);		                           阻塞监听

	for (i = 0; i < ret; i++) {		
        //lfd数据连接
		if (ep[i].data.fd == lfd) {				                           lfd 满足读事件,有新的客户端发起连接请求
			cfd = Accept();

			tep.events = EPOLLIN;				                           初始化  cfd的监听属性_文件描述符可以读
			tep.data.fd = cfd;

			epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);                    将 cfd 添加到监听红黑树上
		}
         //cfd数据通信
         else {						                                       cfd 们 满足读事件, 有客户端写数据来
			n = read(ep[i].data.fd, buf, sizeof(buf));
			if ( n == 0) {
				close(ep[i].data.fd);
				epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL);	   将关闭的cfd,从监听树上摘下
			} else if (n > 0) {
				小--大
				write(ep[i].data.fd, buf, n);
			}
		}
	}
}

epoll优缺点

优点:
		高效。突破1024文件描述符

缺点:
		不能跨平台。 Linux

ctags使用

是vim下方便代码阅读的工具1

    `ctags ./* -R`在项目目录下生成ctags文件;
    
    `Ctrl+]`跳转到函数定义的位置;

    `Ctrl+t`返回此前的跳转位置;

    `Ctrl+o`屏幕左边列出文件列表, 再按关闭;

    `F4`屏幕右边列出函数列表, 再按关闭;



(还是VSCode比较香)

2.server.c

#include "033-035_wrap.h"

#define SERVER_PORT 9527
#define MAXLINE     80
#define OPEN_MAX    1024

int main(int argc,char* argv[]){
    int i=0,n=0,num=0;
    int clientAddrLen=0;
    int listenFd=0,connectFd=0,socketFd=0;
    ssize_t nready,efd,res;
    char buf[MAXLINE],str[INET_ADDRSTRLEN];

    struct sockaddr_in serverAddr,clientAddr;
	
    /*创建一个临时节点temp和一个数组ep*/
		struct epoll_event temp;
		struct epoll_event ep[OPEN_MAX];




    /*创建监听套接字*/
		listenFd=Socket(AF_INET,SOCK_STREAM,0);
	
    /*设置地址可复用*/
		int opt=1;
		setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));

    /*初始化服务器地址结构*/
		bzero(&serverAddr,sizeof(serverAddr));
		serverAddr.sin_family=AF_INET;
		serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
		serverAddr.sin_port=htons(SERVER_PORT);

    /*绑定服务器地址结构*/
		Bind(listenFd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
		
    /*设置监听上限*/
		Listen(listenFd,128);

    /*创建监听红黑树树根*/
		efd=epoll_create(OPEN_MAX);
		if(efd==-1)
			perr_exit("epoll_create error");

    /*将listenFd加入监听红黑树中*/
		temp.events=EPOLLIN;
		temp.data.fd=listenFd;
		res=epoll_ctl(efd,EPOLL_CTL_ADD,listenFd,&temp);
		if(res==-1)
			perr_exit("epoll_ctl error");

    while(1){
        /*阻塞监听写事件*/
			nready=epoll_wait(efd,ep,OPEN_MAX,-1);
			if(nready==-1)
				perr_exit("epoll_wait error");

        /*轮询整个数组(红黑树)*/
			for(i=0;i<nready;++i){
				if(!(ep[i].events&EPOLLIN))
					continue;

            /*如果是建立连接请求*/
				// lfd 满足读事件,有新的客户端发起连接请求
				if(ep[i].data.fd==listenFd){
					clientAddrLen=sizeof(clientAddr);
					connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
					
					printf("Received from %s at PORT %d\n",
							inet_ntop(AF_INET,
							&clientAddr.sin_addr.s_addr,
							str,
							sizeof(str)),
							ntohs(clientAddr.sin_port));
					printf("connectFd=%d,client[%d]\n",connectFd,++num);

					/*将新创建的连接套接字加入红黑树*/
						//初始化  cfd的监听属性_文件描述符可以读
							temp.events=EPOLLIN;
							temp.data.fd=connectFd;
							
							res=epoll_ctl(efd,EPOLL_CTL_ADD,connectFd,&temp);
						
						if(res==-1)
							perr_exit("epoll_ctl errror");
				}else{
					/*不是建立连接请求,是数据处理请求*/
						socketFd=ep[i].data.fd;
						//cfd 们 满足读事件, 有客户端写数据来
						n=read(socketFd,buf,sizeof(buf));
				
						/*读到0说明客户端关闭*/
							//已经读到结尾
							if(n==0){
								res=epoll_ctl(efd,EPOLL_CTL_DEL,socketFd,NULL);
								if(res==-1)
									perr_exit("epoll_ctl error");
								close(socketFd);
								printf("client[%d] closed connection\n",socketFd);
							//报错
							}else if(n<0){	
								/*n<0报错*/
									perr_exit("read n<0 error");
									
									// 将关闭的cfd,从监听树上摘下
										res=epoll_ctl(efd,EPOLL_CTL_DEL,socketFd,NULL);
										close(socketFd);
							//   > 0实际读到的字节数
							}else{
								/*数据处理*/
									for(i=0;i<n;++i)
										buf[i]=toupper(buf[i]);
									write(STDOUT_FILENO,buf,n);
									Writen(socketFd,buf,n);
							}
				}
        }
    }

    close(listenFd);
    close(efd);
    return 0;
}

3.client.c

/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);

	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	while (fgets(buf, MAXLINE, stdin) != NULL) {
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, MAXLINE);
		if (n == 0)
			printf("the other side has been closed.\n");
		else
			Write(STDOUT_FILENO, buf, n);
	}

	Close(sockfd);
	return 0;
}

4.事件模型(epoll 事件触发模型ET和LT)

ET工作模式:边沿触发------------只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致
    作用:当文件描述符从未就绪变为就绪时,内核会通过epoll告诉你一次喊你就绪,直到你做操作导致那个文件描述符不再为就绪状态
		     缓冲区未读尽的数据不会导致epoll_wait返回, 新的数据写入才会触发(等文件描述符不再为就绪状态)		
			    struct epoll_event event
			    event.events = EPOLLIN | EPOLLET

LT工作模式:水平触发------------只要有数据都会触发(默认采用模式)
    作用:内核告诉你一个文件描述符是否就绪,然后可以对这个就绪的fd进行io操作,如果你不做任何操作,内核还会继续通知你
	        缓冲区未读尽的数据会导致epoll_wait返回(继续通知你)
	
结论:epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式
             --- 忙轮询:用于在计算机系统中处理硬件中断
                     忙轮询是一种不进入内核的方式,它在用户空间中轮询检测硬件状态
                     及时响应硬件的中断请求,避免CPU在中断服务程序中处理完所有的中断请求后,又再次触发中断

		struct epoll_event event;
		event.events = EPOLLIN | EPOLLET;

		epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);	

		int flg = fcntl(cfd, F_GETFL);	 非阻塞
		flg |= O_NONBLOCK;
		fcntl(cfd, F_SETFL, flg);

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>

#define MAXLINE 10

int main(int argc, char *argv[])
{
    int efd, i;
    int pfd[2];
    pid_t pid;
    char buf[MAXLINE], ch = 'a';

    pipe(pfd);
    pid = fork();

    if (pid == 0) {             //子 写
        close(pfd[0]);
        while (1) {
            //aaaa\n
            for (i = 0; i < MAXLINE/2; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //bbbb\n
            for (; i < MAXLINE; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //aaaa\nbbbb\n
            write(pfd[1], buf, sizeof(buf));
            sleep(5);
        }
        close(pfd[1]);

    } else if (pid > 0) {       //父 读
        struct epoll_event event;
        struct epoll_event resevent[10];          //epoll_wait就绪返回event
        int res, len;

        close(pfd[1]);
        efd = epoll_create(10);

        event.events = EPOLLIN | EPOLLET;         // ET 边沿触发
       // event.events = EPOLLIN;                 // LT 水平触发 (默认)
        event.data.fd = pfd[0];
        epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);

        while (1) {
            res = epoll_wait(efd, resevent, 10, -1);
            printf("res %d\n", res);
            if (resevent[0].data.fd == pfd[0]) {
                len = read(pfd[0], buf, MAXLINE/2);
                write(STDOUT_FILENO, buf, len);
            }
        }

        close(pfd[0]);
        close(efd);

    } else {
        perror("fork");
        exit(-1);
    }

    return 0;
}

4.1 server.c

#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>

#define MAXLINE 10
#define SERV_PORT 9000

int main(void)
{
    struct sockaddr_in servaddr, cliaddr;
    socklen_t cliaddr_len;
    int listenfd, connfd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int efd;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    listen(listenfd, 20);

    struct epoll_event event;
    struct epoll_event resevent[10];
    int res, len;

    efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;     /* ET 边沿触发 */
    //event.events = EPOLLIN;                 /* 默认 LT 水平触发 */

    printf("Accepting connections ...\n");

    cliaddr_len = sizeof(cliaddr);
    connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
    printf("received from %s at PORT %d\n",
            inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
            ntohs(cliaddr.sin_port));

    event.data.fd = connfd;
    epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);

    while (1) {
        res = epoll_wait(efd, resevent, 10, -1);

        printf("res %d\n", res);
        if (resevent[0].data.fd == connfd) {
            len = read(connfd, buf, MAXLINE/2);         //readn(500)   
            write(STDOUT_FILENO, buf, len);
        }
    }

    return 0;
}

4.2 client.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define MAXLINE 10
#define SERV_PORT 9000

int main(int argc, char *argv[])
{
    struct sockaddr_in servaddr;
    char buf[MAXLINE];
    int sockfd, i;
    char ch = 'a';

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(SERV_PORT);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (1) {
        //aaaa\n
        for (i = 0; i < MAXLINE/2; i++)
            buf[i] = ch;
        buf[i-1] = '\n';
        ch++;
        //bbbb\n
        for (; i < MAXLINE; i++)
            buf[i] = ch;
        buf[i-1] = '\n';
        ch++;
        //aaaa\nbbbb\n
        write(sockfd, buf, sizeof(buf));
        sleep(5);
    }
    close(sockfd);

    return 0;
}

5.epoll 反应堆模型

作用:提高网络IO处理的效率



epoll ET模式 + 非阻塞、轮询 + void *ptr
    void *ptr:指向结构体,该结构体包含socket、地址、端口等信息


原来:epoll实现多路IO转接思路
		socket、bind、listen -- epoll_create 创建监听 红黑树 --  返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--

		-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 

		-- read() --- 小->大 -- write回去


反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件

		socket、bind、listen -- epoll_create 创建监听 红黑树 --  返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--

		-- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 

		-- read() --- 小->大 
		
		-- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听"写"事件-- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 
		
		-- cfd从监听红黑树上摘下 -- EPOLLIN -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听"读"事件 -- epoll_wait 监听

		


eventset函数:设置回调函数
				lfd --> acceptconn()
				cfd --> recvdata();
				cfd --> senddata();
				
eventadd函数:将一个fd, 添加到 监听红黑树
              设置监听读事件,还是监听写事件


网络编程中: read --- recv()            write --- send();

epoll基于非阻塞I/O事件驱动

/*
 *epoll基于非阻塞I/O事件驱动
 */
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define MAX_EVENTS  1024                                    						//监听上限数
#define BUFLEN 4096
#define SERV_PORT   8080															//默认端口号

void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);

/* 描述就绪文件描述符相关信息 */
struct myevent_s {
    int fd;                                                 						//要监听的文件描述符
    int events;                                             						//对应的监听事件
    void *arg;                                              						//泛型参数
    void (*call_back)(int fd, int events, void *arg);       						//回调函数
    int status;                                             						//是否在监听:1->在红黑树上(监听), 0->不在(不监听)
    char buf[BUFLEN];
    int len;
    long last_active;                                       						//记录每次加入红黑树 g_efd 的时间值
};

int g_efd;                                                  						//全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1];                    						//自定义结构体类型数组. +1-->listen fd


	/*将结构体 myevent_s 成员变量 初始化赋值*/
	void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
	{
		ev->fd = fd;
		ev->call_back = call_back;													//设置回调函数
		ev->events = 0;
		ev->arg = arg;
		ev->status = 0;
		memset(ev->buf, 0, sizeof(ev->buf));
		ev->len = 0;
		ev->last_active = time(NULL);                       						//调用eventset函数的时间

		return;
	}

	/* 向 epoll监听的红黑树 添加一个 文件描述符 */
	//eventadd函数: 将一个fd添加到监听红黑树, 设置监听读事件还是写事件
	//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
	void eventadd(int efd, int events, struct myevent_s *ev)
	{
		struct epoll_event epv = {0, {0}};
		int op;
		epv.data.ptr = ev;
		epv.events = ev->events = events;      									    //EPOLLIN 或 EPOLLOUT

		if (ev->status == 0) {                                          			//已经在红黑树 g_efd 里
			op = EPOLL_CTL_ADD;                 									//将其加入红黑树 g_efd, 并将status置1
			ev->status = 1;
		}

		if (epoll_ctl(efd, op, ev->fd, &epv) < 0)                       			//实际添加/修改
			printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		else
			printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);

		return ;
	}

	/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
	void eventdel(int efd, struct myevent_s *ev)
	{
		struct epoll_event epv = {0, {0}};

		if (ev->status != 1)                                        				//不在红黑树上
			return ;

		//epv.data.ptr = ev;
		epv.data.ptr = NULL;
		ev->status = 0;                                             				//修改状态
		epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);                				//从红黑树 efd 上将 ev->fd 摘除

		return ;
	}

			/*  当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
			void acceptconn(int lfd, int events, void *arg)
			{
				struct sockaddr_in cin;
				socklen_t len = sizeof(cin);
				int cfd, i;

				if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
					if (errno != EAGAIN && errno != EINTR) {
						/* 暂时不做出错处理 */
					}
					printf("%s: accept, %s\n", __func__, strerror(errno));
					return ;
				}

				do {
					for (i = 0; i < MAX_EVENTS; i++)                               			//从全局数组g_events中找一个空闲元素
						if (g_events[i].status == 0)                                		//类似于select中找值为-1的元素
							break;                                                  		//跳出 for

					if (i == MAX_EVENTS) {
						printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
						break;                                                      		//跳出do while(0) 不执行后续代码
					}

					int flag = 0;
					if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {             		//将cfd也设置为非阻塞
						printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
						break;
					}

					/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
					eventset(&g_events[i], cfd, recvdata, &g_events[i]);   
					eventadd(g_efd, EPOLLIN, &g_events[i]);                         		//将cfd添加到红黑树g_efd中,监听读事件

				} while(0);

				printf("new connect [%s:%d][time:%ld], pos[%d]\n", 
						inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
				return ;
			}

			//epoll反应堆-wait被触发后read和write回调及监听	
				void recvdata(int fd, int events, void *arg)
				{
					struct myevent_s *ev = (struct myevent_s *)arg;
					int len;

					len = recv(fd, ev->buf, sizeof(ev->buf), 0);            				//读文件描述符, 数据存入myevent_s成员buf中

					eventdel(g_efd, ev);        //将该节点从红黑树上摘除

					if (len > 0) {

						ev->len = len;
						ev->buf[len] = '\0';                                				//手动添加字符串结束标记
						printf("C[%d]:%s\n", fd, ev->buf);

						eventset(ev, fd, senddata, ev);                     				//设置该 fd 对应的回调函数为 senddata
						eventadd(g_efd, EPOLLOUT, ev);                      				//将fd加入红黑树g_efd中,监听其写事件

					} else if (len == 0) {
						close(ev->fd);
						/* ev-g_events 地址相减得到偏移元素位置 */
						printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
					} else {
						close(ev->fd);
						printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
					}

					return;
				}

				void senddata(int fd, int events, void *arg)
				{
					struct myevent_s *ev = (struct myevent_s *)arg;
					int len;

					len = send(fd, ev->buf, ev->len, 0);                    				//直接将数据 回写给客户端。未作处理

					eventdel(g_efd, ev);                                					//从红黑树g_efd中移除

					if (len > 0) {

						printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
						eventset(ev, fd, recvdata, ev);                     				//将该fd的 回调函数改为 recvdata
						eventadd(g_efd, EPOLLIN, ev);                       				//从新添加到红黑树上, 设为监听读事件

					} else {
						close(ev->fd);                                      				//关闭链接
						printf("send[fd=%d] error %s\n", fd, strerror(errno));
					}

					return ;
				}


	/*创建 socket, 初始化lfd */
	void initlistensocket(int efd, short port)
	{
		struct sockaddr_in sin;

		//将socket设为lfd非阻塞
		int lfd = socket(AF_INET, SOCK_STREAM, 0);
		fcntl(lfd, F_SETFL, O_NONBLOCK);                                            

		//设置地址结构
		memset(&sin, 0, sizeof(sin));                                               //bzero(&sin, sizeof(sin))
		sin.sin_family = AF_INET;
		sin.sin_addr.s_addr = INADDR_ANY;
		sin.sin_port = htons(port);

		bind(lfd, (struct sockaddr *)&sin, sizeof(sin));

		listen(lfd, 20);

		/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg);  */
		 /*把g_events数组的最后一个元素设置为lfd,回调函数设置为acceptconn*/
			eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);

		/* void eventadd(int efd, int events, struct myevent_s *ev) */
		/*挂上树*/
			eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);

		return ;
	}




int main(int argc, char *argv[])
{
	/*选择默认端口号或指定端口号*/
    unsigned short port = SERV_PORT;

    if (argc == 2)
		//使用用户指定端口.如未指定,用默认端口
        port = atoi(argv[1]);                           								

	//创建红黑树,返回给全局 g_efd
    g_efd = epoll_create(MAX_EVENTS+1);                 								 
    if (g_efd <= 0)
        printf("create efd in %s err %s\n", __func__, strerror(errno));

	//初始化监听socket
    initlistensocket(g_efd, port);                      								

	//创建一个系统的epoll_event的数组,与my_events的规模相同
		struct epoll_event events[MAX_EVENTS+1];           								//保存已经满足就绪事件的文件描述符数组 
		printf("server running:port[%d]\n", port);

    int checkpos = 0, i;
    while (1) {
        /* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */	
			long now = time(NULL);                          							//当前时间
			for (i = 0; i < 100; i++, checkpos++) {         							//一次循环检测100个。 使用checkpos控制检测对象
				if (checkpos == MAX_EVENTS)
					checkpos = 0;
				if (g_events[checkpos].status != 1)        								//不在红黑树 g_efd 上
					continue;

				long duration = now - g_events[checkpos].last_active;       			//时间间隔,客户端不活跃的世间

				if (duration >= 60) {
					close(g_events[checkpos].fd);                           			//关闭与该客户端链接
					printf("[fd=%d] timeout\n", g_events[checkpos].fd);
					eventdel(g_efd, &g_events[checkpos]);                   			//将该客户端 从红黑树 g_efd移除
				}
			}

        /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
			int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
			if (nfd < 0) {
				printf("epoll_wait error, exit\n");
				break;
			}

			for (i = 0; i < nfd; i++) {
				/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
				struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;  

				//cfd从监听红黑树上摘下  
				if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {          	//读就绪事件
					ev->call_back(ev->fd, events[i].events, ev->arg);
					//lfd  EPOLLIN  
				}
				if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {         //写就绪事件
					ev->call_back(ev->fd, events[i].events, ev->arg);
				}
			}
    }

    /* 退出前释放所有资源 */
    return 0;
}
相关推荐
冬天vs不冷1 小时前
Linux用户与权限管理详解
linux·运维·chrome
凯子坚持 c2 小时前
深入Linux权限体系:守护系统安全的第一道防线
linux·运维·系统安全
✿ ༺ ོIT技术༻2 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
watermelonoops5 小时前
Deepin和Windows传文件(Xftp,WinSCP)
linux·ssh·deepin·winscp·xftp
疯狂飙车的蜗牛6 小时前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
远游客07138 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<8 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟8 小时前
centos-stream9系统安装docker
linux·docker·centos
超爱吃士力架8 小时前
邀请逻辑
java·linux·后端
fantasy_arch9 小时前
CPU性能优化-磁盘空间和解析时间
网络·性能优化