Linux C 网络编程(select 与 poll)

11 篇文章 0 订阅
5 篇文章 0 订阅

功能

有了套接字我们便可以在客户端和服务器之间传递信息,由send和recv函数来发送和接收文件,而我们不能提前知道客户端或服务器什么时候发送信息,如果客户端比较多,还要判断是哪一个客户端发来的信息。这让我们需要类似于监听套接字一样的功能(有连接就返回),select, poll, epoll都提供了这些功能,当有信息发送就返回有信息的套接字描述符,然后用recv接收信息。

select

select介绍

在客户端/服务器模型中,服务器端需要同时处理多个客户端的连接请求,此时就需要使用多路复用。实现多路复用最简单的方法是采用非阻塞方式套接字(阻塞与非阻塞),服务器端不断地查询各个套接字的状态,如果有数据到达则读出数据,如果没有数据则查看下-一个套接字。这种方法虽然简单,但在轮询过程中浪费了大量的CPU时间,效率非常低。另一种方法是服务器进程并不主动地询问套接字状态,而是向系统登记希望监视的套接字,然后阻塞。当套接字上有事件发生时(如有数据到达),系统通知服务器进程告知哪个套接字上发生了什么事件,服务器进程查询对应套接字并进行处理。在这种工作方式下,套接字上没有事件发生时,服务器进程不会去查询套接字的状态,从而不会浪费CPU时间,提高了效率。使用函数select可以实现第二二种多路复用,在Shell下输入“man select"可获得该函数的原型:

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数n是需要监视的文件描述符数,要监视的文件描述符值为0~n-1.参数readfds指定需要监视的可读文件描述符集合,当这个集合中的一个描述符上有数据到达时,系统将通知调用select函数的程序。参数writefds指定需要监视的可写文件描述符集合,当这个集合中的某个描述符可以发送数据时,程序将收到通知。参数exceptfds指定需要监视的异常文件描述符集合,当该集合中的一个描述符发生异常时,程序将收到通知。参数timeout指定了阻塞的时间,如果在这段时间内监视的文件描述符上都没有事件发生,则函数select)将返回0

struct timeval{
	long tv_sec;
	long tv_usec;
}

