网络通信-多路复用

博客围绕服务器 I/O 展开,介绍了阻塞 I/O 中发生阻塞的函数,非阻塞 I/O 的 fcntl 实现方式,以及多路复用中的 select() 函数和 poll() 函数,还给出了 select() 函数的示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阻塞与非阻塞

阻塞说的不是某一个函数,说的是文件描述符的一种属性。因为文件描述符有阻塞属性,所以对这个文件描述符的相关操作是阻塞的。

阻塞 I/O

发生阻塞的函数:

读操作: read recv recvfrom
写操作: write send
其他操作: accept connect
总结:阻塞并不是函数的属性,是文件描述符的属性

非阻塞 I/O

非阻塞实现:fcntl

int flag = fcntl(sockfd,F_GETFL,0); // flag
flag |= O_NONBLOCK; // 修改 flag
fcntl(sockfd,F_SETFL,flag); // flag

多路复用

构建一张有关文件描述符的表,然后调用函数,当这些文件描述符中某个准备好,函数返回告诉哪个描述符就绪,进行相应操作。

 select()函数

 int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

功能:函数可以监视多个文件描述符,监视这些文件描述符是否处于 " 就绪 " 状态。
参数:
nfds:   最大的文件描述符 +1
readfds :读集合,放在这个集合中的文件描述符会被监视是否具备读属性;
writefds :写集合,放在这个集合中的文件描述符会被监视是否具备写属性;
exceptfds:异常属性,放在这个集合中的文件描述符会被监视是否具备异常属性;
返回值:成功返回满足要求的文件描述符的个数,失败返回 -1
一旦有文件描述符 " 就绪 "
1. 解除阻塞
2. 从集合中删除除了刚刚解除其阻塞的文件描述符之外的所有文件描述符。

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set); 从集合中删除一个文件描述符 fd
int FD_ISSET(int fd, fd_set *set); 判断集合中有没有这个文件描述符 fd
void FD_SET(int fd, fd_set *set); 将一个文件描述符 fd 添加到集合中
void FD_ZERO(fd_set *set); 清空集合
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
        nfds : 集合中最大文件描述符 +1
        fd_set :集合类型
        readfds : 监测文件描述符的读属性
        writefds
        exceptfds
        timeout : 超时时间:在规定时间内进行阻塞,超过这个时间就解除阻塞 0 :不阻塞 >0 : 超时时间 NULL :一直阻塞
struct timeval {
        long tv_sec; /* seconds */
        long tv_usec; /* microseconds */
};
fd_set myset;
while(1)
{
        FD_ZERO(&myset);
        FD_SET(0,&myset);//标准输入设备文件
        FD_SET(newfd,&myset); //客户端 socket 文件描述符
        select(newfd+1,&myset,NULL,NULL,NULL);
        if(FD_ISSET(0,&myset)) //标准输入解除阻塞
        {
                scanf( ... );
                send( ... );
        }
        if(FD_ISSET(newfd,&myset)) //newfd 解除阻塞
        {
                recv( ... );
        }
}
定义一个读属性的集合;
把相关方的文件描述符添加到读属性的集合;
调用 select 检测读属性的集合 , 只要集合中任意一个文件描述符就绪 , 才解除阻塞;
判断谁就绪 , 做相应的操作;

示例:

/*
  服务端键盘输入发送字符串客户端,客户端键盘输入发送字符串给服务端
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>

int fd;

void *recv_mess(void *arg)
{

	char buf[50];
	int ret;
	pthread_detach(pthread_self());
	while(1) {
		bzero(buf, 50);
		ret = recv(fd, buf, sizeof(buf),0);
		if (ret<=0) break;
		printf("client recv mess=%s\n",buf);

	}

	return NULL;
}

int main(void)
{
	int ret;
	//1.要有电话
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd==-1) {
		perror("socket");
		exit(1);
	}
	
	//2.绑卡
	#if 0
	struct sockaddr_in client;
	client.sin_family = AF_INET; //ipv4
	client.sin_port = htons(12346);
	client.sin_addr.s_addr = inet_addr("192.168.10.251");
	//inet_aton("192.168.10.251", &server.sin_addr);
	
	ret = bind(fd, (struct sockaddr *)&client, sizeof(client));
	if (ret==-1) {
		perror("bind");
		exit(1);
	}
	#endif
	//3.拨号 请求连接返回成功建立连接
	struct sockaddr_in server;
	server.sin_family = AF_INET; //ipv4
	server.sin_port = htons(12345);
	//server.sin_addr.s_addr = inet_addr("192.168.10.251");
	inet_aton("192.168.10.251", &server.sin_addr);
	ret = connect(fd, (struct sockaddr *)&server, sizeof(server));
	
	if (ret==-1) {
		perror("connect");
		exit(1);
	}
	system("netstat -an | grep 12345");	// 查看连接状态
	
	//创建线程
	pthread_t tid;
	pthread_create(&tid, NULL, recv_mess, NULL);

	//4.收发数据
	char buf[50] ;

	while (1) {
		bzero(buf, 50);
		printf("pls input your string:");
		scanf("%s", buf);
		ret = send(fd, buf, strlen(buf), 0); 
		printf("send=%d\n", ret);

	}

	//5.挂电话
	close(fd);
	return 0;
}

/*
    服务端键盘输入发送字符串客户端
    sanf   fd=0
    客户端键盘输入发送字符串给服务端
    recv   fd=newfd
*/

