linux下TCP socket编程入门案例(二)——非阻塞的TCP server&client

14 篇文章 1 订阅
6 篇文章 0 订阅


上一篇【阻塞的TCP server&client】中,介绍了如何使用socket函数编写第一个socket通信小程序。这篇文章在第一个demo的基础上,将使用select函数实现非阻塞的TCP server&client。

1、相关概念介绍

1.1 阻塞与非阻塞

在理解这个概念前,你要知道在linux系统中,一切皆是文件,不管是普通文件、输入输出设备、目录,或者是套接字,都被linux当做文件处理。

1)阻塞是指,当试图对某个文件描述符进行读写时,如果当前没有东西可读,或者暂时不可写,程序就进入等待状态,直到有东西可读或者可写为止
而对于非阻塞状态,如果没有东西可读,或者不可写,读写函数马上返回,而不会等待(这也与设置的超时时间有关)。
2)非阻塞,就是进程或线程执行某个函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。

1.2 两者区别

①阻塞好控制,不发送完数据程序不会往下走,但是对性能有影响;
非阻塞不太好控制,可能和能力有关,但是性能会得到很大提升。
②阻塞式的编程方便,非阻塞的编程不方便,需要开发人员处理各种返回;
③阻塞处理简单,非阻塞处理复杂;
④阻塞效率低,非阻塞效率高;
⑤阻塞模式,常见的通信模型为多线程模型,服务端accept之后,对每个socket创建一个线程去recv。逻辑上简单,适用于并发量小(客户端数目少),连续传输大数据量的情况下,比如文件服务器。还有就是在客户端接收服务器消息的时候也经常用,因为客户端就一个socket,用阻塞模式不影响效率,而且编程逻辑上要简单得多。
非阻塞模式,常见的通信模型为select模型IOCP模型,适用于高并发,数据量小的情况,比如聊天室;客户端多的情况下,如果采用阻塞模式,需要开很多线程,影响效率。


注意,不要和同步、异步的概念搞混了。下面的解释来源于网络:

I.所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。最常见的例子就是 SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息之前,这个函数不返回。当对方处理完毕以后,该函数才把消息处理函数所返回的 LRESULT值返回给调用者。
II.异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。以 CAsycSocket类为例(注意,CSocket从CAsyncSocket派生,但是其功能已经由异步转化为同步),当一个客户端通过调用 Connect函数发出一个连接请求后,调用者线程立刻可以向下运行。当连接真正建立起来以后,socket底层会发送一个消息通知该对象。这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。可以使用哪一种依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误)。如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

同步/异步和阻塞/非阻塞的区别

1.阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回;
对同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已
2.非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

1.3 select模型

好了,对这些有了了解后,可以帮助你更好的理解select模型。
函数原型:
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
返回值:成功返回做好准备的文件描述符的个数;超时为0;错误为 -1.
参数
maxfdp:是一个整数值,是指集合中所有文件描述符的范围即所有文件描述符的最大值加1,不能错。+1的原因:[0,maxfd],描述符是从0开始的,因此如果最大的描述符为n的话,共有n+1个描述符
fd_set *readset:指向fd_set结构的指针,监视这些文件描述符的读变化;
fd_set *writeset:监视集合中文件描述符的写变化,只要有一个文件可写,函数就返回一个大于0的值;
fd_set *exceptset:同上,监视集合中错误异常文件;
struct timeval *timeout:select的超时时间。这个参数至关重要,它可以使select处于三种状态,
第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一直等到监视文件描述符集合中某个文件描述符发生变化为止;
第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

I.传递给select函数的参数会告诉内核:

  • 我们所关心的文件描述符
  • 对每个描述符,我们所关心的状态。(是想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)
  • 要等待多长时间。(可以等待无限长的时间;等待固定的一段时间;或者根本就不等待)

II.从select函数返回后,内核会告诉我们:

  • 对我们的要求已经做好准备的描述符的个数
  • 对于三种条件(读,写,异常)哪些描述符已经做好准备

对fd_set类型的变量,可以使用以下几个宏控制它
int FD_ZERO(int fd, fd_set *set); //将一个 fd_set类型变量的所有位都置为 0

int FD_CLR(int fd, fd_set *set); //清除某个位

int FD_SET(int fd, fd_set *set); //将变量的某个位置位

int FD_ISSET(int fd, fd_set *set); //测试某个位是否被置位

理解select模型,关键是理解fd_set。为了方便说明,以fd_set长度为1B(1字节)为例,fd_set的每一位(bit)可以对应一个文件描述符(fd)。则1B的fd_set可以对应8个fd.

1)执行fd_set set,FD_ZERO(&set);则set用位表示是0000,0000
2)若fd=3,执行FD_SET(fd,&set);后set变为0000,0100(第3位为1)
3)若再加入fd=2,fd=1,则set变为0000,0111
4)执行select(5,&set,0,0,0)阻塞等待
5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=3位置被清空。

