嵌入式自学之网络编程汇总(6.3-6.6 ,6.9)
由于不同进程内存空间相互隔绝,变量不共享,所以无法直接传递数据。
为实现进程间通信,可以在内核空间开辟一个共享空间,实现不同进程的数据共享。
1、古老的通信方式:无名管道、有名管道、信号(异步通信)
(1)无名管道:只能给有亲缘关系进程通信。有名管道:可以给任意单机进程通信
*管道的特性(底层逻辑就是一个队列):
1)管道是 半双工的工作模式:两个进程都可以收发,但同一时刻只能收或者发。
2)所有的管道都是特殊的文件不支持定位操作。lseek->> fd fseek ->>FILE*
3) 管道是文件,可以读写使用文件IO。fgets,fread,fgetc,
open,read,write,close;
(2)读端存在,F一直向管道中去写,超过64k(man 7 pipe在总体描述查看capacity容量),写会阻塞,等读取一定的数据,比如一页4k读完,写端阻塞才会解除。
写端是存在的,C读管道,如果管道为空的话,读会阻塞。
管道破裂,C读端关闭,F写管道。
读到0,F写端关闭,如果管道没有内容,C读端会read 0 ;
(3)管道使用方法:创建管道 ==》读写管道 ==》关闭管道
1)创建并打开管道: pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建并打开一个无名管道
参数:pipefd[0] ==>无名管道的固定读端
pipefd[1] ==>无名管道的固定写端
返回值:成功 0,失败 -1;
2)无名管道的读写:===》文件IO的读写方式。
读: read() ,写: write()
3)关闭管道: close()
无名管道创建要在fork前,这样父子进程就用的同一个管道
例:利用管道实现数据共享
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{int fd[2];int ret = pipe(fd);if(-1 == ret){fprintf(stderr,"pipe");return -1;}pid_t pid = fork();if(pid > 0){close(fd[0]);write(fd[1], "hell\n", 5);close(fd[1]);}else if(0 == pid){close(fd[1]);char buf[10] = {0};read(fd[0],buf,10);printf("%s",buf);close(fd[0]);}else{perror("fork");return 0;}return 0;
}
验证管道四种状况:
F一直写,C不读,等管道满了(64k)就会写阻塞
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<string.h>
int main()
{int fd[2];int ret = pipe(fd);if(-1 == ret){fprintf(stderr,"pipe");return -1;}pid_t pid = fork();if(pid > 0){close(fd[0]);char buf[1024];memset(buf,'a',1024);int i = 1;while(1){write(fd[1], buf, 1024);printf("%d\n",i++);}close(fd[1]);}else if(0 == pid){close(fd[1]);while(1);close(fd[0]);}else{perror("fork");return 0;}return 0;
}
C一直读,F不写,等管道空了就会读阻塞
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{int fd[2];int ret = pipe(fd);if(-1 == ret){fprintf(stderr,"pipe");return -1;}pid_t pid = fork();if(pid > 0){close(fd[0]);while(1)close(fd[1]);}else if(0 == pid){close(fd[1]);char buf[10] = {0};read(fd[0],buf,10);printf("%s",buf);close(fd[0]);}else{perror("fork");return 0;}return 0;
}
当C把读也关闭,F再往管道写内容会导致管道破裂、
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>int main()
{int fd[2];int ret = pipe(fd);if(-1 == ret){fprintf(stderr,"pipe");return -1;}pid_t pid = fork();if(pid > 0){close(fd[0]);write(fd[1], "hell\n", 5);close(fd[1]);}else if(0 == pid){close(fd[1]);close(fd[0]);}else{perror("fork");return 0;}return 0;
}
如果F把写关闭,管道没有内容,C继续读会读到0
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{int fd[2];int ret = pipe(fd);if (-1 == ret){fprintf(stderr, "pipe");return -1;}pid_t pid = fork();if (pid > 0){close(fd[0]);write(fd[1], "aaa", 3);close(fd[1]);exit(0);}else if (0 == pid){close(fd[1]);sleep(3);char buf[10] = {0};while (1){int ret = read(fd[0], buf, 10);if (0 == ret){printf("read 0");break;}printf("%s", buf);}close(fd[0]);}else{perror("fork");return 0;}return 0;
}
综合应用:利用管道实现文件复制
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main()
{int fd[2];int ret = pipe(fd);if (-1 == ret){fprintf(stderr, "pipe");return -1;}pid_t pid = fork();if (pid > 0){close(fd[0]);int fd1 = open("pipe.c", O_RDONLY);char buf[1024] = {0};int ret = read(fd1, buf, 1024);write(fd[1], buf, ret);close(fd[1]);close(fd1);}else if (0 == pid){close(fd[1]);char buf[1024] = {0};int ret = read(fd[0], buf, 1024);int fd1 = open("1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);write(fd1, buf, ret);close(fd[0]);close(fd1);}else{perror("fork");return 0;}return 0;
}
(4)验证如下问题:
*父子进程是否都有fd[0] fd[1], 如果在单一进程中写fd[1]能否直接从fd[0]中读到。
可以,写fd[1]可以从fd[0]读
*管道的数据存储方式是什么样的,数据是否一直保留?
队列形式存储 读数据会剪切取走数据不会保留
*管道的数据容量是多少,有没有上限值。
操作系统的建议值: 512* 8 = 4k
代码测试实际值: 65536byte= 64k
*管道的同步效果如何验证?读写同步验证。
读端关闭能不能写? 不可以 ===>SIGPIPE 异常终止
写端关闭能不能读? 可以,取决于pipe有没有内容,===>read返回值为0 不阻塞
结论:读写端必须同时存在,才能进行
管道的读写。
*固定的读写端是否就不能互换?
能否写fd[0] 能否读fd[1]? 不可以,是固定读写端。
(5)有名管道:给创建的管道用一个文件名关联,与无名管道不同的是进程都用close关闭管道文件描述符并且程序结束,管道文件不会消失,需要手动删除。
1)有名管道使用:创建有名管道 ==》打开有名管道 ==》读写管道==》关闭管道 ==》卸载有名管道
*1、创建:mkfifo,由于两个进程都要创建管道,而其中一个创建后管道文件就存在了,所以要在创建后根据返回值来加一个判断语句,错误一般是EEXIST文件已存在,配合错误号errno使用。man mkfifo 查看errors
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:在指定的pathname路径+名称下创建一个权限为mode的有名管道文件。
参数:pathname要创建的有名管道路径+名称
mode 8进制文件权限,0666。
返回值:成功 0
失败 -1;
fifo大小一直为0,因为内容是写在内核空间的。
*2、打开有名管道 open,会在此阻塞,等待读写端都打开文件。
注意:该函数使用的时候要注意打开方式,因为管道是半双工模式,所有打开方式直接决定当前进程的读写方式。
一般只有如下方式:
O_RDONLY 文件是固定读端
O_WRONLY 文件是固定写端
逻辑上不能是 O_RDWR 方式打开文件,如果采取这种模式,就没有阻塞了。
不能有 O_CREAT 选项,因为创建管道有指定的mkfifo函数
*3、管道的读写: 文件IO,读: read,写: write
*4、关闭管道:close(fd);
*5、卸载管道:remove(“文件名");
例:有名管道实现文件复制
文件1:写端(类似无名F端)
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>#include <fcntl.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo",O_WRONLY);if(fd < 0){perror("open");return -1;}int fd1 = open("pipecp.c", O_RDONLY);char buf[1024];int ret1 = read(fd1,buf,sizeof(buf));write(fd, buf, ret1);close(fd);close(fd1);
}
读端(类似无名C)
从先后顺序来看先写再读,所以FIFO文件在代码中从读端删除即可。
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>#include <fcntl.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo",O_RDONLY);if(fd < 0){perror("open");return -1;}char buf[1024] = {0};int ret1 = read(fd, buf, sizeof(buf));int fd1 = open("1.txt",O_WRONLY | O_TRUNC);write(fd1,buf,ret1);close(fd);close(fd1);remove("fifo");
}
*1、是否需要同步,以及同步的位置。
有名管道执行过程过必须有读写端同时存在。
如果有一端没有打开,则默认在open函数部分阻塞。
*2、有名管道是否能在fork之后的亲缘关系进程中使用。
结论: 可以在有亲缘关系的进程间使用。
注意: 启动的次序可能会导致其中一个稍有阻塞。
*3、能否手工操作有名管道实现数据的传送。
终端创建管道:mkfifo 管道名
利用cat命令尝试(还有其他命令也可以演示)
终端1:读: cat fifoname
终端2:写:cat > fifoname
效果:在写端输入数据,在读端会输出
(6)信号 signal (用于异步通信,例如:程序崩溃、ctrl c)
1)查看linux所有信号:kill -l ,共64个
进程接收到信号后几种响应(man 7 signal,里面就有下面几种状况以及信号属于下面哪种。):
Term : Default action is to terminate the process.结束进程,停在异常处。:段错误、ctrl c、ctrl \
Ign : Default action is to ignore the signal.忽略,即接收后没反应
wait
Core: Default action is to terminate the process and dump core (see
core(5)).结束进程后保留关键信息,停在异常处。
gdb a.out -c core
Stop : Default action is to stop the process.进程暂停
Cont : Default action is to continue the process if it is currently
stopped.进程继续
例:
上例底层逻辑:每个进程的pcb块里面都有信号对应函数,当接收到终端发来的信号,会立刻停止程序运行并执行信号对应的函数,如果信号不是结束程序就在信号执行完继续程序运行。
(7)进程间发信号kill
kill -xx xxxx
发送进程 信号 接收进程
kill -9 1000
a.out 9 1000
1、发送端
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:通过该函数可以给pid进程发送信号为sig的系统信号。
参数:pid 要接收信号的进程pid
sig 当前程序要发送的信号编号
返回值:成功 0
失败 -1;
例:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>#include <stdlib.h>int main(int argv,char *argc[])
{if(argv < 3){puts("a.out pid sig");return -1;}pid_t pid = atoi(argc[1]);int sig = atoi(argc[2]);int ret = kill(pid,sig);if(-1 == ret){perror("kill");return 1;}return 0;
}
(8)其他发信号函数:
int raise(int sig)== kill(getpid(),int sig);
功能:给进程自己发送sig信号
unsigned int alarm(unsigned int seconds);
功能:定时由系统给当前进程发送信号SIGALAM,程序结束
#include <unistd.h>
#include<stdio.h>
int main()
{alarm(5);while(1){puts("hello");sleep(1);}return 0;
}
int pause(void);
功能:进程暂停,不再继续执行,除非
收到其他信号。
#include <unistd.h>
#include<stdio.h>
int main()
{int i = 0;while(1){if(i == 5){pause();}printf("ha\n");sleep(1);++i;}return 0;
}
(9)接收端
每个进程都会对信号作出默认响应,但不是唯一响应。
一般如下三种处理方式:
1、系统默认处理(SIG_DFL)
2、忽略处理(SIG_IGN) ,只有9,19不能忽略
3、自定义处理、 捕获(9,19不能捕获):signal(sig,handle);
即下图,把信号编号对应功能更改
以上三种方式的处理需要在如下函数上实现。
信号注册函数原型:
void ( *signal(int signum, void (*handler)(int)) ) (int);
typedef void (*sighandler_t)(int);
===》void (*xx)(int); == void fun(int);
===》xx是 void fun(int) 类型函数的函数指针
===》typedef void(*xx)(int) sighandler_t; ///错误
typedef int myint;
===>sighandler_t signal(int signum, sighandler_t handler);
===> signal(int sig, sighandler_t fun);
===> signal(int sig, xxx fun);
===>fun 有三个宏表示:SIG_DFL 表示默认处理
SIG_IGN 表示忽略处理
fun 表示自定义处理
例:自定义处理:让原本5秒后发送的SIGALRM信号改为调用函数handler
#include <signal.h>
#include <stdio.h>
#include <unistd.h>int flag = 0;void handler(int num){flag = 1;
}
int main()
{signal(SIGALRM, handler);alarm(5);while (1){if (0 == flag){puts("hello");}else{puts("hi");}sleep(1);}return 0;
}
捕获:即捕获kill发送的信号,当你不加signal进行信号捕获,程序因pause暂停后,你直接发18号信号没有作用。
#include <unistd.h>
#include<stdio.h>
#include<signal.h>
void handler(int num)
{}int main()
{signal(SIGCONT,handler);int i = 0;while(1){if(i == 5){pause();}printf("pid:%d\n",getpid());sleep(1);++i;}return 0;
}
SIGUSER1和SIGUSER2是留给用户自定义的信号,后者一般比前者强制一些,两个信号默认是终止程序。
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/types.h>
#include <unistd.h>void handle1(int num)
{static int i = 0;printf("老爸叫你...\n");i++;if(i ==3){signal(SIGUSR1,SIG_IGN); //忽略信号}}
void handle2(int num)
{static int i = 0;printf("老妈叫你...\n");i++;if(i ==3){signal(SIGUSR2,SIG_DFL); //回复默认信号}}int main(int argc, char **argv)
{signal(SIGUSR1,handle1);signal(SIGUSR2,handle2);while(1){printf("i'm playing pid:%d\n",getpid());sleep(1);}system("pause");return 0;
}
2、IPC对象通信:消息队列、共享内存(最高效)、信号量集
3、socket通信:网络通信(可实现不同主机间通信)
(1)网络知识扩展
open system interconnect
1、OSI 模型 ===》开放系统互联模型 ==》分为7层:
理想模型 ==》尚未实现
tftp
b /etc/passwd
a /etc/123
应用层 a.out
表示层 网络安全,加密解密 gzip
会话层 网络断开,连接状态信息,keep-close keep-alive
传输层 tcp可靠传输,延迟大,发送失败会重传,网络开销大。 udp不可靠传输 ,延迟小,但发送出去就一下,不会检测是否发送成功,网络开销小 。 文件tcp 视频,音频udp
网络层ip NAT 多个交换机连接起来,用户怎么找到对方。
链路层 交换机 把多个设备串在一起。两个人直接网线一连,多个人就需要交换机。 数据的格式化:把物理层二进制数据转化。 帧 校验
物理层 发送二进制数据,无线:信号塔 有线:网线(近距离)、光纤(远距离)
汇总:
应用层:为网络用户提供各种服务,例如电子邮件、文件传输等。
表示层:为不同主机间的通信提供统一的数据表示形式。
会话层:负责信息传输的组织和协调,管理进程会话过程。
传输层:管理网络通信两端的数据传输,提供可靠或不可靠的传输服务。
网络层:负责数据传输的路由选择和网际互连。
数据链路层,负责物理相邻(通过网络介质相连)的主机间的数据传输,主要作用包括物理地址寻址、数据帧封装、差错控制等。该层可分为逻辑链路控制子层(LLC)和介质访问控制子层(MAC)。
物理层,负责把主机中的数据转换成电信号,再通过网络介质(双绞线、光纤、无线信道等)来传输。该层描述了通信设备的机械、电气、功能等特性。
TCP/IP模型 ==》网际互联模型 ==》分为4层:
实用模型 ===》工业标准
tcp/ip协议栈
应用层 ====》应用程序
传输层 ====》端口号tcp udp
网络层 ====》IP 地址
接口层 ====》网卡 驱动 1GB
TCP/IP协议族:
www.taobao.com ---> 192.168.0.19
dns 域名解析
DHCP动态主机配置协议:自动分配ip 路由器
应用层: HTTP超文本传输 TFTP简单文件传输、FTP文件传输 SNMP网络管理协议 DNS域名解析 ...
传输层: TCP传输控制协议 UDP用户数据报协议 56k猫
网络层: IP互联网协议 ICMP(ping)互联网控制管理协议 RIP最短路径 OSPF最佳路径(时间短) IGMP组播协议 ...
物理层: ARP地址解析ip找mac(ifconfig网络适配器) RARP逆向地址解析 ...
inet ether
ip--->mac 硬件地址 (不同网络设备物理地址唯一)
arp,,,,
192.160.0.112
(2) A B C D E 类
IP地址 == 网络位 + 主机位
IP地址的分类: 点分十进制126.1.1.1 ipv4 网络设备按int型712934传输
A类: 超大规模性网络
8 8 8 8
1.0.0.0 - 126.255.255.255 后三位主机位
126.1.1.2
255.0.0.0
私有:
10.0.0.0 - 10.255.255.255
127.0.0.1
B类: 大中规模型网络
128.0.0.0 - 191.255.255.255 后两位主机
128.2.1.2 128.2.7.2
255.255.0.0
私有:
172.16.0.0 - 172.31.255.255
C类: 中小规模型网络
192.0.0.0 - 223.255.255.255 后一位主机位
255.255.255.0
私有:
192.168.0.0 - 192.168.255.255
静态路由
192.168.0.0
192.168.0.1 网关
192.168.0.255
D类: 组播和广播
组播224.0.0.0 - 239.255.255.255一对多
广播192.168.0.255 == 255.255.255.255一对全部
235.1.2.3
192.168.1.0
192.168.0.1 网关
192.168.1.255 广播
E类: 实验
240.0.0.0 - 255.255.255.255
C 类网络:
ip地址的前三组是网络地址,第四组是主机地址。
二进制的最高位必须是: 110xxxxx开头
十进制表示范围: 192.0.0.0 -223.255.255.255
默认网络掩码: 255.255.255.0
网络个数: 2^24 个 约 209 万个
主机个数: 2^8 个 254 个+2 ===》1 是网关 1是广播
私有地址: 192.168.x.x 局域网地址。
网络编程:
(1)虚拟机网络配置设为桥接
sudo vim /etc/network/interfaces 有这两句即可
sudo reboot
ifconfig 看 ens33 ip是198.168.**.**即是桥接
测试:ping www.baidu.com
netstat -anp查看网络状态,防止刷屏后面加上|less
unix域套接字,本地进程间通信,类似管道。
(2)网络接口
1、socket 套接字(网络设备文件描述符) ==》BSD socket ==》用于网络通信的一组接口函数。socket api application interface
2、ip+port 地址+端口===》地址用来识别主机
端口用来识别应用程序
如下图,因为进程号由系统分配,另一个进程没法看到,所以设置一个端口号和进程绑定,让另一个进程根据端口号识别进程
port分为TCP port / UDP port 范围都是: 1-65535
约定1000 以内的端口为系统使用。推荐自己写的进程端口号申请50000以上
http 80 www.baidu.com
3306
telnet 21
ssh 22
3、网络字节序 ===》大端存储 主机字节序小端存储,,51单片机大端
192.168.0.12
存储时:12.0.168.192
数字转换函数:
#include <arpa/inet.h>
1236234687
主机转网络:uint32_t htonl (uint32_t hostlong);
ipv4 192.168.0.1 1~65535
uint16_t htons(uint16_t hostshort);
网络转主机:host to net
net to host
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
字符串转换函数:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
主机转网络:in_addr_t inet_addr(const char *cp);
inet_addr("192.168.1.20");
cli.sin_addr
网络转主机:char *inet_ntoa(struct in_addr in);
client, server
browser
b/s http
p2p peer
1、模式 C/S 模式 ==》服务器/客户端模型
server:socket()-->bind()--->listen()-->accept()-->recv()-->close()
client:socket()-->connect()-->send()-->close();
int on = 1;
setsockopt(listfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
利用套接字实现不同主机数据传输
服务器端:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:程序向内核提出创建一个基于内存的套接字描述符
参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
PF_UNIX == AF_UNIX ==>单机程序
type 套接字类型:
SOCK_STREAM 流式套接字 ===》TCP
SOCK_DGRAM 用户数据报套接字===>UDP
SOCK_RAW 原始套接字 ===》IP
protocol 协议 ==》0 表示自动适应应用层协议。
返回值:成功 返回申请的套接字id
失败 -1;
man socket
int bind(int sockfd, struct sockaddr *my_addr,
socklen_t addrlen);
功能:如果该函数在服务器端调用,则表示将参数1相关
的文件描述符文件与参数2 指定的接口地址关联,
用于从该接口接受数据。
如果该函数在客户端调用,则表示要将数据从
参数1所在的描述符中取出并从参数2所在的接口
设备上发送出去。
注意:如果是客户端,则该函数可以省略,由默认
接口发送数据。
参数:sockfd 之前通过socket函数创建的文件描述符,套接字id
my_addr 是物理接口的结构体指针。表示该接口的信息。
struct sockaddr 通用地址结构
{
u_short sa_family; 地址族
char sa_data[14]; 地址信息
};
转换成网络地址结构如下:
struct _sockaddr_in ///网络地址结构
{
u_short sin_family; 地址族
u_short sin_port; ///地址端口
struct in_addr sin_addr; ///地址IP ifconfig
char sin_zero[8]; 占位
};
struct in_addr
{
in_addr_t s_addr;
}
socklen_t addrlen: 参数2 的长度。
返回值:成功 0
失败 -1;
man 7 ip
3、 int listen(int sockfd, int backlog);
功能:在参数1所在的套接字id上监听等待链接。
参数:sockfd 套接字id
backlog 允许链接的个数。
返回值:成功 0
失败 -1;
4、int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
功能:从已经监听到的队列中取出有效的客户端链接并
接入到当前程序。
参数:sockfd 套接字id
addr 如果该值为NULL ,表示不论客户端是谁都接入。
如果要获取客户端信息,则事先定义变量
并传入变量地址,函数执行完毕将会将客户端
信息存储到该变量中。
addrlen: 参数2的长度,如果参数2为NULL,则该值
也为NULL;
如果参数不是NULL,&len;
一定要写成len = sizeof(struct sockaddr);
返回值:成功 返回一个用于通信的新套接字id;
从该代码之后所有通信都基于该id
-1失败
5、接受函数:/发送函数:
read()/write () ///通用文件读写,可以操作套接字。
recv(,0) /send(,0) ///TCP 常用套机字读写
recvfrom()/sendto() ///UDP 常用套接字读写
ssize_t recv(int sockfd, void *buf, size_t len,
int flags);
功能:从指定的sockfd套接字中以flags方式获取长度
为len字节的数据到指定的buff内存中。
参数:sockfd
如果服务器则是accept的返回值的新fd
如果客户端则是socket的返回值旧fd
buff 用来存储数据的本地内存,一般是数组或者
动态内存。
len 要获取的数据长度
flags 获取数据的方式,0 表示阻塞接受。
返回值:成功 表示接受的数据长度,一般小于等于len
失败 -1;
6、close() ===>关闭指定的套接字id;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <time.h>
typedef struct sockaddr * (SA);
int main(int argc, char *argv[])
{int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(-1 == sockfd){perror("socket");return 1;}struct sockaddr_in ser,cli;//client man 7 ip bzero(&ser,sizeof(ser));bzero(&cli,sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000); // host to net short ser.sin_addr.s_addr = inet_addr("192.168.116.130");// 小端转大端int ret = bind(sockfd,(SA)&ser,sizeof(ser));if(-1 == ret){perror("bind");return 1;}socklen_t len = sizeof(cli);while(1){char buf[256]={0};recvfrom(sockfd,buf,sizeof(buf),0,(SA)&cli,&len);time_t tm;time(&tm);struct tm * tminfo = localtime(&tm);sprintf(buf,"%s %d:%d:%d\n",buf,tminfo->tm_hour,tminfo->tm_min,tminfo->tm_sec);sendto(sockfd,buf,strlen(buf),0,(SA)&cli,sizeof(cli));}return 0;
}
客户端:
1、int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:该函数固定有客户端使用,表示从当前主机向目标
主机发起链接请求。
参数:sockfd 本地socket创建的套接子id
addr 远程目标主机的地址信息。
addrlen: 参数2的长度。
返回值:成功 0
失败 -1;
2、int send(int sockfd, const void *msg,
size_t len, int flags);
功能:从msg所在的内存中获取长度为len的数据以flags
方式写入到sockfd对应的套接字中。
参数:sockfd:
如果是服务器则是accept的返回值新fd
如果是客户端则是sockfd的返回值旧fd
msg 要发送的消息
len 要发送的消息长度
flags 消息的发送方式。
返回值:成功 发送的字符长度
失败 -1;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr * (SA);
int main(int argc, char *argv[])
{int sockfd = socket(AF_INET,SOCK_DGRAM,0);if(-1 == sockfd){perror("socket");return 1;}struct sockaddr_in ser,cli;//client man 7 ip bzero(&ser,sizeof(ser));bzero(&cli,sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000); // host to net short ser.sin_addr.s_addr = inet_addr("192.168.116.130");// 小端转大端socklen_t len = sizeof(cli);while(1){char buf[256]={0};strcpy(buf,"this is udp test");sendto(sockfd,buf,strlen(buf),0,(SA)&ser,sizeof(ser));bzero(buf,sizeof(buf));recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);printf("server:%s",buf);sleep(1);}close(sockfd);return 0;
}
(1)网络编程之 UDP 用户数据报
1、特性: 无链接(如下图,中间小圈是多个网络节点,发完信息路线就消失,下次可能又是另一条) 不可靠(某个网络节点可能因为数据累计量满了,导致发的信息丢失,丢包。发的一次发五个数据报,发太快,还没收完,最后收到的数据包可能就是后三个报) 大数据
数据报特性:假设数据收三位,那么只会收到hel,然后包就丢了,之间收下一个,如果收前五位,则全部收完,并且包之间有间隔,不会出现粘连。
2、框架: C/S模式
server:socket() ===>bind()===>recvfrom()===>close()
client:socket() ===>bind()===>sendto() ===>close()
注意:socket()的参数需要调整。
socket(PF_INET,SOCK_DGRAM,0);
bind() 客户端是可选的,服务器端是比选的。
发送接收函数:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:用于UDP协议中向对方发送数据。
参数:sockfd 本地的套接字id
buff 本地的数据存储,一般是要发送的数据。
len 要发送的数据长度
flags 要发送数据方式,0 表示阻塞发送。
dest_addr: 必选,表示要发送到的目标主机信息结构体。
addrlen :目标地址长度。
返回值:成功 发送的数据长度
失败 -1;
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:用于UDP协议中获取对方发送的数据。
参数:sockfd 本地的套接字id
buff 要存储数据的内存区,一般是数组或者动态内存。
len 要获取的数据长度,一般是buff的大小。
flags 获取方式,0 阻塞
src_addr 可选,表示对方的地址信息结构体,
如果为NULL,表示不关心对方地址。
addrlen 对方地址信息结构体大小。
如果对方地址是NULL,则该值也为NULL。
返回值:成功 接收到的数据长度
失败 -1;
(2)tcp传输控制
服务器/客户端模型分类:c/s b/s p2p
c客户端是专用、b客户端是通用; b采用http协议、c采用标准协议(tcp、udp等)或自定义协议
cs可以设计的复杂些单机、bs相对简单在线游戏(受到http框架限制); cs单机版资源是离线的来自本地,跟服务器交互快,bs所有资源由服务端发送
ptp在下载软件会用到,比如下图客户端从服务器下载文件,当有多客户时,已经下载的也会帮助未下载的提速。
模式 C/S 模式 ==》服务器/客户端模型 tcp
有链接:发送完数据路线被保留 可靠:发送完数据有应答是否发送成功,失败再发一次。
三次握手,四次挥手。
客户端发送SYN请求建立连接,服务端发送SYN接受,一端随机发个数1000,另一端接收成功就会应答ACK随机数加一1001,另一端再回复个随机数加一,这就是一次三次握手。
客户端发送fin表示断开,另一端回应ack+1,再发送断开信号fin,另一端应答ack+1。
这就是一次四次挥手。
udp数据形式是数据报,半双工
tcp数据是流式套接字,全双工
流式套接字数据特点是连续、有顺序、无边界,如果接收方一次收3个,hel收了,el不会丢,下次接着收,反之一次收100,则三个单词一次全收了
应用层利用tcp编程
调用connect函数和accept函数,底层硬件自动完成三次握手,
调用close底层就会四次挥手。
listen排队数应该少一些,因为一旦进入排队序列,就干不了其他的事了。
利用listen监听三次握手,客户端跟服务器链接成功后,客户端就被分配一个conn通信套接字然后收发,防止多客户端数据重叠。
send发送太快时,最多累计64k数据,当满了返回0。
关闭监听,就没有客户端可以访问了。
服务端:
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{//监听套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 三次握手的排队数 ,listen(listfd, 3);socklen_t len = sizeof(cli);//通信套接字int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");return 1;}int first_flag = 0;int fd = -1;int current_size = 0;int total_size = 0;while (1){PACK pack;bzero(&pack, sizeof(pack));ret = recv(conn, &pack, sizeof(pack), 0);if (0 == first_flag){first_flag = 1;fd = open(pack.filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);if (-1 == fd){perror("open");return 1;}total_size = pack.total_len;}if (0 == pack.buf_len){break;}write(fd, pack.buf, pack.buf_len);current_size += pack.buf_len;printf("%d/%d\n", current_size, total_size);bzero(&pack, sizeof(pack));strcpy(pack.buf,"go on");// send(conn,&pack,sizeof(pack),0);}close(conn);close(listfd);close(fd);// system("pause");return 0;
}
客户端:
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <time.h>
#include <unistd.h>typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{int conn = socket(AF_INET, SOCK_STREAM, 0);if (-1 == conn){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = connect(conn, (SA)&ser, sizeof(ser));if (-1 == ret){perror("connect");return 1;}PACK pack;strcpy(pack.filename, "1.png"); // /home/linux/1.pngstruct stat st;ret = stat("/home/linux/1.png", &st);if (-1 == ret){perror("stat");return 1;}pack.total_len = st.st_size; // total sizeint fd = open("/home/linux/1.png", O_RDONLY);if (-1 == fd){perror("open");return 1;}while (1){pack.buf_len = read(fd, pack.buf, sizeof(pack.buf));send(conn, &pack, sizeof(pack), 0);if (pack.buf_len <= 0){break;}bzero(&pack,sizeof(pack));//recv(conn,&pack,sizeof(pack),0);usleep(1000*10); //10ms}close(conn);close(fd);// system("pause");return 0;
}
tcp粘包问题:由于双方没有约定,导致发送的包没法拆开。比如下方客户端发送了三个信息名字年龄身高,服务端一次性收完了,导致没法区分,数据没边界。
粘包解决:加分隔。 固定大小 如struct结构体。 自定义协议。
为防止写太快导致数据写不完,可以加个recv、send阻塞,或者sleep
tcp文件复制:
服务端
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h> /* See NOTES */
#include <time.h>
#include <unistd.h>
typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{//监听套接字int listfd = socket(AF_INET, SOCK_STREAM, 0);if (-1 == listfd){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = bind(listfd, (SA)&ser, sizeof(ser));if (-1 == ret){perror("bind");return 1;}// 三次握手的排队数 ,listen(listfd, 3);socklen_t len = sizeof(cli);//通信套接字int conn = accept(listfd, (SA)&cli, &len);if (-1 == conn){perror("accept");return 1;}int first_flag = 0;int fd = -1;int current_size = 0;int total_size = 0;while (1){PACK pack;bzero(&pack, sizeof(pack));ret = recv(conn, &pack, sizeof(pack), 0);if (0 == first_flag){first_flag = 1;fd = open(pack.filename, O_WRONLY | O_TRUNC | O_CREAT, 0666);if (-1 == fd){perror("open");return 1;}total_size = pack.total_len;}if (0 == pack.buf_len){break;}write(fd, pack.buf, pack.buf_len);current_size += pack.buf_len;printf("%d/%d\n", current_size, total_size);bzero(&pack, sizeof(pack));strcpy(pack.buf,"go on");// send(conn,&pack,sizeof(pack),0);}close(conn);close(listfd);close(fd);// system("pause");return 0;
}
客户端:
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h> /* See NOTES */
#include <sys/types.h>
#include <time.h>
#include <unistd.h>typedef struct sockaddr*(SA);
typedef struct
{char filename[256];char buf[1024];int buf_len;int total_len;
} PACK;
int main(int argc, char** argv)
{int conn = socket(AF_INET, SOCK_STREAM, 0);if (-1 == conn){perror("socket");return 1;}// man 7 ipstruct sockaddr_in ser, cli;bzero(&ser, sizeof(ser));bzero(&cli, sizeof(cli));ser.sin_family = AF_INET;ser.sin_port = htons(50000);ser.sin_addr.s_addr = inet_addr("127.0.0.1");int ret = connect(conn, (SA)&ser, sizeof(ser));if (-1 == ret){perror("connect");return 1;}PACK pack;strcpy(pack.filename, "1.png"); // /home/linux/1.pngstruct stat st;ret = stat("/home/linux/1.png", &st);if (-1 == ret){perror("stat");return 1;}pack.total_len = st.st_size; // total sizeint fd = open("/home/linux/1.png", O_RDONLY);if (-1 == fd){perror("open");return 1;}while (1){pack.buf_len = read(fd, pack.buf, sizeof(pack.buf));send(conn, &pack, sizeof(pack), 0);if (pack.buf_len <= 0){break;}bzero(&pack,sizeof(pack));//recv(conn,&pack,sizeof(pack),0);usleep(1000*10); //10ms}close(conn);close(fd);// system("pause");return 0;
}
小工具:
1、telnet 远程登录工具,默认都是系统安装。
先关闭乌班图防火墙sudo ufw disable
inactive表示关闭
sudo apt-get install openssh-server openssh-client 安装
网络需要桥接。
ssh2
使用格式: telnet ip地址 端口
eg: telnet 192.168.1.1 8888
注意:如果没有写端口,则默认登录23 号端口。
2、arp 地址解析命令
列出与本机通信的设备ip地址及对应mac
arp -an ===>列出当前主机的地址ARP表
arp -d ip地址
3、netstat 测试查看网络端口使用情况
netstat -anp
netstat -n ===>列出当前所有网络端口使用情况
netstat -n -t ===>列出所有TCP通信的端口信息
netstat -n -u ===>列出所有UDP通信的端口信息
netstat -n -i ===>列出默认接口上的通信信息
netstat -lnp |grep 8888 ===>查看指定端口上的通信详情
4、ping 命令 测试网路的联通状况
ping ip地址
ping 域名
5、wireshark
先细说一下tcpip协议栈(四层模型)数据传输过程:
如图,数据传输不是只有数据,还要跟MAC IP TCP 再加上数据组成分包或者一帧,然后传送。mac来自接口、ip来自网络、tcp来自传输、数据来自应用。数据到了服务端除了要对结构还有校验码。
udp一样。
这个过程在linux系统直接由系统进行拆包封包,如果在stm32就得自己编程实现。
上图就是协议头,分析一下:
以太V2MAC帧:
对方mac地址即目的地址,自己mac地址源地址各6字节。
类型:即后面这一堆的类型,这里是IP数据报。
ip数据报:
FCS:校验码。
ip头:
大小:20字节
一行4字节,共五行。
version字段:版本号ipv4、ipv6. ,头长度:20
TOS:空的
total length :后两个头长度
identification:编号
ip flag:F D M:
分片就是能不能打开。
TTL:生命周期,每次往下一级走,ttl--,当为零时,就不走了
protocol:协议:tcp、udp
header:校验
源地址,目的地址
ip选项:可有可无
tcp头:
端口号
起始编号交互随机数
应答数据
选项大小
tcp flag:
窗口:缓冲区大小
check:校验码
urgent:紧急数据放哪
udp头:
大小:八字节
源端口,目的端口、长度(应用层长度hello),校验码
wireshark:
sudo apt-get install wireshark安装
抓包工具 tcp.port == 50000 && tcp.ip == 192.168.0.183
sudo wireshark ==>可视化界面
选网络设备:
看本地:loopback
看京东等网站:ens33
不知到:any
最上面:一包一包数据,source发送, 协议:icmp length长度 info信息描述
中间:先是总帧,然后四层:MAC ip udp 应用层
然后是网卡发送的数据
tcpdump
抓包过滤规则:
1、根据ip地址过滤:ip.src == x.x.x.x
ip.dst == x.x.x.x
2、根据端口过滤:tcp.srcport == xx;
tcp.dstport == xx;
udp.srcport == xx;
udp.dstport == xx;
3、根据协议类型过滤:
tcp udp icmp .....
4、任意组合以上条件抓包:
如果与的关系: and
ip.src == 192.168.1.100 and tcp.dstport == 9999
如果或关系 : or
ip.src == 192.168.1.100 or ip.dst == 192.168.1.102
tcp host 192.168.1.100
运行前面写的网络数据交换:
前三行就是一次三次握手。
这就跟前面介绍的头对应
网络编程出现问题,不知道在哪里,就可以用这个小工具查找。
还可以用命令行查看:
sudo tcpdump -n -i eth0 -xx src or dst www.taobao.com -AXX -vv|less
tcpdump ==》命令行 ===>www.tcpdump.com
1、tcpdump -n ===>在默认的网卡上开始抓包。
2、根据ip过滤: tcpdump -n src x.x.x.x
tcpdump -n dst x.x.x.x
抓192.168.0.130上面发出和接受到的数据包
sudo tcpdump -n -x src or dst 192.168.0.130
3、查看包中的内容:
tcpdump -n -x src x.x.x.x
tcpdump -n -x dst x.x.x.x
tcpdump -n -x src x.x.x.x >xxx.log
4、根据端口过滤:
tcpdump -n src port xx
tcpdump -n dst port xx
tcpdump -n -p tcp port xx
tcpdump -n udp port xx
tcpdump -n port xx
5、根据协议过滤:
tcpdump -n -p icmp/tcp/udp
6、根据指定接口过滤:
tcpdump -n -i eth0
tcpdump -n -i lo
7、根据以上各种条件组合抓包:
与关系: and
或关系: or
(3)mqtt协议 应用层
消息队列遥测传输协议,常用于物联网设备,低开销、低带宽。心脏起搏器
发布者(客户)上传数据到代理暂存部分(服务端),订阅者根据主题订阅需要的数据,由代理传给它。所以上传者上传数据需要附带数据主题。如下图,温度数据附带主题temp
服务质量qos:0,1,2
0:发布者发个数据,服务就结束了。
1:发布数据对方会应答,订阅者接受后也会给发布者应答。
2:会告诉发布者数据暂存了,还会告诉谁订阅了。
mqtt协议应用层报文三部分:
固定头2字节
可变头(根据固定头不同有改变):建立连接、连接应答、发布主题、发布主题应答、订阅主题、订阅主题应答、取消订阅、取消应答。 可能有心跳包、心跳应答:长时间没有数据交互,就会发送心跳包,检测链接是否断开。比如游戏长时间连线无响应,就会发心跳包查看是否断线。
负载payload:有数据发送就有负载,不是必要的。
编程需要随时查:
实操应用:需要用到物联网服务器:比如onenet云平台、华为云等。
可以查看快速入门了解使用方法。
编程前需要下载mqtt库,网上有开源库
还需要
下载好库之后步骤:
解压
1 .PC版本的编译步骤
1. openssl
进入openssl源码目录
./config enable-shared -fPIC #加入-fPIC 选项,不然,编译paho会出问题。
make #这个过程需要等待下
sudo make install
2.paho 的编译
修改makefile
1.CC ?=gcc 122line
2.133 line 加入这两个选项
CFLAGS += -I /usr/local/ssl/include
LDFLAGS += -L /usr/local/ssl/lib
3.192line 注意-I,-L 后面填写ssl库的头文件目录和库文件目录 我这里是/usr/local/ssl/include 和
/usr/local/ssl/lib,如果你的不是,需要切换下
CCFLAGS_SO += -Wno-deprecated-declarations -DOSX -I /usr/local/ssl/include
LDFLAGS_C += -Wl,-install_name,lib$(MQTTLIB_C).so.${MAJOR_VERSION}
LDFLAGS_CS += -Wl,-install_name,lib$(MQTTLIB_CS).so.${MAJOR_VERSION} -L /usr/local/ssl/lib
LDFLAGS_A += -Wl,-install_name,lib${MQTTLIB_A}.so.${MAJOR_VERSION}
LDFLAGS_AS += -Wl,-install_name,lib${MQTTLIB_AS}.so.${MAJOR_VERSION} -L /usr/local/ssl/lib
FLAGS_EXE += -DOSX
FLAGS_EXES += -L /usr/local/ssl/lib
4.make
quesion:
/usr/bin/ld: /usr/local/ssl/lib/libcrypto.a(x86_64cpuid.o): relocation R_X86_64_PC32 against symbol `OPENSSL_cpuid_setup' can not be used when making a shared object; recompile with -fPIC
初步分析,ssl编译的有问题,需要在编译的时候添加 ./config enable-shared
/tmp/ccM0lsiB.o: relocation R_X86_64_PC32 against symbol `stdout@@GLIBC_2.2.5' can not be used when making a shared object; recompile with -fPIC
检查paho库makefile 中是否加入-fPIC编译选项。
本makefile line:157 CCFLAGS_SO = -g -fPIC
5. sudo make install /usr/local/lib /usr/local/include
最后:
install: cannot stat 'build/output/doc/MQTTClient/man/man3/MQTTClient.h.3': No such file or directory
Makefile:275: recipe for target 'install' failed
make: [install] Error 1 (ignored)
install -m 644 build/output/doc/MQTTAsync/man/man3/MQTTAsync.h.3 /usr/local/share/man/man3
install: cannot stat 'build/output/doc/MQTTAsync/man/man3/MQTTAsync.h.3': No such file or directory
Makefile:275: recipe for target 'install' faile
报这个错不用理会。到此就完成了编译。
pc 版mqtt库的安装。
2.arm版本:
arm-linux-gcc 配置
1.先按照之前方法配置
apt-get install lib32ncurses5 lib32z1(由于是64位系统,
arm-linux-gcc 是32位,需要安装这个库)
openssl arm 版本编译
1.
./config no-asm shared --prefix=$(pwd)/__install
no-asm: 是在交叉编译过程中不使用汇编代码代码加速编译过程,原因是它的汇编代码是对arm格式不支持的。
shared :生成动态连接库。
–prefix :指定make install后生成目录的路径,不修改此项则默认为OPENSSLDIR目录(/usr/local/ssl)
2.修改makefile
1) CC= gcc 改成 CC = arm-linux-gcc;(根据你自己的交叉编译环境设置,我的交叉编译环境是:arm-none-linux-gnueabi-)
2) 删除 CFLAG= 中的 “-march=pentium”;(如果有的话)
3) AR=ar $(ARFLAGS) r 改为 AR=arm-none-linux-gnueabi-ar $(ARFLAGS) r;
4) ARD=ar $(ARFLAGS) d 改为 ARD=arm-none-linux-gnueabi-ar $(ARFLAGS) d;(如果有的话)
5)RANLIB= /usr/bin/ranlib 改为 RANLIB= arm-none-linux-gnueabi-ranlib;
3.m akefile 中去掉
153 63 -m64 去掉
4. make
5. make install
6.生成的arm库子当前目录下__install
2. arm paho
https://blog.csdn.net/ltc844139730/article/details/52553086?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~aggregatepage~first_rank_v2~rank_aggregation-9-52553086.pc_agg_rank_aggregation&utm_term=arm%E7%A7%BB%E6%A4%8Dmqtt&spm=1000.2123.3001.4430
1.进入paho目录
2. 修改makefile 24行
.PHONY: clean, mkdir, install, uninstall, html,在这行的下面加入2行
INCLUDES = -I/home/linux/code_test/mqtt_src/openssl-1.0.0s/__install/include
LIBSDIR = -L/home/linux/code_test/mqtt_src/openssl-1.0.0s/__install/lib
3.
然后到文件的第181行,在${CC} 后面加上$(INCLUDES),在最后加上$(LIBSDIR),生成的时候需要库,要把库的路径添加进去。
${CC} $(INCLUDES) -g -o $@ $< -l${MQTTLIB_CS} ${FLAGS_EXES} $(LIBSDIR)
在第187行,203行,215行做同样处理。最好和我用一样的版本,一个是这个位置不对,另一个是我之前用的1.10版本的,这样子做没有效果。
需要多行加入。行号稍微有点差异。
3. make CC=arm-linux-gcc
问题
报错:build/output/libpaho-mqtt3a.so: undefined reference to `clock_gettime'
修改makefile 最后 加入-lrt
FLAGS_EXES = $(LDFLAGS) -I ${srcdir} ${START_GROUP} -lpthread -lssl -lcrypto -lrt ${END_GROUP} -L ${blddir}
最后,bulid/output 下面就是 arm 版本的库了.
在paho 目录下面有两个makefile ,后缀为arm,是arm版本的makefile 生成arm版本的库。
后缀名为PC的是生成PC版本的库,可以根据情况进行改名。
cp Makefile_arm makefile
cp Makefile_PC makefile
在云平台的设备需要进行密钥转化,利用小工具token
然后就可以编程了:
head.h是开源库提供的,只需要根据创建的设备修改,其中password就是用token生成
#ifndef HEAD_H
#define HEAD_H#include <MQTTAsync.h>
#include <MQTTClient.h>#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>#include <string.h>
//#define OLD_ADDRESS "tcp://218.201.45.7:1883"
#define NEW_ADDRESS "tcp://183.230.40.96:1883"
#define DEV_NAME "first_device"
#define CLIENTID DEV_NAME
#define PRODUCT_ID "3XoV8495r6"
#define PASSWD "version=2018-10-31&res=products%2F3XoV8495r6%2Fdevices%2Ffirst_device&et=1837255523&method=sha1&sign=dtVQufcgwCrn5zmdmWLXFEtJAQY%3D";
#define QOS 0
#define TIMEOUT 10000L
//#define __cplusplus
#endif // HEAD_H
main.c只需要修改主函数,把temp换成自己在云平台创建的产品属性
#include <stdio.h>
#include "head.h"static char topic[2][200] = {0};
static MQTTClient client;
static int id =10000;
volatile static MQTTClient_deliveryToken deliveredtoken;void pack_topic(char * dev_name, char * pro_id)
{sprintf(topic[0], "$sys/%s/%s/thing/property/post/reply", pro_id, dev_name); //订阅sprintf(topic[1], "$sys/%s/%s/thing/property/post", pro_id, dev_name); //发布
}// 发送成功后callback
void delivered(void *context, MQTTClient_deliveryToken dt)
{printf("Message with token value %d delivery confirmed\n", dt);deliveredtoken = dt;
}
// 接收到消息的callback
int msgarrvd(void *context, char *topicName, int topicLen, MQTTClient_message *message)
{int i;char* payloadptr;printf("Message arrived\n");printf(" topic: %s\n", topicName);printf(" message: ");payloadptr = (char*)message->payload;for(i=0; i<message->payloadlen; i++){putchar(*payloadptr++);}putchar('\n');MQTTClient_freeMessage(&message);MQTTClient_free(topicName);return 1;
}
// 掉线后的callback
void connlost(void *context, char *cause)
{printf("\nConnection lost\n");printf(" cause: %s\n", cause);
}int mqtt_init()
{pack_topic(DEV_NAME, PRODUCT_ID);int rc = MQTTClient_create(&client, NEW_ADDRESS, CLIENTID,MQTTCLIENT_PERSISTENCE_NONE, NULL);int ch;if(MQTTCLIENT_SUCCESS != rc){printf("create mqtt client failre...\n");exit(1);}MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;conn_opts.keepAliveInterval = 20;conn_opts.cleansession = 1;conn_opts.username=PRODUCT_ID;conn_opts.password=PASSWD;rc = MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered);if(MQTTCLIENT_SUCCESS !=rc ){printf("Failed to connect, return code %d\n", rc);exit(EXIT_FAILURE);}if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS){printf("Failed to connect, return code %d\n", rc);exit(EXIT_FAILURE);}#if 0//订阅单个主题 如果需要订阅的话 MQTTClient_subscribe(client, topic[0], QOS);printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n", topic[0], CLIENTID, QOS);
#endif return rc;
}int mqtt_send(char * key, int value)
{MQTTClient_deliveryToken deliveryToken;MQTTClient_message test2_pubmsg = MQTTClient_message_initializer;// 需要发送的正文char message[1024]={0};test2_pubmsg.qos = QOS;test2_pubmsg.retained = 0;test2_pubmsg.payload =message;sprintf(message,"{\"id\":\"%d\",\"version\":\"1.0\",\"params\":{\"%s\":{\"value\":%d}}}",id++, key, value);test2_pubmsg.payloadlen = strlen(message);printf("%s\n",message);int rc = MQTTClient_publishMessage(client,topic[1],&test2_pubmsg,&deliveryToken);if(MQTTCLIENT_SUCCESS != rc){printf("client to publish failure.. %lu\n",pthread_self());exit(1);}printf("Waiting for up to %d seconds for publication on topic %s for client with ClientID: %s\n",(int)(TIMEOUT/1000), topic[0], CLIENTID);MQTTClient_waitForCompletion(client,deliveryToken,TIMEOUT);sleep(1);return rc;
}void mqtt_deinit()
{MQTTClient_disconnect(client, 10000);MQTTClient_destroy(&client);
}int main(void)
{mqtt_init();while(1){mqtt_send("tmp", 20);}mqtt_deinit();return 0;
}
这个在云平台手册查看。
其他东西在属性查看
wireshark抓包
(4)IO多路复用
*1、定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力。
*2、作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标的输入、中断信号等等事件,再比如web服务器如nginx,需要同时处理来来自N个客户端的事件。
*逻辑控制流在时间上的重叠叫做 并发
*3、而CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。
*4、使用并发处理的成本:
线程/进程创建成本
CPU切换不同线程/进程成本 Context Switch,上下文切换 页表、寄存器、缓存
多线程的资源竞争
*5、有没有一种可以在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。
因此IO多路复用解决的本质问题是在用更少的资源完成更多的事。
多路复用如下,中间一条河,上下各三条车道阻塞,中间的车道可以左右移动让车通过。
(2)IO模型
用有名管道来演示下列模型:
*1、阻塞IO
写端
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>#include <fcntl.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo",O_WRONLY);if(fd < 0){perror("open");return -1;}while(1){write(fd, "hello", 5);sleep(1);}close(fd);
}
读端
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo", O_RDONLY);if (fd < 0){perror("open");return -1;}while (1){char buf[1024] = {0};read(fd, buf, 1024);puts(buf);fgets(buf, 1024, stdin);puts(buf);}close(fd);remove("fifo");
}
*2、非阻塞IO EAGAIN 忙等待 errno(会导致cpu占有率100%左右)
把打开的文件模式换成非阻塞:fcntl函数:
非阻塞IO ===》在阻塞IO的基础上调整其为不再阻塞等待。
在程序执行阶段调整文件的执行方式为非阻塞:
===》fcntl() ===>动态调整文件的阻塞属性
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:修改指定文件的属性信息。
参数:fd 要调整的文件描述符
cmd 要调整的文件属性宏名称
... 可变长的属性值参数。
返回值:成功 不一定,看cmd
失败 -1;
eg:修改文件的非阻塞属性:
int flag ;
flag = fcntl(fd,F_GETFL,0); ///获取fd文件的默认属性到flag变量中。
flag = flag | O_NONBLOCK; ///将变量的值调整并添加非阻塞属性
fcntl(fd,F_SETFL,flag); ///将新属性flag设置到fd对应的文件生效。
以上代码执行后的阻塞IO将变成非阻塞方式
例:写端:
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo",O_WRONLY);if(fd < 0){perror("open");return -1;}while(1){write(fd, "hello", 5);sleep(3);}close(fd);
}
读端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo", O_RDONLY);if (fd < 0){perror("open");return -1;}//获取设备原来的状态标志未int flag = fcntl(fd, F_GETFL);//在原来的状态标志位基础上加上 非阻塞fcntl(fd, F_SETFL, flag | O_NONBLOCK);flag = fcntl(fileno(stdin), F_GETFL);fcntl(0, F_SETFL, flag | O_NONBLOCK);while (1){char buf[1024] = {0};int ret = read(fd, buf, 1024);if (ret > 0){puts(buf);}char* ret1 = fgets(buf, 1024, stdin);if (ret1 != NULL){puts(buf);}}close(fd);remove("fifo");
}
*3、信号驱动IO SIGIO 用的相对少(了解)
文件描述符需要追加 O_ASYNC 标志。
设备有io事件可以执行时,内核发送SIGIO信号。
1.追加标志
int flag ;
flag = fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag | O_ASYNC);
2.设置信号接收者
fcntl(fd,F_SETOWN,getpid());//常用设置
3.对信号进行捕获
signal(SIGIO,myhandle);//
写端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int main()
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return -1;}}int fd = open("fifo", O_WRONLY);if (fd < 0){perror("open");return -1;}while (1){write(fd, "hello", 5);sleep(3);}close(fd);
}
读端
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int fd;
void handle(int num)
{char buf[128] = {0};read(fd, buf, sizeof(buf));printf("fifo:%s\n", buf);
}
int main(int argc, char **argv)
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return 1;}}signal(SIGIO, handle);fd = open("fifo", O_RDONLY);if (-1 == fd){perror("open fifo");return 1;}int flag = fcntl(fd, F_GETFL);//信号通知 信号驱动iofcntl(fd, F_SETFL, flag | O_ASYNC);//设在接收sigio的进程fcntl(fd, F_SETOWN, getpid());while (1){char buf[128] = {0};fgets(buf, sizeof(buf), stdin);printf("terminal:%s", buf);}close(fd);// remove("fifo");// system("pause");return 0;
}
*4、并行模型 进程,线程
*5、 IO多路复用 select、poll、epoll
select循环服务器 ===> 用select函数来动态检测有数据流动的文件描述符(轮询)
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
功能:完成指定描述符集合(数组)中有效描述符的动态检测。
该函数具有阻塞等待功能,在函数执行完毕后
目标测试集合中将只保留最后有数据的描述符。
参数:nfds 描述符的上限值,一般是链接后描述符的最大值+1;
readfds 只读描述符集
writefds 只写描述符集
exceptfds 异常描述符集
以上三个参数都是 fd_set * 的描述符集合类型
timeout 检测超时 如果是NULL表示一直检测不超时 。
返回值:超时 0
失败 -1
成功 >0
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。
int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。
void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。
void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。
写端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_WRONLY);if (-1 == fd){perror("open fifo");return 1;}while(1){write(fd, "hello", 5);sleep(3);}close(fd);// system("pause");return 0;
}
读端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_RDONLY);if (-1 == fd){perror("open fifo");return 1;}// 1.create setfd_set rd_set, tmpset;FD_ZERO(&rd_set);FD_ZERO(&tmpset);// 2 . add fd to fd_setFD_SET(fd, &tmpset);FD_SET(0, &tmpset);while (1){// 5 clean flagrd_set = tmpset;char buf[128] = {0};// 3 wait eventselect(fd + 1, &rd_set, NULL, NULL, NULL);// find fdint i = 0;for (i = 0; i < fd + 1; i++){if (FD_ISSET(i, &rd_set) && 0 == i){fgets(buf, sizeof(buf), stdin);printf("terminal:%s", buf);}if (FD_ISSET(i, &rd_set) && fd == i){read(fd, buf, sizeof(buf));printf("fifo:%s\n", buf);}}}close(fd);// remove("fifo");// system("pause");return 0;
}
select poll epoll的区别
1. select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
select的调用一般要注意几点:
① readfds等是指针结果参数,会被函数修改,所以一般会另外定义一个allread_fdset,保持全部要监听读的句柄,将它的拷贝传递给select函数,返回可读的句柄集合,类型fdset支持赋值运算符=;
② 要注意计算nfds,当新增监听句柄时比较容易修改,当减少监听句柄时较麻烦些,如果要精确修改需要遍历或者采用最大堆等数据结构维护这个句柄集,以方便的找到第二大的句柄,或者干脆在减少监听句柄时不管nfds;
③ timeout如果为NULL表示阻塞等,如果timeout指向的时间为0,表示非阻塞,否则表示select的超时时间;
④ select返回-1表示错误,返回0表示超时时间到没有监听到的事件发生,返回正数表示监听到的所有事件数(包括可读,可 写,异常),通常在处理事件时 会利用这个返回值来提高效率,避免不必要的事件触发检查。(比如总共只有一个事件,已经在可读集合中处理了一个事件,则可写和异常就没必要再去遍历句柄集 判断是否发生事件了);
⑤ Linux的实现中select返回时会将timeout修改为剩余时间,所以重复使用timeout需要注意。
select的缺点在于:
① 由于描述符集合set的限制,每个set最多只能监听FD_SETSIZE(在Linux上是1024)个句柄(不同机器可能不一样);
② 返回的可读集合是个fdset类型,需要对所有的监听读句柄一一进行FD_ISSET的测试来判断是否可读;
③ nfds的存在就是为了解决select的效率问题(select遍历nfds个文件描述符,判断每个描述符是否是自己关心的,对关心的描述符判断是否发生事件)。但是解决不彻底,比如如果只监听0和1000两个句柄,select需要遍历1001个句柄来检查事件。
3. epoll man 7 epoll i/o时间通知机制,,主动上报
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
写端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_WRONLY);if (-1 == fd){perror("open fifo");return 1;}while(1){write(fd, "hello", 5);sleep(3);}close(fd);// system("pause");return 0;
}
读端:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>int add_fd(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = fd;int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);if (-1 == ret){perror("add fd ");return 1;}return 0;
}
int main(int argc, char **argv)
{int ret = mkfifo("fifo", 0666);if (-1 == ret){if (EEXIST != errno){perror("mkfifo");return 1;}}int fd = open("fifo", O_RDONLY);if (-1 == fd){perror("open fifo");return 1;}// 1. create fd setint epfd = epoll_create(2);if (-1 == epfd){perror("epoll_create");return 1;}struct epoll_event rev[2];// 2 . add fd to setadd_fd(epfd, 0);add_fd(epfd, fd);while (1){char buf[128] = {0};// 3 wait eventint ep_ret = epoll_wait(epfd, rev, 2, -1);int i = 0;for (i = 0; i < ep_ret; i++){if (0 == rev[i].data.fd){fgets(buf, sizeof(buf), stdin);printf("termina:%s", buf);}else{read(fd, buf, sizeof(buf));printf("fifo:%s\n", buf);}}}close(fd);// remove("fifo");// system("pause");return 0;
}
epoll 解决了select和poll的几个性能上的缺陷:①不限制监听的描述符个数(poll也是),只受进程打开描述符总数的限制;②监听性能不随着监听描述 符数的增加而增加,是O(1)的,不再是轮询描述符来探测事件,而是由描述符主动上报事件;③使用共享内存的方式,不在用户和内核之间反复传递监听的描述 符信息;④返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发。
epoll显著提高性能的前提是:监听大量描述符,并且每次触发事件的描述符文件非常少。
epoll的另外区别是:①epoll创建了描述符,记得close;②支持水平触发和边沿触发。