首先这个demo分为四个部分

  1. camera图像采集帧保存在缓冲区
  2. 利用socket发送给指定的server
  3. server端监测client端发送过来的数据,接收完后保存在本地
  4. 使用工具将接收到的灰度图像直接打开显示在PC上

本demo主要采用V4L2接口进行图像采集

V4L2接口定义:(Video For Linux Two) 是内核提供给应用程序访问音、视频驱动的统一接口。

整个采集图片的流程如下:

  1. 打开设备
  2. 检查和配置设备属性
  3. 设置帧格式和帧缓冲区数目
  4. 申请缓冲区内存,并将内核空间映射到用户空间
  5. 将帧放入队列
  6. 开启流
  7. 从队列中取出帧
  8. 将缓冲区中的图像发送给server端
  9. 将取出的帧放回队列以方便下次继续使用
  10. 等待server端接收完成
  11. 关闭流

ioctl(int fd, int cmd, ...);


#include <sys/ioctl.h> /* 使用该函数必须包含的头文件 */

/**
  * @fun:  ioctl
  * @param:
  *        fd  :用户程序打开的设备时使用open函数返回的文件描述符
  *        cmd :是用户程序对设备的控制命令,交互协议,设备驱动将根据 cmd 执行对应操作
  *        ... :可变参数 arg,依赖 cmd 指定长度以及类型
  * @return: 成功返回     0
  *          失败返回    -1
  **/
ioctl(int fd, int cmd, ...);  

下面再看一下ioctl()函数正确使用方式:


int ret;
ret = ioctl(fd, cmd);
if (ret == -1) {
    printf("ioctl: %s\n", strerror(errno));
}

使用strerror()函数根据系统给定的error值自动生成错误信息

在实际应用中,ioctl 最常见的 errorno 值为 ENOTTY(error not a typewriter),顾名思义,即第一个参数 fd 指向的不是一个字符设备,不支持 ioctl 操作,这时候应该检查前面的 open函数是否出错或者设备路径是否正确

ioctl()函数的第二个参数cmd是一个32位的int类型数据,其该32位数据被划分为四个位段,每个位段代表不同的意义。分别是:
dir--------size--------type--------nr
2bit------14bit-------8bit---------8bit

在内核中,提供了宏接口以生成上述格式的ioctl命令:

// include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \
    (((dir)  << _IOC_DIRSHIFT) | \
     ((type) << _IOC_TYPESHIFT) | \
     ((nr)   << _IOC_NRSHIFT) | \
     ((size) << _IOC_SIZESHIFT))
  • dir(direction),ioctl 命令访问模式(数据传输方向),占据 2 bit,可以为_IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;

  • type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char型字符,例如 ‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;

  • nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;

  • size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;

ioctl函数时设备驱动程序中对设备的I/O通道进行管理的函数。所谓I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率,马达的转速等等。

要记住,用户程序所做的只是通过命令码(cmd)告诉驱动程序它想做什么,至于怎么解释这些命令和怎么实现这些命令,这都是驱动程序要做的事情。

因为这个调用拥有与网络相关的代码,所以文件描述符号fd就是socket()系统调用所返回的,而command参数可以是/usr/include/linux/sockios.h 头文件中的任何一个。

同样需要记住printk()的用法是打印调试信息,这个函数和printf()很相像,但是它不能处理浮点数据。printf()函数在内核中是不能被使用的。由printk()产生的输出被转储到了一个录./usr/adm/messages。

常见的命令标识符:

VIDIOC_REQBUFS:  分配内存
VIDIOC_QUERYBUF: 把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP: 查询驱动功能
VIDIOC_ENUM_FMT: 获取当前驱动支持的视频格式
VIDIOC_S_FMT: 设置当前驱动的视频捕获格式
VIDIOC_G_FMT: 读取当前驱动的视频捕获格式
VIDIOC_TRY_FMT: 验证当前驱动的显示格式
VIDIOC_CROPCAP: 查询驱动的修剪能力
VIDIOC_S_CROP: 设置视频信号的边框
VIDIOC_G_CROP: 读取视频信号的边框
VIDIOC_QBUF: 把数据放回缓存队列
VIDIOC_DQBUF: 把数据从缓存中读取出来
VIDIOC_STREAMON: 开始视频显示函数
VIDIOC_STREAMOFF: 结束视频显示函数
VIDIOC_QUERYSTD: 检查当前视频设备支持的标准,例如PAL或NTSC。

