主要介绍linux系统c应用开发,基于IMX6ULL平台,有待完善
/sys 下的子目录 | 说明 |
---|---|
/sys/devices | 这是系统中所有设备存放的目录,也就是系统中的所有设备在 sysfs 中的呈现、表达,也是 sysfs 管理设备的最重要的目录结构。 |
/sys/block | 块设备的存放目录,这是一个过时的接口,按照 sysfs 的设计理念,系统所有的设备都存放在/sys/devices 目录下,所以/sys/block 目录下的文件通常是链接到/sys/devices 目录下的文件。 |
/sys/bus | 这是系统中的所有设备按照总线类型分类放置的目录结 构,/sys/devices 目录下每一种设备都是挂在某种总线下 的,譬如 i2c 设备挂在 I2C 总线下。同样,/sys/bus 目录下的文件通常也是链接到了/sys/devices 目录。 |
/sys/class | 这是系统中的所有设备按照其功能分类放置的目录结构, 同样该目录下的文件也是链接到了/sys/devices 目录。按照设备的功能划分组织在/sys/class 目录下,譬如/sys/class/leds 目录中存放了所有的 LED 设备,/sys/class/input 目录中存放了所有的输入类设备。 |
/sys/dev | 这是按照设备号的方式放置的目录结构,同样该目录下的文件也是链接到了/sys/devices 目录。该目录下有很多以主设备号:次设备号(major:minor)命名的文件,这些文件都是链接文件,链接到/sys/devices 目录下对应的设备。 |
/sys/firmware | 描述了内核中的固件。 |
/sys/fs | 用于描述系统中所有文件系统,包括文件系统本身和按文件系统分类存放的已挂载点。 |
/sys/kernel | 这里是内核中所有可调参数的位置。 |
/sys/module | 这里有系统中所有模块的信息。 |
/sys/power | 这里是系统中电源选项,有一些属性可以用于控制整个系统的电源状态。 |
系统中所有的设备(对象)都会在/sys/devices 体现出来,是 sysfs 文件系统中最重要的目录结构;而/sys/bus、/sys/class、/sys/dev 分别将设备按照挂载的总线类型、功能分类以及设备号的形式将设备组织存放在这些目录中,这些目录下的文件都是链接到了/sys/devices 中。
如果 LED 设备使用的是 Linux 内核标准 LED 驱动框架注册而成,在/dev 目录下并没有其对应的设备节点,其实现使用 sysfs 方式控制
/sys/class/leds 目录下便存放了所有的 LED 类设备, 进入sys-led 文件夹,发现其内有brightness、max_brightness 以及 trigger 三个文件
echo timer > trigger //将 LED 触发模式设置为 timer
echo 1 > brightness //点亮 LED echo 0 > brightness//熄灭 LED
具体可以参考后一小节GPIO输出
与 LED 设备一样,GPIO 同样也是通过 sysfs 方式进行操控,进入到/sys/class/gpio 目录下
echo 0 > export # 导出编号为 0 的 GPIO 引脚(对于 I.MX6UL/I.MX6ULL 来说,也就是GPIO1_IO0)
在gpioX 的文件夹中
echo 0 > unexport # 删除导出的编号为 0 的 GPIO 引脚
#include
#include
#include
#include
#include
#include
#include
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
/* 校验传参 */
if (3 != argc) {
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
int fd;
int len;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
close(fd);
exit(-1);
}
close(fd); //关闭文件
}
/* 配置为输出模式 */
if (gpio_config("direction", "out"))
exit(-1);
/* 极性设置 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 控制GPIO输出高低电平 */
if (gpio_config("value", argv[2]))
exit(-1);
/* 退出程序 */
exit(0);
}
注意:以下可以将Ubuntu当开发板实验
chmod u+x fsl-imx-x11-glibc-x86_64-meta-toolchain-qt5-cortexa7hf-neon-toolchain-4.1.15-2.1.0.sh
②使能环境变量
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
③使用 env 指令查看生效的环境变量,使用 arm-poky-linux-gnueabi-gcc -v 指令可以查看 gcc 版本
④要使用此编译器编译内核和 U-boot 还需要安装以下软件
sudo apt-get update // 先更新软列表
sudo apt-get install lzop // 安装 lzop 工具,用于生成压缩或解压镜像
sudo apt-get install libncurses* // 安装 ncurese 相关库,U-boot 或者内核菜单显示时需要
①环境设置
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
②使用交叉编译工具编译应用程序
${CC} -o testApp testApp.c
③将testApp.c拷贝到开发板跟文件系统中,可使用 scp 命令
④执行该应用程序控制开发板上的 GPIO1_IO01 引脚输出高或低电平
./testApp 1 1 #控制 GPIO1_IO01 输出高电平
输入设备(也称为 input 设备),常见的输入设备有鼠标、键盘、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。
Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序,input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。
基于 input 子系统注册成功的输入设备,都会在/dev/input目录下生成对应的设备节点(设备文件),设备节点名称通常为 eventX(X 表示一个数字编号 0、1、2、3 等),譬如/dev/input/event0、/dev/input/event1、 /dev/input/event2 等,通过读取这些设备节点可以获取输入设备上报的数据。
假设触摸屏设备对应的设备节点为/dev/input/event0,那么数据读取流程如下:
①应用程序打开/dev/input/event0 设备文件;
②应用程序发起读操作(譬如调用 read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
③当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
④应用程序对读取到的数据进行解析。
当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据、应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。那么对于其它输入设备亦是如此,无数据可读时应用程序会进入休眠状态(阻塞式 I/O 方式下),当有数据可读时才会被唤醒。
应用程序打开输入设备对应的设备文件,向其发起读操作,那么这个读操作获取到的是什么样的数据呢?其实每一次 read 操作获取的都是一个 struct input_event 结构体类型数据,该结构体定义在
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
#define EV_SYN 0x00 //同步类事件,用于同步事件
#define EV_KEY 0x01 //按键类事件
#define EV_REL 0x02 //相对位移类事件(譬如鼠标)
#define EV_ABS 0x03 //绝对位移类事件(譬如触摸屏)
#define EV_MSC 0x04 //其它杂类事件
#define EV_SW 0x05
#define EV_LED 0x11
#define EV_SND 0x12
#define EV_REP 0x14
#define EV_FF 0x15
#define EV_PWR 0x16
#define EV_FF_STATUS 0x17
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX+1)
应用程序如何得知本轮已经读取到完整的数据了呢?其实这就是通过同步事件来实现的,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。同步类事件中也包含了多种不同的事件,如下所示:
#define SYN_REPORT 0
#define SYN_CONFIG 1
#define SYN_MT_REPORT 2
#define SYN_DROPPED 3
#define SYN_MAX 0xf
#define SYN_CNT (SYN_MAX+1)
所有的输入设备都需要上报同步事件,上报的同步事件通常是 SYN_REPORT,而 value 值通常为 0。
在 for 循环中,调用 read()读取输入设备上报的数据,当按键按下或松开(以及长按)动作发生时,read()会读取到输入设备上报的数据,首先判断此次上报的事件是否是按键类事件(EV_KEY),如果是按键类事件、接着根据 value 值来判断按键当前的状态是松开、按下还是长按。
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct input_event in_ev = {0};
int fd = -1;
int value = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(-1);
}
/* 打开文件 */
if (0 > (fd = open(argv[1], O_RDONLY))) {
perror("open error");
exit(-1);
}
for ( ; ; ) {
/* 循环读取数据 */
if (sizeof(struct input_event) !=
read(fd, &in_ev, sizeof(struct input_event))) {
perror("read error");
exit(-1);
}
if (EV_KEY == in_ev.type) { //按键事件
switch (in_ev.value) {
case 0:
printf("code<%d>: 松开\n", in_ev.code);
break;
case 1:
printf("code<%d>: 按下\n", in_ev.code);
break;
case 2:
printf("code<%d>: 长按\n", in_ev.code);
break;
}
}
}
}
触摸屏设备是一个绝对位移设备,可以上报绝对位移事件
触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件、告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件,用于描述按下触摸屏和松开触摸屏;具体的按键事件为BTN_TOUCH(code=0x14a,也就是 330),当然,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。
#点击触摸屏时
BTN_TOUCH
ABS_X
ABS_Y
SYN_REPORT
#滑动
ABS_X
ABS_Y
SYN_REPORT
#松开
BTN_TOUCH
SYN_REPORT
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 10
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 11
ABS_MT_POSITION_X
ABS_MT_POSITION_Y
SYN_REPORT
char name[100];
ioctl(fd, EVIOCGNAME(sizeof(name)), name);
#define EVIOCGABS(abs) _IOR('E', 0x40 + (abs), struct input_absinfo)
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct input_event in_ev;
int x, y; //触摸点x和y坐标
int down; //用于记录BTN_TOUCH事件的value,1表示按下,0表示松开,-1表示移动
int valid; //用于记录数据是否有效(我们关注的信息发生更新表示有效,1表示有效,0表示无效)
int fd = -1;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s \n" , argv[0]);
exit(EXIT_FAILURE);
}
/* 打开文件 */
if (0 > (fd = open(argv[1], O_RDONLY))) {
perror("open error");
exit(EXIT_FAILURE);
}
x = y = 0; //初始化x和y坐标值
down = -1; //初始化<移动>
valid = 0;//初始化<无效>
for ( ; ; ) {
/* 循环读取数据 */
if (sizeof(struct input_event) !=
read(fd, &in_ev, sizeof(struct input_event))) {
perror("read error");
exit(EXIT_FAILURE);
}
switch (in_ev.type) {
case EV_KEY: //按键事件
if (BTN_TOUCH == in_ev.code) {
down = in_ev.value;
valid = 1;
}
break;
case EV_ABS: //绝对位移事件
switch (in_ev.code) {
case ABS_X: //X坐标
x = in_ev.value;
valid = 1;
break;
case ABS_Y: //Y坐标
y = in_ev.value;
valid = 1;
break;
}
break;
case EV_SYN: //同步事件
if (SYN_REPORT == in_ev.code) {
if (valid) {//判断是否有效
switch (down) {//判断状态
case 1:
printf("按下(%d, %d)\n", x, y);
break;
case 0:
printf("松开\n");
break;
case -1:
printf("移动(%d, %d)\n", x, y);
break;
}
valid = 0; //重置valid
down = -1; //重置down
}
}
break;
}
}
}
Frame 是帧的意思,buffer 是缓冲的意思,所以 Framebuffer 就是帧缓冲,这意味着 Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是 Linux 系统中的一种显示驱动接口,它将显示设备(譬如 LCD)进行抽象、屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由Framebuffer 设备驱动来完成。
所以在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字,0、1、2、3 等),Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31,开发板出厂系统中,/dev/fb0设备节点便是 LCD 屏。
应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),譬如 LCD 的分辨率是 800 * 480,每一个像素点的颜色用 24 位(譬如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 = 1152000 个字节。譬如执行下面这条命令将 LCD 清屏,也就是将其填充为黑色(假设 LCD 对应的设备节点是/dev/fb0,分辨率为 800*480,RGB888 格式):
对 FrameBuffer 设备(譬如 LCD)进行应用编程,通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的显示缓冲区,LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上,步骤如下:
①首先打开/dev/fbX 设备文件。
②使用 ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。
③通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。
④映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。
⑤完成显示后,调用 munmap()取消映射、并调用 close()关闭设备文件。
通 过 ioctl() 函 数 来 获 取 屏 幕 参 数 信息, 对 于 Framebuffer 设备来说, 常 用 的 request 包 括FBIOGET_VSCREENINFO、FBIOPUT_VSCREENINFO、FBIOGET_FSCREENINFO。三个宏定义以及2 个数据结构 struct fb_var_screeninfo 和 struct fb_fix_screeninfo 都定义在
struct fb_var_screeninfo fb_var;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
存储映射 I/O 这种高级 I/O 方式,它的一个非常经典的使用场景便是用在 Framebuffer 应用编程中。通过 mmap()将显示器的显示缓冲区(显存)映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。
为什么这里需要使用存储映射 I/O 这种方式呢?其实使用普通的 I/O 方式(譬如直接 read、write)也是可以的,只是,当数据量比较大时,普通 I/O 方式效率较低。假设某一显示器的分辨率为 1920 * 1080,像
素格式为 ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数
据会被不断更新。
在这种情况下,数据量是比较庞大的,使用普通 I/O 方式必然导致效率低下,所以才会采用存储映射I/O 方式。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int width; //LCD X分辨率
static int height; //LCD Y分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
screen_base[y * width + x] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
unsigned int end;
unsigned long temp;
/* 对传入参数的校验 */
if (x >= width)
x = width - 1;
if (y >= height)
y = height - 1;
/* 填充颜色 */
temp = y * width + x;//定位到起点
if (dir) { //水平线
end = x + length - 1;
if (end >= width)
end = width - 1;
for ( ; x <= end; x++, temp++)
screen_base[temp] = rgb565_color;
}
else { //垂直线
end = y + length - 1;
if (end >= height)
end = height - 1;
for ( ; y <= end; y++, temp += width)
screen_base[temp] = rgb565_color;
}
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数color所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);//得到RGB565颜色值
unsigned long temp;
unsigned int x;
/* 对传入参数的校验 */
if (end_x >= width)
end_x = width - 1;
if (end_y >= height)
end_y = height - 1;
/* 填充颜色 */
temp = start_y * width; //定位到起点行首
for ( ; start_y <= end_y; start_y++, temp+=width) {
for (x = start_x; x <= end_x; x++)
screen_base[temp + x] = rgb565_color;
}
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 打开framebuffer设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
width = fb_var.xres;
height = fb_var.yres;
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块 */
int w = height * 0.25;//方块的宽度为1/4屏幕高度
lcd_fill(0, width-1, 0, height-1, 0x0); //清屏(屏幕显示黑色)
lcd_fill(0, w, 0, w, 0xFF0000); //红色方块
lcd_fill(width-w, width-1, 0, w, 0xFF00); //绿色方块
lcd_fill(0, w, height-w, height-1, 0xFF); //蓝色方块
lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);//黄色方块
/* 画线: 十字交叉线 */
lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);//白色线
lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);//白色线
/* 画矩形 */
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * width;
s_y = w;
e_x = width - s_x;
e_y = height - s_y;
for ( ; (s_x <= e_x) && (s_y <= e_y);
s_x+=5, s_y+=5, e_x-=5, e_y-=5)
lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
/* 退出 */
munmap(screen_base, screen_size); //取消映射
close(fd); //关闭文件
exit(EXIT_SUCCESS); //退出进程
}
串口全称叫做串行接口,串行接口指的是数据一个一个的按顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接收。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。
终端就是处理主机输入、输出的一套设备,它用来显示主机运算的输出,并且接受主机要求的输入。分类如下:
每一个终端在/dev 目录下都有一个对应的设备节点
串口设备节点为/dev/ttymxc0(UART1)和/dev/ttymxc2(UART3)。因为 Linux 为上层用户做了一层封装,将这些 ioctl()操作封装成了一套标准的 API。要使用 termios API,需要在我们的应用程序中包含termios.h头文件。对于配置来说,一个很重要的数据结构便是 struct termios 结构体:
该数据结构描述了终端的配置信息,这些参数能够控制、影响终端的行为、特性
struct termios
{
tcflag_t c_iflag; /* input mode flags */
tcflag_t c_oflag; /* output mode flags */
tcflag_t c_cflag; /* control mode flags */
tcflag_t c_lflag; /* local mode flags */
cc_t c_line; /* line discipline */
cc_t c_cc[NCCS]; /* control characters */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
宏 | 功能 |
---|---|
IGNBRK | 忽略输入终止条件 |
BRKINT | 当检测到输入终止条件时发送 SIGINT 信号 |
IGNPAR | 忽略帧错误和奇偶校验错误 |
PARMRK | 对奇偶校验错误做出标记 |
INPCK | 对接收到的数据执行奇偶校验 |
ISTRIP | 将所有接收到的数据裁剪为 7 比特位、也就是去除第八位 |
INLCR | 将接收到的 NL(换行符)转换为 CR(回车符) |
IGNCR | 忽略接收到的 CR(回车符) |
ICRNL | 将接收到的 CR(回车符)转换为 NL(换行符) |
IUCLC | 将接收到的大写字符映射为小写字符 |
IXON | 启动输出软件流控 |
IXOFF | 启动输入软件流控 |
宏 | 功能 |
---|---|
OPOST | 启用输出处理功能,如果不设置该标志则其他标志都被忽略 |
OLCUC | 将输出字符中的大写字符转换成小写字符 |
ONLCR | 将输出中的换行符(NL ‘\n’)转换成回车符(CR ‘\r’) |
OCRNL | 将输出中的回车符(CR ‘\r’)转换成换行符(NL ‘\n’) |
ONOCR | 在第 0 列不输出回车符(CR) |
ONLRET | 不输出回车符 |
OFILL | 发送填充字符以提供延时 |
OFDEL | 如果设置该标志,则表示填充字符为 DEL 字符,否则为 NULL字符 |
CBAUD | 波特率的位掩码 |
---|---|
B0 | 波特率为 0 |
…… | …… |
B1200 | 1200 波特率 |
B1800 | 1800 波特率 |
B2400 | 2400 波特率 |
B4800 | 4800 波特率 |
B9600 | 9600 波特率 |
B19200 | 19200 波特率 |
B38400 | 38400 波特率 |
B57600 | 57600 波特率 |
B115200 | 115200 波特率 |
B230400 | 230400 波特率 |
B460800 | 460800 波特率 |
B500000 | 500000 波特率 |
B576000 | 576000 波特率 |
B921600 | 921600 波特率 |
B1000000 | 1000000 波特率 |
B1152000 | 1152000 波特率 |
B1500000 | 1500000 波特率 |
B2000000 | 2000000 波特率 |
B2500000 | 2500000 波特率 |
B3000000 | 3000000 波特率 |
…… | …… |
CSIZE | 数据位的位掩码 |
---|---|
CS5 | 5 个数据位 |
CS6 | 6 个数据位 |
CS7 | 7 个数据位 |
CS8 | 8 个数据位 |
CSTOPB | 2 个停止位,如果不设置该标志则默认是一个停止位 |
CREAD | 接收使能 |
PARENB | 使能奇偶校验 |
PARODD | 使用奇校验、而不是偶校验 |
HUPCL | 关闭时挂断调制解调器 |
CLOCAL | 忽略调制解调器控制线 |
CRTSCTS | 使能硬件流控 |
模式 | 功能 |
---|---|
ISIG | 若收到信号字符(INTR、QUIT 等),则会产生相应的信号 |
ICANON | 启用规范模式 |
ECHO | 启用输入字符的本地回显功能。当我们在终端输入字符的时候,字符会显示出来,这就是回显功能 |
ECHOE | 若设置 ICANON,则允许退格操作 |
ECHOK | 若设置 ICANON,则 KILL 字符会删除当前行 |
ECHONL | 若设置 ICANON,则允许回显换行符 |
ECHOCTL | 若设置 ECHO,则控制字符(制表符、换行符等)会显示成“^X”, 其中 X 的 ASCII 码等于给相应控制字符的 ASCII 码加上 0x40。例如,退格字符(0x08)会显示为“^H”('H’的 ASCII 码为 0x48) |
ECHOPRT | 若设置 ICANON 和 IECHO,则删除字符(退格符等)和被删除的字符都会被显示 |
ECHOKE | 若设置 ICANON,则允许回显在 ECHOE 和 ECHOPRT 中设定的 KILL字符 |
NOFLSH | 在通常情况下,当接收到 INTR、QUIT 和 SUSP 控制字符时,会清空输入和输出队列。如果设置该标志,则所有的队列不会被清空 |
TOSTOP | 若一个后台进程试图向它的控制终端进行写操作,则系统向该后台进程的进程组发送 SIGTTOU 信号。该信号通常终止进程的执行 |
IEXTEN | 启用输入处理功能 |
终端有三种工作模式,分别为规范模式(canonical mode)、非规范模式(non-canonical mode)和原始模式(raw mode)。通过在 struct termios 结构体的 c_lflag 成员中设置 ICANNON 标志来定义终端是以规范模式(设置 ICANNON 标志)还是以非规范模式(清除 ICANNON 标志)工作,默认情况为规范模式。
int fd;
fd = open("/dev/ttymxc2", O_RDWR | O_NOCTTY);
if (0 > fd) {
perror("open error");
return -1;
}
#include
#include
int tcgetattr(int fd, struct termios *termios_p);
调用 tcgetattr 函数之前,我们需要定义一个 struct termios 结构体变量,将该变量的指针作为 tcgetattr()函数的第二个参数传入;tcgetattr()调用成功后,会将终端当前的配置参数保存到 termios_p 指针所指的对象中。
函数调用成功返回 0;失败将返回-1,并且会设置 errno 以告知错误原因。
struct termios new_cfg;
memset(&new_cfg, 0x0, sizeof(struct termios));
//配置为原始模式
cfmakeraw(&new_cfg);
new_cfg.c_cflag |= CREAD; //接收使能
cfsetispeed(&new_cfg, B115200);
cfsetospeed(&new_cfg, B115200);
cfsetspeed(&new_cfg, B115200);
new_cfg.c_cflag &= ~CSIZE;
new_cfg.c_cflag |= CS8; //设置为 8 位数据位
//奇校验使能
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
//偶校验使能
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除 PARODD 标志,配置为偶校验 */
new_cfg.c_iflag |= INPCK;
//无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
// 将停止位设置为一个比特
new_cfg.c_cflag &= ~CSTOPB;
// 将停止位设置为 2 个比特
new_cfg.c_cflag |= CSTOPB;
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
#include
#include
int tcdrain(int fd);
int tcflush(int fd, int queue_selector);
int tcflow(int fd, int action);
tcdrain(fd);
tcflush(fd, TCIOFLUSH);
#include
#include
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
调用该函数会将参数 termios_p 所指 struct termios 对象中的配置参数写入到终端设备中,使配置生效!而参数 optional_actions 可以指定更改何时生效,其取值如下:
该函数调用成功时返回 0;失败将返回-1,、并设置 errno 以指示错误类型。譬如,调用 tcsetattr()将配置参数写入设备,使其立即生效:
tcsetattr(fd, TCSANOW, &new_cfg);
#define _GNU_SOURCE //在源文件开头定义_GNU_SOURCE宏
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct uart_hardware_cfg {
unsigned int baudrate; /* 波特率 */
unsigned char dbit; /* 数据位 */
char parity; /* 奇偶校验 */
unsigned char sbit; /* 停止位 */
} uart_cfg_t;
static struct termios old_cfg; //用于保存终端的配置参数
static int fd; //串口终端对应的文件描述符
/**
** 串口初始化操作
** 参数device表示串口终端的设备节点
**/
static int uart_init(const char *device)
{
/* 打开串口终端 */
fd = open(device, O_RDWR | O_NOCTTY);
if (0 > fd) {
fprintf(stderr, "open error: %s: %s\n", device, strerror(errno));
return -1;
}
/* 获取串口当前的配置参数 */
if (0 > tcgetattr(fd, &old_cfg)) {
fprintf(stderr, "tcgetattr error: %s\n", strerror(errno));
close(fd);
return -1;
}
return 0;
}
/**
** 串口配置
** 参数cfg指向一个uart_cfg_t结构体对象
**/
static int uart_cfg(const uart_cfg_t *cfg)
{
struct termios new_cfg = {0}; //将new_cfg对象清零
speed_t speed;
/* 设置为原始模式 */
cfmakeraw(&new_cfg);
/* 使能接收 */
new_cfg.c_cflag |= CREAD;
/* 设置波特率 */
switch (cfg->baudrate) {
case 1200: speed = B1200;
break;
case 1800: speed = B1800;
break;
case 2400: speed = B2400;
break;
case 4800: speed = B4800;
break;
case 9600: speed = B9600;
break;
case 19200: speed = B19200;
break;
case 38400: speed = B38400;
break;
case 57600: speed = B57600;
break;
case 115200: speed = B115200;
break;
case 230400: speed = B230400;
break;
case 460800: speed = B460800;
break;
case 500000: speed = B500000;
break;
default: //默认配置为115200
speed = B115200;
printf("default baud rate: 115200\n");
break;
}
if (0 > cfsetspeed(&new_cfg, speed)) {
fprintf(stderr, "cfsetspeed error: %s\n", strerror(errno));
return -1;
}
/* 设置数据位大小 */
new_cfg.c_cflag &= ~CSIZE; //将数据位相关的比特位清零
switch (cfg->dbit) {
case 5:
new_cfg.c_cflag |= CS5;
break;
case 6:
new_cfg.c_cflag |= CS6;
break;
case 7:
new_cfg.c_cflag |= CS7;
break;
case 8:
new_cfg.c_cflag |= CS8;
break;
default: //默认数据位大小为8
new_cfg.c_cflag |= CS8;
printf("default data bit size: 8\n");
break;
}
/* 设置奇偶校验 */
switch (cfg->parity) {
case 'N': //无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
break;
case 'O': //奇校验
new_cfg.c_cflag |= (PARODD | PARENB);
new_cfg.c_iflag |= INPCK;
break;
case 'E': //偶校验
new_cfg.c_cflag |= PARENB;
new_cfg.c_cflag &= ~PARODD; /* 清除PARODD标志,配置为偶校验 */
new_cfg.c_iflag |= INPCK;
break;
default: //默认配置为无校验
new_cfg.c_cflag &= ~PARENB;
new_cfg.c_iflag &= ~INPCK;
printf("default parity: N\n");
break;
}
/* 设置停止位 */
switch (cfg->sbit) {
case 1: //1个停止位
new_cfg.c_cflag &= ~CSTOPB;
break;
case 2: //2个停止位
new_cfg.c_cflag |= CSTOPB;
break;
default: //默认配置为1个停止位
new_cfg.c_cflag &= ~CSTOPB;
printf("default stop bit size: 1\n");
break;
}
/* 将MIN和TIME设置为0 */
new_cfg.c_cc[VTIME] = 0;
new_cfg.c_cc[VMIN] = 0;
/* 清空缓冲区 */
if (0 > tcflush(fd, TCIOFLUSH)) {
fprintf(stderr, "tcflush error: %s\n", strerror(errno));
return -1;
}
/* 写入配置、使配置生效 */
if (0 > tcsetattr(fd, TCSANOW, &new_cfg)) {
fprintf(stderr, "tcsetattr error: %s\n", strerror(errno));
return -1;
}
/* 配置OK 退出 */
return 0;
}
/***
--dev=/dev/ttymxc2
--brate=115200
--dbit=8
--parity=N
--sbit=1
--type=read
***/
/**
** 打印帮助信息
**/
static void show_help(const char *app)
{
printf("Usage: %s [选项]\n"
"\n必选选项:\n"
" --dev=DEVICE 指定串口终端设备名称, 譬如--dev=/dev/ttymxc2\n"
" --type=TYPE 指定操作类型, 读串口还是写串口, 譬如--type=read(read表示读、write表示写、其它值无效)\n"
"\n可选选项:\n"
" --brate=SPEED 指定串口波特率, 譬如--brate=115200\n"
" --dbit=SIZE 指定串口数据位个数, 譬如--dbit=8(可取值为: 5/6/7/8)\n"
" --parity=PARITY 指定串口奇偶校验方式, 譬如--parity=N(N表示无校验、O表示奇校验、E表示偶校验)\n"
" --sbit=SIZE 指定串口停止位个数, 譬如--sbit=1(可取值为: 1/2)\n"
" --help 查看本程序使用帮助信息\n\n", app);
}
/**
** 信号处理函数,当串口有数据可读时,会跳转到该函数执行
**/
static void io_handler(int sig, siginfo_t *info, void *context)
{
unsigned char buf[10] = {0};
int ret;
int n;
if(SIGRTMIN != sig)
return;
/* 判断串口是否有数据可读 */
if (POLL_IN == info->si_code) {
ret = read(fd, buf, 8); //一次最多读8个字节数据
printf("[ ");
for (n = 0; n < ret; n++)
printf("0x%hhx ", buf[n]);
printf("]\n");
}
}
/**
** 异步I/O初始化函数
**/
static void async_io_init(void)
{
struct sigaction sigatn;
int flag;
/* 使能异步I/O */
flag = fcntl(fd, F_GETFL); //使能串口的异步I/O功能
flag |= O_ASYNC;
fcntl(fd, F_SETFL, flag);
/* 设置异步I/O的所有者 */
fcntl(fd, F_SETOWN, getpid());
/* 指定实时信号SIGRTMIN作为异步I/O通知信号 */
fcntl(fd, F_SETSIG, SIGRTMIN);
/* 为实时信号SIGRTMIN注册信号处理函数 */
sigatn.sa_sigaction = io_handler; //当串口有数据可读时,会跳转到io_handler函数
sigatn.sa_flags = SA_SIGINFO;
sigemptyset(&sigatn.sa_mask);
sigaction(SIGRTMIN, &sigatn, NULL);
}
int main(int argc, char *argv[])
{
uart_cfg_t cfg = {0};
char *device = NULL;
int rw_flag = -1;
unsigned char w_buf[10] = {0x11, 0x22, 0x33, 0x44,
0x55, 0x66, 0x77, 0x88}; //通过串口发送出去的数据
int n;
/* 解析出参数 */
for (n = 1; n < argc; n++) {
if (!strncmp("--dev=", argv[n], 6))
device = &argv[n][6];
else if (!strncmp("--brate=", argv[n], 8))
cfg.baudrate = atoi(&argv[n][8]);
else if (!strncmp("--dbit=", argv[n], 7))
cfg.dbit = atoi(&argv[n][7]);
else if (!strncmp("--parity=", argv[n], 9))
cfg.parity = argv[n][9];
else if (!strncmp("--sbit=", argv[n], 7))
cfg.sbit = atoi(&argv[n][7]);
else if (!strncmp("--type=", argv[n], 7)) {
if (!strcmp("read", &argv[n][7]))
rw_flag = 0; //读
else if (!strcmp("write", &argv[n][7]))
rw_flag = 1; //写
}
else if (!strcmp("--help", argv[n])) {
show_help(argv[0]); //打印帮助信息
exit(EXIT_SUCCESS);
}
}
if (NULL == device || -1 == rw_flag) {
fprintf(stderr, "Error: the device and read|write type must be set!\n");
show_help(argv[0]);
exit(EXIT_FAILURE);
}
/* 串口初始化 */
if (uart_init(device))
exit(EXIT_FAILURE);
/* 串口配置 */
if (uart_cfg(&cfg)) {
tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置
close(fd);
exit(EXIT_FAILURE);
}
/* 读|写串口 */
switch (rw_flag) {
case 0: //读串口数据
async_io_init(); //我们使用异步I/O方式读取串口的数据,调用该函数去初始化串口的异步I/O
for ( ; ; )
sleep(1); //进入休眠、等待有数据可读,有数据可读之后就会跳转到io_handler()函数
break;
case 1: //向串口写入数据
for ( ; ; ) { //循环向串口写入数据
write(fd, w_buf, 8); //一次向串口写入8个字节
sleep(1); //间隔1秒钟
}
break;
}
/* 退出 */
tcsetattr(fd, TCSANOW, &old_cfg); //恢复到之前的配置
close(fd);
exit(EXIT_SUCCESS);
}
网络通信本质上是一种进程间通信,是位于网络中不同主机上的进程之间的通信,属于 IPC 的一种,通常称为 socket IPC.所以网络通信是为了解决在网络环境中,不同主机上的应用程序之间的通信问题。大概可以分为三个层次:
网络通信中,数据从上层到下层交付时,要进行封装;同理,当目标主机接收到数据时,数据由下层传递给上层时需要进行拆封。这就是数据的封装与拆封。
当用户发送数据时,将数据向下交给传输层,但是在交给传输层之前,应用层相关协议会对用户数据进行封装,譬如 MQTT、HTTP 等协议,其实就是在用户数据前添加一个应用程序头部,这是处于应用层的操作,最后应用层通过调用传输层接口来将封装好的数据交给传输层。
传输层会在数据前面加上传输层首部(此处以 TCP 协议为例,图中的传输层首部为 TCP 首部,也可以是 UDP 首部),然后向下交给网络层。
同样地,网络层会在数据前面加上网络层首部(IP 首部),然后将数据向下交给链路层,链路层会对数据进行最后一次封装,即在数据前面加上链路层首部(此处使用以太网接口为例,对应以太网首部),然后将数据交给网卡。
最后,由网卡硬件设备将数据转换成物理链路上的电平信号,数据就这样被发送到了网络中。这就是网络数据的发送过程,从图中可以看到,各层协议均会对数据进行相应的封装,可以概括为 TCP/IP 模型中的各层协议对数据进行封装的过程。
TCP/IP 协议它其实是一个协议族,包含了众多的协议,譬如应用层协议 HTTP、FTP、MQTT…以及传输层协议 TCP、UDP 等这些都属于 TCP/IP 协议
Internet 依靠 TCP/IP 协议,在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的主机之间的互联。在 Internet 上,每一个节点都依靠唯一的 IP 地址相互区分和相互联系,IP 地址用于标识互联网中的每台主机的身份,设计人员为每个接入网络中的主机都分配一个 IP 地址(Internet Protocol Address),只有合法的 IP 地址才能接入互联网中并且与其他主机进行网络通信,IP 地址是软件地址,不是硬件地址,硬件 MAC 地址是存储在网卡中的,应用于局域网中寻找目标主机。
IP 地址的分类
根据 IP 地址中网络地址和主机地址两部分分别占多少位的不同,将 IP 地址划分为 5 类,分别为 A、B、C、D、E 五类,如下所示:
A 类地址的第一组数字为 1~126。 B 类地址的第一组数字为 128~191。 C 类地址的第一组数字为 192~223。
A 类地址的表示范围为:0.0.0.0~126.255.255.255,默认网络掩码为:255.0.0.0;A 类地址分配给规模特别大的网络使用。A 类地址用第一组数字表示网络地址,后面三组数字作为连接于网络上的主机对应的地址。分配给具有大量主机而局域网络个数较少的大型网络,譬如 IBM 公司的网络。
B 类地址的表示范围为:128.0.0.0~191.255.255.255,默认网络掩码为:255.255.0.0;B 类地址分配给一般的中型网络。B 类地址用第一、二组数字表示网络地址,后面两组数字代表网络上的主机地址。
C类地址的表示范围为:192.0.0.0~223.255.255.255,默认网络掩码为:255.255.255.0;C 类地址分配给小型网络,如一般的局域网和校园网,它可连接的主机数量是最少的,采用把所属的用户分为若干的网段进行管理。C 类地址用前三组数字表示网络地址,最后一组数字作为网络上的主机地址。
特殊的 IP 地址
①其中在 A、B、C 三类地址中,各保留了一个区域作为私有地址:
A 类地址:10.0.0.0~10.255.255.255
B 类地址:172.16.0.0~172.31.255.255
C 类地址:192.168.0.0~192.168.255.255
②直接广播(Direct Broadcast Address):向某个网络上所有的主机发送报文
A 类地址的广播地址为:XXX.255.255.255
B 类地址的广播地址为:XXX.XXX.255.255
C 类地址的广播地址为:XXX.XXX.XXX.255
③受限广播地址:255.255.255.255
④多播地址
多播地址用在一对多的通信中,即一个发送者,多个接收者,不论接受者数量的多少,发送者只发送一次数据包。多播地址属于 D 类地址,D 类地址只能用作目的地址,而不能作为主机中的源地址。
⑤环回地址
环回地址(Loopback Address)是用于网络软件测试以及本机进程之间通信的特殊地址。把 A 类地址中的 127.XXX.XXX.XXX 的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。比如在电脑中使用 ping 命令去 ping 127.1.1.1 就可以测试本地 TCP/IP 协议是否正常。
⑥0.0.0.0 地址
IP 地址 32bit 全为 0 的地址(也就是 0.0.0.0)表示本网络上的本主机,只能用作源地址。0.0.0.0 是不能被 ping 通的,在服务器中,0.0.0.0 并不是一个真实的的 IP 地址,它表示本机中所有的IPv4 地址。监听 0.0.0.0 的端口,就是监听本机中所有 IP 的端口。
如何判断 2 个 IP 地址是否在同一个网段内
可通过网络标识来进行判断,网络标识定义:
网络标识 = IP 地址 & 子网掩码
2 个 IP 地址的网络标识相同,那么它们就处于同一网络。譬如 192.168.1.50 和 192.168.1.100,这 2 个都是 C 类地址,对应的子网掩码为 255.255.255.0,很明显,这两个 IP 地址与子网掩码进行按位与操作时得到的结果(网络标识)是一样的,所以它们处于同一网络。
HTTP 超文本传输协议(英文:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP 是万维网数据通信的基础。HTTP 的应用最为广泛,譬如大家经常会打开网页浏览器查询资料,通过浏览器便可开启 HTTP 通信。
HTTP 协议工作于客户端(用户)、服务器端(网站)模式下,浏览器作为 HTTP 客户端通过 URL 向HTTP 服务端即 WEB 服务器发送请求。Web 服务器根据接收到的请求后,向客户端发送响应信息。借助这种浏览器和服务器之间的 HTTP 通信,我们能够足不出户地获取网络中的各种信息。
FTP 协议的英文全称为 File Transfer Protocol,简称为 FTP,它是一种文件传输协议,从一个主机向一个主机传输文件的协议。FTP 协议同样也是基于客户端-服务器模式,在客户端和服务器之间进行文件传输,譬如我们通常会使用 FTP 协议在两台主机之间进行文件传输,譬如一台 Ubuntu 系统主机和一台 Windows系统主机,将一台主机作为 FTP 服务器、另一台主机作为 FTP 客户端,建立 FTP 连接之后,客户端可以从服务器下载文件,同样也可以将文件上传至服务器。
FTP 除了基本的文件上传/下载功能外,还有目录操作、权限设置、身份验证等机制,许多网盘的文件传输功能都是基于 FTP 实现的。
UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是一种无连接、不可靠的协议,同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过 IP 层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外,UDP 协议更没有流量控制、拥塞控制等功能,在发送的一端,UDP 只是把上层应用的数据封装到 UDP 报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到 IP 数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP 报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。UDP 协议的特点:
UDP 虽然有很多缺点,但也有自己的优点,所以它也有很多的应用场合,因为在如今的网络环境下,UDP 协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以,UDP协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于 IP 的传输协议。TCP 协议工作在传输层,对上服务 socket 接口,对下调用 IP 层(网络层)。关于 TCP 协议我们需要理解的重点如下:
当数据由上层发送到传输层时,数据会被封装为 TCP 数据段,我们将其称为 TCP 报文(或 TCP 报文段),TCP 报文由 TCP 首部+数据区域组成,一般 TCP 首部通常为 20 个字节大小
- URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。
- ACK:只有当 ACK=1 时,确认序号字段才有效。
- PSH:当PSH=1 时,接收方应该尽快将本报文段立即传送给其应用层。
- RST:当 RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接。
- SYN:SYN=1,ACK=0 时表示请求建立一个连接,携带 SYN 标志的 TCP 报文段为同步报文段。
- FIN:为 1表示发送方没有数据要传输了,要求释放连接。
互联网中的每一台主机都需要一个唯一的 IP 地址以标识自己的身份,网络中传输的数据包通过 IP 地址找到对应的目标主机;一台主机通常只有一个 IP 地址,但主机上运行的网络进程却通常不止一个,譬如 Windows 电脑上运行着 QQ、微信、钉钉、网页浏览器等,这些进程都需要进行网络连接,它们都可通过网络发送/接收数据,那么这里就有一个问题?主机接收到网络数据之后,如何确定该数据是哪个进程对应的接收数据呢?其实就是通常端口号来确定的。
端口号本质上就是一个数字编号,用来在一台主机中唯一标识一个能上网(能够进行网络通信)的进程,端口号的取值范围为 0~65535。一台主机通常只有一个 IP 地址,但是可能有多个端口号,每个端口号表示一个能上网的进程。一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、SMTP 服务等,这些服务都是能够进行网络通信的进程,IP 地址只能区分网络中不同的主机,并不能区分主机中的这些进程,显然不能只靠 IP 地址,因此才有了端口号。通过“IP 地址+端口号”来区分主机不同的进程。
很多常见的服务器它都有特定的端口号,具体详情如下表所示:
服务 | 端口号 | 说明 |
---|---|---|
HTTP服务 | 80 | 超文本传输协议 |
FTP 服务 | 21 | 文件传输协议,使得主机间可以共享文件 |
SMTP 服务 | 25 | 简单邮件传输协议,它帮助每台计算机在发送或中转信件时找到下一个目的地。 |
TFTP 服务 | 69 | 简单文件传输协议,主机之间进行简单文件传输 |
SSH 服务 | 22 | 安全外壳协议,专为远程登录会话和其他网络服务提供安全性的协议 |
Telnet 服务 | 23 | 终端远程登录协议,它为用户提供了在本地计算机上完成远程主机工作的能力。 |
POP3 服务 | 110 | 邮局协议版本 3,本协议主要用于支持使用客户端远程管理在服务器上的电子邮件 |
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过,使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC 通常使用客户端<—>服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了 socket 接口,对于应用程序开发人员来说,我们只需要调用 socket 接口开发自己的应用程序即可!socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中,socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议。所以,我们无需深入的去理解 tcp/udp 等各种复杂的 TCP/IP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然遵循 tcp/udp 标准的。
当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准(BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到任何实现 BSD socket 标准的平台,譬如 LwIP,它兼容 BSD Socket;又譬如 Windows,它也实现了一套基于socket 的套接字接口,更甚至在国产操作系统中,如 RT-Thread,它也实现了 BSD socket 标准的 socket 接口。
使用 socket 接口需要在我们的应用程序代码中包含两个头文件:
#include /* See NOTES */
#include
int socket(int domain, int type, int protocol);
协议族名字 | 说明 | 帮助信息 |
---|---|---|
AF_UNIX, AF_LOCAL | Local communication | unix(7) |
AF_INET | IPv4 Internet protocols | ip(7) |
AF_INET6 | IPv6 Internet protocols | ipv6(7) |
AF_IPX | IPX - Novell protocols | |
AF_NETLINK | Kernel user interface device | netlink(7) |
AF_X25 | ITU-T X.25/ISO-8208 protocol | x25(7) |
AF_AX25 | Amateur radio AX.25 protocol | |
AF_ATMPVC | Access to raw ATM PVCs | |
AF_APPLETALK | AppleTalk | ddp(7) |
AF_PACKET | Low level packet interface | packet(7) |
AF_ALG | Interface to kernel crypto API |
type | 说明 |
---|---|
SOCK_STREAM | 提供有序的、可靠的、双向的、基于连接的字节流,能保证数据正确传送到对方,用于 TCP 协议;可以支持带外数据传输机制。 |
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传递,用于 UDP 协议 |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_RAW | 表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。 |
SOCK_RDM | 提供不保证排序的可靠数据报层。 |
SOCK_PACKET | 已过时,不应在应用程序中使用 |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。参数addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣,可以把 arrd 和 addrlen 均置为空指针 NULL。
accept()函数通常只用于服务器应用程序中,为了能够正常让客户端能正常连接到服务器,服务器必须遵循以下处理流程:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr对象的字节大小。
客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器IP 地址与端口号,而不发送任何数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
标志 | 描述 |
---|---|
MSG_CMSG_CLOEXEC | 为 UNIX 域套接字上接收的文件描述符设置执行时关闭标志 |
MSG_DONTWAIT | 启动非阻塞操作(相当于 O_NONBLOCK) |
MSG_ERRQUEUE | 接收错误信息作为辅助数据 |
MSG_OOB | 如果协议支持,获取带外数据 |
MSG_PEEK | 返回数据包内容而不真正取走数据包 |
MSG_TRUNC | 即使数据包被截断,也返回数据包的长度 |
MSG_WAITALL | 等待知道所有的数据可用(仅 SOCK_STREAM) |
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
标志 | 描述 |
---|---|
MSG_CONFIRM | 提供链路层反馈以保持地址映射有效 |
MSG_DONTROUTE | 勿将数据包路由出本地网络 |
MSG_DONTWAIT | 允许非阻塞操作(等价于使用 O_NONBLOCK) |
MSG_EOR | 如果协议支持,标志记录结束 |
MSG_MORE | 延迟发送数据包允许写更多数据 |
MSG_NOSIGNAL | 在写无连接的套接字时不产生 SIGPIPE 信号 |
MSG_OOB | 如果协议支持,发送带外数据 |
对于人来说,我们更容易阅读的是点分十进制的 IP 地址,譬如 192.168.1.110、192.168.1.50,这其实是一种字符串的形式,但是计算机所需要理解的是二进制形式的 IP 地址,所以我们就需要在点分十进制字符串和二进制地址之间进行转换。
int inet_pton(int af, const char *src, void *dst)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_pton()函数将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址,inet_ntop()函数执行与 inet_pton()相反的操作
将字符串 src 转换为二进制地址,参数 af 必须是 AF_INET 或 AF_INET6,AF_INET 表示待转换的 Ipv4地址,AF_INET6 表示待转换的是 Ipv6 地址;并将转换后得到的地址存放在参数 dst 所指向的对象中,如果参数 af 被指定为 AF_INET,则参数 dst 所指对象应该是一个 struct in_addr 结构体的对象;如果参数 af 被指定为 AF_INET6,则参数 dst 所指对象应该是一个 struct in6_addr 结构体的对象。
inet_pton()转换成功返回 1(已成功转换)。如果 src 不包含表示指定地址族中有效网络地址的字符串,则返回 0。如果 af 不包含有效的地址族,则返回-1 并将 errno 设置为 EAFNOSUPPORT。
参数 af 与 inet_pton()函数的 af 参数意义相同。
参数 src 应指向一个 struct in_addr 结构体对象或 struct in6_addr 结构体对象,依据参数 af 而定。函数inet_ntop()会将参数 src 指向的二进制 IP 地址转换为点分十进制形式的字符串,并将字符串存放在参数 dts
所指的缓冲区中,参数 size 指定了该缓冲区的大小。
inet_ntop()在成功时会返回 dst 指针。如果 size 的值太小了,那么将会返回 NULL 并将 errno 设置为
ENOSPC。
①调用 socket()函数打开套接字,得到套接字描述符;
②调用 bind()函数将套接字与 IP 地址、端口号进行绑定;
③调用 listen()函数让服务器进程进入监听状态;
④调用 accept()函数获取客户端的连接请求并建立连接;
⑤调用 read/recv、write/send 与客户端进行通信;
⑥调用 close()关闭套接字。
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于5000
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的IP地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for ( ; ; ) {
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if(0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.1.150" //服务器的IP地址
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用connect连接远端服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); //端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if(0 > ret){
perror("send error");
break;
}
//输入了"exit",退出循环
if(0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}
can工作原理可以参照单片机
CAN 总线套接字的创建采用标准的网络套接字操作来完成,网络套接字在头文件
int sockfd = -1;
/* 创建套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
socket 函数在之前介绍过,第一个参数用于指定通信域,在 SocketCan 中,通常将其设置为PF_CAN,指定为CAN通信协议;第二个参数用于指定套接字的类型,通常将其设置为SOCK_RAW;第三个参数通常设置为 CAN_RAW。
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 和 struct sockaddr_can,其中 struct ifreq 定义在
如果没有设置过滤规则,应用程序默认会接收所有 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));
struct can_filter 结构体中只有两个成员,can_id 和 can_mask。
如果应用程序不接收所有报文,在这种仅仅发送数据的应用中,可以在内核中省略接收队列,以此减少CPU 资源的消耗。此时可将 setsockopt()函数的第 4 个参数设置为 NULL,将第 5 个参数设置为 0
每一次通信都采用 struct can_frame 结构体将数据封装成帧
struct can_frame {
canid_t can_id; /* CAN 标识符 */
__u8 can_dlc; /* 数据长度(最长为 8 个字节) */
__u8 __pad; /* padding */
__u8 __res0; /* reserved / padding */
__u8 __res1; /* reserved / padding */
__u8 data[8]; /* 数据 */
};
/* special address description flags for the CAN_ID */
#define CAN_EFF_FLAG 0x80000000U /* 扩展帧的标识 */
#define CAN_RTR_FLAG 0x40000000U /* 远程帧的标识 */
#define CAN_ERR_FLAG 0x20000000U /* 错误帧的标识,用于错误检查 */
/* mask */
#define CAN_SFF_MASK 0x000007FFU /* 获取标准帧 ID */
#define CAN_EFF_MASK 0x1FFFFFFFU /* 获取标准帧 ID */
#define CAN_ERR_MASK 0x1FFFFFFFU /* omit EFF, RTR, ERR flags */
/* error class (mask) in can_id */
#define CAN_ERR_TX_TIMEOUT 0x00000001U /* TX timeout (by netdevice driver) */
#define CAN_ERR_LOSTARB 0x00000002U /* lost arbitration / data[0] */
#define CAN_ERR_CRTL 0x00000004U /* controller problems / data[1] */
#define CAN_ERR_PROT 0x00000008U /* protocol violations / data[2..3] */
#define CAN_ERR_TRX 0x00000010U /* transceiver status / data[4] */
#define CAN_ERR_ACK 0x00000020U /* received no ACK on transmission */
#define CAN_ERR_BUSOFF 0x00000040U /* bus off */
#define CAN_ERR_BUSERROR 0x00000080U /* bus error (may flood!) */
#define CAN_ERR_RESTARTED 0x00000100U /* controller restarted */
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");
如果要发送远程帧(帧 ID 为 123),可采用如下方法进行发送:
struct can_frame frame;
frame.can_id = CAN_RTR_FLAG | 123;
write(sockfd, &frame, sizeof(frame));
struct can_frame frame;
int ret = read(sockfd, &frame, sizeof(frame));
在默认情况下,CAN 的本地回环功能是开启的,在本地回环功能开启的情况下,所有的发送帧都会被回环到与 CAN 总线接口对应的套接字上,可以使用下面的方法关闭或开启本地回环功能:
int loopback = 0; //0 表示关闭,1 表示开启(默认)
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
在 Linux 系统中,CAN 总线设备作为网络设备被系统进行统一管理。在控制台下,CAN 总线的配置和以太网的配置使用相同的命令。使用 ifconfig 命令查看 CAN 设备
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
struct can_frame frame = {0};
int sockfd = -1;
int ret;
/* 打开套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 指定can0设备 */
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);
}
/* 设置过滤规则:不接受任何报文、仅发送数据 */
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
/* 发送数据 */
frame.data[0] = 0xA0;
frame.data[1] = 0xB0;
frame.data[2] = 0xC0;
frame.data[3] = 0xD0;
frame.data[4] = 0xE0;
frame.data[5] = 0xF0;
frame.can_dlc = 6; //一次发送6个字节数据
frame.can_id = 0x123;//帧ID为0x123,标准帧
for ( ; ; ) {
ret = write(sockfd, &frame, sizeof(frame)); //发送数据
if(sizeof(frame) != ret) { //如果ret不等于帧长度,就说明发送失败
perror("write error");
goto out;
}
sleep(1); //一秒钟发送一次
}
out:
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(void)
{
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
struct can_frame frame = {0};
int sockfd = -1;
int i;
int ret;
/* 打开套接字 */
sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if(0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 指定can0设备 */
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);
}
/* 设置过滤规则 */
//setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0);
/* 接收数据 */
for ( ; ; ) {
if (0 > read(sockfd, &frame, sizeof(struct can_frame))) {
perror("read error");
break;
}
/* 校验是否接收到错误帧 */
if (frame.can_id & CAN_ERR_FLAG) {
printf("Error frame!\n");
break;
}
/* 校验帧格式 */
if (frame.can_id & CAN_EFF_FLAG) //扩展帧
printf("扩展帧 <0x%08x> ", frame.can_id & CAN_EFF_MASK);
else //标准帧
printf("标准帧 <0x%03x> ", frame.can_id & CAN_SFF_MASK);
/* 校验帧类型:数据帧还是远程帧 */
if (frame.can_id & CAN_RTR_FLAG) {
printf("remote request\n");
continue;
}
/* 打印数据长度 */
printf("[%d] ", frame.can_dlc);
/* 打印数据 */
for (i = 0; i < frame.can_dlc; i++)
printf("%02x ", frame.data[i]);
printf("\n");
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}