事件选择模型核心:
为每一个socket创建一个事件对象,将socket和对应的事件对象绑定到一起,并投递给操作系统,由操作系统帮我们进行监视,当对应的socket有响应,该对应的事件会被置成有信号,我们获取该信号,进行分类处理。
UDP/IP 事件选择模型:
UDP/IP 是面向非连接的,不可靠的,基于数据报的传输层协议;
对于服务端只有一个socket(服务端socket),直接创建一个事件对象,将该socket和对应的事件对象进行绑定,并投递给系统,之后等待信号,获取信号,之后进行分类处理。
事件选择模型的逻辑:
1. 创建一个事件对象:
WSAEVENT WSAAPI WSACreateEvent();
2. 绑定并投递
int WSAAPI WSAEventSelect
( // 函数功能:给事件绑上socket与对应的操作,并投递给操作系统
SOCKET s, // 被绑定的socket 最终,每个socket都会被绑定一个事件
WSAEVENT hEventObject, // 事件对象 逻辑,就是讲参数1与参数2绑定在一起
long lNetworkEvents // 具体的事件 FD_READ FD_WRITE ...
);
3. 循环等待事件产生信号
DWORD WSAAPI WSAWaitForMultipleEvents( // 作用:等待事件产生信号
DWORD cEvents, // 事件个数
const WSAEVENT *lphEvents, // 事件列表
BOOL fWaitAll, // 事件等待方式
DWORD dwTimeout, // 超时间隔
BOOL fAlertable // 这里填FALSE
);
4. 获取信号,进行分类处理
int WSAAPI WSAEnumNetworkEvents( // 获取事件类型,并将事件上的信号重置
SOCKET s, // 对应的socket
WSAEVENT hEventObject, // 对应的事件
LPWSANETWORKEVENTS lpNetworkEvents // 触发的事件类型 声明一个结构体指针
);
服务端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
UDP/IP (User Datagram Protocol/Internet Protocol) 用户数据报协议
TCP/IP 协议的特点:
面向连接的,可靠的,基于字节流的传输层协议。
UDP/IP 协议的特点:
面向非连接的,不可靠的,基于数据报的传输层协议。
1.基本C/S模型中服务端代码:
UDP/IP 与 TCP/IP 的区别:
UDP/IP:
是面向非连接的,不用进行listen(进行监听)、不进行accept(接收连接)
与客户端进行收发消息调用的是 recvfrom、sendto函数
TCP/IP:
是面向连接的,进行listen(进行监听)、进行accept(接收连接 本质创建客户端socket)
与客户端进行收发消息调用的是 recv、send函数
2.select模型服务端代码:
UDP/IP:
UDP/IP协议的C/S模型不存在傻等的问题,而select模型只是能让recvfrom更灵活一些
select 代码逻辑:
1.所有的socket装进一个集合FD_SET UDP只有一个socket即将服务端的socket装进集合中就可以了
2.通过select函数,检测中的socket集合中那个有响应了,就是检测有没有消息来了,
3.有消息了,则进行处理 调用 recvfrom
TCP/IP:
TCP/IP协议的C/S模型中accept、recv存在傻等问题,select模型是为了解决accept、recv傻等问题,
select 代码逻辑:
1.每个客户端都有socket,服务器也有自己的socket,将所有的socket装进一个数据结构里,即数组 fd_set
2.通过select函数,遍历1中socket数组,当某个socket有响应,select就会通过其参数/返回值反馈出来,之后做相应的处理
3.处理有响应的socket,
判断检测到是的服务端socket,有客户端来链接了,调用accept(创建客户端socket)
判断检测到的是客户端socket,客户端请求通信,调用send或者recv进行发收消息
windows处理用户行为的两种方式:
消息机制:
核心:消息队列
处理过程:
所有的用户操作,比如鼠标,按下键盘键,点击软件的按钮... 等,
所有的操作均是依次按顺序被记录,装进一个队列中
特点:
消息队列由操作系统维护,我们把消息取出,然后进行分类处理
事件机制:
核心:事件集合
处理过程:
根据需求,为用户的特定操作绑定一个事件,事件由我们自己调用API创建,需要多少创建多少
将事件投递系统,系统就帮我们进行监视着,创建事件不能无限创建,创建多了,系统就会卡
如果操作发生了,比如用户按鼠标,该事件会被置成有信号
直接获取有事件的信号,进行处理
特点:
所有的事件都是我们自定义的,系统只是我们置成有无信号
3.事件选择模型服务端代码:
UDP/IP事件选择:
事件选择模型是为每一个socket绑定一个事件对象,(而UDP/IP服务端代码只有一个socket)
并投递给操作系统,系统为我们进行监视着,当有客户端发来了消息,该事件就会被置成有信号的,
那么我们获取该有信号的事件,进行处理
代码逻辑:
第一步:创建一个事件对象(变量) WSACreateEvent
第二步:将事件与socket绑定到一起,并指定监视类型(recvfrom,sendto)并投递给系统 WSAEventSelect
第三步:循环等待事件产生信号 WSAWatiForMultipleEvents 该函数等待过程将所在线程挂起,释放CPU时间
第四步:有信号的话,就进行分类处理 WSAEnumNetworkEvents
TCP/IP事件选择:
整体逻辑跟UDP/IP事件选择模型一致,注意的是TCP/IP是面向连接的,要进行accept处理
代码逻辑:
第一步:创建一个事件对象(变量) WSACreateEvent
第二步:为每一个事件对象绑定一个socket以及操作accept、read、close...,并投递给系统
第三步:查看事件是否有信号 WSAWaitForMultipleEvents
第四步:有信号的话就进行分类处理 WSAEnumNetworkEvents
*/
// 声明变量接收创建事件对象
HANDLE nCreEvn;
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建服务端socket
SOCKET socketServer;
// 4.绑定地址与端口号
void Bind(SOCKET sock);
// 5.实现事件选择模型核心代码
void Event_Select(SOCKET sock);
// 监视窗口回调函数
BOOL WINAPI fun(DWORD dwCtrlType);
int main()
{
// 监视关闭窗口按钮
SetConsoleCtrlHandler(fun, TRUE);
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
socketServer = Sock_Server();
// 4.绑定地址与端口号
Bind(socketServer);
// 5.核心事件选择模型
Event_Select(socketServer);
//关闭事件对象
WSACloseEvent(nCreEvn);
//关闭socket
closesocket(socketServer);
//关闭网络库
WSACleanup();
system("pause");
return 0;
}
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
// 定义WSAStartup函数中参数1 所需的版本号
WORD wVersionRequestd = MAKEWORD(2, 2); // 设置2.2版本号
// 声明WSAStartup函数中参数1 所需的结构体
WSADATA WSAData;
// 打开网络库
int nRet = WSAStartup(wVersionRequestd, &WSAData);
if (0 != nRet)
{
//出错了
int a = WSAGetLastError();
printf("网络库打开失败 %d\n", a);
return;
}
// 检验版本号
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
printf("校验版本号错误\n");
// 关闭网络库
WSACleanup();
return;
}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
// 创建服务端 socket
SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 判断服务端socket是否成功创建
if (INVALID_SOCKET == sock_server)
{
//出错了
int a = WSAGetLastError();
printf("创建服务端socket失败,获取的错误码为 %d\n", a);
//关闭网络库
WSACleanup();
return 0;
}
return sock_server;
}
// 4.绑定地址与端口号
void Bind(SOCKET sock)
{
// 声明bind函数参数2所需的 结构体
struct sockaddr_in sockMsg;
// 给结构体成员赋值
sockMsg.sin_family = AF_INET;
sockMsg.sin_port = htons(12345);
sockMsg.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 绑定地址与端口号
int nRet = bind(sock, (struct sockaddr*)&sockMsg, sizeof(sockMsg));
// 判断是否成功绑定地址与端口号
if (SOCKET_ERROR == nRet)
{
//出错了
int a = WSAGetLastError();
printf("绑定地址与端口号失败,获取的错误码为 %d\n", a);
//关闭socket
closesocket(sock);
//关闭网络库
WSACleanup();
return;
}
}
// 5.实现事件选择模型核心代码
void Event_Select(SOCKET sock)
{
/*int WSAAPI WSAEventSelect
( // 函数功能:给事件绑上socket与对应的操作,并投递给操作系统
SOCKET s, // 被绑定的socket 最终,每个socket都会被绑定一个事件
WSAEVENT hEventObject, // 事件对象 逻辑,就是讲参数1与参数2绑定在一起
long lNetworkEvents // 具体的事件 FD_READ FD_WRITE ...
);
*/
/* DWORD WSAAPI WSAWaitForMultipleEvents( // 作用:等待事件产生信号
DWORD cEvents, // 事件个数
const WSAEVENT *lphEvents, // 事件列表
BOOL fWaitAll, // 事件等待方式
DWORD dwTimeout, // 超时间隔
BOOL fAlertable // 这里填FALSE
*/
/*int WSAAPI WSAEnumNetworkEvents( // 获取事件类型,并将事件上的信号重置
SOCKET s, // 对应的socket
WSAEVENT hEventObject, // 对应的事件
LPWSANETWORKEVENTS lpNetworkEvents // 触发的事件类型 声明一个结构体指针
);*/
// 1.创建服务端socket对应的事件对象
nCreEvn = WSACreateEvent();
// 判断是否成功创建事件对象
if (WSA_INVALID_EVENT == nCreEvn)
{
// 出错了
int a = WSAGetLastError();
printf("创建事件对象失败,获取的错误码为: %d\n", a);
// 关闭socket
closesocket(sock);
// 关闭网络库
WSACleanup();
return;
}
// 2.将socket和对应的事件对象绑定到一起,并投递给系统
int nEvnSel = WSAEventSelect(sock, nCreEvn, FD_READ | FD_WRITE);
// 判断是否成功绑定并投递
if (SOCKET_ERROR == nEvnSel)
{
// 出错了
int a = WSAGetLastError();
printf("绑定并投递失败,获取的错误码为: %d\n", a);
// 关闭socket
closesocket(sock);
// 关闭事件对象
WSACloseEvent(nCreEvn);
// 关闭网络库
WSACleanup();
return;
}
// 3.循环等待事件
while (1)
{
// 等待事件
DWORD nWaiFor = WSAWaitForMultipleEvents(1, &nCreEvn, FALSE, WSA_INFINITE, FALSE);
// 判断是否等待事件
if (WSA_WAIT_FAILED == nWaiFor)
{
// 没有等待到事件
int a = WSAGetLastError();
printf("没有等待到事件,出现的错误码为: %d\n", a);
// 直接退出函数
break;
}
// 声明WSAEnumNetworkEvents函数中参数3所需的结构体
WSANETWORKEVENTS NetworkEvents;
// 4.等到了事件,进行列举事件
int nEnm = WSAEnumNetworkEvents(sock, nCreEvn, &NetworkEvents);
// 判断是否成功列举事件
if (SOCKET_ERROR == nEnm)
{
//出错了
int a = WSAGetLastError();
printf("列举事件失败,获取的错误码为: %d\n", a);
//函数调用失败
continue;
}
// 5.成功列举事件,进行分类处理 FD_READ FD_WRITE
// 处理FD_READ
if (NetworkEvents.lNetworkEvents & FD_READ)
{
// 通过列举函数参数3的结构体成员变量进行判断是否进行接收消息
if (0 == NetworkEvents.iErrorCode[FD_READ_BIT])
{
// 声明发送对方(客户端)的结构体对象
// 声明recvfrom函数中参数5所需的对方结构体对象
struct sockaddr sockClient;
// 获取对方结构体大小
int len = sizeof(sockClient);
// 声明recvfrom函数中参数2所需的 接收数据的缓存区字节数组
char strBuf[548] = { 0 };
int nRecv = recvfrom(sock, strBuf, 548, 0, &sockClient, &len);
// 判断是否成功接收数据
if (SOCKET_ERROR == nRecv)
{
//出错了
int a = WSAGetLastError();
printf("接收数据失败,获取的错误码为: %d\n", a);
}
// 打印数据
printf("%s\n", strBuf);
// 进行发送数据(给客户端发送数据)
int nSend = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, len);
// 判断是否成功发送数据
if (SOCKET_ERROR == nSend)
{
//出错了
int a = WSAGetLastError();
printf("发送数据失败,获取的错误码为: %d\n", a);
}
}
}
// 处理FD_WRITE
if (NetworkEvents.lNetworkEvents & FD_WRITE)
{
// 通过列举函数参数3的结构体成员变量进行判断是否成功发送sendto
if (0 == NetworkEvents.iErrorCode[FD_WRITE_BIT])
{
printf("成功发送\n");
}
}
}
}
// 监视窗口回调函数
BOOL WINAPI fun(DWORD dwCtrlType)
{
switch (dwCtrlType)
{
case CTRL_CLOSE_EVENT:
// 关闭socket
closesocket(socketServer);
// 关闭对应的事件
WSACloseEvent(nCreEvn);
// 清理网络库
WSACleanup();
break;
}
return TRUE;
}
客户端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
UDP/IP 基本C/S模型 客户端:
与 UDP/IP 基本C/S模型 服务端代码的区别:
代码基本一致,只有bind 函数有区别
服务端需要进行绑定地址与端口号,因为服务端是一对多
客户端只需要创建服务端地址与端口结构体,与服务端发送数据报时,
直接传递的是创建的结构体就可以了,不用进行绑定,因为服务端的ip与端口号固定不变的
*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建客户端socket
SOCKET socketClient;
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock);
int main()
{
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET socketClient = Sock_Server();
// 4.与客户端进行发收数据
SendToAndRecvFrom(socketClient);
//释放socket
closesocket(socketClient);
//清理网络库
WSACleanup();
system("pause");
return 0;
}
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
// 定义WSAStartup函数中参数1 所需的版本号
WORD wVersionRequestd = MAKEWORD(2, 2); // 设置2.2版本号
// 声明WSAStartup函数中参数1 所需的结构体
WSADATA WSAData;
// 打开网络库
int nRet = WSAStartup(wVersionRequestd, &WSAData);
if (0 != nRet)
{
//出错了
int a = WSAGetLastError();
printf("网络库打开失败 %d\n", a);
return;
}
// 检验版本号
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
printf("校验版本号错误\n");
// 关闭网络库
WSACleanup();
return;
}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
// 创建服务端 socket
SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 判断服务端socket是否成功创建
if (INVALID_SOCKET == sock_server)
{
//出错了
int a = WSAGetLastError();
printf("创建服务端socket失败,获取的错误码为 %d\n", a);
//关闭网络库
WSACleanup();
return 0;
}
return sock_server;
}
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock)
{
/*int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
recvfrom函数:
作用:得到当前服务端接收到的消息
本质:复制 将协议缓存区的数据复制黏贴进自定义的buf
与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
参数1:客户端的socket
特点:
TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
参数2:客户端消息的存储空间,也就是一个字符数组
广域网:各级路由器上的MTU最小值是576字节
tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
参数3:想要读取的字节个数, 参数2的字节数
参数4:数据的读取方式
0 从协议缓存区取一个数据报,然后就删除掉
MSG_PEEK 取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
MSG_OOB 带外数据
参数5:对方的IP地址端口号
此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
参数6:参数5结构体的大小 int len = sizeof(&sockClient); 填 &len;
*/
/*int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
sendto函数:
作用:向目标发送数据报
本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
参数1:当前客户端的socket(自己的socket)
参数2:给接收方发送的字节串 UDP统一字节数 548
参数3:参数2的大小(字节个数 548)
参数4:填0
参数5:对方IP地址与端口号结构体
参数6:参数5的大小
*/
// 声明sendto函数中参数5所需的 服务端地址与端口号结构体
struct sockaddr_in sockServer;
// 给服务端地址与端口号结构体成员赋值
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(12345);
sockServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 循环与客户端进行收发消息
while (1)
{
// 定义要发送的字节数组
char strBuf[548] = { 0 };
// 手动发送
scanf_s("%s", strBuf, 548);
// 设置输入0时,将关闭客户端
if ('0' == strBuf[0])
{
break;
}
// 1.发消息
int nSet = sendto(sock, strBuf, sizeof(strBuf), 0, (struct sockaddr*)&sockServer, sizeof(sockServer));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSet)
{
// 出错了
int a = WSAGetLastError();
printf("发送消息失败,获取的错误码为:%d\n", a);
continue;
}
// 2.收消息
// 定义recvfrom 函数所需的接收字节数组
char buf[548] = { 0 };
// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
struct sockaddr sockClient; // 对方的IP地址与端口号
// 定义recvfrom函数中参数6所需的 参数5结构体的大小
int len = sizeof(sockClient);
int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
// 判断是否成功接收消息
if (SOCKET_ERROR == nRet)
{
// 出错了
int a = WSAGetLastError();
printf("接收消息失败,获取的错误码为:%d\n", a);
continue;
}
// 进行接收消息
printf("Server say: %s\n", buf);
}
}
程序运行结果:
?
?
|