select模型适用于服务端
服务端:
1.打开网络库
int WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);
2.检验版本号
2 != HIBYTE(wsaData.wVersion) || 2 != LOBYTE(wsaData.wVersion) // (检验版本号是 2.2)
3.创建socket 套接字
SOCKET socket(int af, int type, int protocol);
4.绑定地址与端口号
int bind(SOCKET s, const sockaddr* addr, int namelen);
5.进行监听
int WSAAPI listen(SOCKET s, int backlog);
6.select 核心:
处理基本C/S模型中的accept recv傻等问题,调用select() 函数循环遍历所有的socket
select() 函数原型:
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
通过参数2检查是否有可读的所有的socket;
通过参数3检查是否有可写的所有的socket;
通过参数4检查是否有异常的所有的socket
7.调用回调函数,处理点击服务端右上角叉号关闭程序是释放所有的socket
SetConsoleCtrlHandler() ,函数原型:
SetConsoleCtrlHandler(
_In_opt_ PHANDLER_ROUTINE HandlerRoutine,
_In_ BOOL Add
);
服务端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
select 模型的特点:
1、解决基本 C/S 模型中,accpet recv 傻等的问题
傻等阻塞函数:accept recv send
执行阻塞:
accept recv send 在执行的赋值黏贴过程中都是阻塞的,
select模型只是解决傻等的问题,但解决不了这几个函数本身阻塞的问题
2、实现多个客户端链接,与多个客户端分别通信
3、用于服务端,客户端不用select模型,因为只有一个socket
解决客户端recv的时候会傻等,不能send的问题:
只需要单独创建一根线程,recv放到线程中就可以了
// select主要是处理的是 accept 和 recv 傻等阻塞问题
// 声明集合(数组)fd_set 用来装所有的socket ,通过select模型遍历所有的socket,
// 通过select函数的参数处理socket分类处理可读,可写,异常
fd_set 的原型
typedef struct fd_set {
u_int fd_count; how many are SET?
SOCKET fd_array[FD_SETSIZE]; an array of SOCKETs
} fd_set;
*/
// 定义全局的WSAStartup()函数所需的数据结构体
WSADATA WSAData;
// 定义全局的集合 fd_set 装所有的socket
// 声明select函数中所需的结构体 fd_set
fd_set allSocket; // 装所有的socket
// 1.打开网络库
void OpenWSAStartup()
{
WORD version = MAKEWORD(2, 2); //版本号 2.2
int nRet = WSAStartup(version, &WSAData);
// 判断是否成功打开网络库
if (0 != nRet)
{
// 分类处理错误码 给提示
switch (nRet)
{
case WSASYSNOTREADY:
printf("重启电脑试试,或者检查ws_2_32库是否存在\n");
break;
case WSAVERNOTSUPPORTED:
printf("当前库版本号不支持,尝试更换版本试试\n");
break;
case WSAEPROCLIM:
printf("已达到对Windows套接字实现支持的任务数量的限制\n");
break;
case WSAEINPROGRESS:
printf("正在阻止Windows Sockets 1.1操作,当前函数运行期间,因为某些原因造成阻塞\n");
break;
case WSAEFAULT:
printf("lpWASData参数不是有效的指针,参数写错了\n");
break;
default:
break;
}
}
}
// 2.检验版本
void CheckVersion()
{
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
// 打开版本号 2.2 失败
printf("打开版本号 2.2 失败, 正在退出程序...\n");
// 关闭网络库
WSACleanup();
// 退出程序
return;
}
}
// 3.创建socket 套接字(服务端)
SOCKET CreateSocket()
{
SOCKET createSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 检验是否成功创建套接字
if (INVALID_SOCKET == createSocket)
{
// 创建套接字失败
// 获取错误码
int error = WSAGetLastError();
printf("创建套接字失败,返回错误号为:%d\n", error);
// 关闭网络库
WSACleanup();
// 退出程序
return 0;
}
return createSocket;
}
// 4.绑定地址与端口号
void Bind(SOCKET tempSocket)
{
struct sockaddr_in server_Msg;
// 给结构体赋值
server_Msg.sin_family = AF_INET; //IP地址类型
server_Msg.sin_port = htons(12345); //端口号
server_Msg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //ip地址(本地回环地址)
int Ret_bin = bind(tempSocket, (struct sockaddr*)&server_Msg, sizeof(server_Msg));
// 检验是否成功绑定地址和端口号
if (SOCKET_ERROR == Ret_bin)
{
// 绑定失败
// 获取错误码
int error = WSAGetLastError();
printf("绑定地址和端口号失败,该错误码为:%d\n", error);
// 关闭套接字
closesocket(tempSocket);
// 关闭网络库
WSACleanup();
// 退出程序
return;
}
}
// 5.进行监听
void Listen(SOCKET tempSocket)
{
// 检验监听情况
if (SOCKET_ERROR == listen(tempSocket, SOMAXCONN))
{
// 监听失败
// 获取错误码
int error = WSAGetLastError();
printf("监听失败,该错误码为:%d\n", error);
// 关闭套接字
closesocket(tempSocket);
// 关闭网络库
WSACleanup();
// 退出程序
return;
}
}
// 6. select模型核心代码
void Select(SOCKET tempSocket)
{
// 处理fd_set 的4个参数宏
FD_ZERO(&allSocket); // 清空集合
FD_SET(tempSocket, &allSocket); // 向集合中添加socket
// 这里暂时只需清空集合 将服务端的socket装进集合中就可以了
//FD_CLR(socketServer, &allSocket); // 集合中删除指定的socket
//FD_ISSET(socketServer, &allSocket); // 判断指定的socket 是否在集合中
// 遍历处理所有的socket 通过select函数
while (1)
{
// 记录集合allSokcket 防止被修改
// 定义select函数中所需的参数2,参数3,参数4 用于检查所有的socket
fd_set readSockets = allSocket; // 检查是否有可读的socket
fd_set writeSockets = allSocket; // 检查是否有可写的socket
fd_set errorSockets = allSocket; // 检查是否以异常的socket
// 声明select函数中的参数5的时间结构体
// 最大等待时间
struct timeval ts;
// 给结构体成员赋值
ts.tv_sec = 3; // 3秒
ts.tv_usec = 0; // 0微秒
// 调用select() 函数
int retSel = select(0, &readSockets, &writeSockets, NULL, &ts);
// 处理select函数的返回值
if (0 == retSel)
{
// 客户端在等待的时间内没有反应
// 直接continue 继续等待就可以了
continue;
}
else if (0 < retSel)
{
// 处理客户端的交流请求
// 处理select函数中的参数2 参数2 的功能是检测所有的socket 判断是否有可读的socket,
// 将可读的socket装到allSocket的记录的集合中
for (u_int i = 0; i < readSockets.fd_count; i++)
{
// 1.处理服务端的可读socket
// 判断是否有可读的socket
if (readSockets.fd_array[i] == tempSocket)
{
// 服务端接收连接 调用accept 本质是创建客户端的socket
SOCKET socketClient = accept(tempSocket, NULL, NULL);
// 判断是否成功创建客户端
if (INVALID_SOCKET == socketClient)
{
// 客户端创建失败 服务端接收连接出错
// 通过WSAGetLastError() 获取错误码
int error = WSAGetLastError();
printf("处理select函数中的参数2,服务端接收连接,创建客户端出错,该错误码为:%d\n", error);
}
// 将成功创建的客户端socket装进集合中
FD_SET(socketClient, &allSocket);
// 这里可以send信息告诉客户端服务端创建客户端socket成功
if (SOCKET_ERROR == send(socketClient, "服务端创建客户端socket成功!", sizeof("服务端创建客户端socket成功!"), 0))
{
printf("select函数中参数2 处理服务端接收链接成功,发送信息给客户端失败!!!\n");
}
}
else // 处理客户端的可读的socket
{
// 客户端进行recv消息
// 定义接收服务端数据的缓存区数组
char recvFrom[1024] = { 0 };
// 注意这里的recv函数的第一个参数填的是可读的客户端socket,即readSockets.fd_array[i],如果填的是服务端socketServer,会出出现10057错误码
int retRecv = recv(readSockets.fd_array[i], recvFrom, 1024, 0);
// 处理recv 函数的返回值
if (0 == retRecv)
{
// 客户端正常下线
printf("客户端正常下线了...\n");
// 从集合中释放客户的socket
// 定义中间变量记录集合中的要释放的客户端socket
SOCKET tempSocket_1 = readSockets.fd_array[i];
// 利用fd_set 宏参数 FD_CLR 删除下线的客户端socket
FD_CLR(readSockets.fd_array[i], &allSocket);
// 关闭套接字
closesocket(tempSocket_1);
}
else if (0 < retRecv)
{
// 打印服务端发来的信息
printf("服务端say: %s\n", recvFrom);
}
else
{
// SOCKET_ERROR 出错了
// 注意这里客户端强制关闭也叫出错 错误码为 10054
int error = WSAGetLastError();
printf("select函数的参数2处理可读的客户端socket recv的返回值出错,该错误码为:%d\n", error);
switch (error)
{
case 10054:
{
// 客户端正常下线
printf("注意客户端非正常下线!!!\n");
// 从集合中释放客户的socket
// 定义中间变量记录集合中的要释放的客户端socket
SOCKET tempSocket_2 = readSockets.fd_array[i];
// 利用fd_set 宏参数 FD_CLR 删除下线的客户端socket
FD_CLR(readSockets.fd_array[i], &allSocket);
// 关闭套接字
closesocket(tempSocket_2);
}
}
}
}
}
// 2.处理select函数中的参数3 检查可写的所有的socket
for (u_int i = 0; i < writeSockets.fd_count; i++)
{
// 这里可以随时 send 发信息 注意send() 函数第一个参数要填 writeSockets.fd_array[i]
if (SOCKET_ERROR == send(writeSockets.fd_array[i], "ok", 2, 0))
{
// 通过WSAGetLastError() 获取错误码
int error = WSAGetLastError();
}
}
// 3.处理select函数中的参数4 检查异常的所有的socket
for (u_int i = 0; i < errorSockets.fd_count; i++)
{
// 通过getsockopt 获取错误信息
// 定义getsockopt 函数中所需的参数4 接收错误信息的缓存区
char errorBuf[1024] = { 0 };
int len = sizeof(errorBuf); // 获取接收信息的缓存区大小
if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, errorBuf, &len))
{
// 通过WSAGetLastError() 获取错误码
int error = WSAGetLastError();
}
printf("%s", errorBuf);
}
}
else
{
// SOCKET_ERROR 出错了
// 通过WSAGetLastError() 获取错误码
int error = WSAGetLastError();
printf("select 函数的返回值是SOCKET_ERROR,该错误码为:%d\n", error);
// 可以退出循环,也可以根据实际情况进行处理
break;
}
}
}
// 7.监控控制台点击叉号
// 控制当前服务端点右上角叉号退出程序是释放所有的socket
BOOL WINAPI fun(DWORD dwCtrlType)
{
switch (dwCtrlType)
{
case CTRL_CLOSE_EVENT:
// 释放所有的socket
for (u_int i = 0; i < allSocket.fd_count; i++)
{
closesocket(allSocket.fd_array[i]);
}
// 清理网络库
WSACleanup();
}
return TRUE;
}
int main()
{
// 控制台点击退出 主函数投递一个监视
SetConsoleCtrlHandler(fun, TRUE);
// 1.打开网络库
OpenWSAStartup();
// 2.检验版本
CheckVersion();
// 3.创建socket 套接字(服务端)
SOCKET socketServer = CreateSocket();
// 4.绑定地址与端口号
Bind(socketServer);
// 5.进行监听
Listen(socketServer);
// 6. select模型核心代码
Select(socketServer);
// 释放所有的socket
for (u_int i = 0; i < allSocket.fd_count; i++)
{
// 关闭套接字
closesocket(allSocket.fd_array[i]);
}
关闭套接字
//closesocket(socketServer);
// 关闭网络库
WSACleanup();
system("pause");
return 0;
}
客户端代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <Winsock.h>
#pragma comment(lib,"ws2_32.lib")
WSADATA WSAData;
// 1,打开网络库
void OpenWSAStarup()
{
// 1,打开网络库
WORD version = MAKEWORD(2, 2); //设置要使用的库的版本 MAKEWORD(主版本,副版本);
int n = WSAStartup(version, &WSAData);
// 检验是否成功打开网络库
if (0 != n)
{
// 打开网络库失败,通过n 返回的错误码,给出提示
switch (n)
{
case WSASYSNOTREADY:
printf("重启电脑试试,或者检查ws_2_32库是否存在\n");
break;
case WSAVERNOTSUPPORTED:
printf("当前库版本号不支持,尝试更换版本试试\n");
break;
case WSAEPROCLIM:
printf("已达到对Windows套接字实现支持的任务数量的限制\n");
break;
case WSAEINPROGRESS:
printf("正在阻止Windows Sockets 1.1操作,当前函数运行期间,因为某些原因造成阻塞\n");
break;
case WSAEFAULT:
printf("lpWASData参数不是有效的指针,参数写错了\n");
break;
}
}
}
// 2.检验版本
void CheckVersion()
{
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
// 打开版本号 2.2 失败
printf("打开版本号 2.2 失败, 正在退出程序...\n");
// 关闭网络库
WSACleanup();
// 退出程序
return;
}
}
// 3.创建socket
SOCKET CreateSocket()
{
// 3.创建套接字 socket 创建服务端的socket
SOCKET socketCreate = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 检验是否成功创建套接字
if (INVALID_SOCKET == socketCreate)
{
// 创建套接字失败
// 获取错误码
int error = WSAGetLastError();
printf("创建套接字失败,返回错误号为:%d\n", error);
// 关闭网络库
WSACleanup();
// 退出程序
return 0;
}
return socketCreate;
}
// 4.连接服务器
void Connection(SOCKET socket_)
{
// 4.连接到服务器
// 创建connect() 函数中参数2所需的结构体 用来装IP地址和端口号
sockaddr_in client_s;
//给结构体成员赋值
client_s.sin_family = AF_INET; //IP地址类型
client_s.sin_port = htons(12345); //端口号
client_s.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int nCon = connect(socket_, (struct sockaddr*)&client_s, sizeof(client_s));
// 检验是否成功连接到服务端
if (SOCKET_ERROR == nCon)
{
// 连接服务端失败
// 获取错误码
int error = WSAGetLastError();
printf("连接服务端失败,返回错误号为:%d\n", error);
// 关闭套接字
closesocket(socket_);
// 关闭网络库
WSACleanup();
// 退出程序
return;
}
}
int main()
{
// 1,打开网络库
OpenWSAStarup();
// 2.校验版本
CheckVersion();
// 3.创建socket
SOCKET socketServer = CreateSocket();
// 4.连接服务器
Connection(socketServer);
// 只收一次 信息
// 定义接收服务端信息的缓存区
char recvBuf[1024] = { 0 };
int res = recv(socketServer, recvBuf, 1024, 0);
// 判断是否成功接收到信息
if (0 == res)
{
printf("服务端下线了\n");
return 0;
}
else if (SOCKET_ERROR == res)
{
// 获取错误码
int error = WSAGetLastError();
printf("连接服务端失败,返回错误号为:%d", error);
printf("\n");
return 0;
}
else
{
// 打印信息
printf("服务端say: %s\n", recvBuf);
}
printf("我是客户端,正在发送信息,请稍等...\n");
// 5.与服务端进行收发信息
while (1)
{
// 发信息
char sendBuf[1024] = { 0 };
scanf_s("%s", sendBuf, 1024);
// 设置输入 0 时自动退出
if ('0' == sendBuf[0])
{
break; //退出循环
}
send(socketServer, sendBuf, 1024, 0);
}
// 关闭套接字
closesocket(socketServer);
// 关闭网络库
WSACleanup();
system("pause");
return 0;
}
程序运行结果如下: