select 模型
select
模型的意义是在单线程中处理 多个socket
链接- 由于
accept
,recv
会阻塞线程,因此出现了select
select
可用于判断是否有accept
,recv
事件,有的话再处理- 避免了长时间的线程阻塞
理论上一个线程可以处理
64
个socket链接- 这里 注意下
windows
平台下 可以自定义突破 64的限制 - 官方文档 自定义
sockst
数量
- 这里 注意下
这里有一点要注意,
select
->recv
收到的数据可能是不完整的- 解决办法,每个
socket
都配一个缓冲区,用于缓冲所有数据 - 每次接受数据后都判断 这个
socket
有没有收到一个完整的数据包 - 这里还要小心点,缓冲区可能有多个数据包的实体
- 重点是 如何在 缓冲区中 分离提取数据
- 解决办法,每个
函数原型
int select(
int nfds, //忽略,仅为了兼容
fd_set FAR *readfds, //指向一个套接字集合,用来检查其可读性
fd_set FAR *writefds,//指向一个套接字集合,用来检查其可写性
fd_set FAR *exceptfds,//指向一个套接字集合,用来检查错误
const struct timeval FAR *timeout //指定此函数等待的最长时间// //如果为NULL,则最长时间为无限大。
);
//关键函数
fd_set 结构:套接字集合。Select函数可以测试这个集合中哪些套接字有事件发生。
FD_ZERO(*set) 初始化set为空,set在使用前需要清空
FD_SET(s, *set) 添加套接字到集合
FD_ISSET(s, *set) 检查s是不是set的成员,如果是则返回TRUE
FD_CLR(s, *set) 从set移除套接字s
单线程服务多socket实例
#include "pch.h"
#include <thread>
#include <iostream>
#include <Winsock2.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
/*
windows专属的 初始化api
使用Winsock 之前一定要运行这段代码
*/
void initial_win_socket()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if(err != 0)
{
/* Tell the user that we could not find a usable */
/* WinSock DLL. */
return;
}
}
//回掉函数的函数指针
typedef int(*p_socket_callback)(SOCKET, sockaddr_in);
//每当建立一个链接就调用这个函数
//建议在线程中条用
/*
关于 recv 的 返回值
0 表示 通道正常关闭(close/closesocket)
-1 表示 通道异常关闭(没有 close/closesocket 程序直接关闭了,有操作系统发送这数据)
*/
int socket_callback(SOCKET in_sock, sockaddr_in recvName)
{
printf("开始处理 socket: %ld \r\n", in_sock);
int nRet = 0;
//while(1)
//{
//接收数据
char data_rec[128] = { 0 };
char ip_sender[128] = { 0 };
inet_ntop(AF_INET, &(recvName.sin_addr), ip_sender, 16); //获取客户端的ip
nRet = recv(in_sock, data_rec, sizeof(data_rec), 0); //获取接收到的数据
if(nRet == SOCKET_ERROR || nRet == 0)
{
printf("数据接收 异常 错误码:%d\r\n", nRet);
return nRet;
//break;
}
printf("receive from %s:%d : %s\r\n", ip_sender, ntohs(recvName.sin_port), data_rec);
//向客户端发送数据
char str_to_clinet[] = "这是一条服务器回复的数据";
nRet = send(in_sock, str_to_clinet, sizeof(str_to_clinet), 0);
if(nRet == SOCKET_ERROR || nRet == 0)
{
printf("发送数据 异常 错误码:%d\r\n", nRet);
return nRet;
//break;
}
//}
printf("结束处理 socket: %ld \r\n", in_sock);
/*closesocket(in_sock);*/
}
/*
输入 ip端口 开始绑定
输入 回掉函数,每建立一个socks链接就会调用
最后 别忘记 在回掉函数中关闭 socket接口
closesocket(s);
*/
void bind_port(const char * bind_ip, int bind_port, p_socket_callback fx_cb = NULL)
{
//1 建立SOCKET(套接字)
SOCKET s = socket(AF_INET,
SOCK_STREAM,//数据流形式
IPPROTO_TCP);
if(s == INVALID_SOCKET)
{
return;
}
int nRet;
//2 配置监听端口的结构体
sockaddr_in name;
name.sin_family = AF_INET;
name.sin_port = htons(bind_port);
inet_pton(AF_INET, bind_ip, &(name.sin_addr.S_un.S_addr));//将ip地址转化为 api使用的比特流形式
// 3 将socket 绑定到端口
nRet = bind(s,
(struct sockaddr*)&name,
sizeof(name));
if(nRet == SOCKET_ERROR)
{
return;
}
//4 开始监听
nRet = listen(s, SOMAXCONN/* 设置socket队列最大数量*/);
if(nRet == SOCKET_ERROR)
{
return;
}
//5 配置 select模型
fd_set fdReadTotal;//我们把所有要监听的socket链接放在这里
fd_set fdRead;// 由于每次检查后都会改变队列.我们那这个作为 fdReadTotal 的马甲去select
FD_ZERO(&fdRead);
FD_ZERO(&fdReadTotal);
SOCKET sClient;
FD_SET(s, &fdReadTotal);//将监听 连接建立的socket先放进去
// 配置 select 的延时
timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
while(1)
{
// 配置接受链接信息的结构体
sockaddr_in recvName = { 0 };
recvName.sin_family = AF_INET;
int nLength = sizeof(sockaddr_in);
//1 配置马甲
fdRead = fdReadTotal;
//2 select选择
nRet = select(0, &fdRead, NULL, NULL, &timeout);
if(nRet == SOCKET_ERROR)
{
//表示出错了
return;
}
else if(nRet == 0)
{
//表示等待超时
printf("wait for select!");
}
else//表示成功的等到了对应的socket事件触发
{
/*
如果是 listen的socket句柄在里面
说明有新的链接要建立
*/
if(FD_ISSET(s, &fdRead))
{
//表示可以接收连接请求了
sClient = accept(s, (sockaddr*)&recvName, &nLength);
FD_SET(sClient, &fdReadTotal);
}
else//否则我们就循环检查所有其他socket通信句柄
{
for(int i = 0; i < fdReadTotal.fd_count; i++)
{
if(FD_ISSET(fdReadTotal.fd_array[i], &fdRead))
{
//表示可以收包了
SOCKET curSocket = fdReadTotal.fd_array[i];
/*
在正常逻辑中 我们这里需要使用多线程处理 链接
但是由于使用的是select模型直接处理即可
*/
//std::thread t(fx_cb, sClient, recvName);
//t.detach();
int res = fx_cb(curSocket, recvName);
if(res <= 0)//如果socket句柄异常那么就删除这个句柄
{
closesocket(curSocket);
FD_CLR(curSocket, &fdReadTotal);
}
}
}
}
}
}
closesocket(s);
}
int main()
{
initial_win_socket();
bind_port("0.0.0.0", 10086, socket_callback);
}