其次介绍一下V4L2里常使用的几个结构体,依次执行配置以下五个结构体后就可以开始采集图像数据

  • struct v4l2_capability
  • struct v4l2_fmtdesc
  • struct v4l2_format
  • struct v4l2_requestbuffers
  • struct v4l2_buffer

1、struct v4l2_capability
该结构体和一个宏定义VIDIOC_QUERYCAP搭配起来使用,用来获取设备支持的操作模式

例:ioctl(fd,VIDIOC_QUERYCAP,&cap)


struct v4l2_capability {
    __u8    driver[16];     /* 驱动名字 */
    __u8    card[32];       /* 设备名字 */
    __u8    bus_info[32];   /* 设备在系统中的位置 */
    __u32   version;        /* 驱动版本号 */
    __u32    capabilities;  /* 设备支持的操作 */
    __u32    reserved[4];   /* 保留字段 */
};

struct v4l2_capability  cap;/* 首先定义一个结构体 */

capabilities成员变量代表设备支持的操作模式,常见的值有V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING表示是一个视频捕捉设备并且具有数据流控制模式;

下面是结构体v4l2_capability 的整个读取和结果输出代码:


if((fd = open(FILE_VIDEO,O_RDWR)) == -1) { /* 打开设备,open()函数返回的是设备id */                                      
        printf("Error opening V4L interface\n");
        return (FALSE);
    }
    if(ioctl(fd,VIDIOC_QUERYCAP,&cap) == -1) { /* 读取video_capability中的信息,判断设备是否为视频采集设备以及是否支持流I/O操作 */                                     
        printf("Error opening device %s: unable to query device.\n",FILE_VIDEO);
        return (FALSE);
    } else {
	printf("driver:\t\t%s\n",cap.driver);
	printf("card:\t\t%s\n",cap.card);
	printf("bus_info:\t%s\n",cap.bus_info);
	printf("version:\t%d\n",cap.version);
	printf("capabilities:\t%x\n",cap.capabilities);
        
	if((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) == V4L2_CAP_VIDEO_CAPTURE) {/* 判断该设备是否支持视频采集 */
		printf("Device %s: supports capture.\n",FILE_VIDEO);
	}
	if((cap.capabilities & V4L2_CAP_STREAMING) == V4L2_CAP_STREAMING) {/* 判断是否支持流I/O操作 */
		printf("Device %s: supports streaming.\n",FILE_VIDEO);
	}
}

此段代码的输出可能是:
driver: ***
card: ***
bus_info: ***
version: ***
capabilities: ***
Device /dev/video0: supports capture.
Device /dev/video0: supports streaming.

2、struct v4l2_fmtdesc
该结构体和一个宏定义VIDIOC_ENUM_FMT搭配起来使用,用来查询并显示摄像头所有支持的像素格式(RGB、YUY2、YUYV、YVYU、UYVY、AYUV、GREY)

例:ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)


struct v4l2_fmtdesc
{
	u32 index; 			        /* 要查询的格式序号,应用程序设置 */
	enum v4l2_buf_type type; 	/* 帧类型,应用程序设置 */
	u32 flags; 			        /* 是否为压缩格式 */
	u8 description[32]; 		/* 格式名称 */
	u32 pixelformat;		    /* 格式 */
	u32 reserved[4]; 		    /* 保留 */
};

struct v4l2_fmtdesc fmtdesc;    /* 首先定义一个结构体 */

下面看一下如何编写关于此结构体的代码:


	/* 列举摄像头所支持的像素格式 */
    fmtdesc.index = 0;
    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    printf("Support format:\n");
    while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) != -1) {
        printf("\t%d.%s\n",fmtdesc.index+1,fmtdesc.description);
        fmtdesc.index++;
    }
    

