报式套接字
1.动态报式套接字
在之前的例子上,发送的结构体中的名字由定长改变长。可以用变长结构体 。
变长结构体 是由gcc扩展的一种技术,它是指其最后一个成员的长度不固定(flexible array member,也叫柔性数组)。
使用范围:数据长度不固定,例如协议对接中有固定的头结构体,数据结构体不固定。
cpp
struct Var_Len_Struct
{
int nsize;
char buffer[0];
// 或者不指定大小 char buffer[];
};
结构体中的最后一个元素:一个没有元素的数组 (柔性数组)。我们可以通过动态开辟一个比结构体大的空间 ,然后让buffer去指向那些额外的空间,这样就可以实现可变长的结构体了。更为巧妙的是,我们甚至可以用nsize存储字符串buffer的长度。
修改代码如下
proto.h
定义NAMEMAX接收名字的最大长度,因为接收方不知道大小,需要按照最大长度接收
使用变长数组,最后一个元素为0或者为空
cpp
#ifndef __PROTO_H_
#define __PROTO_H_
#define RECVPORT 1986
#define NAMEMAX (512-8-8) // 512为udp包推荐的字节数,8为udp的报头大小,8为结构体中固定长度的大小,即math和chinese
struct msg_t
{
uint32_t chinese;
uint32_t math;
//变长
uint8_t name[0];
}__attribute__((packed));
# endif
snder.cpp
1.发送的结构体改为结构体指针
2.计算发送的结构体大小 sizeof(struct) + strlen(name)
3.动态申请内存malloc
4.sendto函数相应修改
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc, char **argv) {
int sd;
//结构体改为结构体指针
msg_t* sendmssg;
struct sockaddr_in raddr;
if (argc < 3) {
perror("Usage");
exit(1);
}
//strlen()不包括字符串结尾的空字符 '\0'
//sizeof(argv[2]) 返回的是指针的大小,而不是字符串的长度
if (strlen(argv[2]) > NAMEMAX) {
std::cout << "name id too long" << std::endl;
exit(1);
}
//创建socket
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//本地绑定(可以省略)
//填写发送消息
//变长,先计算结构体的长度
int size = sizeof (msg_t) + strlen(argv[2]);
//申请内存
sendmssg = (msg_t*)malloc(size);
if (sendmssg == NULL) {
perror("malloc()");
exit(1);
}
memcpy(sendmssg->name, argv[2], strlen(argv[2])+1);
sendmssg->chinese = ntohs(100);
sendmssg->math = ntohs(100);
//对端地址
raddr.sin_family = AF_INET;
raddr.sin_port = ntohs(RECVPORT);
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
//发送
if (sendto(sd, sendmssg, size, 0, (const sockaddr*)&raddr, sizeof(raddr)) == -1) {
perror("sendto()");
exit(1);
}
//关闭
close(sd);
}
rcver.cpp
1.接收的结构体改结构体指针
2.按最大长度NAMEMAX计算接收结构体的长度
3.动态分配内存
4.recvfrom函数相应修改
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPSTRSIZE 64
int main() {
// 套接字描述符
int sd;
// laddr -- local address -- 本机地址
// raddr -- remote address -- 对端地址
sockaddr_in laddr, raddr;
socklen_t raddr_len;
// 结构体指针,存储接收到的结构体
msg_t* mssg;
// 存储对端地址,点分式
char ipstr[IPSTRSIZE];
//创建socket,创建协议为ipv4的报式套接字,0为默认协议,即UDP
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//填写本机的地址信息
laddr.sin_family = AF_INET;
//ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(RECVPORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
//绑定接收的ip地址和端口号
if (bind(sd, (const sockaddr*)&laddr, sizeof(laddr)) < 0) {
perror("sendto()");
exit(1);
}
// 接收
// !!!!这里一定要初始化对端地址的大小!!!
raddr_len = sizeof(raddr);
//不知道对端的地址大小,按最大的来接
int size = sizeof(msg_t) + NAMEMAX - 1;
mssg = (msg_t*)malloc(size);
while (1) {
if (recvfrom(sd, mssg, size, 0, (sockaddr*)&raddr, &raddr_len) < 0) {
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
std::cout << "---------recive message from " << std::string(ipstr) << ":" << ntohs(raddr.sin_port) << "---------" << std::endl;
// 单字节传输不涉及到大端小端的存储情况
std::cout << "name" << ":" << mssg->name << std::endl;
std::cout << "math" << ":" << ntohs(mssg->math) << std::endl;
std::cout << "chinese" << ":" << ntohs(mssg->chinese) << std::endl;
}
//关闭
close(sd);
exit(1);
}
bash
vratdrh7771.rsv.ven.veritas.com [60]: ./snder 10.85.171.130 "hahhaahha"
vratdrh7771.rsv.ven.veritas.com [61]: ./snder 10.85.171.130 "44kkkkk"
vratdrh7771.rsv.ven.veritas.com [71]: ./rcver
---------recive message from 10.85.171.130:33261---------
name:hahhaahha
math:100
chinese:100
---------recive message from 10.85.171.130:36716---------
name:44kkkkkha
math:100
chinese:100
2.报式套接字的广播
在使用TCP/IP 协议的网络中,主机标识段host ID 为全1 的 IP 地址为广播地址。
广播数据有如下特点:
- TCP/IP协议栈中,传输层只有UDP可以广播,TCP没有广播的概念
- UDP广播不需要经过路由器转发,因为路由器不会转发广播数据
套接字机制提供了两个套接字选项接口来控制套接字行为。一个接口用来设置选项,另一个接口可以查询选项的状态。
cpp
GETSOCKOPT(2) Linux Programmer's Manual GETSOCKOPT(2)
NAME
getsockopt, setsockopt - get and set options on sockets
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
DESCRIPTION
getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd. Options may exist
at multiple protocol levels; they are always present at the uppermost socket level.
level
: 标识了选项应用的协议。- 如果选项是通用的套接字层次选项,则
level
设置成SOL_SOCKET
。否则,level设置成控制这个选项的协议编号 - 对于
TCP
,level是IPPROTO_TCP
- 对于
IP
,level是IPPROTO_IP
- 如果选项是通用的套接字层次选项,则
optname
: 需设置的选项optval
:根据选项的不同指向一个数据结构或者一个整数。一些选项是on/off开关。如果整数非0,则启用选项。如果整数为0,则禁止选项。optlen
:指定了optval
指向的对象的大小。
代码示例
snder.cpp:设置套接字,打开广播选项,并向广播地址255.255.255.255发送数据报
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
int main() {
int sd;
msg_t sendmssg;
struct sockaddr_in raddr;
//创建socket
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//本地绑定(可以省略)
//设置套接字,打开广播
//不同的层面封装了不同的属性,可以用man 7查看
//man 7 socket 找 socket option
//man 7 udp udp层可以改进的socket option
//man 7 ip...
//man 7 tcp
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
//填写发送消息
memset(&sendmssg, '\0', sizeof(sendmssg));
//strcpy(sendmssg.name, "tracy");
memcpy(sendmssg.name, "tracy", sizeof("tracy"));
sendmssg.chinese = ntohs(100);
sendmssg.math = ntohs(100);
//对端地址
raddr.sin_family = AF_INET;
raddr.sin_port = ntohs(RECVPORT);
inet_pton(AF_INET, "255.255.255.255", &raddr.sin_addr);
//发送
if (sendto(sd, &sendmssg, sizeof(sendmssg), 0, (const sockaddr*)&raddr, sizeof(raddr)) < 0) {
perror("sendto()");
exit(1);
}
//关闭
close(sd);
}
rcver.cpp:设置套接字,打开广播选项,如果不打开,可能收到,可能收不到
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPSTRSIZE 64
int main() {
// 套接字描述符
int sd;
// laddr 本机地址
// raddr 对端地址
sockaddr_in laddr, raddr;
// 存储接收到的结构体
msg_t mssg;
// 存储对端地址,点分式
char ipstr[IPSTRSIZE];
// 创建socket,创建协议为ipv4的报式套接字,0为默认协议,即UDP
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
int val = 1;
if(setsockopt(sd, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val)) < 0) {
perror("setsockopt()");
exit(1);
}
// 填写本机的地址信息
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(RECVPORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
//绑定接收(本机)的ip地址和端口号
if (bind(sd, (const sockaddr*)&laddr, sizeof(laddr)) < 0) {
perror("sendto()");
exit(1);
}
// 接收
// !!!!这里一定要初始化对端地址的大小!!!
socklen_t addr_len = sizeof(raddr);
while (1) {
if (recvfrom(sd, (void*)&mssg, sizeof(mssg), 0, (sockaddr*)&raddr, &addr_len) < 0) {
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
std::cout << "---------recive message from " << std::string(ipstr) << ":" << ntohs(raddr.sin_port) << "---------" << std::endl;
// 单字节传输不涉及到大端小端的存储情况
std::cout << "name" << ":" << mssg.name << std::endl;
std::cout << "math" << ":" << ntohs(mssg.math) << std::endl;
std::cout << "chinese" << ":" << ntohs(mssg.chinese) << std::endl;
}
//关闭
close(sd);
exit(1);
}
proto.h
cpp
#ifndef __PROTO_H_
#define __PROTO_H_
#define RECVPORT 1986
#define NAMESIZE 11
struct msg_t
{
//定长,不可能有负值
uint8_t name[NAMESIZE];
uint32_t chinese;
uint32_t math;
}__attribute__((packed));
# endif
注意:如果仍然接收不到,可以查看防火墙
bash
vratdrh7771.rsv.ven.veritas.com [70]: ./rcver
---------recive message from 10.85.171.130:43497---------
name:tracy
math:100
chinese:100
3.报式套接字的多播
多播地址,也叫组播地址,组播报文的目的地址使用D类IP地址, D类地址不能出现在IP报文的源IP地址字段。组播地址可以分为四类:
- 224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
- 224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;
- 224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;
- 239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
代码示例
proto.h:设置一个约定的多播地址
cpp
#ifndef __PROTO_H_
#define __PROTO_H_
#define MGROUP "224.2.2.2"
#define RECVPORT 1986
#define NAMESIZE 11
struct msg_t
{
//定长,不可能有负值
uint8_t name[NAMESIZE];
uint32_t chinese;
uint32_t math;
}__attribute__((packed));
# endif
snder.c:创建多播组
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>
#include "proto.h"
int main(int argc, char **argv) {
int sd;
msg_t sendmssg;
struct sockaddr_in raddr;
if (argc < 2) {
perror("uasge()");
exit(1);
}
//创建socket
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//设置套接字,创建多播组
//man 7 ip
ip_mreqn mreq;
//multicast group address
inet_pton(AF_INET, MGROUP, &mreq.imr_multiaddr);
//local本机address
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address);
//网络索引号
mreq.imr_ifindex = if_nametoindex("ens192");
//设置套接字,创建多播组
if(setsockopt(sd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt()");
exit(1);
}
//填写发送消息
memset(&sendmssg, '\0', sizeof(sendmssg));
//strcpy(sendmssg.name, "tracy");
memcpy(sendmssg.name, argv[1], sizeof(argv[1]) + 1);
sendmssg.chinese = ntohs(100);
sendmssg.math = ntohs(100);
//对端地址
raddr.sin_family = AF_INET;
raddr.sin_port = ntohs(RECVPORT);
inet_pton(AF_INET, MGROUP, &raddr.sin_addr);
//发送
if (sendto(sd, &sendmssg, sizeof(sendmssg), 0, (const sockaddr*)&raddr, sizeof(raddr)) < 0) {
perror("sendto()");
exit(1);
}
//关闭
close(sd);
}
其中,可以使用命令查看网络设备的索引号:
bash
ip ad sh
如下,1、2
为索引号,lo, ens192
为设备名
bash
vratdrh7771.rsv.ven.veritas.com [60]: ip ad sh
1: lo: <LOOPBACK,UP,LOWER_UP> .....
........
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> .....
........
或者可以通过下列函数来获取网络设备名的索引编号:
cpp
#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
rcver.cpp:加入多播组
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>
#include "proto.h"
#define IPSTRSIZE 64
int main() {
// 套接字描述符
int sd;
// laddr 本机地址
// raddr 对端地址
sockaddr_in laddr, raddr;
// 存储接收到的结构体
msg_t mssg;
// 存储对端地址,点分式
char ipstr[IPSTRSIZE];
// 创建socket,创建协议为ipv4的报式套接字,0为默认协议,即UDP
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//加入多播组
ip_mreqn mreq;
inet_pton(AF_INET, MGROUP, &mreq.imr_multiaddr);
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("ens192");
if(setsockopt(sd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt()");
exit(1);
}
// 填写本机的地址信息
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(RECVPORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
//绑定接收(本机)的ip地址和端口号
if (bind(sd, (const sockaddr*)&laddr, sizeof(laddr)) < 0) {
perror("sendto()");
exit(1);
}
// 接收
// !!!!这里一定要初始化对端地址的大小!!!
socklen_t addr_len = sizeof(raddr);
while (1) {
if (recvfrom(sd, (void*)&mssg, sizeof(mssg), 0, (sockaddr*)&raddr, &addr_len) < 0) {
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
std::cout << "---------recive message from " << std::string(ipstr) << ":" << ntohs(raddr.sin_port) << "---------" << std::endl;
// 单字节传输不涉及到大端小端的存储情况
std::cout << "name" << ":" << mssg.name << std::endl;
std::cout << "math" << ":" << ntohs(mssg.math) << std::endl;
std::cout << "chinese" << ":" << ntohs(mssg.chinese) << std::endl;
}
//关闭
close(sd);
exit(1);
}
运行结果
bash
vratdrh7771.rsv.ven.veritas.com [109]: ./rcver
---------recive message from 10.85.171.130:48514---------
name:mike
math:100
chinese:100
---------recive message from 10.85.171.130:39871---------
name:hahah
math:100
chinese:100
多播中有一个特殊的 ip 地址(224.0.0.1
),它表示,所有支持多播的地址默认都存在这个组当中,并且无法离开。如果 snder 方向这个 ip 地址发送信息,就相当于向 255.255.255.255
上发消息。
4.UDP协议分析
4.1.丢包原因
UDP丢包并不是因为TTL,TTL是当前包的要跳转的路由的个数,linux环境下一般默认为64,Windows一般为128,一般情况下完全足够。丢包其实是由于阻塞造成的。路由有等待队列,并不是我的数据包从本路由到下一个路由是无条件发送的,而是有等待队列,这个等待队列会有丢包的算法实现。比如当前队列已经排列百分之N的容量时,就会随机的丢包等操作。
解决:闭环流控(停等式流控)
4.2.停等式流量控制
问题1 发送端发送后,接收端返回ACK,发送端会在一段时间后才能收到消息
问题2:发送端发送消息后丢包了,在一定时间(RTT)后没有收到接收端ACK,则重新发送消息
问题3:发送端发送消息后,接收端收到了数据,并且发送了ACK,但是ACK数据丢了,在一定时间后没有收到接收端ACK,则重新发送消息。接收端如何判断收到的包已经收到过了(data包加编号),则直接放弃该包,直接回复ACK。
问题4:如图 d2 丢包,ACK1 延迟回复给了发送端,则发送端认为d2发送成功,继续发d3,实际接受端是没有收到过d2的。所以ACK也要加编号,否则就可能出现这类问题。