说明
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);
针对内存块进行的拷贝。
-
函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置(右边的数据拷贝到左边来)
-
函数遇到'\0’不会停下来
-
如果source和destination有任何的重叠,复制的结果都是未定义的
-
返回值:memcpy拷贝结束后,返回的是目标空间的起始地址,而且是void*类型(实现各种类型数据的拷贝)
-
使用该函数,需要引用头文件: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#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)
-
拷贝消息可使用memcpy,这是基于内存的拷贝,适用于任何数据类型的拷贝。速度较块。
-
结构体初始化使用memset。
-
想要对于一个指针操作,可以在函数内部创建另一个指针变量,使用别名的方式操作:
1char* implict_name; 2implict_name=&string;
-
错误退出可以使用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
-
可以在开头定义很多出现很多次的东西,如
- 编译器
- 创建静态库的工具
- 参数
- 库目录
- 包含目录
- 目标文件
- 结果文件
使用
名称 := 字符串
即可定义变量 -
ar 命令是一个用于创建、修改和提取归档文件的工具,通常用于创建静态库(静态链接库)。这些静态库可以包含多个目标文件(.o 文件),并在链接时将这些目标文件打包成一个单一的库文件(通常以 .a 结尾)。
- 参数c:创建归档文件。如果归档文件存在,则不提示。
- 参数r:添加或替换文件到归档文件中。如果归档文件不存在,则创建一个新的归档文件。
- 参数u:只替换比当前归档内容更新的文件
-
Makefile函数
- wildcard可以使变量定义时,括号里的通配符仍然生效
- notdir:去掉路径,只保留文件名
- patsubst <模式>,<替换后的模式>,<要替换的文本>
-
伪目标:
是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行所在规则定义的命令,有时也可以将一个伪目标称为标签。伪目标通过PHONY来指明。PHONY定义伪目标的命令一定会被执行
无论当前目录下是否存在“clean”这个文件,输入“make clean”后,命令都会被执行。
1.PHONY : clean 2clean: 3 rm -rf $(OBJS)