这段代码的输出可能是:
Support format:
1.RGB
2.GREY

3、struct v4l2_format
该结构体和一个宏定义VIDIOC_S_FMT搭配起来使用,用来设置当前驱动的视频捕获格式,设置帧的格式,比如宽度、高度等

例:ioctl(fd,VIDIOC_S_FMT,&fmt)


struct v4l2_format {
    enum v4l2_buf_type type;
    union {
        struct v4l2_pix_format         pix;     /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
        struct v4l2_window             win;     /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
        struct v4l2_vbi_format         vbi;     /* V4L2_BUF_TYPE_VBI_CAPTURE */
        struct v4l2_sliced_vbi_format  sliced;  /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
        __u8   raw_data[200];                   /* user-defined */
    } fmt;
};

enum v4l2_buf_type {
    V4L2_BUF_TYPE_VIDEO_CAPTURE        = 1,     /*此类型为 视频捕捉模式 */
    V4L2_BUF_TYPE_VIDEO_OUTPUT         = 2,
    V4L2_BUF_TYPE_VIDEO_OVERLAY        = 3,
    ...
    V4L2_BUF_TYPE_PRIVATE              = 0x80,
};

struct v4l2_pix_format {
    __u32                   width;            /*宽,必须是16的倍数*/
    __u32                   height;           /*高,必须是16的倍数*/
    __u32                   pixelformat;      /*视频数据存储类型,例如是YUV4:2:2还是RGB*/
    enum v4l2_field         field;
    __u32                   bytesperline;     /* for padding, zero if unused */
    __u32                   sizeimage;
    enum v4l2_colorspace    colorspace;
    __u32                   priv;             /* private data, depends on pixelformat */
};

struct v4l2_format fmt;						  /* 首先定义一个结构体 */

下面继续展示一下代码如何编写:

如下配置的视频数据格式是灰度图像格式GREY格式


    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;              /* 视频捕捉模式 */
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_GREY;         /* 视频数据格式 */
    fmt.fmt.pix.height = IMAGEHEIGHT;                    /* 视频的高 */
    fmt.fmt.pix.width = IMAGEWIDTH;                      /* 视频的宽 */
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    if(ioctl(fd,VIDIOC_S_FMT,&fmt) == -1) {
        printf("Unable to set format\n");
        return FALSE;
    }
    if(ioctl(fd,VIDIOC_G_FMT,&fmt) == -1) {              /* 获取设置支持的视频格式 */
        printf("Unable to get format\n");
        return FALSE;
    }
    printf("fmt.type:\t\t%d\n",fmt.type);
    printf("pix.pixelformat:\t%c%c%c%c\n",fmt.fmt.pix.pixelformat & 0xFF,(fmt.fmt.pix.pixelformat >> 8) & 0xFF,(fmt.fmt.pix.pixelformat >> 16) & 0xFF,(fmt.fmt.pix.pixelformat >> 24) & 0xFF);
    printf("pix.height:\t\t%d\n",fmt.fmt.pix.height);
    printf("pix.width:\t\t%d\n",fmt.fmt.pix.width);
    printf("pix.field:\t\t%d\n",fmt.fmt.pix.field);

此段代码的输出就是输出刚配置完的参数,然后观察设置的一些格式是否设置成功,是否设置生效。
输出大概是这样:
(此处我的视频输出格式为灰度格式GREY)
fmt.type: 1
pix.pixelformat: GREY
pix.height: 480
pix.width: 640
pix.field: ***

4、struct v4l2_requestbuffers
该结构体和一个宏定义VIDIOC_REQBUFS搭配起来使用,用来申请一块连续的内存用来缓存视频信息,相当于作为缓冲区的作用

例:ioctl(fd,VIDIOC_REQBUFS,&req)