#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>

int main(void)
{
	int ret;
	//1.要有电话
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd==-1) {
		perror("socket");
		exit(1);
	}
	int on=1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); //重用端口号 on设置为真
	//2.绑卡
	struct sockaddr_in server;
	server.sin_family = AF_INET; //ipv4
	server.sin_port = htons(12345);
	server.sin_addr.s_addr = inet_addr("192.168.10.251");
	//inet_aton("192.168.10.251", &server.sin_addr);
	
	ret = bind(fd, (struct sockaddr *)&server, sizeof(server));
	if (ret==-1) {
		perror("bind");
		exit(1);
	}
	//3.开机 监听
	listen(fd, 5);
	
	//4.接听 连接连接
	struct sockaddr_in client; //做客户端来电显示对象  ip+port
	socklen_t len=sizeof(client);
	printf("tcp sever have started ...\n");
	system("netstat -an | grep 12345");	// 查看连接状态
	int newfd = accept(fd, (struct sockaddr *)&client, &len); //建立连接前处于阻塞状态
	if (newfd==-1) {
		perror("accept");
		exit(1);
	}
	// 打印连接服务器端的客户端的ip和port
	printf("ip=%s\n", inet_ntoa(client.sin_addr));
	printf("port=%d\n", ntohs(client.sin_port));
	printf("len=%d\n", len);
	printf("newfd=%d\n", newfd);

	//5.收发数据
	//  键盘输入 fd1=0 ,  客户端输入  fd2=newfd,用select 监控fd1,fd2
	//1)定义一个读属性的集合
	fd_set myset;
	char buf[50];
	while(1) {
		//2)把相关的文件描述符添加到读属性的集合
		FD_ZERO(&myset);
		FD_SET(0, &myset);
		FD_SET(newfd, &myset);
		//3)调用select检测读属性的集合,只要集合中任意一个文件描述符就绪,才解除阻塞
		ret=select(newfd+1, &myset, NULL, NULL, NULL);
		printf("select ret=%d\n", ret);
	
		//4)判断谁就绪,做相应的操作
		if(FD_ISSET(0, &myset)) {
			//键盘输入
			bzero(buf, 50);
			//printf("pls a string:");
			scanf("%s", buf);
			ret = send(newfd, buf, strlen(buf), 0); 
			printf("send=%d\n", ret);
		}
		if(FD_ISSET(newfd, &myset)) {
			//客户端输入
			bzero(buf, 50);
			ret = recv(newfd, buf, sizeof(buf), 0);
			printf("recv mess:%s\n", buf);
		}

	}

	//6.挂电话
	close(newfd);
	close(fd);

	return 0;
}

