基于select模型的TCP服务器
实验工程文件下载
https://gryffinbit.lanzous.com/iSR7Gjhg68b
实验目的
- 熟悉非阻塞套接字的工作模式
- 理解select模型的基本思路
实验环境
Windows10 x64,Visual Studio 2017
实验要求
将第三章的点对点的C/S模型改用select模型。为了方便理解,我们只是关注可读套接字,也就是select函数的第三个和第四个参数均为NULL。
实验原理(模型)
select的函数
int select(
int nfds,
fd_set FAR* readfds,
fd_set FAR* writefds,
fd_set FAR* exceptfds,
const struct timeval FAR* timeout
);
- fd_set数据类型,代表着一系列特定套接字的集合。
- nfds : 为保持与早期套接字应用程序兼容。
- readfds: 用于检查可读性。该集合包括想要检查是否符合下述任何一个条件的套接字。
- 有数据到达,可以读入
- 连接已关闭、重设或中止
- 假如已调用了listen,而且一个连接正在建立,那么accept函数调用会成功。
- wirtefds:用于检查可写性。该集合包括想要检查是否符合下述任何一个条件的套接字。
- 发送缓冲区已空,可以发送数据。
- 如果已完成了对一个非锁定连接调用的处理,连接就会成功。
- exceptfds:用于检查带外数据。该集合包括想要检查是否符合下述任何一个条件的套接字。
- 假如已完成了对一个非锁定连接调用的处理,连接尝试就会失败。
- 有带外(out-of-band,OOB)数据可供读取
- timeout:是一个指向一个timeval结构的指针,用于决定select等待I/O操作完成的最长时间。如果timeout是一个空指针,那么select调用会无限制地等待下去,直到至少有一个套接字符合制定的条件后才结束。
timeval结构的定义
strcut timeval{
long tv_sec; //以秒为单位指定等待时间
long tv_usec; //以毫秒为单位指定等待时间
};
select模型的操作步骤
用select操作一个或多个套接字句柄,一般采用下述步骤。
- 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set集合。
- 使用FD_SET宏,将要检查的套接字句柄添加到自己感兴趣的每哥fd_set集合中,相当于在指定的fd_set集合中,设置好要检查的I/O活动。
- 调用select函数,然后等待。select完成返回后,会修改每个fd_set结构,删除那些不存在待决I/O操作的套接字句柄,在各个fd_set集合中返回符合条件的套接字。
- 根据select的返回值,使用FD_ISSET宏,对每个fd_set集合进行检查,判断一个待定的套接字是否仍在集合中,便可判断出哪些套接字存在着尚未完成的I/O操作。
- 知道了每个集合中未完成的I/O操作之后,对相应的套接字的I/O进行处理,然后返回步骤1,继续进行select处理。
实验过程
将客户端中调用recv函数的部分都删掉,也就是客户端只能给服务器发消息,不能接收消息。
select模型也需要引用头文件库文件,打开网络库,校验版本,创建监听套接字,绑定地址,开始监听,然后创建一个fd_set结构体,并把监听套接字放这个集合中。
在一个死循环中调用select函数,并判断select的返回值。
若返回值为SOCKET_ERROR,则报错并做相应处理;
若返回值为0, 则继续;
若返回值大于0,说明select函数有反应。有反应就要区分是监听套接字还是响应套接字导致的。若是监听套接字导致的,就调用accept函数,创建一个新的响应套接字,并把这个响应套接字放到第2步定义的fd_set结构当中。如果是响应套接字,就调用recv函数接收客户机发来的消息。
最后释放掉所有的套接字,并关闭网络库
测试。先开始运行select模型,然后在客户端的Debug/Release文件夹中双击exe文件即可打开多个客户端,这些客户端均可给服务器发消息。
测试完成后,可以在accept函数之后调用send函数,发消息给客户端提示连接成功。相应的,就需要先在客户端调用recv函数接收此消息。
👉创建服务器端
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
//winsock2.h转到文档,可以查看当且编译器环境支持的最高版本
//引用库文件
#pragma comment(lib,"Ws2_32.lib")
int main(void)
{
//打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (0 != WSAStartup(wdVersion, &wsaData))
{
printf("WSAStartup fail!");
return -1;
}
//校验版本
if (2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion))
{
printf("Version fail!");
//关闭库
WSACleanup();
return -1;
}
//创建监听套接字
SOCKET socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == socketListen)
{
printf("socket fail!");
//关闭库
WSACleanup();
return -1;
}
//绑定地址
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET;//通信域为IPV4
//sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");//回送地址,主要用于网络软件测试以及本地机进程间通信
inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr);
sockAddress.sin_port = htons(51314);
if (SOCKET_ERROR == bind(socketListen, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{
printf("bind fail!");
//关闭监听套接字
closesocket(socketListen);
//关闭库
WSACleanup();
return -1;
}
//开始监听
if (SOCKET_ERROR == listen(socketListen, 5))
{
printf("listen fail!");
//关闭监听套接字
closesocket(socketListen);
//关闭库
WSACleanup();
return -1;
}
//申明一个fd_set结构体存放套接字,可以右键转定义查看它的结构
fd_set allSockets;
//初始化
FD_ZERO(&allSockets);
//把监听套接字放到fd_set这个集合中
FD_SET(socketListen, &allSockets);
while (true)
{
//由于给select传入一个fd_set结构体,它只返回fd_set中有响应的套接字,所以需要定义一个中转变量
fd_set TempSockets = allSockets;
//最大等待时间
struct timeval st;
st.tv_sec = 10;
st.tv_usec = 0;
//调用select函数,这里我们将第三个和第四个参数设为了NULL,
//即只是关心可读的套接字,也就是调用accept和recv的情况
int ret = select(0, &TempSockets, NULL, NULL, &st);
//判断select的返回值分为SOCKET_ERROR,0, 和大于0三种情况
if (ret == SOCKET_ERROR)
{
printf("select fail!");
break;
}
if (0 == ret) //没有反应,继续
{
continue;
}
if (ret > 0)
{
//select函数有反应,有反应就要区分是监听套接字还是响应套接字导致的
for (u_int i = 0; i < TempSockets.fd_count; i++)
{
//是监听套接字导致的
if (TempSockets.fd_array[i] == socketListen)
{
//需要创建响应套接字
SOCKET SockAccept = accept(socketListen, NULL, NULL);
//判断accept函数是否调用失败,失败的返回值是INVALID_SOCKET,不是SOCKET_ERROR
if (SockAccept == INVALID_SOCKET)
{
//创建响应套接字失败
printf("创建响应套接字失败!");
continue;
}
//把新创建的响应套接字也放到fd_set集合中
FD_SET(SockAccept, &allSockets);
//给客户端发消息
if (SOCKET_ERROR == send(SockAccept, "我是服务器,您已成功连接", strlen("我是服务器,您已成功连接"), 0))
{
printf("send fail!");
//关闭响应套接字
closesocket(SockAccept);
//关闭库
WSACleanup();
return -1;
}
}
else
{
//是响应套接字导致的,那就需要调用recv函数
//判断客户端连接的集合中是否有需要接收的数据
char szRecvBuffer[1024] = { 0 };
int nReturnValue = recv(TempSockets.fd_array[i], szRecvBuffer, sizeof(szRecvBuffer) - 1, 0);
if (0 == nReturnValue)
{
//客户端正常关闭
printf("客户端已下线");
//从集合中删掉Socket,还得调用closesocket关闭这个socket
SOCKET SockTemp = TempSockets.fd_array[i];
FD_CLR(TempSockets.fd_array[i], &allSockets);
closesocket(SockTemp);
}
else if (SOCKET_ERROR == nReturnValue)
{
//网络中断
int nRes = WSAGetLastError();
printf("响应套接字%d所对应的客户端中断连接\n", TempSockets.fd_array[i]);
//同样需要从集合删掉套接字和关闭它。
}
else
{
//接收到客户端消息
printf("Data from Socket %d : %s \n", TempSockets.fd_array[i],szRecvBuffer);
}
}
}
}
}
//释放所有socket
for (u_int i = 0; i < allSockets.fd_count; i++)
{
closesocket(allSockets.fd_array[i]);
}
//关闭网络库
WSACleanup();
system("pause");
return 0;
}
👉创建客户端
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//打开网络库
WORD wdVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (0 != WSAStartup(wdVersion, &wsaData))
{
printf("WSAStartup fail!");
return -1;
}
//校验版本
if (2 != HIBYTE(wsaData.wHighVersion) || 2 != LOBYTE(wsaData.wVersion))
{
printf("Version fail!");
//关闭库
WSACleanup();
return -1;
}
//创建一个SOCKET 监听
//套接字的类型,使用的协议一定要和服务器的类型和协议匹配上
SOCKET socketUser = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == socketUser)
{
printf("socket fail!");
//关闭库
WSACleanup();
return -1;
}
//初始化服务器端的地址,IP和端口一定要和想要连接的服务器一样,不然就连接不上
SOCKADDR_IN sockAddress;
sockAddress.sin_family = AF_INET; //IPv4的协议
//sockAddress.sin_addr.s_addr = inet_addr("127.0.0.1");
inet_pton(AF_INET, "127.0.0.1", (void*)&sockAddress.sin_addr); //服务器地址
sockAddress.sin_port = htons(51314); //服务器的端口
//客户机端主动连接服务器端
if (SOCKET_ERROR == connect(socketUser, (struct sockaddr*)&sockAddress, sizeof(sockAddress)))
{
printf("connect fail!\n");
//int a=WSAGetLastError();
//printf("error code : %d\n", a);
//关闭套接字
closesocket(socketUser);
//关闭库
WSACleanup();
return -1;
}
//接收服务器返回的信息
char szBuffer[1024] = { 0 };
//接收数据, 跟服务器端一样
int nReturnValue = recv(socketUser, szBuffer, sizeof(szBuffer), 0);
if (0 == nReturnValue)
{
//客户端正常关闭 释放Socket
closesocket(socketUser);
}
else if (SOCKET_ERROR == nReturnValue)
{
//网络中断
printf("连接中断");
}
else
{
//接收到客户端消息
printf("server data : %s\n", szBuffer);
//发送消息给服务器
while (1)
{
//发送数据, 跟服务器端一样
char szSendData[1024] = { 0 };
printf("Input Something : ");
scanf_s("%s", szSendData, 1024);
if (SOCKET_ERROR == send(socketUser, szSendData, strlen(szSendData), 0))
{
printf("send fail!");
//关闭客户机Socket
closesocket(socketUser);
//关闭库
WSACleanup();
return -1;
}
}
}
//关闭socket
closesocket(socketUser);
//关闭网络库
WSACleanup();
system("pause");
return 0;
}
👉最后运行客户端,生成解决方案之后,到工程文件夹里找到exe,开启多个客户端。