lightrtp项目学习

说明

company内部项目,故不贴出源代码,只记录学习到的知识。

这次学习深深知道了阅读文档和源码的重要性,以及C语言的精密性,多多学习!

基础知识

首先1字节(Byte)是8位(bit)。一般网络,文件编程都是以字节为单位。

在Linux中,一些类型的存储大小如下:(uname -m查看)

类型 Linux i686(32位) Linux x86_64(64位) Windows
char 1字节 1字节 1字节
unsigned char 1 1 1
short 2 2 2
unsigned short 2 2 2
int 4 4 4
unsigned int 4 4 4
long 4 8 4
unsigned long 4 8 4
float 4 4 4
double 8 8 8
long int 4 8 4
long long 8 8 8
long double 12 16 8

C语言为了方便精确编程定义了很多类型:

  • uint16_t:unsigned short,无符号16位整数
  • uint32_t:unsigned int,无符号32位整数
  • socklen_t:unsigned int, 无符号32位整数, 用于表示socket地址结构长度的数据类型。
  • size_t: unsigned long, 是一种用于表示内存大小的数据类型, 说明说Linux 64位系统中内存大小是8字节。
  • ssize_t: long.

函数说明

<string.h>

1.memcpy

1void *memcpy(void dest[restrict .n], const void src[restrict .n], size_t n);

针对内存块进行的拷贝。

  1. 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置(右边的数据拷贝到左边来)

  2. 函数遇到'\0’不会停下来

  3. 如果source和destination有任何的重叠,复制的结果都是未定义的

  4. 返回值:memcpy拷贝结束后,返回的是目标空间的起始地址,而且是void*类型(实现各种类型数据的拷贝)

  5. 使用该函数,需要引用头文件:string.h0

2.memset

1void *memset(void s[.n], int c, size_t n);

初始化函数,作用是将某一块内存中的全部设置为指定的值。

  • s指向要填充的内存块。
  • c是要被设置的值。
  • n是要被设置该值的字符数。
  • 返回类型是一个指向存储区s的指针。

memset函数是按照字节对内存块进行初始化,其实c的实际范围应该在0~255,因为memset函数只能取c的后八位给所输入范围的每个字节。也就是说无论c多大只有后八位二进制是有效的

sys/socket.h

1.socket

1int socket(int domain, int type, int protocol);

建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符

如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。

函数参数:

  • domain:

    函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。

  • type:

    函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等。

  • protocol:

    函数socket()的第3个参数protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

2.bind

1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数参数:

  • sockfd 表示socket函数创建的通信文件描述符

  • addrlen 表示所指定的结构体变量的大小

  • addr 表示struct sockaddr的地址,用于设定要绑定的ip和端口

     1struct sockaddr {
     2   sa_family_t sa_family;
     3   char        sa_data[14];
     4}
     5  
     6//sa_family 用于指定AF_***表示使用什么协议族的ip
     7//sa_data 存放ip和端口
     8  
     9//这里有一个问题,直接向sa_data中写入ip和端口号有点麻烦,内核提供struct sockaddr_in结构体进行写入,通过/usr/include/linux/in.h可以看到结构体原型
    10//使用该结构体时需要包含<netinet/in.h>头文件,且sockaddr_in结构体是专门为tcp/ip协议族使用,其他协议族需要使用其对应的转换结构体,比如“域通信协议族” 使用的是sockaddr_un结构体
    

例:

1struct sockaddr_in addr;
2addr.sin_family = AF_INET; //设置tcp协议族
3addr.sin_port = htons(6789); //设置端口号
4addr.sin_addr.s_addr = inet_addr("192.168.1.105"); //设置ip地址
5
6ret = bind(skfd, (struct sockaddr*)&addr, sizeof(addr));

3.sendto

1ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数:前面三个参数分别表示:套接字描述符,指向写出缓冲区的指针和写字节数。 dest_addr:指向一个含有数据报接收者的协议地址(如IP地址和端口号)的套接字地址结构(上面所说的sockaddr_in),其大小由addrlen参数指定。

4.recvfrom

1ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
2                        int flags,
3                        struct sockaddr *_Nullable restrict src_addr,
4                        socklen_t *_Nullable restrict addrlen);

参数:

  • sockfd:要接收数据的套接字文件描述符。
  • buf:存储数据的缓冲区。
  • len:缓冲区的大小。
  • flags:指定接收数据时的行为标志,通常设置为0。
  • src_addr:(可选)用于接收发送方地址信息的结构体。
  • addrlen:(可选)指向src_addr结构体的长度。

返回值:

如果成功接收到数据,返回接收到的字节数。

如果发生错误,返回-1,并设置errno以指示错误的类型。