成员tv_ sec指定秒数,tv_ usec指定微秒数。如果将timeout设为NULL,则函数select0)将一直被阻塞,直到某个文件描述符上发生了事件。如果将timeout设为0,则此时相当于非阻塞方式,函数select(查询完文件描述符集合的状态后立即返回。如果将timeout设成某-一时间值, 在这个时间内如果没有事件发生,函数select(将返回;如果在这段时间内有事件发生,程序将收到通知。
注意:这里的文件描述符既可以是普通文件的描述符,也可以是套接字描述符。

FD_CLR(int fd, fd_set *set);   //将文件描述符fd从文件描述符集合set中删除
FD_ISSET(int fd, fd_set *set); //测试fd是否在set中
FD_SET(int fd, fd_set *set);   //在文件描述符集合set中增加文件描述符fd
FD_ZERO(fd_set *set);          //将文件描述符集合set清空

如果select()设定的要监视的文件描述符集合中有描述符发生了事件,则select将返回发生事件的文件描述符的个数。

select应用

服务器:server.c
客户端:client.c
server.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> //atoi()
 
int get_max_socket_fd(int socket_arr[]);//获取最大的通信socket描述符。
 
int main(int argc, char *argv[])
{
	//判断命令行参数是否满足
	if(argc != 2)
	{
		printf("请传递一个端口号\n");
		return -1;
	}
 
	//将接收端口号并转换为int
	int port = atoi(argv[1]);
	if( port<1025 || port>65535 )//0~1024一般给系统使用,一共可以分配到65535
	{
		printf("端口号范围应为1025~65535");
		return -1;
	}
 
	//1 创建tcp通信socket
	int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(socket_fd == -1)
	{
		perror("创建tcp通信socket失败!\n");
		return -1;
	}
 
	//2 绑定socket地址
	struct sockaddr_in server_addr = {0};//存放地址信息
	server_addr.sin_family = AF_INET;//AF_INET->IPv4  
	server_addr.sin_port = htons(port);//端口号
	server_addr.sin_addr.s_addr = INADDR_ANY;//让系统检测本地网卡,自动绑定本地IP
	int mw_optval = 1;
    setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&mw_optval,sizeof(mw_optval));
	int ret = bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr) );
	if(ret == -1)
	{
		perror("bind failed!\n");
		return -1;
	}
 
	//3 设置监听队列,设置为可以接受5个客户端连接
	ret = listen(socket_fd, 5);
	if(ret == -1)
	{
		perror("listen falied!\n");
	}
 
 
	printf("server is running!\n");
 
	
	struct sockaddr_in client_addr = {0};//用来保存客户端的地址信息
	int len = sizeof(client_addr);
	int socket_fd_arr[5] = {-1, -1, -1, -1, -1};//用来保存5个客户端的通信socket
	
 
	//定义一个文件描述符集合
	fd_set fds;
	//定义一个时间结构体  
	struct timeval  time;
	time.tv_sec = 3;//超时时间
	time.tv_usec = 0;
	//循环监视文件描述符集合 
	int new_socket_fd = -1;
	int max_socket_fd = -1;//保存最大的socket描述符
	int index = -1, i;
	
	//循环监视文件描述符集合
	while(1)
	{
		//清空文件描述符集合
		FD_ZERO(&fds);
		//把标准输入设备加入到集合中 
		FD_SET(0, &fds);
		FD_SET(socket_fd, &fds);//将监听socket添加到集合中
		
		//把网络通信文件描述符加入到集合中 
		for(i=0; i<=index; i++)
		{
			FD_SET(socket_fd_arr[i],&fds);
		}
		
		//获取最大的通信socket描述符。
		max_socket_fd = get_max_socket_fd(socket_fd_arr);
		if(max_socket_fd == -1)
		{
			max_socket_fd = socket_fd;
		}
		
		ret = select(max_socket_fd+1,&fds,NULL,NULL,0);//检查集合中是否有活跃的描述符
		if(ret < 0)//错误
		{
			perror("select fail");
			return -1;
		}
		else if(ret == 0) //超时
		{
			printf("timeout1\n");
		}
		else if(ret > 0) //有活跃的 
		{
			if(FD_ISSET(socket_fd,&fds))//监听socket活跃,说明有客户端请求连接
			{
				new_socket_fd = accept( socket_fd, (struct sockaddr *)&client_addr, &len);
				if(new_socket_fd == -1)
				{
					perror("accpet error!\n");
					continue;
				}
				else
				{
					if(index>=5)
					{
						index = 4;
						printf("index>=5\n");
						continue;
					}
					socket_fd_arr[++index] = new_socket_fd;//将通信socket保存到数组中
					printf("[ID:%d] IP:%s, PORT:%d [connected]\n", index, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
					
				}
			}
			
			//判断是否 标准输入设备活跃 假设是则发送数据 
			if(FD_ISSET(0,&fds))
			{
				char buf[1024] = {0};
				int client = -1;
				scanf("%d %s", &client, buf);
				if(client>=0 && client<=index)
				{
					write(socket_fd_arr[client],buf,strlen(buf));
					
				}
				else //给所有的客户端发消息
				{
					for(i=0; i<=index; i++)
					{
						write(socket_fd_arr[i],buf,strlen(buf));
					}
			
				}
				if(strcmp(buf, "exit") == 0)
				{
					break;
				}
			}
			
			//判断是否有收到消息 
			for(i=0; i<=index; i++)
			{
				if(FD_ISSET(socket_fd_arr[i],&fds))//判断通信socket是否有活跃
				{
					char buf[1024] = {0};
					read(socket_fd_arr[i], buf, sizeof(buf));        //读出数据
					
					if(strcmp(buf, "exit") == 0 || strcmp(buf, "") == 0)
					{
						break;
					}
					else if(strlen(buf)>0)
					{
						printf("receive msg form [ID:%d]:%s\n", i, buf);      //打出数据
					}
				}
			}
		}
		
	}
 
	//5 关闭通信socket
	close(new_socket_fd);
	close(socket_fd);
 
	return 0;
}
 