struct v4l2_requestbuffers
{
	__u32 count; 			    /* 缓存数量,也就是说在缓存队列里保持多少张照片 */
	enum v4l2_buf_type type; 	/* 数据流类型,一般设置为视频捕捉模式 */
	enum v4l2_memory memory; 	/* V4L2_MEMORY_MMAP 或 V4L2_MEMORY_USERPTR */
	__u32 reserved[2];
};
enum v4l2_buf_type {
    V4L2_BUF_TYPE_VIDEO_CAPTURE        = 1,     /*此类型为 视频捕捉模式 */
    V4L2_BUF_TYPE_VIDEO_OUTPUT         = 2,
    V4L2_BUF_TYPE_VIDEO_OVERLAY        = 3,
    ...
    V4L2_BUF_TYPE_PRIVATE              = 0x80,
};
enum v4l2_memory {  
    V4L2_MEMORY_MMAP             = 1,  
    V4L2_MEMORY_USERPTR          = 2,  
    V4L2_MEMORY_OVERLAY          = 3,  
 }; 

struct v4l2_requestbuffers req;    			   /* 首先定义一个结构体 */

下面看一下代码:
首先创建一个结构体类型,供下面申请缓冲区使用


	/* 缓冲区结构体 */
	typedef struct buffer
	{
	    void * start;        /* 缓冲区起始地址 */
	    unsigned int length; /* 缓冲区空间长度 */
	
	}buffers;
	
	buffers * Buffers = NULL;
	

v4l2_requestbuffers定义的结构体配置初始参数,然后调用 ioctl() 函数向设备写入命令,然后申请count个帧单位大小的动态内存


	/* request for 4 buffers */
    req.count = 4;                       		   /* 缓冲区内缓冲帧的数目 */
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;        /* 设置为视频捕捉模式 */
    req.memory = V4L2_MEMORY_MMAP;                 /* 区别是内存映射还是用户指针方式 */
    
    if(ioctl(fd,VIDIOC_REQBUFS,&req) == -1) {
        printf("request for buffers error\n");
    }

    /* mmap for buffers */
    Buffers = (buffers*)malloc(req.count*sizeof(buffers));
    
    if(!Buffers) {
        printf("Out of memory\n");
        return (FALSE);
    } else {
	    printf("malloc success!\n");
    }
    

如果申请空间成功的话,输出结果为:malloc success!

5、struct v4l2_buffer
该结构体和一个宏定义VIDIOC_QUERYBUF搭配起来使用,用来查询驱动申请的内存信息

例:ioctl(fd,VIDIOC_QUERYBUF,&buf)


struct v4l2_buffer {
    __u32                   index;      /* buffer序号 */
    enum v4l2_buf_type      type;		/* buffer类型 */
    __u32                   bytesused;	/* buffer中已使用的字节数 */
    __u32                   flags;		/* 区分是 MMAP 还是 USERPTR */
    enum v4l2_field         field;		/*  */
    struct timeval          timestamp;	/* 获取第一个字节时的系统时间 */
    struct v4l2_timecode    timecode;	/*  */
    __u32                   sequence;	/* 队列中的序号 */

    /* memory location */
    enum v4l2_memory        memory;		/* IO方式,被应用程序设置 */
    union {
            __u32           offset;		/* 缓冲帧地址,只对MMAP有效 */
            unsigned long   userptr;	/*  */
    } m;
    __u32                   length;		/* 缓冲帧长度 */
    __u32                   input;		/*  */
    __u32                   reserved;	/*  */
};

struct v4l2_buffer buf;                 /* 首先定义一个结构体 */

下面看一下代码:

  1. 主要做的工作就是将count个缓冲区使用mmap函数将内核空间映射到用户空间
  2. 然后将缓冲区依次加入到队列当中进行入队操作
  3. 然后开启流进行数据采集后存进缓冲区
  4. 采集完后再将队列中的帧取出

