列车实时数据通信协议(TRDP)探索之路【三】

TRDP协议中传输的PD(Process-Data)和MD(Message-Data)。

PD主要用于列车控制,传输命令和状态信息,数据量大,要求高可靠性、实时性和确定性,一般为周期性传送。

MD主要用于故障和诊断信息,数据量长短不一,一般都是按需传送,需要确保实时性。

PD和MD通信方式都是基于了生产者/消费者模型,包含了PUSH和PULL操作,设备角色有publisher、subscriber和requester。

PD消息PUSH操作一般是publisher周期性发布消息,subscriber接收此PD消息;PULL操作是subscriber或者requester请求消息,publisher发布需求的COMID消息,在这里requester可以是subscriber。

MD消息通常请求者发出请求,已经监听的应答者做出回复,请求者可以再确认收到应答。

/***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/

现在总结一下PD通信方式。

PD一般是周期性传输数据,协议规定通信方式为UDP。之前学习基于socket通信时,也只是知道UDP和TCP相比,UDP是面向无连接的,具体还怎么使用过,这次PD的收发包模型可以好好学习一波了!!!

UDP,用户数据包协议,属于传输层协议。传输数据时,不与对面先连接,而是直接把数据传送给对方。因此,UDP适用于传送数据量少,对可靠性要求不高的场景。例如比较常见的SNMP协议,TFTP协议,DNS协议等。如果SNMP协议每次请求MIB数据都需要先三次握手的话,那效率也太低了。

UDP协议通信流程非常简单。Client一般只要socket()一个socket,从而使用sendto()就可以向目的地址发送数据报了。Server一般socket()一个socket,对这个socket进行bind(),绑定端口号和IP,使用recvfrom就可以收到发送给自己的数据报了,如果需要回应,使用sendto()就可以发送了。当然,最后我们都需要close()申请的socket。

#include <sys/types.h>          
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain参数用于设置网络通信的域,AF_INET,IPV4,   AF_INET6, IPV6,其余的还有很多,一般只需牢记这两个。

type参数用于指定数据类型,

SOCK_STREAM         Provides sequenced, reliable, two-way, connection-based byte streams.   //用于TCP

SOCK_DGRAM          Supports datagrams (connectionless, unreliable messages ). //用于UDP

SOCK_RAW              Provides raw network protocol access.  //RAW类型,用于提供原始网络访问

protocol参数指定协议类型,默认可以置0,UDP是17.
返回值成功时返回socket文件描述符,失败则返回-1

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:正在监听端口的套接口文件描述符,通过socket获得
数buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
len:发送缓冲区的大小,单位是字节
flags:填0即可
dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
addrlen:表示第五个参数所指向内容的长度
返回值:成功:返回发送成功的数据长度
       失败: -1
这里要注意的是,在准备数据时,一般不会直接使用struct sockaddr类型,而是用srtuct sockaddr_in ,在sendto时强制转换类型。这里是因为,底层会再次类型转换回去。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:正在监听端口的套接口文件描述符,通过socket获得
buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
len:接收缓冲区的大小,单位是字节
flags:填0即可
src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
addrlen:表示第五个参数所指向内容的长度
返回值:成功:返回接收成功的数据长度
       失败: -1

int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
sockfd:正在监听端口的套接口文件描述符,通过socket获得
my_addr:需要绑定的IP和端口
addrlen:my_addr的结构体的大小
返回值:成功:0
       失败:-1


#include <unistd.h>
int close(int fd);

高级UDP编程【参考了大佬https://www.cnblogs.com/skyfsm/p/6287787.html

其实,在UDP中,也有connect()函数,但是仅仅只是为了确定对方的地址,没有其他的含义。

因为UDP自身的特点,决定了UDP会相对于TCP存在一些难以解决的问题。第一个就是UDP报文缺失问题。
在UDP服务器客户端的例子中,如果客户端发送的数据丢失,服务器会一直等待,直到客户端的合法数据过来。如果服务器的响应在中间被路由丢弃,则客户端会一直阻塞,直到服务器数据过来。

防止这样的永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,大概有这么两种方法:
1)使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。

使用select为recvfrom设置超时
设置select函数的第五个参数即可。

UDP发送报文可能会产生乱序问题,乱序就是发送数据的顺序和接收数据的顺序不一致,例如发送数据的顺序为A、B、C,但是接收到的数据顺序却为:A、C、B。产生这个问题的原因在于,每个数据报走的路由并不一样,有的路由顺畅,有的却拥塞,这导致每个数据报到达目的地的顺序就不一样了。UDP协议并不保证数据报的按序接收。

解决这个问题的方法就是发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报。

UDP流量控制问题

总所周知,TCP有滑动窗口进行流量控制和拥塞控制,反观UDP因为其特点无法做到。UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失(因为内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。

解决方法一般采用增大UDP缓冲区,使得接收方的接收能力大于发送方的发送能力。

int n = 220 * 1024; //220kB

setsocketopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

这样我们就把接收方的接收队列扩大了,从而尽量避免丢失数据的发生。

猜你喜欢

转载自blog.csdn.net/sinat_33518009/article/details/84728614