文章目录
前言
由于知识内容较多,Linux应用开发基础共分为上、下两篇。此篇为下篇,承接【嵌入式Linux笔记】第三篇:Linux应用开发基础(上)。若存在版权问题,请联系删除。
六、网络编程
1. 网络通信概述
1.1 背景:在多个设备存在的情况下,若采用串口的方式实现多设备通信,将会导致管理困难和通信效率低等问题。因此,网络通信横空出世,能够实现客户端与服务器之间的高效率通信。
1.2 数据传输三要素:源、目的、长度。在网络通信前,需要指定通信的源、目的地以及发送数据的长度大小。使用“ IP 和端口”来表示源或目的。
1.3 网络通信对象及传输方式:如下图所示,当我们访问网站时,通信过程涉及客户端(client)和服务器(server)这两个对象。而二者的传输通过TCP/UDP协议。
2. TCP编程
2.1 TCP网络通信交互图:
- 服务器:①服务器利用socket函数返回一个文件描述符fd。②使用bind函数将服务器的fd、IP和端口绑定在一起。③调用listen函数启动检测有无新客户端的连接请求。④采用accept函数来接受客户端发起的连接请求。⑤调用send和recv函数来发送和接收数据。⑥使用close函数来关闭该连接。
- 客户端:①使用socket函数返回一个文件描述符fd。②调用connect函数向服务器发起连接请求。③调用send和recv函数来发送和接收数据。④使用close函数来关闭该连接。
2.2 多客户端单服务器实验:
(1) 目标:实现多个客户端给服务器发送数据,服务器接收数据并显示数据。
(2) 思路:server.c和client.c分别实现服务器和客户端的程序代码。
(3) server.c代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <netinet/in.h> #include <unistd.h> #include <signal.h> #define SERVER_PORT 8888 #define BACKLOG 10 int main(int argc, char* argv[]) { int client_num = -1; //记录客户端连接的数量 signal(SIGCHLD,SIG_IGN);//避免客户端退出造成server子进程僵死 /* 1.socket */ int iSocketServer = socket(AF_INET, SOCK_STREAM, 0); if (iSocketServer == -1) { printf("socket error!\n"); return -1; } /* 2.bind */ struct sockaddr_in tSocketServerAddr; tSocketServerAddr.sin_family = AF_INET; //使用Internet一般为AF_INET tSocketServerAddr.sin_port = htons(SERVER_PORT);//将服务器端口转化为网络字节序 tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; //表示可以和任何客户端通信 memset(tSocketServerAddr.sin_zero, 0, 8); int bind_ret = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); if (bind_ret == -1) { printf("bind error!\n"); return -1; } /* 3. listen */ int listen_ret = listen(iSocketServer, BACKLOG);//最多监听10个 if (listen_ret == -1) { printf("listen error!\n"); return -1; } while(1) { /* 4.accept */ int clientaddr_len = sizeof(struct sockaddr); struct sockaddr_in tSocketClientAddr; int accept_ret = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &clientaddr_len); if (accept_ret == -1) { printf("accept error!\n"); return -1; } else { client_num++; printf("Get connect from %d: %s\n", client_num, inet_ntoa(tSocketClientAddr.sin_addr)); if (!fork()) //创建子进程,且复制父进程的执行顺序 { /* 5. 服务器循环接收数据 */ while(1) { char Recvbuf[1000]; int recv_ret = recv(accept_ret, Recvbuf, 999, 0); if (recv_ret <= 0) { close(iSocketServer); return -1; } else { Recvbuf[recv_ret] = '\0'; printf("Get Msg from %d: %s\n", client_num, Recvbuf); } } } } } /* 6. close */ close(iSocketServer); return 0; }
(4) client.c代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <netinet/in.h> #include <unistd.h> #include <signal.h> #define SERVER_PORT 8888 int main(int argc, char* argv[]) { /* 0.命令行格式 */ if (argc != 2) { printf("格式:%s <server_ip>\n", argv[0]); return -1; } /* 1.socket */ int iSocketClient = socket(AF_INET, SOCK_STREAM, 0); if (iSocketClient == -1) { printf("socket error!\n"); return -1; } /* 2.connect */ struct sockaddr_in tSocketServerAddr; //2.1 给tSocketServerAddr结构体赋值 tSocketServerAddr.sin_family = AF_INET; tSocketServerAddr.sin_port = htons(SERVER_PORT); if (inet_aton(argv[1], &tSocketServerAddr.sin_addr) == 0) { printf("invalid server ip!\n"); return -1; } memset(tSocketServerAddr.sin_zero, 0, 8); //2.2 连接到服务器 int connect_ret = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); if (connect_ret == -1) { printf("connect error!\n"); return -1; } /* 3. 键盘获取数据,循环发送数据*/ char Sendbuf[1000]; while(1) { if (fgets(Sendbuf, 999, stdin)) { int send_ret = send(iSocketClient, Sendbuf, strlen(Sendbuf), 0); if (send_ret <= 0) { close(iSocketClient); return -1; } } } return 0; }
(5) 执行结果:首先,打开三个命令行终端,第一个作为server,第二个和第三个作为client,结果如下图。两个client连接到server,server都会显示连接的客户端ip。两个client发送数据时,server都能收到并且打印出来。
(6) 说明:
- 为了防止server子进程退出造成的僵死,需要在server.c中包含signal(SIGCHLD,SIG_IGN);语句。另外,可以使用命令"ps -a"来查看进程。
- 无论是server.c还是client.c,都要注意send和recv函数的返回值,这涉及到程序退出。server.c中的recv_ret<=0包含了接收错误和接收0字节(即客户端退出)两种情况。client.c中的send_ret<=0包含了发送错误和发送0字节(即退出)两种情况。
3. UDP编程
3.1 UDP网络通信交互图:
- 服务器:①服务器利用socket函数返回一个文件描述符fd(注意使用SOCK_DGRAM选项)。②使用bind函数将服务器的fd、IP和端口绑定在一起。③调用sendto和recvfrom函数来发送和接收数据。④使用close函数来关闭该连接。
- 客户端:①使用socket函数返回一个文件描述符fd(注意使用SOCK_DGRAM选项)。②调用sendto和recvfrom函数来发送和接收数据。④使用close函数来关闭该连接。
3.2 多客户端单服务器实验:与TCP类似!
(1) 目标:实现多个客户端给服务器发送数据,服务器接收数据并显示数据。
(2) 思路:server.c和client.c分别实现服务器和客户端的程序代码。
(3) server.c代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <netinet/in.h> #include <unistd.h> #include <signal.h> #define SERVER_PORT 8888 #define BACKLOG 10 int main(int argc, char* argv[]) { /* 1.socket */ int iSocketServer = socket(AF_INET, SOCK_DGRAM, 0); if (iSocketServer == -1) { printf("socket error!\n"); return -1; } /* 2.bind */ struct sockaddr_in tSocketServerAddr; tSocketServerAddr.sin_family = AF_INET; //使用Internet一般为AF_INET tSocketServerAddr.sin_port = htons(SERVER_PORT);//将服务器端口转化为网络字节序 tSocketServerAddr.sin_addr.s_addr = INADDR_ANY; //表示可以和任何客户端通信 memset(tSocketServerAddr.sin_zero, 0, 8); int bind_ret = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); if (bind_ret == -1) { printf("bind error!\n"); return -1; } /* 3.recvfrom */ while(1) { char Recvbuf[1000]; struct sockaddr_in tSocketClientAddr; int addrlen = sizeof(struct sockaddr); int recv_ret = recvfrom(iSocketServer, Recvbuf, 999, 0, (struct sockaddr*)&tSocketClientAddr, &addrlen); if (recv_ret > 0) { Recvbuf[recv_ret] = '\0'; printf("Get Msg from %s: %s\n", inet_ntoa(tSocketClientAddr.sin_addr), Recvbuf); } } /* 4. close */ close(iSocketServer); return 0; }
(4) client.c代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <netinet/in.h> #include <unistd.h> #include <signal.h> #define SERVER_PORT 8888 int main(int argc, char* argv[]) { /* 0.命令行格式 */ if (argc != 2) { printf("格式:%s <server_ip>\n", argv[0]); return -1; } /* 1.socket */ int iSocketClient = socket(AF_INET, SOCK_DGRAM, 0); if (iSocketClient == -1) { printf("socket error!\n"); return -1; } /* 2.指明server的ip与端口:初始化tSocketServerAddr结构体 */ struct sockaddr_in tSocketServerAddr; tSocketServerAddr.sin_family = AF_INET; tSocketServerAddr.sin_port = htons(SERVER_PORT); if (inet_aton(argv[1], &tSocketServerAddr.sin_addr) == 0) { printf("invalid server ip!\n"); return -1; } memset(tSocketServerAddr.sin_zero, 0, 8); /* 3. 键盘获取数据,循环发送数据*/ char Sendbuf[1000]; while(1) { if (fgets(Sendbuf, 999, stdin)) { int send_ret = sendto(iSocketClient, Sendbuf, strlen(Sendbuf), 0, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr)); if (send_ret <= 0) { close(iSocketClient); return -1; } } } return 0; }
(5) 执行结果:首先,打开三个命令行终端,第一个作为server,第二个和第三个作为client,结果如下图。两个client发送数据时,server都能收到并且打印出来。
(6) 说明:
- 程序与TCP类似,但socket函数调用时记得采用SOCK_DGRAM选项。
- client.c中的connect函数可用可不用,用的话与send函数搭配;不用的话,直接使用sendto函数。
七、多线程编程
1. 线程的使用
更多线程的使用详见百问网手册。
1.1 背景:实际程序中,可能存在一边打游戏一遍听歌的需求,若将两种子功能程序写到一个while循环中,可能会造成打游戏的时候无法听歌,听歌的时候无法打游戏,从而降低用户体验。因此,有必要使用多线程,线程1单独处理打游戏,线程2单独处理听歌,可以完美解决上述问题。
1.2 什么是线程
- 线程是操作系统调度的最小单元。
- 普通进程中,只有一个线程执行对应的逻辑。
- 使用多线程编程时,在一个进程中有多个线程执行不同任务。
- 与多进程编程相比,单个进程中的多个线程之间共享该进程的资源(进程中的全局变量)。
1.3 线程的标识
(1) 每一个进程都有一个唯一对应的PID号来表示该进程,而对于线程而言,也有一个“类似于进程的PID号”,名为tid,其本质是一个pthread_t类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。
(2) pthread_t pthread_self(void)函数:返回当前线程的线程号。
(3) 打印tid例程代码:若gcc编译不通过,可以添加-lpthread库选项。
#include <stdio.h> #include <pthread.h> int main(int argc, char* argv[]) { pthread_t tid = pthread_self(); printf("tid = %lu\n", (unsigned long)tid); return 0; }
1.4 线程的创建:调用pthread_create函数
(1) 函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
(2) 参数说明:
第一个参数 用来保存新建线程的线程号 第二个参数 表示线程的属性,一般传入 NULL 表示默认属性 第三个参数 函数指针,就是线程执行函数的地址。这个函数返回值为 void*,形参为 void* 第四个参数 表示向线程执行函数传入的参数,若不传入,可用NULL填充 (3) 例程代码:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* mythread_fun(void* arg) { // 打印新创建线程的tid printf("tid_new = %lu\n", (unsigned long)pthread_self()); } int main(int argc, char* argv[]) { /* 创建一个新线程 */ pthread_t tid; int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL); if (create_ret) { printf("pthread create error\n"); return -1; } /* 打印两个线程号 */ printf("tid_main = %lu\n", (unsigned long)pthread_self()); sleep(1);//以防主线程结束了新线程还没创建完 return 0; }
2. 线程的等待与唤醒
2.1 二进制信号量:一般常用于保护一段代码,使其每次只被一个执行线程运行。
- 初始化信号量函数:int sem_init(sem_t *sem, int pshared, unsigned int value);
- 第一个参数是信号量对象;第二个参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享;第三个参数为sem的初始值。返回值:调用成功时返回0,失败返回-1
2.2 线程的等待:等待其他线程将其唤醒,使用sem_wait函数。如果信号量的值大于零,则将其减一,否则阻塞进程直到信号量变为非零。
- 函数原型:int sem_wait(sem_t *sem);
- 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1
2.3 线程的唤醒:将信号量的值加1,使用sem_post函数。如果有等待的进程,其中一个将被唤醒。
- 函数原型:int sem_post(sem_t *sem);
- 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1
2.4 信号量销毁:调用sem_destroy函数来销毁信号量。
- 函数原型:int sem_destroy(sem_t *sem);
- 第一个参数为信号量对象;返回值:调用成功时返回0,失败返回-1
2.5 实验:主线程监测键盘输入的数据,并保存在缓存数组中。新线程将缓存数组中的内容打印出来。
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <semaphore.h> char inputBuf[1000];//输入缓存数组 sem_t sem; //信号量,初始化为0 void* mythread_fun(void* arg) { /* 新线程:输出主线程中保存的数据 */ while(1) { sem_wait(&sem); printf("tid_new: %lu, Buf: %s\n", (unsigned long)pthread_self(), inputBuf); } } int main(int argc, char* argv[]) { /* 初始化信号量 */ sem_init(&sem, 0, 0); /* 创建一个新线程 */ pthread_t tid; int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL); if (create_ret) { printf("pthread create error\n"); return -1; } /* 主线程:保存键盘输入数据 */ while(1) { fgets(inputBuf, 999, stdin); sem_post(&sem); } return 0; }
3. 基于互斥量的线程互斥
3.1 互斥量:使用互斥量来保护共享数据,首先要定义和初始化互斥量。然后是使用互斥量的加锁、解锁来保护共享数据,最后使用完销毁互斥量。
3.2 初始化互斥量:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
3.3 互斥量的加锁和解锁:pthread_mutex_lock函数和pthread_mutex_unlock函数。
3.3 改进2.5实验:由于2.5实验中主线程和新线程可能会在同一时间来操作数组inputBuf,从而导致inputBuf内容错误。为此,我们添加线程互斥代码,防止上述情况出现。注意:引入temp_buf是因为sem_post函数通知新线程之后,可能主线程又会直接抢占互斥锁,无法使新线程打印信息。为此,利用fgets的休眠特性将输入数据保存到temp_buf中。这样,就能避免上述问题。如果在sem_post后加sleep函数也能实现效果,但是这样就降低了程序响应。
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <semaphore.h> #include <string.h> char inputBuf[1000];//输入缓存数组 sem_t sem; //信号量,初始化为0 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//初始化互斥量 void* mythread_fun(void* arg) { /* 新线程:输出主线程中保存的数据 */ while(1) { sem_wait(&sem); //sem为1时才会被唤醒,否则一直休眠 pthread_mutex_lock(&mutex); printf("tid_new: %lu, Buf: %s\n", (unsigned long)pthread_self(), inputBuf); pthread_mutex_unlock(&mutex); } } int main(int argc, char* argv[]) { /* 初始化信号量 */ sem_init(&sem, 0, 0); /* 创建一个新线程 */ pthread_t tid; int create_ret = pthread_create(&tid, NULL, mythread_fun, NULL); if (create_ret) { printf("pthread create error\n"); return -1; } /* 主线程:保存键盘输入数据 */ char temp_buf[1000];//临时缓存数组,输入数据首先保存在此处 while(1) { fgets(temp_buf, 999, stdin);//有输入数据时才被唤醒 pthread_mutex_lock(&mutex); memcpy(&inputBuf, &temp_buf, 999); pthread_mutex_unlock(&mutex); sem_post(&sem); //sem从0加到1 } return 0; }
八、I2C应用编程
1. I2C协议简介
- I2C协议内容简述:由SDA数据线和SCL时钟线组成,且主机和从机之间的SDA和SCL均连接在一起,同时这两条线接上拉电阻且配置为开漏输出(为了避免短路情况发生)。主机和从机之间的通信主要通过发送读写数据帧来完成。每一个数据帧由固定的二进制格式组成,此处不详细展开。
- 注意:①两条线均有可能被主机和从机驱动,当从机忙碌时,可以拉低SCL以执行内部操作。②SCL高电平期间,主/从设备要读取SDA上的数据,因此SCL高电平期间不能修改SDA的值以防读写错误。③I2C发送时序是先发高位再发低位(高位先行)。
2. SMBus协议
SMBus协议是I2C协议的子集,在I2C的基础上定义了更严格的要求,分为硬件要求和软件要求(传输格式),如下图所示。推荐使用SMBus协议,即使I2C中没有给出相关函数,也可以使用软件来模拟SMBus协议。