阻塞模式(旧)
Select模式
1.处理阻塞的网络模型
2.释放服务端
3.客户端在等待数据反馈的时候处理内容
4.处理多客户端
select模型
int(
//_In_ 表示只传入,而不会被改变
_In_ int nfds,//伯克利socket,windows下没意义,在linux有意义表最大描述符+1
//_Inout_opt_ 输入且会输出的可操作对象
_Inout_opt_ fd_set FAR * readfds,//fd_set(fds)是一个套接字集合,可读集合
_Inout_opt_ fd_set FAR * writefds,//可写集合
_Inout_opt_ fd_set FAR* exceptfds,//有异常的集合
_In_opt_ const struct timeval FAR *timeout//查询时间的延迟,在时间内没有查到任何集合,就会让select变成一个非阻塞模式
);
fd_set模型
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif /* FD_SETSIZE */
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
//默认放64个socket
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
server端代码
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
//winsock2一定要在windows前,否则会有宏定义重编译问题。sock2是新库,windows是老库。
#include<WinSock2.h>
#include<Windows.h>
#include<iostream>
#include<vector>
//加入静态链接库,否则会出现问题:无法解析的外部符号 __imp__WSAStartup@8
//也可以在属性页、连接器、输入、附加依赖项中输入ws_32.lib
#pragma comment(lib,"ws2_32.lib")
//枚举定义命令,方便判断
enum CMD
{
CMD_LOGIN,
CMD_LOGIN_RESULT,
CMD_LOGOUT_RESULT,
CMD_LOGOUT,
CMD_ERROR
};
//消息结构体
struct DataHeader {
short dataLength;//数据长度
short cmd;//命令:接收的命令,服务器处理后反馈数据。
};
struct Login :public DataHeader {
Login()
{
dataLength = sizeof(Login);
cmd = CMD_LOGIN;
}
char userName[32];
char PassWord[32];
};
struct LoginResult :public DataHeader {
LoginResult()
{
dataLength = sizeof(LoginResult);
cmd = CMD_LOGIN_RESULT;
result = 0;
}
int result;
};
struct LogoutResult :public DataHeader {
LogoutResult()
{
dataLength = sizeof(LogoutResult);
cmd = CMD_LOGOUT_RESULT;
result = 0;
}
int result;
};
struct Logout :public DataHeader {
Logout()
{
dataLength = sizeof(Logout);
cmd = CMD_LOGOUT;
}
char userName[32];
};
//创建一个全局动态数组,用来存入新加入的客户端
std::vector<SOCKET>g_clients;
//处理进程的函数
int processor(SOCKET _clientSock) {
//第一次接受一个缓冲数据
char szRecv[1024] = {};
//5.接收客户端数据
//第一次收了header的数据包,只剩下CMD的数据长度,指针移动到数据包包体位置
int nLen = recv(_clientSock, szRecv, sizeof(DataHeader), 0);
DataHeader* header = (DataHeader*)szRecv;
if (nLen <= 0)
{
printf("客户端已经退出,任务结束。");
return -1;
}
printf("收到命令:%d ,数据长度:%d\n", header->cmd, header->dataLength);
//6.处理请求
if (nLen >= sizeof(DataHeader))
switch (header->cmd)
{
case CMD_LOGIN:
{
//取剩余的长度
// 收数据包的sock 数据包读取起点 数据包读取长度
recv(_clientSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Login* login = (Login*)szRecv;
//判断用户名和密码是否正确
std::cout << login->userName << " " << login->PassWord << std::endl;
char name[32] = "李逵";
char pw[32] = "110";
if (0 == (strcmp(name, login->userName) | strcmp(pw, login->PassWord)))
{
//这个数据包的长度为32+32+2+2=68
printf("输入正确!,name is %s , password is %s ,数据长度:%d \n", login->userName, login->PassWord, login->dataLength);
//short 的长度为2
std::cout << "sizeof cmd " << sizeof(login->cmd) << " " << "sizeof datalength" << sizeof(login->dataLength) << std::endl;
}
else
printf("重新输入密码\n");
LoginResult result;
//7.发送请求
//一定要先发送消息头,然后在发送消息体,这样才是一个完整的报文
//传过来的header带有cmd,根据cmd选择参数命令。所以直接返回接收的header就行
send(_clientSock, (char*)&result, sizeof(LoginResult), 0);
}
break;
case CMD_LOGOUT:
{
recv(_clientSock, szRecv + sizeof(DataHeader), header->dataLength - sizeof(DataHeader), 0);
Logout* logout = (Logout*)szRecv;
printf("登出!,name is %s ,数据长度:%d \n", logout->userName, logout->dataLength);
LogoutResult result;
send(_clientSock, (char*)&result, sizeof(LogoutResult), 0);
}
break;
//定义默认的错误信息
default:
{
DataHeader header = { 0,CMD_ERROR };
send(_clientSock, (char*)&header, sizeof(DataHeader), 0);
}
break;
}
}
int main()
{
//版本号
WORD ver = MAKEWORD(2, 2);
//数据指针
WSADATA dat;
//windows下socket的启动函数,启动socket环境
WSAStartup(ver, &dat);
//开始编写网络环境
//1.建立一个socket。套接字inet6为最新,数据类型:数据流,网络类型tcp,udp
//定义返回类型,用来储存socket数据(uint型)
SOCKET _sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//2 bind 绑定用于接受客户端连接的网络端口
//sock地址的结构体
sockaddr_in _sin = {};
//网络类型
_sin.sin_family = AF_INET;
//端口号htons(host to net unsigned short)主机到网络的字节序的转换.用来寻找服务程序
_sin.sin_port = htons(4567);
//定义IP地址,主机不只一个IP地址。cmd中ipconfig可以查看。
//如果纯内网127就可以。还可以屏蔽外网的访问。
//S_un是一个联合体。
//INADDR_ANY不限定网路地址。
//_sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
_sin.sin_addr.S_un.S_addr = INADDR_ANY;
//sock名,地址,地址长度。强制转换地址类型sockaddr结构体里的数据类型不利于我们直接填写,
//addr_in的结构体与普通addr结构体相同,而且有常见的数据类型方便填写。
if (SOCKET_ERROR == bind(_sock, (sockaddr*)&_sin, sizeof(_sin)))//判断是否绑定成功
{
//如果端口号被别的程序占用,就是绑定失败
printf("ERROR,绑定用于接受客户端连接的网络端口失败\n");
}
else
{
printf("绑定网络端口成功\n");
}
//3.listen监听网络端口
//最大等待5人来连接
if (SOCKET_ERROR == listen(_sock, 5))
{
printf("ERROR,监听失败\n");
}
else
{
printf("监听成功\n");
}
//接收缓冲区
char _recvBuf[128] = {};
while (true)
{
//创建集合
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
//清空集合
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
//定义一个可操作socket集合的宏
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
//与int固定四个字节不同有所不同,size_t的取值range是目标平台下最大可能的数组尺寸,一些平台下size_t的范围小于int的正数范围,又或者大于unsigned int.
//使用Int既有可能浪费,又有可能范围不够大。
//把新客户放到可读集合里查询一下
//for (size_t n = 0; n < g_clients.size(); n++)
for(int n=(int)g_clients.size()-1;n>=0;n--)
{
FD_SET(g_clients[n], &fdRead);
}
//nfds是一个整数值,是指fd_set集合中所有的描述符的范围。而不是数量。即所有文件描述符最大值+1
//windows中有count,自动处理,所以没有意义,填0即可,linux中没有count所以需要手动数据描述符范围+1
int ret=select(_sock + 1, &fdRead, &fdWrite, &fdExp, NULL);
//返回值小于0,即证明出错误,直接跳出
if (ret < 0)
{
printf("客户端已退出,任务结束.\n");
break;
}
//判断socket是否在集合中,因为select每次查询完后,就会清空正在执行的socket
if (FD_ISSET(_sock, &fdRead))
{
//先清理下标志位
FD_CLR(_sock, &fdRead);
//4 accept 等待客户端连接
sockaddr_in clientAddr = {};
//定义client返回的socket数据的长度
int nAddrlen = sizeof(sockaddr_in);
//定义一个无效的sock地址用来进行判断是否接收到客服端
SOCKET _clientSock = INVALID_ATOM;
//如果固定长读就在在循环外计算出发送数据的长度,节省计算时间
//int bufSize = sizeof(msgBuf);
//循环重复执行
//新用户加入
//返回一个客户端的socket网络地址
_clientSock = accept(_sock, (sockaddr*)&clientAddr, &nAddrlen);
//判断
if (INVALID_SOCKET == _clientSock)
{
printf("错误,接收到无效客户端SOCKET\n");
}
else {
g_clients.push_back(_clientSock);
//inet_ntoa 把客户端转换成可读的ip地址
//inet_ntoa 过时了,需要定义一个宏define _WINSOCK_DEPRECATED_NO_WARNINGS
// add the port name & IP name
printf("新客户端加入:IP=%s, socket= %d \n", inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
}
}
for(size_t n=0;n<fdRead.fd_count;n++)
{
if (-1 == processor(fdRead.fd_array[n]))
{
auto iter = std::find(g_clients.begin(), g_clients.end(), fdRead.fd_array[0]);
//如果只有一个socket,那iter一定会等于最后一个,所以iter!=socket的end值
if (iter != g_clients.end())
{
g_clients.erase(iter);
}
}
}
}
//关闭全部socket
for (size_t n = g_clients.size() - 1; n >= 0; n--)
{
closesocket(g_clients[n]);
}
//8. 关闭自身的套节字
closesocket(_sock);
//结束编写网路环境
//结束socket
WSACleanup();
printf("任务结束。");
getchar();
return 0;
}
客户端不用修改
运行时出现的问题:
问题的原因应该是堆栈越界,
1.调试,点击警告
2.错误在size_t
3.点size_t,按f12跳转到size_t
发现在size_t中 ,不论在64位还是32位中,都是无符号类型。无符号类型不能进行“- -”操作,因为无符号类型只能表示正数。所以把size_t换成int即可。
//在vc中编译器可以自动把size_t自动转换成int,但是在其他编译器中会出错,所以还是手动强转。
for(int n=(int)g_clients.size()-1;n>=0;n--)
{
FD_SET(g_clients[n], &fdRead);
}
二.查看线程集合
1.调试server端,发现可读集合里有一个280的socket
2.当你不启动客户端的时候,发现程序server会阻塞在select语句中
启动client后,sever会检查到可读的socket就会运行下去
而FD_CLR()也只是清理了计数器而没有清理socket
所以如果只是应答型的网络服务,select()模型就可以。单是如果需要服务器主动的推送一些参数那么,select()函数中最后一个参数就起到了作用。