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进行发收消息
select() 函数:
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
select() 函数: ?? ?作用:监视socket集合,如果某个socket发送事件(链接或者发收数据),通过返回值以及参数反馈给我们 参数1:直接填 0 忽略 参数2:检测是否有可读的socket,即客户端发来消息了,该socket就会被设置,这里只有一个服务端socket 参数3:检测是否有可读的socket,没有客户端就不用了,直接填 NULL 参数4:检测套接字上的异常错误,这里只有一个服务端socket,直接填 NULL 参数5:最大等待时间,比如当客户端没有请求时,那么select函数可以等一会,一段时间过后,还没有, ?? ??? ?就继续执行select下面的语句,如果有了,就立即执行下面的语句 ?? ??? ?//定义select函数中的参数5 最大等待事件 ?? ??? ?timeval ts; ?? ??? ?ts.tv_sec = 3;?? ?//秒 ?? ??? ?ts.tv_usec = 0;?? ?//微秒
服务端代码:
#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 协议的特点:
面向非连接的,不可靠的,基于数据报的传输层协议。
UDP/IP 与 TCP/IP 的区别:
1.基本C/S模型中服务端代码:
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进行发收消息
*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建服务端socket
SOCKET socketServer;
// 4.绑定地址与端口号
void Bind(SOCKET sock);
// 5.与客户端进行发收数据
void RecvAndSendBySelect(SOCKET sock);
// 6.监视关闭窗口按钮函数所需的参数1 回调函数
BOOL WINAPI fun(DWORD CtrlType);
int main()
{
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
socketServer = Sock_Server();
// 4.绑定地址与端口号
Bind(socketServer);
// 5.与客户端进行发收数据
RecvAndSendBySelect(socketServer);
// 6.监视关闭窗口
SetConsoleCtrlHandler(fun, TRUE);
// 关闭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 RecvAndSendBySelect(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的大小
*/
/*int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
select() 函数:
作用:监视socket集合,如果某个socket发送事件(链接或者发收数据),通过返回值以及参数反馈给我们
参数1:直接填 0 忽略
参数2:检测是否有可读的socket,即客户端发来消息了,该socket就会被设置,这里只有一个服务端socket
参数3:检测是否有可读的socket,没有客户端就不用了,直接填 NULL
参数4:检测套接字上的异常错误,这里只有一个服务端socket,直接填 NULL
参数5:最大等待时间,比如当客户端没有请求时,那么select函数可以等一会,一段时间过后,还没有,
就继续执行select下面的语句,如果有了,就立即执行下面的语句
//定义select函数中的参数5 最大等待事件
timeval ts;
ts.tv_sec = 3; //秒
ts.tv_usec = 0; //微秒
*/
// 循环与客户端进行收发消息
while (1)
{
//select模型
// 声明select所需的结构体 集合fd
fd_set fd;
// 清空集合
FD_ZERO(&fd);
// 将服务端socket装进集合中
FD_SET(sock, &fd);
// 声明select函数中参数5所需的最大等待时间结构体
struct timeval ts;
// 给结构体成员赋值
ts.tv_sec = 0;
ts.tv_usec = 0;
// 调用select函数
int nSel = select(0, &fd, NULL, NULL, &ts);
// 判断select函数的返回值
if (SOCKET_ERROR == nSel)
{
// 出错了,通过WSAGetLastError() 获取错误码
int a = WSAGetLastError();
printf("执行select函数错误,%d\n", a);
break; // 退出函数
}
if (0 == nSel)
{
// 在最大等待时间内没有等待到事件
continue; // 进行继续等待
}
if (0 < nSel)
{
// socket有消息了,进行收发消息处理
// 1.收消息
// 定义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("client say: %s\n", buf);
// 2.发消息
int nSet = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, sizeof(sockClient));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSet)
{
// 出错了
int a = WSAGetLastError();
printf("发送消息失败,获取的错误码为:%d\n", a);
continue;
}
}
}
}
// 6.监视关闭窗口按钮函数所需的参数1 回调函数
BOOL WINAPI fun(DWORD CtrlType)
{
switch (CtrlType)
{
case CTRL_CLOSE_EVENT:
// 关闭socket
closesocket(socketServer);
// 关闭网络库
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);
}
}
程序运行结果:
?
|