for(n_buffers = 0;n_buffers < req.count; n_buffers++) {
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;         /* 缓冲区类型 */
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = n_buffers;                          /* 缓冲区的序号 */
    
        /* query buffers */
        if(ioctl(fd,VIDIOC_QUERYBUF,&buf) == -1) {      /* 获取缓冲帧的地址,长度 */
            printf("query buffer error\n");
            return (FALSE);
        }

        Buffers[n_buffers].length = buf.length;
        
	    /* map */
        Buffers[n_buffers].start = mmap(NULL,buf.length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,buf.m.offset);/* 将内核空间映射当用户空间 */

        if(Buffers[n_buffers].start == MAP_FAILED) {
            printf("buffer map error\n");
            return (FALSE);
        }
    }
    /* queue */
    for(n_buffers = 0;n_buffers< req.count; n_buffers++) {
        buf.index = n_buffers;
        if(ioctl(fd,VIDIOC_QBUF,&buf) == -1) {            /* 把count个帧放入队列 */
	        printf("QBUF failed\n");
        }
    }

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if(ioctl(fd,VIDIOC_STREAMON,&type) == -1) {           /* 开启流 */
        printf("stream on failed!\n");
    }

    if(ioctl(fd,VIDIOC_DQBUF,&buf) == -1) {               /* 从队列中取出帧 */
	    printf("DQBUF failed\n");
    }

    printf("grab grey OK\n");
    

数据采集到缓冲区后接下来就要使用套接字实现一个TCP的客户端,用TCP将缓冲区中采集到的数据发送出去,等服务端接收。

接下来开始介绍使用socket实现一个简单的TCP客户端:

一、TCP Client:

1、所谓socket通常也称作套接字,用于描述IP地址和端口,是一个通信链的句柄。
2、应用程序中通常使用套接字实现网络发出请求和网络请求的应答。
3、socket是连接运行在网络上的两个程序间的双向通信的端点。
4、网络通讯实际上指的就是socket间的通讯
5、网络的两端都有socket,数据在两个socket之间通过IO来进行传输

1、int socket(int domain, int type, int protocol);


#include<sys/types.h>   /* 必须包含的头文件 */
#include<sys/socket.h>

/**
  * @fun:  socket
  * @param:
  *        domain    :	即协议域,又称为协议族(family)。
  * 					常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等.
  * 					协议族决定了socket的地址类型,在通信中必须采用对应的地址,
  * 					如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  * 
  *        type      :	指定socket类型。
  * 					常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
  * 
  *        protocol  :	故名思意,就是指定协议。
  * 					常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,
  * 					它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
  *
  * @return: 
  * 		成功  返回 socket文件描述符
  * 		失败  返回 -1 并设置 errno
  * 
  * 		当我们调用socket函数创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。
  * 		如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
  * 
  */
int socket(int domain, int type, int protocol);

例:int sock = socket(AF_INET, SOCK_STREAM, 0);

socket()函数创建一个socket描述符(sd),它唯一标识一个socket。这个socket描述字跟文件描述符字一样,把它作为参数可以进行一些读写,和普通的文件write、read、open、close同样操作。

2、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该函数主要针对客户端调用connect()函数发出连接请求,服务端就会接收到这个请求
connect()函数通常用于客户端建立TCP连接。


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  connect
  * @param:
  *        sockfd    :	套接字描述符,还参数直接传入socket()函数返回的socket文件描述符
  * 					
  *        addr		 :	指向数据结构sockaddr的指针,其中包括目的端口和IP地址
  * 
  *        addrlen	 :	参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
  * @return: 
  * 		成功  返回  0
  * 		失败  返回 -1 并设置 errno
  */
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

调用connect函数将激发TCP的三次握手过程,而且仅在连接建立成功或出错时才返回

按照TCP状态转换图,connect函数导致当前套接字从CLOSED状态转移到SYN_SENT状态,若成功再转移到ESTABLISHED状态。若connect失败,则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。

下面介绍一下上面提到的两个结构体,struct sockaddr和struct sockaddr_in