poll() 函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:检测 fds 数组中的文件描述符对应需要检测的属性,如果具备检测的属性,则解除阻塞。
参数:
fds :文件描述符所在结构体的数组。
struct pollfd {
        int fd; /* 文件描述符 */
        short events; /* 检测的事件 */
        short revents; /* 返回的事件 */
    用于判定检测的事件是否发生,不是自己填充,当发生检测事件时由系统填充,用户判断
}
nfds :检测的个数 ( 结构体数组的成员个数 )
timeout >0 : 具体阻塞时间 ( 毫秒 ) 0 :不阻塞 <0 :一直阻塞
返回值:
成功:非 0 0 表示到了阻塞时间还没有发生检测事件
失败: -1 ,并置错误
struct pollfd {
        int fd; /* 文件描述符 */
        short events; /* 请求事件 */
                POLLERR 异常
                POLLRDNORM POLLIN 读                                           
                POLLWRNORM 写
        short revents; /* 返回事件 */ fds[0].revents&POLLIN 用于判断
};
fds :存放文件描述符的结构体数组首地址
nfds :文件描述符的个数
timeout : 超时时间 毫秒为单位 0 :直接返回 >0 : 阻塞时间 -1 :永远阻塞
返回值:成功大于 0 ,失败返回 -1
struct pollfd fds[2];
fds[0].fd = 0;
fds[0].events = POLLIN; // 可读事件
fds[1].fd = fd;
fds[1].events = POLLIN;
poll(fds,2,-1); // 一直阻塞 , 直到某个文件描述符就绪
if(fds[0].revents & POLLIN) // 判断这个文件描述符实际发生的事件与可读事件是否一致
{
        scanf
        send
}
if(fds[1].revents & POLLIN)
{
        recv
}
/*	
	服务端键盘输入发送字符串客户端,客户端键盘输入发送字符串给服务端
	poll函数
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <poll.h>

int main(void)
{
	int ret;
	int fd;
	//1.要有电话
	fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd==-1) {
		perror("socket");
		exit(1);
	}
	
	//2.绑卡
	#if 0
	struct sockaddr_in client;
	client.sin_family = AF_INET; //ipv4
	client.sin_port = htons(12346);
	client.sin_addr.s_addr = inet_addr("192.168.10.251");
	//inet_aton("192.168.10.251", &server.sin_addr);
	
	ret = bind(fd, (struct sockaddr *)&client, sizeof(client));
	if (ret==-1) {
		perror("bind");
		exit(1);
	}
	#endif
	//3.拨号 请求连接返回成功建立连接
	struct sockaddr_in server;
	server.sin_family = AF_INET; //ipv4
	server.sin_port = htons(12345);
	//server.sin_addr.s_addr = inet_addr("192.168.10.251");
	inet_aton("192.168.10.251", &server.sin_addr);
	ret = connect(fd, (struct sockaddr *)&server, sizeof(server));
	
	if (ret==-1) {
		perror("connect");
		exit(1);
	}
	system("netstat -an | grep 12345");	// 查看连接状态

	
	//4.收发数据
	char buf[50] ;

	//创建pollfd数组
	struct pollfd fds[2];
	//分别初始化
	fds[0].fd = 0; //键盘输入
	fds[0].events = POLLIN;//注册事件

	fds[1].fd = fd; //读服务器端数据的fd
	fds[1].events = POLLIN;

	while (1) {
		//调用poll 监控fds数组的fd是否就绪
		poll(fds, 2, -1); //没就绪全部阻塞
		if(fds[0].revents & POLLIN) {
			bzero(buf, 50);
			printf("pls input your string:");
			scanf("%s", buf);
			ret = send(fd, buf, strlen(buf), 0); 
			printf("send=%d\n", ret);
		}

		if(fds[1].revents & POLLIN) {
			bzero(buf, 50);
			ret = recv(fd, buf, sizeof(buf),0);
			if (ret<=0) break;
			printf("client recv mess=%s\n",buf);
		}

	}

	//5.挂电话
	close(fd);
	return 0;
}


/*
	服务端键盘输入发送字符串客户端,客户端键盘输入发送字符串给服务端
*/

#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 *send_mess(void *arg)
{
	int newfd = *((int *)arg);
	int ret;
	char buf[50] ;
	pthread_detach(pthread_self());
	while (1) {
		bzero(buf, 50);
		printf("pls input your string:");
		scanf("%s", buf);
		ret = send(newfd, buf, strlen(buf), 0); 
		printf("send=%d\n", ret);

	}
}

int main(void)
{
	int ret;
	//1.要有电话
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd==-1) {
		perror("socket");
		exit(1);
	}
	
	int on=1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); //重用端口号 on设置为真
	//2.绑卡
	struct sockaddr_in server;
	server.sin_family = AF_INET; //ipv4
	server.sin_port = htons(12345);
	server.sin_addr.s_addr = inet_addr("192.168.10.251");
	//inet_aton("192.168.10.251", &server.sin_addr);
	
	ret = bind(fd, (struct sockaddr *)&server, sizeof(server));
	if (ret==-1) {
		perror("bind");
		exit(1);
	}
	//3.开机 监听
	listen(fd, 5);
	

	//4.接听 连接连接
	struct sockaddr_in client; //做客户端来电显示对象  ip+port
	socklen_t len=sizeof(client);
	printf("tcp sever have started ...\n");
	system("netstat -an | grep 12345");	// 查看连接状态
	int newfd = accept(fd, (struct sockaddr *)&client, &len); //建立连接前处于阻塞状态
	if (newfd==-1) {
		perror("accept");
		exit(1);
	}
	// 打印连接服务器端的客户端的ip和port
	printf("ip=%s\n", inet_ntoa(client.sin_addr));
	printf("port=%d\n", ntohs(client.sin_port));
	printf("len=%d\n", len);

	
	//创建线程
	pthread_t tid;
	pthread_create(&tid, NULL, send_mess, &newfd);
	
	//5.收发数据
	char buf[50];
	while (1) {
		bzero(buf, 50);
		ret = recv(newfd, buf, sizeof(buf), 0);
		printf("recv mess:%s\n", buf);
		printf("ret=%d\n", ret);
		if (ret==0) break;
	}
	
	//6.挂电话
	close(newfd);
	close(fd);

	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宁静的海2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值