目录
基于Select模型的通信仿真
一、实验要求
编写Win32程序模拟实现基于Select模型的两台计算机之间的通信,要求编程实现服务器端与客户端之间双向数据传递。客户端向服务器端发送“计算从1到100的奇数和”,服务器回应客户端并给出从1到100的奇数和结果。
二、 编程环境
vs2022
由于Win32工程中要自己创建和释放控制台,较为麻烦,所以使用vs2022的控制台应用程序,可以直接在windows的控制台上输出和输入
注意要关掉每个项目的SDL检查,否则可能会编译不通过。
如果要使用Visual C++ 6.0,可参考
https://www.cnblogs.com/wa2211lq/p/18509428
三、流程图(TCP)
1、创建监听套接字 SOCKET()
2、给监听套接字绑定端口号 BIND()
3、给监听套接字开启监听属性 LISTEN()
4、初始化文件描述符集合 FD_ZERO()
5、添加要检测的监听文件描述符 FD_SET()
6、不停地检测文件描述符 while(1)SELECT()
6.1 超时 select()=0 再次检测或关闭套接字
6.2 异常 select()=-1 异常处理
6.3 成功 selct()>0
7、判断文件描述符属于哪一类 FD_ISSET()
通过将原来redset集合中的文件描述符与select处理过的tmp集合比较,判断哪些文件描述符就绪,如果就绪,是哪一类
7.1 是否为监听文件描述符 //监听套接字的读缓冲区是否有数据,有新的连接
等待客户端连接 ACCEPT() //不会阻塞,因为select已经检测过此监听描述符的读缓冲区里有客户端连接请求
添加得到的通信文件描述符 FD_SET()
开始新一轮的检测
7.2 通信文件描述符
接收数据 RECV()
RECV()=0,客户端已断开连接
关闭通信套接字 CLOSE()
从集合中删除该通信文件描述符 FD_CLR()
RECV()>0,服务器接收到客户端的数据
发送数据 SEND()
前三步和socket套接字流程一样,直到第四步开始通过select()函数实现在单个线程内同时并发处理多个套接字连接:
IO多路转接(复用)将对文件描述符的缓冲区的检测交给内核,同时检测多个文件描述符的读写缓冲区,每检测一轮之后内核将可以使用的文件描述符告诉我们,此时再调用accept、recv、send,不会导致阻塞。
四、编程准备工作
创建1个基于select模型的server和3个客户端client1、client2、client3用来模拟并发通信
- 创建控制台项目,不要将解决方案和项目放在同一目录。
- 关掉每个项目的SDL检查,否则可能会编译不通过。
- 正确调用头文件和库
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
- 初始化和清理winsock
// 初始化 Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
//网络编程相关的操作,如创建套接字等
// 清理 Winsock
WSACleanup();
五、select()
批量检测缓冲区
服务器端有两类文件描述符(套接字):
- 监听:(仅1个)
读缓冲区:(检测是否为空)
存储所有客户端的连接请求
accept读取里面是否有有客户端的请求(是否为空),如果为空,就一直阻塞,直到里面有客户端的请求,则解除阻塞,建立连接
写缓冲区 - 通信:(N个:每和一个客户端建立一个新连接,就加1个)
读缓冲区:(检测是否为空)
存储客户端发来的数据
调用recv方法读取,如果为空,则阻塞
写缓冲区:(检测剩余空间)
存储服务器通过send发送的数据
如果写缓冲区满了,则数据无法通过send写入写缓冲区,无法发送到客户端,阻塞,直到写缓冲区的数据被发送到客户端,清空写缓冲 区。
客户端只有通信文件描述符(套接字)
通信:(仅1个)
读缓冲区:(检测是否为空)
存储服务端发来的数据,
调用recv方法读取,如果为空,则阻塞
写缓冲区:(检测剩余空间)
存储客户端通过send要发送的数据
如果写缓冲区满了,则数据无法通过send写入写缓冲区,无法发送到服务器,阻塞,直到写缓冲区的数据被发送到服务器,清空写缓冲区。
accept、recv、send检测缓冲区是否可用,三个互斥,一次只能执行一个(顺序执行),若其中有一个阻塞,则无法继续
IO多路转接(复用)将对文件描述符的缓冲区的检测交给内核,同时检测多个文件描述符的读写缓冲区,每检测一轮之后内核将可以使用的文件描述符告诉我们,此时再调用accept、recv、send,不会导致阻塞
即select模型批量检测缓冲区是否可用
函数及其参数详解
select()
int select //返回值:>0 成功,返回集合中已就绪的文件描述符的总个数;
// 0 超时,没有检测到就绪的文件描述符;-1 函数调用失败
(
int nfds, //检测的三个文件描述符集合中最大的文件描述符+1
//将集合拷贝到内核,内核要线性遍历文件描述符,这个值是循环结束的条件
//Window中此参数无效,指定为-1
fd_set * readfds, //要检测的读集合的指针,检测后可读集合的指针
fd_set * writefds, //要检测的写集合的指针,检测后可写集合的指针
fd_set * exceptfds, //要检测是否有异常的集合的指针,检测后异常集合的指针
//(传入传出参数)内核检测成功后返回可读/可写/异常的文件描述符到对应指针指向的地址
const struct timeval * timeout //select函数检测时长,
//等待固定时长:函数检测不到就绪的文件描述符,在固定时长之后解除阻塞,函数返回0
//如果 timeout 设为 NULL,select 将会无限阻塞。
);
第一个参数
select模型是跨平台的。
在类 Unix 系统中,select 的第一个参数是** maxfd + 1,表示需要检查的文件描述符集合中最大的文件描述符加一。这是循环结束的条件,因为 select 函数会检查所有小于或等于 maxfd 的文件描述符。
在 Windows 系统中,select 的第一个参数是-1**,表示 select 函数应该检查所有套接字,直到找到可读的套接字为止,而不需要指定最大的文件描述符加一。
在 Windows 中,select 函数的第一个参数通常被忽略,因此即使传入 maxfd + 1 也能正常工作,但是最好遵循每个平台的最佳实践。
即在 Windows 上,使用 -1 作为 select 的第一个参数;在类 Unix 系统上,使用 maxfd + 1。
fd_set
typedef struct fd_set {
u_int fd_count; //套接字数量
SOCKET fd_array[FD_SETSIZE]; //套接字集合
};
fd_set类型参数的操作函数
void FD_CLR(int fd,fd_set *set); //将文件描述符fd从set集合中删除(fd对应标志位设为0)
int FD_ISSET(int fd,fd_set *set); //判断文件描述符fd是否在set集合中(判断fd对应标志位是否为1)
void FD_SET(int fd,fd_set *set); //将文件描述符fd添加到set集合中(fd对应标志位设为1)
void FD_ZERO(fd_set *set); //初始化set集合(所有标志位置0)
struct timeval * timeout
struct timeval //总时长=秒+微秒
{
long tv_sec; //秒
long tv_usec; //微秒,用不到时也要初始化为0
};
六、Server端详解
前三步和socket通信一模一样,详情参考
https://www.cnblogs.com/wa2211lq/p/18509428
或直接看下面完整代码
4、初始化文件描述符集合
fd_set redset;
//4、初始化文件描述符集合
FD_ZERO(&redset);
5、添加要检测的监听文件描述符
//5、添加要检测的监听文件描述符
FD_SET(listen_socket,&redset);
6、不停地检测文件描述符
window
printf("This is SERVER!\n");
while (1)
{
fd_set tmp = redset;
/* 6、不停地检测文件描述符
6.1 超时 select() = 0 再次检测或关闭套接字
6.2 异常 select() = -1 异常处理
6.3 成功 selct() >0
*/
printf("selecting...\n");
int ret = select(-1, &tmp, NULL, NULL, NULL);
if (ret <= 0)
{
printf("select failed!!! errcode: %d\n", GetLastError());
closesocket(listen_socket);
WSACleanup();
return -1;
}
// printf("select = %d\n",ret);
//7、判断文件描述符属于哪一类
}
类unix
int maxfd = listen_socket;
printf("This is SERVER!\n");
while (1)
{
fd_set tmp = redset;
/* 6、不停地检测文件描述符
6.1 超时 select() = 0 再次检测或关闭套接字
6.2 异常 select() = -1 异常处理
6.3 成功 selct() >0
*/
printf("selecting...\n");
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if (ret <= 0)
{
printf("select failed!!! errcode: %d\n", GetLastError());
closesocket(listen_socket);
WSACleanup();
return -1;
}
//7、判断文件描述符属于哪一类
}
7、判断文件描述符属于哪一类
window
//7、判断文件描述符属于哪一类
//通过将原来redset集合中的文件描述符与select处理过的tmp集合比较,判断哪些文件描述符就绪,如果就绪,是哪一类
for (int i = 0; i < (int)redset.fd_count; i++)
{
if (FD_ISSET(redset.fd_array[i], &tmp))//判断文件描述符(套接字)i的读缓冲区是否有数据
{
//就绪文件描述符是监听描述符
if (redset.fd_array[i] == listen_socket) // 监听套接字接收到新连接
{
if (redset.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
//接收客户端的连接请求
SOCKET client_socket = ::accept(listen_socket, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(client_socket, &redset);
printf("与主机 %s 建立连接\n", inet_ntoa(addrRemote.sin_addr));
}
else
{
printf("Too much connections!\n");
continue;
}
}
else//就绪文件描述符不是监听描述符,是通信描述符
{
//接收信息
char rbuffer[1024] = { 0 };
int len = recv(redset.fd_array[i], rbuffer, 1024, 0);
if (len <= 0)
{
printf("The client %d has disconnected.\n", i);
FD_CLR(redset.fd_array[i], &redset);
shutdown(redset.fd_array[i], SD_BOTH);
closesocket(redset.fd_array[i]);
break;
}
printf("recive from client%d:\t%s\n", i, rbuffer);
//发送信息
char sbuffer[1024] = { 0 };
// 检查接收到的消息
if (strcmp(rbuffer, "计算从1到100的奇数和") == 0)
{
int sum = 0;
for (int j = 1; j <= 100; j += 2)
{
sum += j;
}
printf("send to client%d:\t1到100的奇数和是 %d\n", i, sum);
sprintf(sbuffer, "1到100的奇数和是 %d\n", sum);
}
else
{
printf("send to client%d:\tunknow!\n", i);
sprintf(sbuffer, "unknow!");
}
len = send(redset.fd_array[i], sbuffer, strlen(sbuffer), 0);
if (len == -1)
{
perror("send error");
exit(1);
}
}
}
}
类unix
//7、判断文件描述符属于哪一类
if (FD_ISSET(listen_socket, &tmp))//7.1监听套接字的读缓冲区有数据
{
printf("\nWaiting for connect...\n");//不阻塞
//等待客户端连接
SOCKET client_socket = accept(listen_socket, NULL, NULL);
if (INVALID_SOCKET == client_socket)
{
printf("Connect invalid!!!\n");
}
else
{
printf("Connected successfully!\n\n");
//添加得到的通信文件描述符
FD_SET(client_socket, &redset);
maxfd = client_socket > maxfd ? client_socket : maxfd;
}
}
for (int i = 0; i <= maxfd; ++i)
{
if (i != listen_socket && FD_ISSET(i, &tmp))// 7.2 i为通信文件描述符(套接字),且读缓冲区有数据
{
//接收信息
char rbuffer[1024] = { 0 };
int len = recv(i, rbuffer, 1024, 0);
if (len <= 0)
{
printf("The client %d has disconnected.\n", i);
FD_CLR(i, &redset);
shutdown(i, SD_BOTH);
closesocket(i);
break;
}
printf("recive from client%d:\t%s\n", i, rbuffer);
//发送信息
char sbuffer[1024] = { 0 };
// 检查接收到的消息
if (strcmp(rbuffer, "计算从1到100的奇数和") == 0)
{
int sum = 0;
for (int j = 1; j <= 100; j += 2)
{
sum += j;
}
printf("send to client%d:\t1到100的奇数和是 %d\n", i, sum);
sprintf(sbuffer, "1到100的奇数和是 %d\n", sum);
}
else
{
printf("send to client%d:\tunknow!\n", i);
sprintf(sbuffer, "unknow!");
}
send(i, sbuffer, strlen(sbuffer), 0);
}
}
七、完整代码
server
window
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 初始化 Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
//1.创建socket套接字
SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == listen_socket)
{
printf("create listen socket failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
//2.给socket绑定端口号
struct sockaddr_in local = { 0 };
local.sin_family = AF_INET;
local.sin_port = htons(8080);//绑定端口
//local.sin_addr.s_addr = htonl(INADDR_ANY);//接收全部网卡的数据 大小端转化
local.sin_addr.s_addr = inet_addr("0.0.0.0");//接收全部网卡的数据 字符串ip转成整数ip
if (-1==bind(listen_socket, (struct sockaddr*)&local, sizeof(local)))
{
printf("bind failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
//3.给socke开启监听属性,只用来接收连接
if (-1 == listen(listen_socket, 10))
{
printf("start listen failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
fd_set redset;
//4、初始化文件描述符集合
FD_ZERO(&redset);
//5、添加要检测的监听文件描述符
FD_SET(listen_socket,&redset);
printf("This is SERVER!\n");
while (1)
{
fd_set tmp = redset;
/* 6、不停地检测文件描述符
6.1 超时 select() = 0 再次检测或关闭套接字
6.2 异常 select() = -1 异常处理
6.3 成功 selct() >0
*/
printf("selecting...\n");
int ret = select(-1, &tmp, NULL, NULL, NULL);
if (ret <= 0)
{
printf("select failed!!! errcode: %d\n", GetLastError());
closesocket(listen_socket);
WSACleanup();
return -1;
}
// printf("select = %d\n",ret);
//7、判断文件描述符属于哪一类
//通过将原来redset集合中的文件描述符与select处理过的tmp集合比较,判断哪些文件描述符就绪,如果就绪,是哪一类
for (int i = 0; i < (int)redset.fd_count; i++)
{
if (FD_ISSET(redset.fd_array[i], &tmp))//判断文件描述符(套接字)i的读缓冲区是否有数据
{
//就绪文件描述符是监听描述符
if (redset.fd_array[i] == listen_socket) // 监听套接字接收到新连接
{
if (redset.fd_count < FD_SETSIZE)
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
//接收客户端的连接请求
SOCKET client_socket = ::accept(listen_socket, (SOCKADDR*)&addrRemote, &nAddrLen);
FD_SET(client_socket, &redset);
printf("与主机 %s 建立连接\n", inet_ntoa(addrRemote.sin_addr));
}
else
{
printf("Too much connections!\n");
continue;
}
}
else//就绪文件描述符不是监听描述符,是通信描述符
{
//接收信息
char rbuffer[1024] = { 0 };
int len = recv(redset.fd_array[i], rbuffer, 1024, 0);
if (len <= 0)
{
printf("The client %d has disconnected.\n", i);
FD_CLR(redset.fd_array[i], &redset);
shutdown(redset.fd_array[i], SD_BOTH);
closesocket(redset.fd_array[i]);
break;
}
printf("recive from client%d:\t%s\n", i, rbuffer);
//发送信息
char sbuffer[1024] = { 0 };
// 检查接收到的消息
if (strcmp(rbuffer, "计算从1到100的奇数和") == 0)
{
int sum = 0;
for (int j = 1; j <= 100; j += 2)
{
sum += j;
}
printf("send to client%d:\t1到100的奇数和是 %d\n", i, sum);
sprintf(sbuffer, "1到100的奇数和是 %d\n", sum);
}
else
{
printf("send to client%d:\tunknow!\n", i);
sprintf(sbuffer, "unknow!");
}
len = send(redset.fd_array[i], sbuffer, strlen(sbuffer), 0);
if (len == -1)
{
perror("send error");
exit(1);
}
}
}
}
}
closesocket(listen_socket);
// 清理 Winsock
WSACleanup();
return 0;
}
类unix
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 初始化 Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
//1.创建socket套接字
SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == listen_socket)
{
printf("create listen socket failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
//2.给socket绑定端口号
struct sockaddr_in local = { 0 };
local.sin_family = AF_INET;
local.sin_port = htons(8080);//绑定端口
//local.sin_addr.s_addr = htonl(INADDR_ANY);//接收全部网卡的数据 大小端转化
local.sin_addr.s_addr = inet_addr("0.0.0.0");//接收全部网卡的数据 字符串ip转成整数ip
if (-1 == bind(listen_socket, (struct sockaddr*)&local, sizeof(local)))
{
printf("bind failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
//3.给socke开启监听属性,只用来接收连接
if (-1 == listen(listen_socket, 10))
{
printf("start listen failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
fd_set redset;
//4、初始化文件描述符集合
FD_ZERO(&redset);
//5、添加要检测的监听文件描述符
FD_SET(listen_socket, &redset);
int maxfd = listen_socket;
printf("This is SERVER!\n");
while (1)
{
fd_set tmp = redset;
/* 6、不停地检测文件描述符
6.1 超时 select() = 0 再次检测或关闭套接字
6.2 异常 select() = -1 异常处理
6.3 成功 selct() >0
*/
printf("selecting...\n");
int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
if (ret <= 0)
{
printf("select failed!!! errcode: %d\n", GetLastError());
closesocket(listen_socket);
WSACleanup();
return -1;
}
printf("select = %d\n", ret);
//7、判断文件描述符属于哪一类
if (FD_ISSET(listen_socket, &tmp))//7.1监听套接字的读缓冲区有数据
{
printf("\nWaiting for connect...\n");//不阻塞
//等待客户端连接
SOCKET client_socket = accept(listen_socket, NULL, NULL);
if (INVALID_SOCKET == client_socket)
{
printf("Connect invalid!!!\n");
}
else
{
printf("Connected successfully!\n\n");
//添加得到的通信文件描述符
FD_SET(client_socket, &redset);
maxfd = client_socket > maxfd ? client_socket : maxfd;
}
}
for (int i = 0; i <= maxfd; ++i)
{
if (i != listen_socket && FD_ISSET(i, &tmp))// 7.2 i为通信文件描述符(套接字),且读缓冲区有数据
{
//接收信息
char rbuffer[1024] = { 0 };
int len = recv(i, rbuffer, 1024, 0);
if (len <= 0)
{
printf("The client %d has disconnected.\n", i);
FD_CLR(i, &redset);
shutdown(i, SD_BOTH);
closesocket(i);
break;
}
printf("recive from client%d:\t%s\n", i, rbuffer);
//发送信息
char sbuffer[1024] = { 0 };
// 检查接收到的消息
if (strcmp(rbuffer, "计算从1到100的奇数和") == 0)
{
int sum = 0;
for (int j = 1; j <= 100; j += 2)
{
sum += j;
}
printf("send to client%d:\t1到100的奇数和是 %d\n", i, sum);
sprintf(sbuffer, "1到100的奇数和是 %d\n", sum);
}
else
{
printf("send to client%d:\tunknow!\n", i);
sprintf(sbuffer, "unknow!");
}
send(i, sbuffer, strlen(sbuffer), 0);
}
}
}
closesocket(listen_socket);
// 清理 Winsock
WSACleanup();
return 0;
}
client1、2、3
客户端代码没有改变,和socket客户端代码相同
#include<stdio.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
// 初始化 Winsock
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
printf("WSAStartup failed with error: %d\n", result);
return 1;
}
//1.创建socket套接字
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == client_socket)
{
printf("create socket failed!!! errcode: %d\n", GetLastError());
WSACleanup();
return -1;
}
//2.连接服务器
struct sockaddr_in target;//目标服务器的ip结构体
target.sin_family = AF_INET;
target.sin_port = htons(8080);
target.sin_addr.s_addr = inet_addr("127.0.0.1");
if (-1 == connect(client_socket, (struct sockaddr*)&target, sizeof(target)))
{
printf("connect server failed!!!\n");
shutdown(client_socket, SD_BOTH);
closesocket(client_socket);
WSACleanup();
return -1;
}
//3.开始通讯
printf("This is Cilent1.\n\n");
while (1)
{
//发送信息
printf("send:\t");
char sbuffer[1024] = { 0 };
scanf_s("%s", sbuffer,1024);
send(client_socket, sbuffer, strlen(sbuffer), 0);
//接收消息
char rbuffer[1024] = { 0 };
int ret = recv(client_socket, rbuffer, 1024, 0);
if (ret <= 0)
{
break;//断开连接
}
printf("recive:\t%s\n", rbuffer);
}
//4.关闭连接
shutdown(client_socket, SD_BOTH); shutdown(client_socket, SD_BOTH);
closesocket(client_socket);
// 清理 Winsock
WSACleanup();
return 0;
}
八、调试结果
- 打开服务端,再依次打开客户端,client1、client2、client3依次与服务端建立连接
- 三个客户端同时向服务器发送消息“计算从1到100的奇数和”,分别得到回应“ 1到100的奇数和是 2500”
- 关闭client3,服务端依然与client1、client2通信
- 重新开启client3,与服务端连接后继续通信
5、上一个套接字的实验服务器明显不能与多个客户端并发通信,当client1与服务器连接之后,client2能与服务端连接但不能通信,client2阻塞等待通信。即若client2与服务端断开连接,client2才能和服务端通信。
九、相关链接
socket通信
https://www.cnblogs.com/wa2211lq/p/18509428
标签:仿真,文件,socket,模型,描述符,fd,Select,printf,select From: https://www.cnblogs.com/wa2211lq/p/18553178