这两个结构体用来处理网络通信的端口和地址。

	struct sockaddr结构体:
	
		····(1)	sockaddr在头文件#include <sys/socket.h>中定义
		····(2)	sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
		
					struct sockaddr {  
						sa_family_t sin_family;//地址族
						char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
			   	}; 
			   	
	struct sockaddr_in结构体:
	
		····(1)	sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,
					该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
		····(2)	sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。
		
					struct sockaddr_in {  
						sa_family_t 		sin_family;		//地址族(addres family)
						uint16_t    		sin_port;		//16位TCP/UDP端口号
						struct in_addr 		sin_addr;		//32位IP地址
						char				sin_zero[8];    //不使用
					};
					
					该结构体中提到的另一个结构体in_addr定义如下,它来存放32位的IP地址
					struct in_addr
					{
						In_addr_t			s_addr;			//32位IPv4地址
					}
					
			········二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

3、int send(int sockfd, const void *msg, int len, int flags);

send() 函数是一个系统调用函数,用来发送消息到一个套接字中,和sendto,sendmsg函数功能相似。

说明:send()函数只能在套接字处于连接成功的状态下使用,只有这样才知道接收者是谁。

send()函数和write()函数唯一的区别就是最后一个参数,flags的存在,当我们把flags设置成0时,write()函数和send()函数等同。

当消息不适合套接字的发送缓冲区时,send通常会阻塞,除非套接字在事先设置为非阻塞的模式,那样他不会阻塞,而是返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  send
  * @param:
  *        sockfd    :	套接字描述符,还参数直接传入socket()函数返回的socket文件描述符
  * 					
  *        msg       :	要发送的消息地址
  * 
  * 	   len       :  要发送的字节数
  * 
  *        flags     :	可以设置为0
  * @return: 
  * 		成功  返回  发送成功的字节数
  * 		失败  返回 -1 并设置 errno
  */
int send(int sockfd, const void *msg, int len, int flags);

TCP Client Code:

实现步骤:
  1. 做准备工作。创建用于存放IP和port的结构体,然后使用socket函数创建一个socket。
  2. 为存放地址和端口的结构体赋初始值,并调用connect()函数发送连接请求
  3. 调用send()函数向传入的socket发送帧缓冲区中存放的数据,每次只发送DATA_LEN长度的数据
  4. 发送完成后调用close()函数关闭刚创建的socket

#define PORT_      8000        /* 端口号一定要保证客户端和服务端统一 */
#define DATA_LEN   1
#define IP_ADDR    "127.0.0.1" /* 指定服务端的IP地址 */

int tcp_client(void) 
{    
    int sockfd = 0;
    int real_sdcnt = 0;                                  /* send()函数实际发送出去的字节数 */
    long int send_sum = 0;                               /* 已发送出去的字节数的总和 */

    struct sockaddr_in  socket_addr;
    memset((void*)&socket_addr,0,sizeof(socket_addr));
    
    sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    
    if(sockfd == -1) {
        perror("Create socket failed: ");
        return -1;
    }
    
    socket_addr.sin_family = AF_INET;                   /* IPv4,TCP通信 */
    socket_addr.sin_port = htons(PORT_);
    socket_addr.sin_addr.s_addr = inet_addr(IP_ADDR);   /* INADDR_ANY表示自动获取本机地址 */
    
    printf("creating a connect......\n");
    if(connect(sockfd,(struct sockaddr*)&socket_addr,sizeof(socket_addr)) < 0) {
        perror("connect: ");
        return -1;
    } else {
        printf("connect success!\n");
    }
 
    while(1) {
        if((real_sdcnt = send(sockfd,Buffers[0].start+send_sum,DATA_LEN,0)) == -1) {
            perror("send: ");
            return -1;
        }
        send_sum += real_sdcnt;
        if(send_sum >= 307200) {
            printf("send success!\n");
            break;
        }
    }
    close(sockfd);
    return 1;
}


此处等client发送完成后,
要将从队列中取出的帧缓冲仍然放回原来的队列中以便下次继续使用。


void Recycle_Push_Queue(void){                      //读取完数据后将该帧继续入队列,方便下次继续使用
    int ret = ioctl(fd, VIDIOC_QBUF,&buf);          //帧缓冲入列 ,返回值为0表示成功
    if(0 != ret){
        printf("VIDIOC_QBUF failed!\n");
        exit(-1);
    }
}

