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

2023-05-16

文章目录

  • 1、相关概念介绍
    • 1.1 阻塞与非阻塞
    • 1.2 两者区别
    • 1.3 select模型
  • 2、编码实现
    • 2.1 代码改进
    • 2.2 实现
      • 服务端
      • 客户端
  • 3、运行结果

在 上一篇【阻塞的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,非阻塞的可以连接更多客户端。阻塞的则只能连接一个,新的连接请求会被阻塞,直到上一个连接关闭。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • Spyder cell分块运行 run cell

    Spyder是一个使用方便的Python开发环境 xff0c 安装Anaconda时自带 python代码分块 xff1a 使用Spyder xff0c 可以在python文件 xff08 py xff09 里使用 In 进行分块 如下图
  • C语言中实现bool(布尔型变量)

    C语言中 xff0c 本身没有bool xff08 布尔型变量 xff09 但是我们可以用其他方式来模拟 一 如果简单的使用char int long变量来表示0 1 xff0c 则太浪费空间了 二 这里介绍一种巧妙的方式实现bool xf
  • 随机解调-多频点信号与伪随机序列混频

    随机解调的多频点信号x与伪随机序列经过混频后 xff0c 被均匀的涂抹到了整个频率轴上 xff0c 然后经低通滤波 xff0c 低速均匀采样 xff0c 最后通过OMP等算法恢复原始信号 xff0c 整体上是压缩感知求解欠定方程y 61 A
  • 寻路算法 Astar A星算法

    lt span style 61 34 white space pre 34 gt lt span gt lt span style 61 34 white space pre 34 gt lt span gt 首先是创建一些变量 lt p
  • eclipse调试C代码时printf()不能输出到控制台的解决方法

    1 问题 在ecplise下使用CDT开发C C 43 43 程序中 xff0c 使用debug调试时 xff0c 到了printf 打印函数 xff0c 在console窗口中并没有打印出信息来 xff0c 停止后才会有输出 2 原因 在
  • C语言中带参宏定义

    include lt stdio h gt 1 带参宏定义中 xff0c 宏名和形参表之间不能有空格出现 2 宏定义中不存在值传递 xff0c 它只是一个符号的替换过程 3 带参宏定义中 xff0c 形参不分配内存空间 xff0c 因此不必
  • kaldi新手入门及语音识别的流程(标贝科技)

    kaldi新手入门及语音识别的流程 标贝科技 欢迎体验标贝语音开放平台 地址 xff1a https ai data baker com source 61 qaz123 xff08 注 xff1a 填写邀请码hi25d7 xff0c 每日
  • 结构体字节对齐详解【含实例】

    一 前言 结构体字节对齐属于老生常谈的问题 xff0c 看似简单 xff0c 却很容易忘记 而且因为结构体使用的普遍性 xff0c 使得字节对齐也成为了一个不得不谈的话题 二 什么是结构体字节对齐 假设现在有一个结构体如下 xff0c 问你
  • Mina基础(五):编写自定义协议及编解码器

    为什么要制定协议呢 xff1f 我们知道 xff0c 底层传输的都是二进制数据 xff0c 服务端和客户端建立连接后进行数据的交互 xff0c 接受这对方发送来的消息 xff0c 如何判定发送的请求或者响应的数据结束了呢 xff1f 总不能
  • c++之存储类

    C 43 43 存储类 存储类定义 C 43 43 程序中变量 函数的范围 xff08 可见性 xff09 和生命周期 这些说明符放置在它们所修饰的类型之前 下面列出 C 43 43 程序中可用的存储类 xff1a autoregister
  • STM32串口通信 中断配置

    一 关于如何配置通过中断的方式配置串口的收发 xff0c 一共就是这8个步骤 1 使能串口时钟 使能GPIO时钟 2 引脚复用映射 3 GPIO端口模式设置 4 串口参数初始化设置 5 开启中断初始化NVIC 6 使能串口 7 编写中断处理
  • altium designer导出bom表和贴片图

    altium designer的简单使用 xff0c 做一下记录 1 导出bom表 xff0c https jingyan baidu com article cb5d6105133e8f005c2fe0fe html 2 导出贴片图 xf
  • 基于RPLIDAR激光雷达开发无人机机载室内二维重建装置(2)——RPLIDAR测试

    从官网上 xff08 http www slamtec com xff09 下载开发用的SDK及数据手册等相关资料 xff0c 但Arduino相关资料貌似已失效 xff0c 之后尝试从其他渠道下载 先安装对应系统的驱动 xff0c 之后打
  • Visio2016与office2016安装解决方法终极版

    基本解决方案 删除注册表 office软件在我们日常生活中的应用十分广泛 xff0c 在购买电脑时大部分会给电脑装上office家庭版 xff0c 但是有些人由于工作需要可能使用office更多的功能 xff0c 因此可以选择安装专业版 而
  • 微带天线学习

    微带天线学习 侧馈矩形微带天线同轴馈电矩形微带天线双频微带天线 学习方法是根据HFSS软件学习微带天线的优化 参数计算 xff0c 以及根据HFSS仿真结果进一步理解微带天线中参数对天线性能的影响 微带天线贴片尺寸计算方法及matlab代码
  • Zotero IEEE trans期刊cls格式调整

    Zotero IEEE trans期刊cls格式调整 Zotero软件自带的IEEE期刊的引用格式不符合期刊投稿要求 xff0c 因此需要改变cls文件 xff0c 改变引用格式 cls格式编辑网站 xff1a https editor c
  • ADS2021安装

    1 ADS简介 先进设计系统 Advanced Design system xff08 ADS xff09 Agilent Technologies 是领先的电子设计自动化软件 xff0c 适用于射频 微波和信号完整性应用 2 ADS的安装
  • Mysql整体介绍(适用于5.X版本)(上)(标贝科技)

    标贝科技 https ai data baker com source 61 qwer12 填写邀请码fwwqgs xff0c 每日免费调用量还可以翻倍 Mysql整体介绍 xff08 适用于5 X版本 xff09 标贝科技 Mysql 8
  • TE、TM、TEM模式的区别

    在一众电磁仿真软件的使用中 xff0c 牵涉到平面波的设置或Floquet端口的设置 在设置平面波时 xff0c 论坛里有不少人提到TE波 TM波 xff1b 在设置Floquet端口时 xff0c 又有不少人提到TE极化 TM极化 其实
  • linux下TCP socket编程入门案例(二)——非阻塞的TCP server&client

    文章目录 1 相关概念介绍1 1 阻塞与非阻塞1 2 两者区别1 3 select模型 2 编码实现2 1 代码改进2 2 实现服务端客户端 3 运行结果 在 上一篇 阻塞的TCP server amp client 中 xff0c 介绍了