注意,
a.可监控的文件描述符个数取决与sizeof(fd_set)的值;
b.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,把array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
c.必须在select前循环array(加入fd,取maxfd),select返回后循环array(使用FD_ISSET判断是否有(读/写/异常)事件发生)。

2、编码实现

2.1 代码改进

在阻塞的TCP server&client中,加入select函数
即在listen之后,将套接字描述符全部加入fd_set,然后按照下面的顺序编写代码
1)FD_ZERO()清空fd_set;
2)FD_SET()将要测试的fd加入fd_set;
3)select()测试fd_set中所有的fd;
4)FD_ISSET()测试是否有符合条件的描述符

2.2 实现

服务端

server.cpp

/*
 * server.cpp --非阻塞TCP server
 *
 *  Created on: Nov 23, 2019
 *      Author: xb
 */
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>

#define CONCURRENT_MAX 3	//服务端同时支持的最大连接数
#define SERVER_PORT 9999	//端口
#define BUFFER_SIZE 1024	//缓冲区大小

int main(int argc, char* argv[]) {

	int client_fd[CONCURRENT_MAX] = {0};//用于存放客户端套接字描述符
	int server_sock_fd;//服务器端套接字描述符
	char send_msg[BUFFER_SIZE];//数据传输缓冲区
	char recv_msg[BUFFER_SIZE];
	struct sockaddr_in server_addr;

	memset(&server_addr,0,sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	//server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//限定只接受本地连接请求
	server_addr.sin_addr.s_addr = INADDR_ANY;

	/*测试*/
	/*for(int a = 0; a < CONCURRENT_MAX; a++){
		printf("client_fd[%d] = %d\n",a,client_fd[a]);
	}*/

	//1.创建socket
	server_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_sock_fd < 0) {
		printf("create socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//2.绑定socket和端口号
	if (bind(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(server_addr)) < 0) {
		printf("bind socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//3.监听listen
	if (listen(server_sock_fd, 5) < 0) {
		printf("listen socket error:%s(errno:%d)\n",strerror(errno),errno);
		return -1;
	}

	//fd_set
	fd_set server_fd_set; //文件描述符集合
	int max_fd = -1;
	struct timeval tv;

	while (1) {
		tv.tv_sec = 10;/* 超时时间10s */
		tv.tv_usec = 0;
		/*
		1)FD_ZERO()清空fd_set;
		2)FD_SET()将要测试的fd加入fd_set;
		3)select()测试fd_set中所有的fd;
		4)FD_ISSET()测试是否有符合条件的描述符
		*/
		FD_ZERO(&server_fd_set); 
		//STDIN_FILENO:接收键盘输入
		FD_SET(STDIN_FILENO, &server_fd_set);
		if (max_fd < STDIN_FILENO) {
			max_fd = STDIN_FILENO;
		}
		//printf("STDIN_FILENO=%d\n", STDIN_FILENO);//STDIN_FILENO = 0
		//服务器端socketfd
		FD_SET(server_sock_fd, &server_fd_set);
		// printf("server_sock_fd=%d\n", server_sock_fd);
		if (max_fd < server_sock_fd) {
			max_fd = server_sock_fd;
		}

		//客户端连接
		for (int i = 0; i < CONCURRENT_MAX; i++) {
			//printf("client_fd[%d]=%d\n", i, client_fd[i]);
			if (client_fd[i] != 0) {
				FD_SET(client_fd[i], &server_fd_set);
				if (max_fd < client_fd[i]) {
					max_fd = client_fd[i];
				}
			}
		}
		/*
			int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
			maxfdp1:集合中所有文件描述符的范围,即最大值+1
			readset:监视文件描述符的读变化
			writeset:监视写变化
			exceptset:监视错误异常文件
			timeout:超时时间

			函数返回值:<0:发生错误;
						=0:没有满足条件的文件描述符,等待超时;
						>0:文件描述符满足条件
		*/
		int result = select(max_fd + 1, &server_fd_set, NULL, NULL, &tv);
		if (result < 0) {
			printf("select error:%s(errno:%d)\n",strerror(errno),errno);
			continue;
		} else if (result == 0) {
			printf("select 超时\n");
			continue;
		} else {
			//result为位状态发生变化的文件描述符的个数
			//STDIN_FILENO:系统API接口库,是打开文件的句柄
			/*  服务器端输入信息  */
			if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
				bzero(send_msg, BUFFER_SIZE);//清空
				scanf("%s",&send_msg);
				//输入"q"则关闭服务端
				if (strcmp(send_msg, "q") == 0) {
					close(server_sock_fd);
					exit(0);
				}
				for (int i = 0; i < CONCURRENT_MAX; i++) {
					if (client_fd[i] != 0) {
						printf("发送消息给客户端:");
						printf("client_fd[%d]=%d\n", i, client_fd[i]);
						send(client_fd[i], send_msg, strlen(send_msg), 0);
					}
				}
			}
			/*  处理新的连接请求  */
			if (FD_ISSET(server_sock_fd, &server_fd_set)) {
				struct sockaddr_in client_address;
				socklen_t address_len;
				int client_sock_fd = accept(server_sock_fd,(struct sockaddr *) &client_address, &address_len);
				printf("new connection client_sock_fd = %d\n", client_sock_fd);
				if (client_sock_fd > 0) {
					int index = -1;//判断连接数量是否达到最大值
					for (int i = 0; i < CONCURRENT_MAX; i++) {
						//如果还有空闲的连接数量,就分配给新的连接
						if (client_fd[i] == 0) {
							index = i;
							client_fd[i] = client_sock_fd;
							break;
						}
					}
					if (index >= 0) {
						printf("新客户端[%d] [%s:%d]连接成功\n", index,
								inet_ntoa(client_address.sin_addr),
								ntohs(client_address.sin_port));
					} else {
						bzero(send_msg, BUFFER_SIZE);
						strcpy(send_msg, "服务器已连接的客户端数量达到最大值,连接失败!\n");
						send(client_sock_fd, send_msg, strlen(send_msg), 0);
						printf("客户端连接数量达到最大值,新客户端[%s:%d]连接失败\n",
								inet_ntoa(client_address.sin_addr),
								ntohs(client_address.sin_port));
					}
				}
			}
			/*  处理某个客户端发过来的消息  */
			for (int i = 0; i < CONCURRENT_MAX; i++) {
				if (client_fd[i] != 0) {
					if (FD_ISSET(client_fd[i], &server_fd_set)) {
						bzero(recv_msg, BUFFER_SIZE);
						int n = recv(client_fd[i], recv_msg,BUFFER_SIZE, 0);
						// >0,接收消息成功
						if (n > 0) {
							if (n > BUFFER_SIZE) {
								n = BUFFER_SIZE;
							}
							recv_msg[n] = '\0';
							printf("收到客户端[%d]发来的消息:%s\n", i, recv_msg);
						} else if (n < 0) {
							printf("从客户端[%d]接收消息出错!\n", i);
						} else { //=0,对端连接关闭
							FD_CLR(client_fd[i], &server_fd_set);
							client_fd[i] = 0;
							printf("客户端[%d]断开连接\n", i);
						}
					}
				}
			}
		}
	}

	return 0;
}

客户端

client.cpp

/*
 * client.cpp -- 非阻塞 TCP client
 *
 *  Created on: Nov 23, 2019
 *      Author: xb
 */
#include<stdio.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

#define BUFFER_SIZE 1024

int main(int argc, char* argv[]) {

	char recv_msg[BUFFER_SIZE];
	char send_msg[BUFFER_SIZE];//数据收发缓冲区
	struct sockaddr_in server_addr;
	int server_sock_fd;

	memset(&server_addr,0,sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(9999);
	server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	//创建套接字
	if ((server_sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
		return -1;
	}

	//连接服务器
	if (connect(server_sock_fd, (struct sockaddr *) &server_addr,sizeof(struct sockaddr_in)) < 0) {
		printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
        return -1;
	}

		fd_set client_fd_set;
		struct timeval tv;

		while (1) {
			tv.tv_sec = 2;
			tv.tv_usec = 0;
			FD_ZERO(&client_fd_set);
			FD_SET(STDIN_FILENO, &client_fd_set);
			FD_SET(server_sock_fd, &client_fd_set);

			select(server_sock_fd + 1, &client_fd_set, NULL, NULL, &tv);
			if (FD_ISSET(STDIN_FILENO, &client_fd_set)) {
				bzero(send_msg, BUFFER_SIZE);
				fgets(send_msg, BUFFER_SIZE, stdin);
				if (send(server_sock_fd, send_msg, BUFFER_SIZE, 0) < 0) {
					printf("send message error: %s(errno:%d)\n",strerror(errno),errno);
				}
			}
			if (FD_ISSET(server_sock_fd, &client_fd_set)) {
				bzero(recv_msg, BUFFER_SIZE);
				int n = recv(server_sock_fd, recv_msg, BUFFER_SIZE, 0);
				if (n > 0) {
					printf("recv %d byte\n",n);
					if (n > BUFFER_SIZE) {
						n = BUFFER_SIZE;
					}
					recv_msg[n] = '\0';
					printf("收到服务器发送的信息:%s\n", recv_msg);
				} else if (n < 0) {
					printf("接收消息出错!\n");
				} else {
					printf("服务器已关闭!\n");
					close(server_sock_fd);
					exit(0);
				}
			}
		}

	return 0;
}

3、运行结果

使用g++编译:
g++ server.cpp -o server
g++ client.cpp -o client
程序运行结果:
1)启动server
在这里插入图片描述
2)启动client
在这里插入图片描述
3)发送信息给server
在这里插入图片描述
4)发送信息给client
在这里插入图片描述
第4个client尝试连接server时
在这里插入图片描述
相比较阻塞的TCP server&client,非阻塞的可以连接更多客户端。阻塞的则只能连接一个,新的连接请求会被阻塞,直到上一个连接关闭。

  • 9
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值