最后再做一下清理工作,包括关闭采集流,解除mmap函数的内存映射,关闭最开始被打开的/dev/video0设备文件。


int close_v4l2(void)
{
    int i=0;
 
    if(ioctl(fd,VIDIOC_STREAMOFF,&type) == -1) {   /* 关闭流 */
	    printf("Stream off failed\n");
    }

    for(i=0; i<req.count; ++i){                    /* 解除内存映射 */
        if(-1 == munmap(Buffers[i].start, Buffers[i].length)){
            printf("munmap error! \n");
            exit(-1);
        }
    }

    if(fd != -1) {
        close(fd);
        return (TRUE);
    }
    return (FALSE);
}

二、TCP Server:

当接收到client端发送过来的请求时,server端要accept请求。

下面分别介绍一下bind()函数、listen()函数、accept()函数和recv()函数。

1、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数功能描述: 在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  bind
  * @param:
  *        sockfd    :	套接字描述符,还参数直接传入socket()函数返回的socket文件描述符
  * 					
  *        addr		 :	my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
  * 					在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,
  * 					这样进行绑定后才能将套接字文件描述符与地址等接合在一起。
  * 
  *        addrlen	 :	参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
  * @return: 
  * 		成功  返回  0
  * 		失败  返回 -1 并设置 errno
  */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

该函数必须在socket()函数创建socket成功后再使用。

2、int listen(int sockfd, int backlog);

函数功能描述: socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  bind
  * @param:
  *        sockfd    :	需要监听的socket描述字
  * 					
  *        backlog   :	相应socket可以排队的最大连接个数
  * @return: 
  * 		成功  返回  0
  * 		失败  返回 -1 并设置 errno
  */
int listen(int sockfd, int backlog);

3、int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数功能描述: 接收来自客户端发送过来的请求,并返回一个新的套接字,后面使用write、read、send、recv函数均采用该套接字进行操作。该套接字称为已连接套接字。该套接字唯一标识了接收的新连接。后续双方可以利用已连接套接字进行通信。


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  accept
  * @param:
  *        sockfd    :	上述listen函数指定的监听socket
  * 					
  *        addr      :	请求连接方(即客户端)地址,该参数是一个结构体,会预先定义并赋值
  *  
  *        addrlen   :  客户端地址长度
  * 
  * @return: 
  * 		成功  返回  0
  * 		失败  返回 -1 并设置 errno
  */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

4、int recv(int sockfd,void *buf,int len,unsigned int flags);

函数功能描述: 接收来自客户端建立连接成功后发送过来的数据


#include <sys/types.h>     /* 必须包含的头文件 */
#include <sys/socket.h>

/**
  * @fun:  recv
  * @param:
  *        sockfd    :	上述 accept() 函数返回的 socket
  * 
  * 	   buf       :  指明一个接收缓冲区,该缓冲区用来存放客户端发送的数据
  * 
  *        len       :	指明 buf 缓冲区的内存大小
  *  
  *        flags     :  一般设置为 0 即可
  * 
  * @return: 
  * 		成功         返回  接收到字节数
  * 		另一种情况:  如果客户端已关闭则返回 0
  * 		失败         返回 -1 并设置 errno
  */
int recv(int sockfd,void *buf,int len,unsigned int flags);

TCP Server Code :

实现步骤:
  1. 创建一个存放地址和端口的结构体,使用memset()函数进行内存清理,创建一个接收存储数据的缓冲区并使用memset()函数将内存清理
  2. 打开我们要准备写入数据的文件。使用open()函数传入文件路径和文件名,这里注意,必须用"wb"方式进行打开,因为该数据必须以二进制的形式写入进文件。
  3. 创建一个服务端的socket。
  4. 为上面创建的结构体赋初值,包括端口、地址、协议簇。
  5. 使用bind()函数将传入的地址和服务端的socket进行绑定。
  6. 使用listen()函数监听刚刚绑定完的socket端口。
  7. 当收到客户端发送过来的请求时,使用accept()函数接收请求,并返回一个已连接套接字
  8. 使用recv()函数接收客户端发送过来的数据,并保存至申请的缓冲区中
  9. 使用fwrite函数将缓冲区中的数据写入到相应的文件中去
  10. 接收完客户端发送过来的所有数据后,close保存数据的文件

