UDP.01.基础知识+基础模型
Posted oldmao_2001
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UDP.01.基础知识+基础模型相关的知识,希望对你有一定的参考价值。
文章目录
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
本节内容其实属于计算机网络基础方面的知识,大多数在网上以及书本上都有讲这里再次罗列一下,权当记录。
UDP协议头
基础信息
UDP源端口号(2字节) | UDP目的端口号(2字节) |
---|---|
校验和(2字节) | 信息长度(2字节) |
1、协议头(包头)总共8字节;
2、UDP源端口号:发送方的端口号;
3、UDP目的端口号:接收方端口号;
4、校验和:发送的数据,通过一定的算法计算出一个值,接收方接到该包,也会用同样的算法,计算出一个校验值,假设,数据包中途被拦截并篡改,那么接收端收到包之后,计算的校验值就与接收方不一样,得知出现问题,看不明白可以类比一下下载文件的md5校验码;
5、信息长度:包头+数据总字节数。
数据报特点
UDP全称是用户数据报协议(User Datagram Protocol),因此它是基于数据报的,其特点是:一份一份的,每份不能断,两份又不能拼倒一起;相对于TCP的数据流:源源不断,一个接着一个,流当然可以随意拆分和合并。
比如:在TCP模型中,客户端连续发送多次消息,服务器在recv的时候,能够一下都读出来。因为是水流嘛,都是源源不断的,所以有多少我都能一下读出来,或者读出来制定的个数,可拼,可断,其实这个也有不好的地方,之前在TCP的几个模型结构里面可以看出来,当我们调试程序的时候,服务器程序下断点停住,这个时候如果客户端发送多个消息,那么服务器继续运行后就会一次性recv出来(除非发送的消息长度大于buff长度);
数据报即使客户端连续发了很多消息,服务器一次recv只能读出一份数据报,后面的消息需要调用相应次数的recv才能读取完毕。
与TCP的对比
1、TCP更复杂,有20字节包头,以及可拓展最大60字节包头;
2、UDP无SYN(连接标志位)FIN(断开连接标志位),说明:UDP不用连接,即客户端无connect函数,服务器无listen,accept函数;
3、UDP无窗口大小,说明不能平衡双方的带宽(TCP窗口大小机制比这个说法更加复杂),一方send几次,另一方就要recv几次,当一方尝试发送超过UDP包限制大小的数据,超过部分会丢失;
4、UDP无ACK(确认收到标记位),说明对方收到无反馈,无法确认对方是否收到;
5、UDP无发送,接收顺序号,说明数据发送,接收无序,收没收到,完不完整无反馈,侧面说明不可靠(一般需要在应用层进行自行验证)。
小结
1、UDP简单,速度更快,更高效,(无连接,无验证,头信息少协议代码判断就少);
2、数据不可靠,(无验证);
3、不管是数据报,还是字节流,都是在传输线路上传输的数据,对于传输线路而言是没有区别的,区别在于数据到了传输层之后的处理方式,数据报就是数据报的形式处理,字节流就以流的特点处理。
思考:如何让其可靠?
我们可以在应用层模仿TCP的验证过程,减小数据报包的大小(包越大,出现丢帧的概率就越大)
还可以为数据包自行添加序号,然后在应用层进行拼接。
UDP基础模型
注意和TCP基础模型的不同(步骤、参数),其实UDP比TCP要简单。
服务器端
无Listen、Accept
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET
5、绑定地址与端口
6、与客户端收发消息
1、包含网络头文件网络库
# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")
64位系统也是这个Ws2_32.lib库,这里不区分大小写
2、打开网络库
打开网络库后这个库里的函数/功能才能使用。https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsastartup
int WSAAPI WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
参数1:网络库的版本,类型是WORD(实际上是unsigned short),长度是2字节
通过MAKEWORD函数将版本号分成两块分别放到2字节里面。
WORD MAKEWORD(
BYTE bLow,
BYTE bHigh
);
bLow(低地址):主版本
bHigh(高地址):副版本
参数2:传址调用,lpWSAData:是一个WSADATA结构体指针
typedef struct WSAData {
WORD wVersion;//实际上打开的网络库版本
WORD wHighVersion;//系统可提供的网络库最高版本
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;//返回可用的SOCKET句柄数量,已舍弃
unsigned short iMaxUdpDg;//UDP数据包大小,已舍弃
char FAR * lpVendorInfo;//供应商信息,已舍弃
} WSADATA, FAR * LPWSADATA;
返回值:
0,表示正确
其他,用WSAGetLastError()获取错误码
int main()
{
WORD wVersionRequested = MAKEWORD(2,2);//版本
WSADATA wsaDATA;
int iret = WSAStartup(wVersionRequested,&wsaDATA);
if (iret!=0)
{
//有错
switch(iret)
{
case WSASYSNOTREADY:
printf("解决方案:重启。。。");
break;
case WSAVERNOTSUPPORTED:
printf("解决方案:更新网络库");
break;
case WSAEINPROGRESS:
printf("解决方案:重启。。。");
break;
case WSAEPROCLIM:
printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
break;
case WSAEFAULT:
printf("解决方案:程序有误");
break;
}
getchar();
return 0;
}
//关闭网络库
WSACleanup();
system("pause");
return 0;
}
打开后要记得关闭网络库:
WSACleanup();
3、校验版本
这里可以用两个宏:
HIBYTE是获取高位副版本
LOBYTE是获取低位主版本
//校验版本,只要有一个不是2,说明系统不支持我们要的2.2版本
if (2!=HIBYTE(wsaDATA.wVersion)|| 2!=LOBYTE(wsaDATA.wVersion))
{
printf("版本有问题!");
WSACleanup();//关闭网络库
return 0;
}
版本不对记得关闭网络库并返回
4、创建SOCKET
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
SOCKET WSAAPI socket(
int af,
int type,
int protocol
);
参数1:地址类型
名称 | 取值 | 含义 |
---|---|---|
AF_INET | 2 | Internet协议版本4(IPv4)地址系列。 |
AF_INET6 | 23 | Internet协议版本6(IPv6)地址系列。 |
AF_BTH | 32 | 蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。 |
AF_IRDA | 26 | 红外数据协会(IrDA)地址系列。仅当计算机安装了红外端口和驱动程序时,才支持此地址系列。 |
参数2:套接字类型(数据的传递方式)
名称 | 数值 | 内容 |
---|---|---|
SOCK_STREAM | 1 | 提供带有OOB数据传输机制的顺序,可靠,双向,基于连接的字节流。此套接字类型使用传输控制协议(TCP)作为Internet地址系列(AF_INET或AF_INET6)。 |
SOCK_DGRAM | 2 | 一种支持数据报的套接字类型,它是固定(通常很小)最大长度的无连接,不可靠的缓冲区。此套接字类型使用用户数据报协议(UDP)作为Internet地址系列(AF INET或AF_INET6)。 |
SOCK_RAW | 3 | 提供允许应用程序操作下一个上层协议头的原始套接字。要操作lPv4标头,必须在套接字上设置IP_HDRINCL套接字选项。要操作lPv6标头,必须在套接字上设置IPV6_HDRINCL套接字选项。 |
SOCK_RDM | 4 | 提供可靠的消息数据报。这种类型的一个示例是Windows中的实用通用多播(PGM)多播协议实现,通常称为可靠多播节目。仅在安装了可靠多播协议时才支持此类型值。 |
SOCK_SEQPACKET | 5 | 提供基于数据报的伪流数据包。 |
参数3:协议类型,常见搭配如下表所示:
名称 | 数值 | 协议名称 | 地址参数 | 套接字类型 |
---|---|---|---|---|
IPPROTO_TCP | 6 | 传输控制协议(TCP) | AF_INET或AF_INET6 | SOCK_STREAM |
IPPROTO_UDP | 17 | 用户数据报协议(UDP) | AF_INET或AF INET6 | SOCK_DGRAM |
IPPROTO_ICMP | 1 | Internet控制消息协议(ICMP) | AF UNSPEC,AF_INET或AF_INET6 | SOCK RAW或未指定 |
IPPROTO_IGMP | 2 | Internet组管理协议(IGMP) | AF UNSPEC,AF_INET或AF_INET6 | SOCK RAW或未指定 |
IPPROTO_RM | 113 | 用于可靠多播的PGM协议(Windows Vista以上版本叫IPPROTO_PGM) | AF_INET | SOCK_RDM(仅在安装了可靠多播协议时才支持此协议值) |
这里要用IPPROTO_UDP
返回值:
成功则返回Socket句柄,使用完毕要用CloseSocket(socket)销毁该句柄。
失败返回INVALID_SOCKET,此时要关闭网络库,可用WSAGetLasterror()返回错误码。此时不用关闭Socket句柄。
具体代码:
// 4、创建SOCKET
SOCKET socketServer = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(INVALID_SOCKET == socketServer)
{
//清理网络库,不关闭句柄
WSACleanup();
return 0;
}
closesocket(socketServer);//与4、创建SOCKET对应,如果有创建客户端SOCKET句柄,也要关闭
5、绑定地址与端口
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind
int WSAAPI bind(
SOCKET s,
const sockaddr *name,
int namelen
);
参数1:服务器SOCKET句柄
参数2:sockaddr 结构体指针
struct sockaddr {
ushort sa_family;//2字节
char sa_data[14];//14字节
};
struct sockaddr_in {
short sin_family;//2字节
u_short sin_port;//2字节
struct in_addr sin_addr;//4字节
char sin_zero[8];//8字节
};
可以看到两个结构体内容不一样,但是大小是一样的,可以把下面的强转为上面的类型,而并不报错。
可以看到sockaddr_in 更加的方便我们填写端口号和IP地址,因此参数2通常是用sockaddr_in来定义,然后再强转为sockaddr类型。(搞这么复杂的原因还是考虑兼容性,很多系统里面只支持sockaddr格式)
参数3:参数2的长度,写sizeof(参数2)
返回值:成功返回0
失败处理方式和创建SOCKET处理方式一样
6、与客户端收发消息
收recvfrom
从协议缓冲区(UDP协议规定的)将数据复制到我们的buff(参数2),该函数是阻塞的。
int WSAAPI recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
参数1:服务器端的SOCKET句柄,这里和TCP的recv不一样,recv的参数1是客户端SOCKET句柄,recv的阻塞是等这里指定的客户端,是1对1的关系;UDP则没有指定接收哪个客户端的消息,因此recvfrom中服务器SOCKET和客户端SOCKET是1对多的关系。
参数2:字符数组,用于客户端消息的存储。关于字符数组的大小设置原则应尽量设置大一些,避免不必要的包的拆分和组合。
网络类型 | MTU | TCP最大长度 | UDP最大长度 |
---|---|---|---|
局域网 | 以太网1500 | min ( 1500 − ( 20 ∼ 60 ) T C P 包 头 − 2 0 I P 包 头 ) = 1420 \\min(1500-(20\\sim60)_{TCP包头}-20_{IP包头})=1420 min(1500−(20∼60)TCP包头−20IP包头)=1420 | 1500 − 8 U D P 包 头 − 2 0 I P 包 头 = 1472 1500-8_{UDP包头}-20_{IP包头}=1472 1500−8UDP包头−20IP包头=1472 |
广域网 | 路由器576 | min ( 576 − ( 20 ∼ 60 ) T C P 包 头 − 2 0 I P 包 头 ) = 496 \\min(576-(20\\sim60)_{TCP包头}-20_{IP包头})=496 min(576−(20∼60)TCP包头−20IP包头)=496 | 576 − 8 U D P 包 头 − 2 0 I P 包 头 = 548 576-8_{UDP包头}-20_{IP包头}=548 576−8UDP包头−20IP包头=548 |
两种网络可设置的大小不一样,对于TCP而言,设置为1420还是496都可以,反正可靠流传输,不会丢;对于UDP,按小的弄:548。
参数3:设定要读取的数据报字节个数,一般设置为参数2的大小
如果设置的数据报字节个数比参数2小,那么后面多出来的那部分估计会被丢弃;如果是TCP则可下一次再读取出来。
参数4:数据的读取方式,这个之前有:
默认是0即可。正常情况下recvfrom根据参数3读取数据缓冲区指定长度的数据后(指定长度大于数据长度则全部读取),数据缓冲区中被读取的数据会清除,把空间留出来给别的消息进来(不清理的话时间长了内存会溢出,数据缓冲区数据结构相当于队列)。
例如数据缓冲区中有如下数据:
a | b | c | d | e | f |
---|
调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到a,b。则变成
c | d | e | f |
---|
这个时候再调用recv(socketClient,buff,2,0);从数据缓冲区读取两个字节的数据得到c,d。
懂得正常逻辑后我们可以看下其他几种模式。
数值 | 含义 |
---|---|
0(默认值) | 从数据缓冲区读取数据后清空被读取的数据 |
MSG_PEEK(不建议使用,内存会爆) | 从数据缓冲区读取数据后不清空被读取的数据 |
MSG_OOB | 接收带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,因此也不推荐使用 |
MSG_WAITALL | 等待知道系统缓冲区字节数大于等于参数3所指定的字节数,才开始读取 |
如果使用MSG_PEEK模式,那么调用recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据得到a,b。由于不清空被读取的数据,缓冲区还是不变:
a | b | c | d | e | f |
---|
如果再次执行recv(socketClient,buff,2,MSG_PEEK);从数据缓冲区读取两个字节的数据还是得到a,b。
参数5:sockaddr结构体的传址调用,获取到发来消息的客户端信息
参数6:客户端信息的大小
返回值:
成功:返回读出来的字节大小,如果没有信息可读则在这里阻塞。
这里和TCP不一样的是,TCP如果客户端下线会收到0(正常下线)和10054(点×强制下线)。
失败:返回SOCKET_ERROR。
struct sockaddr sa;
int iSaLen = sizeof(sa);
char strRecvBuff[548]={0};
if(recvfrom(socketServer,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("服务器recvfrom失败错误码为:%d\\n",err);
}
发sendto
该函数将数据复制到系统的协议发送缓冲区,在伺机发送出去。
int WSAAPI sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
参数1:自己的SOCKET句柄(这里和send不一样,send的参数1是目标SOCKET句柄)
参数2:要发送的字符串
参数3:字符串长度
参数4:数据的发送方式。默认是0即可。当然还有其他取值,意义如下表:
数值 | 含义 |
---|---|
0(默认值) | 从数据缓冲区发送数据后清空被发送的数据 |
MSG_OOB | 传输带外数据,每次可以额外传输1个字节的数据,具体数据内容可以自己定义,这个方法可以用分别调用两次send函数,而且在不同TCP协议规范这个模式还不怎么兼容,不推荐使用 |
MSG_DONTROUTE | 指定数据不应受路由限制。由于Windows套接字服务提供程序可以选择忽略此标志,因此该模式只能在Linux系统下面用。 |
参数5:目标IP地址端口号的sockaddr结构体
参数6:sockaddr结构体大小
返回值:
成功:返回发送的字节大小。
失败:返回SOCKET_ERROR。
//发
if(sendto(socketServer,"This is a message from server~!",sizeof("This is a message from server~!"),0,&sa,sizeof(sa))==SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("服务器sendto失败错误码为:%d\\n",err);
}
客户端
无Connect
1、包含网络头文件网络库
2、打开网络库
3、校验版本
4、创建SOCKET(这里变量名改下)
// 4、创建SOCKET
SOCKET socketClient = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(INVALID_SOCKET == socketClient)
{
//清理网络库,不关闭句柄
WSACleanup();
return 0;
}
//服务器地址与端口
struct sockaddr_in si;
si.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
si.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
si.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
5、与服务器端收发消息
以上步骤和服务器端步骤基本一致,少了绑定,在创建SOCKET步骤上是创建服务器的地址端口结构体。
5、与服务器端收发消息
这里为了和服务器互动,不阻塞卡死,先发再收。
//发
char strSendBuff[548] = {0};
scanf("%s",strSendBuff);
if(sendto(socketClient,strSendBuff,sizeof(strSendBuff),0,(const struct sockaddr *)&si,sizeof(si))==SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("客户端sendto失败错误码为:%d\\n",err);
continue;
}
//收
struct sockaddr sa;
int iSaLen = sizeof(sa);
char strRecvBuff[548]={0};
if(recvfrom(socketClient,strRecvBuff,548,0,&sa,&iSaLen)==SOCKET_ERROR)
{
int err = WSAGetLastError();//取错误码
printf("客户端recvfrom失败错误码为:%d\\n",err);
continue;
}
printf("客户端recvfrom消息是:%s\\n",strRecvBuff);
优化: 处理点×关闭
点×关闭属于强制关闭,这个时候程序还有很多内核对象(SOCKET句柄)没有销毁,很容易造成内存泄露。
首先吧SOCKET句柄改成全局变量,然后写一段在强制时间关闭事件发生后要处理的代码
SOCKET socketServer;
//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
switch (dwType)
{
case CTRL_CLOSE_EVENT:
//关闭socket
closesocket(socketServer);
//关闭网络库
WSACleanup();
break;
}
return FALSE;
}
在主函数开头加一句:
//投递关闭事件
SetConsoleCtrlHandler(CtrlFun, TRUE);
完整代码
服务器
#include <stdio.h>
//1、包含网络头文件网络库
# include <WinSock2.h>
# pragma comment(lib, "Ws2_32.lib")
SOCKET socketServer;
//处理强制关闭事件
BOOL WINAPI CtrlFun(DWORD dwType)
{
switch (dwType[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础