请注意,recvfrom函数通常与sendto函数配对使用,用于在网络编程中进行双向通信。

netinet/in.h

1.inet_addr

1in_addr_t inet_addr(const char *cp);

它将参数cp所指向的字符串形式的IP地址(“192.168.1.11”)转换为二进制的网络字节序的IP地址形式。

该函数的缺点是:如果IP地址是255.255.255.255。那么调用inet_addr()函数后将返回-1(因为-1的补码形式是0xFFFFFFFF)。所以不建议使用inet_addr()函数,而是使用inet_aton()函数。

2.htons

1uint16_t htons(uint16_t hostshort);

htons 是把你机器上的整数转换成“网络字节序”, 网络字节序是 big-endian,也就是整数的高位字节存放在内存的低地址处。 而我们常用的 x86 CPU (intel, AMD) 电脑是 little-endian,也就是整数的低位字节放在内存的低字节处。

通信规范

C语言中的RTP

对于通信协议规范,应该使用位域来定义,且变量的类型需要更加严格。

在构造数据包时,注意rtp协议中的变长部分(csrc),并且基于变长部分的大小赋值后面的负载。

C语言中的UDP

使用socket编程的对应参数即可,内部已经封装了UDP头。注意网络最大传输单元是1500字节,除去各种协议的头部剩下的才是负载用的空间。

编程技巧

  1. 编程时可以用枚举定义统一的错误返回值,这样比较规范也容易找到错误,同时要注意分类。

  2. 在使用位域时,可以在定义位域的上下加上编译器对齐参数,这样会使位域定义的更严密:

     1//编译器参数, 保存原来的对齐方式,并设新的对齐方式设置为一个字节对齐
     2#pragma pack(push, 1)
     3   
     4struct {
     5	int a:1;
     6	int b:2;
     7	char c:1;
     8}sample;
     9   
    10//还原为原来的对齐方式
    11#pragma pack(pop)
    
  3. 拷贝消息可使用memcpy,这是基于内存的拷贝,适用于任何数据类型的拷贝。速度较块。

  4. 结构体初始化使用memset。

  5. 想要对于一个指针操作,可以在函数内部创建另一个指针变量,使用别名的方式操作:

    1char* implict_name;
    2implict_name=&string;
    
  6. 错误退出可以使用perror+exit

程序流程图

发送数据:

 1graph LR;
 2    start(开始) --> rtp_stream[rtp流初始化-socket,地址];
 3    start --> rtp_message[rtp消息初始化-版本,ssrc];
 4    start --> read_file[打开传输文件];
 5    read_file --> payload1
 6    read_file --> payload2
 7    read_file --> payload3[...]
 8    rtp_message --> rtp_packet_create[生成rtp包]
 9    payload1 --> rtp_packet_create
10    payload2 --> rtp_packet_create
11    payload3 --> rtp_packet_create
12    rtp_packet_create --> sendto;
13    rtp_stream --> sendto;
14    sendto[sendto函数] --> rtp_send(发送消息);
15    
16    subgraph UDP相关
17    	rtp_stream
18    end
19    
20    subgraph rtp相关
21    	rtp_message
22    end
23    
24    subgraph 文件
25    	payload1
26    	payload2
27    	payload3
28    end

接受数据过程差不多,基本上是反过来,就不写了。

Makefile

  1. 可以在开头定义很多出现很多次的东西,如

    • 编译器
    • 创建静态库的工具
    • 参数
    • 库目录
    • 包含目录
    • 目标文件
    • 结果文件

    使用名称 := 字符串即可定义变量

  2. ar 命令是一个用于创建、修改和提取归档文件的工具,通常用于创建静态库(静态链接库)。这些静态库可以包含多个目标文件(.o 文件),并在链接时将这些目标文件打包成一个单一的库文件(通常以 .a 结尾)。

    • 参数c:创建归档文件。如果归档文件存在,则不提示。
    • 参数r:添加或替换文件到归档文件中。如果归档文件不存在,则创建一个新的归档文件。
    • 参数u:只替换比当前归档内容更新的文件
  3. Makefile函数

    • wildcard可以使变量定义时,括号里的通配符仍然生效
    • notdir:去掉路径,只保留文件名
    • patsubst <模式>,<替换后的模式>,<要替换的文本>
  4. 伪目标:

    是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行所在规则定义的命令,有时也可以将一个伪目标称为标签。伪目标通过PHONY来指明。PHONY定义伪目标的命令一定会被执行

    无论当前目录下是否存在“clean”这个文件,输入“make clean”后,命令都会被执行。

    1.PHONY : clean
    2clean:
    3	rm -rf $(OBJS)