#define PORT_    8080       /* 端口号一定要保证客户端和服务端统一 */
#define PATH     "./image_grey"
#define LEN_MAX  307200
#define RECV_LEN 10240

int tcp_Server()
{
    int sockfd = 0;
    int rev_length = 0;
    FILE* fd = NULL;
    int write_cnt = 0;
    int recv_sum = 0;

    struct sockaddr_in  socket_addr;
    char *data_buf = (char*)malloc(sizeof(char)*10240);
    memset((void*)&socket_addr,0,sizeof(socket_addr));
    memset((void*)data_buf, 0, sizeof(char)*10240);

    fd = fopen(PATH,"wb");
    if (fd == NULL) {
        perror("fopen(): ");
        return -1;
    }

    sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    if (sockfd == -1) {
        perror("Create socket failed: ");
        fclose(fd);
        return -1;
    }

    socket_addr.sin_family = AF_INET;
    socket_addr.sin_port = htons(PORT_);
    socket_addr.sin_addr.s_addr = inet_addr(INADDR_ANY);

    if (bind(sockfd,&socket_addr,sizeof(socket_addr))) {
        perror("bind(): ");
        fclose(fd);
        close(sockfd);
        return -1;
    }

    if (listen(sockfd,1)) {
        perror("listen(): ");
        fclose(fd);
        close(sockfd);
        return -1;
    }
    while(1) {
        struct sockaddr_in  client_addr;

        int new_spckfd = accept(sockfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
        if (new_spckfd < 0) {
            perror("accept(): ");
            fclose(fd);
            close(sockfd);
            close(new_spckfd);
            return -1;
        }

        rev_length = recv(new_spckfd,data_buf,RECV_LEN,0);
        if (rev_length < 0) {
            perror("recv(): ");
            fclose(fd);
            close(sockfd);
            close(new_spckfd);
            return -1;
        }
        write_cnt = fwrite(data_buf,1,rev_length,fd);

        if (write_cnt < rev_length) {
            fwrite(data_buf+write_cnt,rev_length - write_cnt,fd);
        }
        recv_sum  += write_cnt;
        if(recv_sum >= LEN_MAX) {
            fclose(fd);
            close(sockfd);
            close(new_spckfd);
            return 1;
        }

        memset((void*)data_buf, 0, sizeof(char)*10240);
    }
    return 0;
}

终于将采集的图像数据保存在本地了,下面简单介绍一下图像数据将如何显示在PC上。

采集的数据是纯像素点的数据,而要想直接能通过windows常用的工具直接打开显示图片还是不可以。

一张.bmp格式的文件包含四个部分:

  • 位图文件头(14个字节)
  • 位图信息头(40个字节)
  • 颜色表()
  • 颜色点阵数据

现在采集的数据就只有颜色点阵数据,所以想用常用的图片工具它是不能解析出来的,必须让我们添加缺少的内容才能直接使用工具打开。

这里提供几篇关于讲解.bmp格式图片的blog:

https://blog.csdn.net/qingchuwudi/article/details/25785307
https://www.cnblogs.com/liuwt0911/articles/3730142.html

因为文件中存储的是灰度的8位像素点的图像,不分RGB颜色,只有白和黑之间的差别,所以要想能打开文件,就需要将灰度图像的每一个像素点的值都分别赋值给R、G、B各个一份。最后在文件最开始加上bmp的文件头和必要的参数然后将图片保存为.bmp后缀的图片文件,就可以使用windows的常用工具直接将图片显示在PC上。

最后展示我当时采集的图片效果:
在这里插入图片描述

Logo

更多推荐