这篇笔记记一下网络应用编程以及CAN总线的应用编程。
网络基础知识
这个在学习lwIP的时候已经接触过了,这边再过一下,我自己觉得没什么意思的我就跳过了。
网络通信概述
网络通信本质上是一种进程间通信,是位于网络中不同主机上的进程之间的通信,属于IPC的一种,通常称为socket IPC,以此解决在网络环境中,不同主机上应用程序之间的通信问题。
可以分成以下的三个层次:
- 硬件层:网卡设备,收发网络数据
- 驱动层:网卡驱动(Linux内核网卡驱动代码)
- 应用层:上层应用程序(调用socket接口或更高级别接口实现网络相关应用程序)
网络互连模型:OSI七层模型
七层模型,亦称OSI(Open System Interconnection)。OSI七层参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间网络互联的标准体系,一般称为OSI参考模型或七层模型。OSI七层模型是一个网络互连模型,从上到下依次是:
- 应用层
应用层(Application Layer)是OSI参考模型中的最高层,是最靠近用户的一层,为上层用户提供应用接口,也为用户直接提供各种网络服务。常见应用层的网络服务协议有:HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP。
- 表示层
表示层(Presentation Layer)提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩/解压缩和加密/解密(提供网络的安全性)也是表示层可提供的功能之一。
- 会话层
会话层(Session Layer)对应主机进程,指本地主机与远程主机正在进行的会话。会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。将不同实体之间表示层的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。
- 传输层
传输层(Transport Layer)定义传输数据的协议端口号,以及端到端的流控和差错校验。该层建立了主机端到端的连接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括差错校验处理和流控等问题。通常说的,TCP、UDP协议就工作在这一层,端口号既是这里的“端”。
- 网络层
进行逻辑地址寻址,实现不同网络之间的路径选择。本层通过IP寻址来建立两个节点之间的连接,为源端发送的数据包选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。网络层(Network Layer)也就是通常说的IP层。该层包含的协议有:IP(Ipv4、Ipv6)、ICMP、IGMP 等。
- 数据链路层
数据链路层(Data Link Layer)是OSI参考模型中的第二层,负责建立和管理节点间逻辑连接、进行硬件地址寻址、差错检测等功能。将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。
数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。MAC子层的主要任务是解决共享型网络中多用户对信道竞争的问题,完成网络介质的访问控制;LLC子层的主要任务是建立和维护网络连接,执行差错校验、流量控制和链路控制。
数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。
- 物理层
物理层(Physical Layer)是OSI参考模型的最低层,物理层的主要功能是:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。使数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
实际上,网络数据信号的传输是通过物理层实现的,通过物理介质传输比特流。物理层规定了物理设备标准、电平、传输速率等。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆等,这些都是物理层的传输介质。
TCP/IP四层/五层模型
TCP/IP五层模型中,将OSI七层模型的最上三层(应用层、表示层和会话层)合并为一个层,即应用层,所以TCP/IP五层模型包括:应用层、传输层、网络层、数据链路层以及物理层。四层模型就是把数据链路层和物理层合并为网络接口层。
数据封装/拆封
其实就是加各种头部和尾部:
IP地址
在Internet上,每一个节点都依靠唯一的IP地址相互区分和相互联系,IP地址用于标识互联网中的每台主机的身份,设计人员为每个接入网络中的主机都分配一个IP地址(Internet Protocol Address),只有合法的IP地址才能接入互联网中并且与其他主机进行网络通信,IP地址是软件地址,不是硬件地址,硬件MAC地址是存储在网卡中的,应用于局域网中寻找目标主机。
关于IP地址的更多知识可以直接看教程,这里就不记录了。这里就记一下几个特殊的。
直接广播(Direct Broadcast Address):向某个网络上所有的主机发送报文。TCP/IP规定,主机号各位全部为“1”的IP地址用于广播,叫作广播地址。用的最多的C类地址,这个就是广播:xxx.xxx.xxx.255。
受限广播地址,是在本网络内部进行广播的一种广播地址,TCP/IP规定,32比特全为“1”的IP地址用于本网络内的广播,也就是 255.255.255.255。适用于此时不知道本网络的网络号时进行广播。
多播地址,有一个发送者多个接受者,且发送者只发一次数据包,属于D类地址,只能用做目的地址而不是主机源地址。
环回地址(Loopback Address)是用于网络软件测试以及本机进程之间通信的特殊地址。把A类地址中的127.XXX.XXX.XXX的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。
IP地址32bit全为0的地址(也就是0.0.0.0)表示本网络上的本主机,只能用作源地址。
对于C类地址,前三段一样即在同一网段下。
TCP/IP协议
应用层协议HTTP、FTP、MQTT…以及传输层协议TCP、UDP等这些都属于TCP/IP协议。
- HTTP协议
HTTP超文本传输协议(HyperText Transfer Protocol)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网数据通信的基础。
HTTP协议工作于客户端、服务器端模式下,浏览器作为HTTP客户端通过URL向HTTP服务端即 WEB 服务器发送请求。Web服务器根据接收到的请求后,向客户端发送响应信息。
- FTP协议
FTP协议(File Transfer Protocol),是一种文件传输协议,从一个主机向一个主机传输文件的协议。FTP协议同样也是基于客户端-服务器模式,在客户端和服务器之间进行文件传输。
TCP协议
这个在学习lwIP的时候学过了,这里直接略过,可以直接看教程。
UDP协议
这个在学习lwIP的时候学过了,这里直接略过,可以直接看教程。
端口号
端口号本质上就是一个数字编号,用来在一台主机中唯一标识一个能上网(能够进行网络通信)的进程,端口号的取值范围为0-65535。一台主机通常只有一个IP地址,但是可能有多个端口号,每个端口号表示一个能上网的进程。通过“IP地址+端口号”来区分主机不同的进程。
socket编程基础
socket简介
套接字(socket)是Linux下的一种进程间通信机制(socket IPC),使用socket IPC可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC通常使用客户端<—>服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了socket接口,只需要调用socket接口开发应用程序即可!socket是应用层与TCP/IP协议通信的中间软件抽象层,它是一组接口。
socket编程接口介绍
使用socket接口需要包含以下两个头文件:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
socket()函数
原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把这个文件描述符称为socket描述符(socket descriptor),这个socket描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。调用失败,则会返回-1,并且会设置errno变量以指示错误类型。
参数domain用于指定一个通信域;这将选择将用于通信的协议族。对于TCP/IP协议来说,通常选择AF_INET就可以。参数type指定套接字的类型。参数protocol通常设置为0,表示为给定的通信域和套接字类型选择默认协议。
示例如下:
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
if (0 > socket_fd) {
perror("socket error");
exit(-1);
}
......
......
close(socket_fd); //关闭套接字
bind()函数
原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于将一个IP地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。将参数sockfd指定的套接字与一个地址addr进行绑定,成功返回0,失败情况下返回-1,并设置errno以提示错误原因。参数addr是一个指针,指向一个struct sockaddr类型变量。当然一般使用的时候会用struct sockaddr_in结构体。
使用示例如下:
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
htons和htonl并不是函数,只是一个宏定义,主要的作用在于为了避免大小端的问题,需要这些宏需要在我们的应用程序代码中包含头文件<netinet/in.h>。
listen()函数
只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在bind()函数之后调用,在accept()函数之前调用,它的函数原型是:
int listen(int sockfd, int backlog);
无法在一个已经连接的套接字(即已经成功执行connect()的套接字或由accept()调用返回的套接字)上执行listen()。参数backlog用来描述sockfd的等待连接队列能够达到的最大值。
accept()函数
服务器调用listen()函数之后,就会进入到监听状态,等待客户端的连接请求,使用accept()函数获取客户端的连接请求并建立连接。函数原型如下所示:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()函数通常只用于服务器应用程序中,如果调用accept()函数时,并没有客户端请求连接(等待连接队列中也没有等待连接的请求),此时accept()会进入阻塞状态,直到有客户端连接请求到达为止。当有客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字,连接到调用connect()的客户端。
参数addr是一个传出参数,用来返回已连接的客户端的IP地址与端口号等这些信息。参数addrlen应设置为addr所指向的对象的字节长度。
connect()函数
原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于客户端应用程序中,客户端调用connect()函数将套接字sockfd与远程服务器进行连接,参数addr指定了待连接的服务器的IP地址以及端口号等信息,参数addrlen指定addr指向的struct sockaddr对象的字节大小。函数调用成功则返回0,失败返回-1,并设置errno以指示错误原因。
发送/接收数据
可以调用read()或recv()函数读取网络数据,调用write()或send()函数发送数据。
recv()函数原型如下所示:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数sockfd指定套接字描述符,参数buf指向了一个数据接收缓冲区,参数len指定了读取数据的字节大小,参数flags可以指定一些标志用于控制如何接收数据,一般可设置为0。
send()函数原型如下:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
同样可通过flags指定标志来改变处理传输数的方式。send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,能保证的只是当send成功返回时,数据已经被无错误的发送到网络驱动程序上。
close()函数
当不再需要套接字描述符时,可调用close()函数来关闭套接字,释放相应的资源。
IP地址格式转换
自行输入的IP地址是点分十进制的,计算机所需要理解的是二进制形式的 IP 地址,就需要在点分十进制字符串和二进制地址之间进行转换。
inet_ntop、inet_pton函数
将二进制Ipv4或Ipv6地址转换成以点分十进制表示的字符串形式,或将点分十进制表示的字符串形式转换成二进制Ipv4或Ipv6地址。使用这两个函数只需包含<arpa/inet.h>头文件即可!
inet_pton()函数原型如下所示:
int inet_pton(int af, const char *src, void *dst);
inet_pton()函数将点分十进制表示的字符串形式转换成二进制Ipv4或Ipv6地址。
将字符串src转换为二进制地址,参数af必须是AF_INET或AF_INET6;并将转换后得到的地址存放在参数dst所指向的对象中,如果参数af被指定为AF_INET,则参数dst所指对象应该是一个struct in_addr结构体的对象;如果参数af被指定为AF_INET6,则参数dst所指对象应该是一个struct in6_addr结构体的对象。
inet_pton()转换成功返回1(已成功转换)。如果src不包含表示指定地址族中有效网络地址的字符串,则返回0。如果af不包含有效的地址族,则返回-1并将errno设置为EAFNOSUPPORT。
inet_ntop()函数执行与inet_pton()相反的操作,函数原型如下所示:
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数af与inet_pton()函数的af参数意义相同。
其余参数一样,参数size指定了该缓冲区的大小。inet_ntop()在成功时会返回dst指针。如果size的值太小了,那么将会返回NULL并将errno设置为ENOSPC。
socket编程实战
编写服务器程序
流程如下:
- 调用socket()函数打开套接字,得到套接字描述符;
- 调用bind()函数将套接字与IP地址、端口号进行绑定;
- 调用listen()函数让服务器进程进入监听状态;
- 调用accept()函数获取客户端的连接请求并建立连接;
- 调用read/recv、write/send与客户端进行通信;
- 调用close()关闭套接字。
首先定义一下端口号,一般会大于5000防止冲突。
main函数中,定义好sockaddr_in结构体变量server_addr和client_addr,定义好ip_str字符串数组,定义好int变量sockfd和connfd,定义addrlen就是sizeof(client_addr),最后定义recvbuf字符串数组作为缓冲。
调用socket打开socket传入sockfd中,然后通过server_addr的成员变量初始化端口号并调用bind绑定,之后调用listen使得sockfd进入监听状态,调用accept阻塞等待客户端连接,accept成功返回就进入printf打印成功连接;之后进入for死循环中,memset清空recvbuf然后调用recv读数据之后printf打印出来。
编写客户端程序
首先定义好服务器的端口号以及服务器IP地址,IP地址需要事先查好之后定义。
main函数中,调用socket打开sockfd,然后设置server_addr的成员变量调用connect连接服务器;之后进入for死循环,memset清空buf缓冲,然后fgets获取输入的字符并调用send发送数据给服务器。
CAN应用编程基础
CAN基础知识
这个在精英板包括驱动开发的时候都有学过,这里就略过了。
SocketCan应用编程
由于Linux系统将CAN设备作为网络设备进行管理,因此在CAN总线应用开发方面,Linux提供了SocketCAN应用编程接口,使得CAN总线通信近似于和以太网的通信,应用程序开发接口更加通用,也更加灵活。
SocketCAN中大部分的数据结构和函数在头文件linux/can.h中进行了定义,所以,在应用程序中一定要包含<linux/can.h>头文件。
创建socket套接字
CAN总线套接字的创建采用标准的网络套接字操作来完成,网络套接字在头文件<sys/socket.h>中定义。创建CAN套接字的方法如下:
int sockfd = -1;
/* 创建套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
在SocketCan中,通常将第一个参数设置为PF_CAN,指定为CAN通信协议;第二个参数用于指定套接字的类型,通常将其设置为SOCK_RAW;第三个参数通常设置为CAN_RAW。
套接字与CAN设备绑定
将创建的套接字与can0进行绑定,示例代码如下所示:
......
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
int ret;
......
strcpy(ifr.ifr_name, "can0"); //指定名字
ioctl(sockfd, SIOCGIFINDEX, &ifr);
can_addr.can_family = AF_CAN; //填充数据
can_addr.can_ifindex = ifr.ifr_ifindex;
/* 将套接字与 can0 进行绑定 */
ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
struct ifreq定义在<net/if.h>头文件中,而struct sockaddr_can定义在<linux/can.h>头文件中。
设置过滤规则
如果没有设置过滤规则,应用程序默认会接收所有ID的报文;如果应用程序只需要接收某些特定ID的报文,则可以通过setsockopt函数设置过滤规则。示例如下:
struct can_filter rfilter[2]; //定义一个 can_filter 结构体对象
// 填充过滤规则,只接收 ID 为(can_id & can_mask)的报文
rfilter[0].can_id = 0x60A;
rfilter[0].can_mask = 0x7FF;
rfilter[1].can_id = 0x60B;
rfilter[1].can_mask = 0x7FF;
// 调用 setsockopt 设置过滤规则
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
如果应用程序不接收所有报文,在这种仅仅发送数据的应用中,可以在内核中省略接收队列,以此减少CPU资源的消耗。此时可将 setsockopt()函数的第4个参数设置为NULL,将第5个参数设置为0。
数据发送接收
CAN总线与标准套接字通信稍有不同,每一次通信都采用struct can_frame结构体将数据封装成帧。
其中,can_id为帧的标识符,如果是标准帧,就使用can_id的低11位;如果为扩展帧,就使用0-28位。can_id的第29、30、31位是帧的标志位,用来定义帧的类型。
数据发送,使用write()函数来实现,譬如要发送的数据帧包含了三个字节数据0xA0、0xB0 以及0xC0,帧ID为123,可采用如下方法进行发送:
struct can_frame frame; //定义一个 can_frame 变量
int ret;
frame.can_id = 123;//如果为扩展帧,那么 frame.can_id = CAN_EFF_FLAG | 123;
frame.can_dlc = 3; //数据长度为 3
frame.data[0] = 0xA0; //数据内容为 0xA0
frame.data[1] = 0xB0; //数据内容为 0xB0
frame.data[2] = 0xC0; //数据内容为 0xC0
ret = write(sockfd, &frame, sizeof(frame)); //发送数据
if(sizeof(frame) != ret) //如果 ret 不等于帧长度,就说明发送失败
perror("write error");
数据接收使用read()函数来实现,如下所示:
struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));
当应用程序接收到一帧数据之后,可以通过判断can_id中的CAN_ERR_FLAG位来判断接收的帧是否为错误帧。如果为错误帧,可以通过can_id的其他符号位来判断错误的具体原因。错误帧的符号位在头文件<linux/can/error.h>中定义。
回环功能设置
在默认情况下,CAN的本地回环功能是开启的,可以使用下面的方法关闭或开启本地回环功能:
int loopback = 0; //0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
CAN应用编程实战
测试前需要先配置开发板的can设备:
ifconfig can0 down #先关闭 can0 设备 ip link set can0 up type can bitrate 1000000 #设置波特率为 1000000 |
如果只是can通讯测试,可以直接通过如下命令:
cansend can0 123#01.02.03.04.05.06.07.08 |
“#”前表示帧ID,后面就是数据。
接收数据可以用如下命令:
candump -ta can0 |
CAN数据发送
需要先定义ifreq结构体变量ifr,sockaddr_can结构体变量can_addr,cna_frame结构体变量frame以及int类型的sockfd。
调用socket打开套接字sockfd,然后调用strcpy把“can0”这个设备文件的字符串复制到ifr.ifr_name中,调用ioctl(sockfd, SIOCGIFINDEX, &ifr);然后设置can_addr的成员变量指定can0设备,之后调用bind进行绑定;调用setsockopt设置过滤规则,只发送数据,然后设置frame.data来设置发送数据,frame.can_dlc就是数据长度,最后设置frame.can_id设置帧ID;之后进入for死循环,调用write发送数据。
CAN数据接受
同样的步骤,只不过无需设置过滤规则直接进入for死循环,之后调用read接收,并通过frame.can_id&CAN_ERR_FLAG判断是否为错误帧,如果不是就通过frame.can_id&CAN_EFF_FLAG校验帧格式是否扩展帧,再frame.can_id&CAN_RTR_FLAG校验数据帧还是远程帧,最后通过for循环frame.can_dlc打印frame.data[i]。
总结
这一篇其实就是Socket的应用编程,后期可能会做相关的项目,所以这里可以多看看,物联网的话这个属于是必修课。