//获取最大的通信socket描述符。
int get_max_socket_fd(int socket_arr[])
{
	int max = -1;
	int i;
	for(i=0; i<5; i++)
	{
		if(socket_arr[i]>max)
		{
			max = socket_arr[i];
		}
	}
	
	return max;
}

client.c

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h> //atoi()
 
int main(int argc, char *argv[])
{
	//检查命令行参数是否匹配
	if(argc != 3)
	{
		printf("请传递要连接的服务器的ip和端口号");
		return -1;
	}
	
	int port = atoi(argv[2]);      //从命令行获取端口号
	if( port<1025 || port>65535 )       //0~1024一般给系统使用,一共可以分配到65535
	{
		printf("端口号范围应为1025~65535");
		return -1;
	}
	
	//1 创建tcp通信socket
	int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(socket_fd == -1)
	{
		perror("socket failed!\n");
	}
 
	//2 连接服务器
	struct sockaddr_in server_addr = {0};//服务器的地址信息
	server_addr.sin_family = AF_INET;//IPv4协议
	server_addr.sin_port = htons(port);//服务器端口号
	server_addr.sin_addr.s_addr = inet_addr(argv[1]);         //设置服务器IP
	int ret = connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));       //连接服务器
	if(ret == -1)
	{
		perror("connect failed!\n");
	}
	else
	{
		printf("connect server successful!\n");
	}
 
	//要监视的描述符集合
	fd_set fds;
	
	//3 send msg
	while(1)
	{
		FD_ZERO(&fds);                  //清空文件描述符集合
		
		FD_SET(0,&fds);                 //把标准输入设备加入到集合中 
		
		FD_SET(socket_fd,&fds);         //把网络通信文件描述符加入到集合中 
		
		ret = select(socket_fd+1,&fds,NULL,NULL,NULL);
		if(ret < 0)//错误
		{
			perror("select fail:");
			return -1;
		}
		else if(ret > 0) //有活跃的
		{
			//判断是否 标准输入设备活跃 假设是则发送数据
			if(FD_ISSET(0,&fds))
			{
				char buf[1024] = {0};
				scanf("%s",buf);
				write(socket_fd,buf,strlen(buf));
				if(strcmp(buf, "exit") == 0)
				{
					break;
				}
			}
 
			//判断是否有收到数据
			if(FD_ISSET(socket_fd,&fds))
			{
				char buf[1024]={0};
				read(socket_fd,buf,sizeof(buf));
				printf("receive msg:%s\n",buf);
				if(strcmp(buf, "exit") == 0 || strcmp(buf, "") == 0)
				{
					break;
				}
 
 
			}
 
 
		}
	
	}
 
	//4 关闭通信socket
	close(socket_fd);
 
	return 0;
}
 

服务器:

  • gcc server.c
  • ./a.out 9999
    客户端:
  • gcc client.c
  • ./a.out 127.0.0.1 9999
    运行结果:
    注意:因为输入信息使用的是scanf所以发送数据只能发送一个单词,遇到空格会出错。服务器发送信息格式 客户端编号 + 空格 + 一个单词

服务器:

server is running!
[ID:0] IP:127.0.0.1, PORT:59898 [connected]       //可能不同
[ID:1] IP:127.0.0.1, PORT:59900 [connected]       //可能不同
receive msg form [ID:0]:hello       //接收来自客户端0的信息
0 hi                                //向客户端0发送信息
receive msg form [ID:1]:hi          //接收来自客户端1的信息
1 hello                             //向客户端1发送信息

客户端0:

connect server successful!
hello               //发送给服务器
receive msg:hi      //接收到来自服务器的信息

客户端1:

connect server successful!
hi                  //发送给服务器
receive msg:hello   //接收到来自服务器的信息

poll

因为select有套接字个数上有限制,要解决这个问题才出现了poll,我学习poll是看的别人的一篇博客,这里我就不多说了 POLL讲解 因为这篇博客给的代码中只有服务器(server)代码,客户端代码可以使用上面select中的client.c的代码。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值