select模型与一般模型的区别是selete模型使用fd_set数据类型和select函数在内核里维护一张存有连接上的socket表,使用select轮循处理这些socket,实现同时与多个socket连接。
基本工作原理:创建socket,绑定,监听,开个死循环,当发现有socket请求连接后把该socket写入全局变量的fd_set数据类型中,给另一个线程处理,总监听数-1,处理线程会接受fd_set全局变量,(开死循环)使用select函数堵塞监听表中的socket(轮循该表(fd-set)),当发现有表中的某个或多个socket有信息传过来时,再轮循一次该表,逐个处理传过来的信息。如果某个socket断开连接(recv发送消息过来,返回值是0),则把该socket从表中移除。总监听数量+1.
使用的函数:
int select(
int nfds,//忽略,只是为了保持与早期的Berkeley套接字应用程序的兼容
fd_set FAR* readfds,//可读性检查(有数据可读入,连接关闭,重设,终止),为空则不检查可读性
fd_set FAR* writefds,//可写性检查(有数据可发出),为空则不检查可写性
fd+set FAR* exceptfds,//带外数据检查(带外数据),为空则不检查
const struct timeval FAR* timeout//超时
);
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
};
void FD_SET(int fd, fd_set *set); //在set中设置文件描述符fd
void FD_CLR(int fd, fd_set *set); //清除set中的fd位
int FD_ISSET(int fd, fd_set *set); //判断set中是否设置了文件描述符fd
void FD_ZERO(fd_set *set); //清空set中的所有位(在使用文件描述符集前,应该先清空一下)
//(注意FD_CLR和FD_ZERO的区别,一个是清除某一位,一个是清除所有位)
实例:
服务器:
#include <stdio.h>
#include <WinSock2.h>//必须放在windows.h前面
#include <Windows.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib")
fd_set g_fdClientSock; //可以理解为一张存了需要等待的socket的数组。
int clientNum = 0;
DWORD WINAPI ListenThreadProc(LPARAM lparam)
{
fd_set fdRead;
FD_ZERO(&fdRead);
int nRet = 0;
char *recvBuffer = (char*)malloc(1024);
if (!recvBuffer)
{
return -1;
}
memset(recvBuffer, 0, 1024);
while (1)
{
fdRead = g_fdClientSock;
timeval vt;
vt.tv_sec = 0; //秒
vt.tv_usec = 100;//毫秒
nRet = select(0, &fdRead, 0, 0, &vt);//会阻塞检查集合中所有socket是否有信号
if (nRet != SOCKET_ERROR)
{
for (int i = 0; i<g_fdClientSock.fd_count; i++)
{
if (FD_ISSET(g_fdClientSock.fd_array[i], &fdRead))
{
memset(recvBuffer, 0, 1024);
nRet = recv(g_fdClientSock.fd_array[i], recvBuffer, 1024, 0);//返回字节数
if (nRet >0)
{
//todo:
printf("接收到数据:%s", recvBuffer);
send(g_fdClientSock.fd_array[i], recvBuffer, strlen(recvBuffer), 0);
}
else//如果接收失败,则从集合中清除响应socket,并把客户端数量减1??为什么要这个操作,不删除,FD表的负担太大了么?
{ //因为当recv返回值是0时,表示断开连接了,所以删掉,不让他站位置
closesocket(g_fdClientSock.fd_array[i]);
clientNum--;
FD_CLR(g_fdClientSock.fd_array[i], &g_fdClientSock);//在FD_SET表格中删除该socket
}
}
}
}
}
if (recvBuffer)
{
free(recvBuffer);
recvBuffer = nullptr;
}
return 0;
}
int main()
{
int port = 5099;
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to load Winsock");
return 0;
}
//创建用于监听的套接字 AF_INET:IPV4版本
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
if (sockSrv == INVALID_SOCKET)
{
return 0;
}
//地址绑定-告诉操作系统是在哪一个地址及端口
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(port); //1024以上的端口号,htons本地转换为网络数据
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//电脑上所有的网络ip
int retVal = bind(sockSrv, (LPSOCKADDR)&addrSrv, sizeof(SOCKADDR_IN));
if (retVal == SOCKET_ERROR)
{
printf("绑定bind失败:%d\n", WSAGetLastError());
return 0;
}
if (listen(sockSrv, 5/*SOMAXCONN*/) == SOCKET_ERROR)
{
printf("监听listen失败:%d", WSAGetLastError());
return 0;
}
SOCKADDR_IN addrClient;//用于获取连接上来的人的地址信息
int len = sizeof(SOCKADDR);
//创建线程
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)ListenThreadProc, NULL, NULL, NULL);
//绑定好之后就在这里监听,每次监听到sockket就把他写入内核的FD_SET(g_fdClientSock是全局,另一个线程能看到),之后的就交给另一个线程,总共就需要2个线程
while (clientNum < FD_SETSIZE)
{
//等待客户请求到来
SOCKET clientSock = accept(sockSrv, (SOCKADDR *)&addrClient, &len);
if (clientSock == SOCKET_ERROR)
{
printf("接收Accept失败:%d", WSAGetLastError());
break;
}
else
{
printf("接收Accept到客户端IP:[%s]\n", inet_ntoa(addrClient.sin_addr));
}
FD_SET(clientSock, &g_fdClientSock);//添加到集合中去
clientNum++;//每次接收到一个人就+1,目前最多接收64个客户,可以改到最大值的1024
}
closesocket(sockSrv);
WSACleanup();
system("pause");
return 0;
}
客户端:(客户端和一般模型的客户端一样)
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
//不要安全检测
#define _CRT_SECURE_NO_WARNINGS
int main()
{
//加载套接字
WSADATA wsaData;
char buff[1024];
memset(buff, 0, sizeof(buff));
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to load Winsock");
return 0;
}
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(5099);//http默认端口
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//htonl(INADDR_ANY); //服务器地址为INADDR_ANY,即为(0.0.0.0)上监听(任意ip),监听端口为9990
//创建套接字
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
if (SOCKET_ERROR == sockClient) {
printf("Socket() error:%d", WSAGetLastError());
return 0;
}
//向服务器发出连接请求
if (connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv)) == INVALID_SOCKET) {
printf("Connect failed:%d", WSAGetLastError());
return 0;
}
int iRecvLen = 0;
//发送数据
char* buffSend = "hello, this is a Client....";
iRecvLen = send(sockClient, buffSend, strlen(buffSend), 0);
//接收数据
iRecvLen = recv(sockClient, buff, sizeof(buff), 0);
printf("%s\n", buff);
//关闭套接字
closesocket(sockClient);
WSACleanup();
system("pause");
return 0;
}
参考文章及书籍
《windows网络编程(第二版)》