字节跳动牛客网面准备

2020寒假实习

Posted by 敬方 on December 17, 2019

问答项目讲述

1. 讲一下你的项目和中间遇到的问题

之前做过一个无人驾驶远程控制的项目;主要有路径规划与控制;实时定位与地图重建,视频传输与目标检测。这几个部分;我主要做的是视频传输和目标检测;其中视频传输是之前的主要工作内容;大体上分为三个部分:集中显示和控制客户端,车载电脑的视频流传输服务端,数据存储中心。,客户端使用C++的是Qt进行GUI界面的编写,利用QtLocation加载离线的地图瓦片和实时显示自动驾驶车辆的位置;使用ffmpeg播放车载电脑的监控。另外因为远程监控的实时性要求特别高,因此自己写了一个基于mjpg流的,C++视频传输服务器;相对于ffmpeg的H264和H256视频rtsp流;传输时延从2s降低到了0.5s。

下面先说一下MJPG流媒体服务端把。服务器端主要分为三个部分,Linux使用Linux 的v4l2摄像头YUV数据图像读取,libjpeg和cuda图像格式的转换和压缩编码(这里可能会问jpeg编码);HTTP的图像数据传输。

使用一个全局变量存储图像缓冲,和服务器状态和访问互斥量以及信号量,使用互斥访问量和信号量来实现几个线程之间的同步(这里可能会问C++中线程的同步和互斥;几种内存序,锁的种类);

TCP链接,使用epoll进行socket套接字的监听;(这里会问socket)。使用创建使用为read和wirte进行socket的读写(为什么要使用read和write),为每一个建立链接的客户端建立一个响应线程,根据它的HTTP请求(这里会问HTTP的东西),建立线程池,使用loop用来定义工作的函数,并且给每一个Thread分配一个loop;发送对应的回应。如果请求的是一个stream流,就进入循环中。持续获取buffer中的数据,并封装成HTTP帧进行传输。对于错误的请求按照状态码进行回应。在请求buffer。如果写入失败就中断链接

中间在做这个的时候,为了保证实时性,遇到了很多问题,因为使用的MJPE压缩,因此发送的图片非常大,需要的带宽大致是1-2M/s,因此当网络状态较差时会出现卡帧的情况,或者帧延迟(这里可能会问网络拥塞)。减少buffer的缓冲队列的大小;同时更改前台的请求方式,由stream请求更改为循环的,单次图片请求,队列中一般值存储当前的2-3帧图像,取消写入的buffer,减少缓冲(为什么read需要缓冲)。从4G网络更换到校园局域网;使用cuda进行YUV到RGB的图像颜色通道的转换增加速度。

可能提问

2. 说一下C++中锁的种类

  • a. 互斥锁(Mutex):std::mutex some_mutex;只有一个线程可以获得锁;使用std::lock_guard<std::mutex>或者std::lock<std::mutex>

  • b. 条件锁:std::condition_variable;只和std::mutex一起工作;data_cond.notify_one();和data_cond.wait(lk,{[]return !data_queue.empty();});进行条件判断和继续

  • c. 自旋锁:C++中没有自旋锁;可以自己实现

  • d. 读写锁:

  • e. 递归锁std::recursive_mutex;线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。所谓递归锁,就是在同一线程上该锁是可重入的,对于不同线程则相当于普通的互斥锁。

    3. C++中的6中内存序:

  • a. std::memory_order_relaxed;没有顺序一致性的要求,也就是说同一个线程的原子操作还是按照happens-before关系,但不同线程间的执行关系是任意。
  • b. std::memory_order_consume:这个内存序是“获取-释放”的一部分,它依赖于数据,可以展示线程间的先行关系。a->b->c 顺序执行
  • c. std::memory_order_acquire:内存一致性;这个是以牺牲优化效率,来保证指令的顺序一致执行,相当于不打开编译器优化指令,按照正常的指令序执行(happens-before),多线程各原子操作也会Synchronized-with;读操作需要在“一个写操作对所有处理器可见”的时候才能读,适用于基于缓存的体系结构。
  • d. std::memory_order_release:最后简单说下栅栏吧,栅栏相当于给内存加了一层栅栏,约束内存乱序。典型用法是和 relaxed一起使用。保证栅栏的前后有序。
  • e. std::memory_order_acq_rel:获取-释放一致性;这个是对relaxed的加强,relax序由于无法限制多线程间的排序,所以引入synchronized-with,但并不一定意味着,统一的操作顺序
  • f. std::memory_order_seq_cst:
  1. 为甚没有使用future 因为future主要使用在异步中,这边的实时性要求非常高,使用future主要是和async实现异步。与其使用future来进行等待buffer的缓冲,不如直接使用条件锁,进行发送。

future主要的异步函数的配合:

stl中: thread async和feature 启动一个异步线程,返回一个结果 std::launch::async, 创建线程异步 std::launch::deferred: 不创建线程 feature.get(): 等待并取值 feature.wait(): 等待

promise: std::promise 类模板,我们能够在某个线程中给它赋值,然后我们可以在其他线程中把这个值取出来用; 通过promise保存一个值,在将来某时刻我们通过把一个future绑定到这个promise上来得到这个绑定的值。 promise.set_value(): 设置值 promise.get_future(): 将promise和feature相关联, 以供异步调用

packaged_task std::packaged_task是个模板类,它的模板参数是各种可调用对象; 通过std::packaged_task来把各种可调用对象包装起来,方便将来作为线程入口函数来调用。 packaged_task包装起来的可调用对象还可以直接调用,所以从这个角度来讲,packaged_task对象,也是一个可调用对 还可以通过packaged_task.get_feature和feature进行关联, 以实现异步调用

4. 为什么使用条件锁std::condition_variable

因为视频传输的帧率也就是是20~30帧之间,甚至15帧,相对帧间的间隔比较大,使用条件锁进行通知就可以了。

5. 为什么没有使用原子操作作为缓冲区的访问锁

因为读写缓冲区是一个系列的操作,需要的是一段临界代码,使用原子操作代码复杂性太高,并且对于多个线程进行访问时,数据的争用反而会降低效率。

6. jpeg的编码问题

使用libjpeg进行图像的编码: (1) 块准备:块准备将一帧图象分成数据块。

(2) DCT变换:通过函数进行映射,变成对应的系数块 (3)量化:使用线性量化,将相似的进行压缩;为了达到压缩的目的,DCT系数需作量化,量化表针对的设计。例如,例如,利用人眼的视觉特性,对在图象中占有较大能量的低频成分,赋予较小的量化间隔和较少的比特表示,以获得较高的压缩比。对于近似值进行合并 (4)DCT直流值和AC交流系数的编码:使用AC稀疏矩阵来记录矩阵中为0值的点。 (5)熵编码(哈夫曼编码):对于上面给出的码序列,再进行统计特性的熵编码。这仅对于序列中每个符号对中的第一个字节进行,第二个幅值字节不作编码,仍然直接传送。 (6) 图像质量,输入参数确定图像质量

– FDCT

– 使用加权函数对变换系数量化,加权函数根据人的视觉系统确定。

– 编码顺序Zigzag:使系数为0的值更集中。

– 使用DPCM对直流系数编码

– 使用RLE对交流系数进行编码

– Huffman 熵编码。

libjpeg对应流程:

  1. 创建 jpeg_decompress_struct(jpeg_create_decompress);
  2. jpeg_error_mgr(jpeg_std_error)结构体并进行初始化;
  3. jpeg_stdio_src绑定错误流->设置宽高,质量等压缩参数;
  4. jpeg_start_decompress(启动解压);
  5. 循环调用jpeg_read_scanlines复制压缩后的数据到缓冲区
  6. 变量销毁jpeg_finish_decompress,jpeg_destroy_decompress。

7. 说一下haffuman编码

哈夫曼编码,主要目的是根据使用频率来最大化节省字符(编码)的存储空间。使用哈夫曼树来对数据进行编码,哈夫曼树是一个二叉树,其中左代表0,右代表1。数据在其叶子节点,根节点到叶子节点的路径01序列,就是它的编码值。

8. YUV转与GRB的相互转换

Y = 0.299R + 0.587G + 0.114*B;

U = -0.169R - 0.331G + 0.5 *B ;

V = 0.5 R - 0.419G - 0.081*B;

R = Y + 1.4075 * (V-128);
G = Y - 0.3455 * (U-128) - 0.7169*(V-128);
B = Y + 1.779 * (U-128);

9. 介绍一下YUV

YUV是一种颜色编码方式:Y是灰阶值,U和V是影像色彩和饱和度。 YUV 4:4:4采样,每一个Y对应一组UV分量; YUV 4:2:2采样,每两个Y共用一组UV分量。 YUV 4:2:0采样,每四个Y共用一组UV分量。

此外还有YUV422P,YV12,YU12等数据格式

网络编程可能会问道的问题:

1. 说一下TCP的三次握手和四次挥手

TCP-三次握手-四次挥手-13状态

  • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN 位置为1,Sequence Number 为x;然后,客户端进入SYN_SEND 状态,等待服务器的确认;
  • 第二次握手:服务器收到SYN 报文段。服务器收到客户端的SYN 报文段,需要对这个SYN 报文段进行确认,设置Acknowledgment Number 为x+1(Sequence Number+1);同时,自己自己还要发送SYN 请求信息,将SYN 位置为1,Sequence Number 为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK 报文段)中,一并发送给客户端,此时服务器进入SYN_RECV 状态;
  • 第三次握手:客户端收到服务器的SYN+ACK 报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK 报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED 状态,完成TCP 三次握手。
  • 四次挥手
  • 当客户端和服务器通过三次握手建立了TCP 连接以后,当数据传送完毕,肯定是要断开TCP 连接的啊。那对于TCP 的断开连接,这里就有了神秘的“四次分手”。
  • 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2 发送一个FIN 报文段;此时,主机1 进入FIN_WAIT_1 状态;这表示主机1 没有数据要发送给主机2 了;
  • 第二次分手:主机2 收到了主机1 发送的FIN 报文段,向主机1 回一个ACK 报文段,Acknowledgment Number 为Sequence Number 加1;主机1 进入FIN_WAIT_2 状态;主机2 告诉主机1,我“同意”你的关闭请求;
  • 第三次分手:主机2 向主机1 发送FIN 报文段,请求关闭连接,同时主机2 进入LAST_ACK 状态;
  • 第四次分手:主机1 收到主机2 发送的FIN 报文段,向主机2 发送ACK 报文段,然后主机1 进入TIME_WAIT 状态;主机2 收到主机1 的ACK 报文段以后,就关闭连接;此时,主机1 等待2MSL 后依然没有收到回复,则证明Server 端已正常关闭,那好,主机1 也可以关闭连接了。
  • 六大标志位
    • SYN,同步标志位;
    • ACK 确认标志位;
    • PSH 传送标志位;
    • FIN 结束标志位;
    • RST 重置标志位;
    • URG 紧急标志位;
  • seq 序号;
  • ack 确认号

3. TCP 为啥挥手要比握手多一次?

  • 因为当处于LISTEN 状态的服务器端收到来自客户端的SYN 报文(客户端希望新建一个TCP 连接)时,它可以把ACK(确认应答)和SYN(同步序号)放在同一个报文里来发送给客户端。
  • 但在关闭TCP 连接时,当收到对方的FIN 报文时,对方仅仅表示对方已经没有数据发送给你了,但是你自己可能还有数据需要发送给对方,则等你发送完剩余的数据给对方之后,再发送FIN 报文给对方来表示你数据已经发送完毕,并请求关闭连接,所以通常情况下,这里的ACK 报文和FIN 报文都是分开发送的。

4. 为什么一定进行三次握手?

  • 当客户端向服务器端发送一个连接请求时,由于某种原因长时间驻留在网络节点中,无法达到服务器端,由于TCP 的超时重传机制,当客户端在特定的时间内没有收到服务器端的确认应答信息,则会重新向服务器端发送连接请求,且该连接请求得到服务器端的响应并正常建立连接,进而传输数据,当数据传输完毕,并释放了此次TCP 连接。
  • 若此时第一次发送的连接请求报文段延迟了一段时间后,到达了服务器端,本来这是一个早已失效的报文段,但是服务器端收到该连接请求后误以为客户端又发出了一次新的连接请求,于是服务器端向客户端发出确认应答报文段,并同意建立连接。
  • 如果没有采用三次握手建立连接,由于服务器端发送了确认应答信息,则表示新的连接已成功建立,但是客户端此时并没有向服务器端发出任何连接请求,因此客户端忽略服务器端的确认应答报文,更不会向服务器端传输数据。而服务器端却认为新的连接已经建立了,并在一直等待客户端发送数据,这样服务器端一直处于等待接收数据,直到超出计数器的设定值,则认为服务器端出现异常,并且关闭这个连接。在这个等待的过程中,浪费服务器的资源。
  • 如果采用三次握手,客户端就不会向服务器发出确认应答消息,服务器端由于没有收到客户端的确认应答信息,从而判定客户端并没有请求建立连接,从而不建立该连接。

5. 说一说HTTP

参考HTTP面试总结

6. 说一说TCP的拥塞控制和流量控制

  • 慢开始
    • 发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。
    • 拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。
    • 发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。
    • 慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
    • 当然收到单个确认但此确认多个数据报的时候就加相应的数值。
    • 所以一次传输轮次之后拥塞窗口就加倍。
    • 这就是乘法增长,和后面的拥塞避免算法的加法增长比较。
    • 为了防止cwnd 增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。
    • ssthresh 的用法如下:
    • 当cwnd<ssthresh 时,使用慢开始算法。
    • 当cwnd>ssthresh 时,改用拥塞避免算法。
    • 当cwnd=ssthresh 时,慢开始与拥塞避免算法任意。
    • 拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT 就把发送方的拥塞窗口cwnd 加1,而不是加倍。
    • 这样拥塞窗口按线性规律缓慢增长。无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。
  • 然后把拥塞窗口设置为1,执行慢开始算法。
  • 如下图: 慢开始和拥塞避免
  • 快重传和快恢复
    • 快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。
    • 快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
  • 快重传配合使用的还有快恢复算法,有以下两个要点:
    • 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。
    • 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd 设置为ssthresh 的大小,然后执行拥塞避免算法。
  • 如下图: 快重传和快恢复

7. TCP 可靠性保证

  • 序号
    • TCP 首部的序号字段用来保证数据能有序提交给应用层,TCP 把数据看成无结构的有序的字节流。
    • 数据流中的每一个字节都编上一个序号字段的值是指本报文段所发送的数据的第一个字节序号。
  • 确认
    • TCP 首部的确认号是期望收到对方的下一个报文段的数据的第一个字节的序号;
  • 重传
    • 超时重传
    • 冗余ACK 重传
  • 流量控制
    • TCP 采用大小可变的滑动窗口进行流量控制,窗口大小的单位是字节。
    • 发送窗口在连接建立时由双方商定。
    • 但在通信的过程中,接收端可根据自己的资源情况,随时动态地调整对方的发送窗口上限值(可增大或减小)。
  • 窗口
    • 接受窗口rwnd,接收端缓冲区大小。接收端将此窗口值放在TCP 报文的首部中的窗口字段,传送给发送端。
    • 拥塞窗口cwnd,发送缓冲区大小。
    • 发送窗口swnd, 发送窗口的上限值 = Min [rwnd, cwnd]
  • 拥塞控制
  • 流量控制与拥塞控制的区别
    • 所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。
    • 拥塞控制所要做的都有一个前提,就是网络能承受现有的网络负荷。
    • 流量控制往往指的是点对点通信量的控制,是个端到端的问题。
    • 流量控制所要做的就是控制发送端发送数据的速率,以便使接收端来得及接受。

8. 说一下SOKCET 编程的主要流程

TCP 过程: 客户端:

  1. 创建socket
  2. 绑定ip、端口号到socket 字
  3. 连接服务器,connect()
  4. 收发数据,send()、recv()
  5. 关闭连接 服务器端:
  6. 创建socket 字
  7. 设置socket 属性
  8. 绑定ip 与端口号
  9. 开启监听,listen()
  10. 接受发送端的连接accept()
  11. 收发数据send()、recv()
  12. 关闭网络连接
  13. 关闭监听

9. HTTP状态吗

  1. 1XX 信息码,服务器收到请求,需要请求者继续执行操作;
  2. 2XX 成功码,操作被成功接收并处理;
  3. 3XX 重定向,需要进一步的操作以完成请求;
  4. 4XX 客户端错误,请求包含语法错误或无法完成请求;
  5. 5XX 服务器错误,服务器在处理请求的过程中发生了错误404 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置”您所请求的资源无法找到”的个性页面

10. 你实现了那几个HTTP的状态码?

  • 200 ok传输正常
  • 401
  • 404 找不到文件请求
  • 400 请求参数错误
  • 500 服务器错误
  • 501 Not Implemented

11. 说一下HTTP的请求方式

OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*‘的请求来测试服务器的功能性。 HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。 GET:向特定的资源发出请求。 POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。 PUT:向指定资源位置上传其最新内容。 DELETE:请求服务器删除Request-URI所标识的资源。 TRACE:回显服务器收到的请求,主要用于测试或诊断。

12. 为什么使用的水平模式而非边缘模式

  • 水平模式:只要socket缓冲区有数据,且执行了EPOLL_CTL_ADD监听socket,内核就一直通知进程;即,只要内核通知了进程,但是进程没有read或者没有read完数据,就会触发唤醒进程。
  • 边缘模式(ET):socket有数据,会通知进程,不管进程有没有read,后续不再通知。 但是一种情况例外,就是唤醒进程不read或者read部分数据之后,如果有再次调用了 EPOLL_CTL_MOD 添加EPOLLIN事件的情况,则该进程还是会被再次唤醒。

书中的解释是,et唤醒次数少,但要求反复读取直到eagain,系统调用次数更多,总体上不一定比lt快。此外lt的代码实现可能会略简单一些。gejun提到过,在极端critical的情况下,如果epoll wait和读取被分散到了不同的线程,此时lt会导致epoll陷入busy loop,所以必须用et。

不会丢失数据或者消息。对于追求低延迟的程序来说,这么做是高效的;实现简单;照顾了每个连接的公平性。不会因为某个连接上数据量过大而影响其他连接处理消息,采用LT延迟低。而且边缘模式,必须使用非阻塞IO;并要一次性全部读写完数据。

14. 为什么使用jpeg编码

因为之前使用h264/h265编码的时候,

  • MJPEG是很多摄像头都支持的格式。普遍支持比h264/265较好;
  • 而且算法复杂度比较低,对设备的性能要求不高,树莓派就可以了。
  • 并且是单帧压缩,而不是h265/h264那样的需要对多个关键帧进行等待压缩。因此时延会低很多。
  • MJPEG改良了帧间预测机制,提高了纠错和网络兼容性,对网络传输具有更好的支持功能。

13 . 为什么使用TCP和http,而不使用UDP

答:其实当初也有使用UDP的想法。被老师否定了,

  • 但是因为是先写的客户端;客户端为了后期兼容使用的ffmpeg;ffmpeg只支持比较成熟的方案:rtmp和rtsp,mjpeg这几种;其中rtsp使用的是UDP但是需要使用h264/h265压缩编码。这样的延迟比较高。

  • 自己定义UDP的话,就需要重新修改客户端代码。成本比较高,而且UDP只是尽最大可能的交互,没有序号确认和选择重传,拥塞控制,流量控制。对于实时的视频流来说是比较致命的。本身jpeg编码的图像对网络带宽要求就比较高(1M左右的带宽);使用UDP很容易造成拥塞而丢包;传输不稳定。

  • 老师告诉我,如果非要使用需要自定义一套类似于TCP的重传确认机制,这个是很困难的。即使成功也不一定会比TCP的快。因为TCP的很多算法都是直接写入到转发的硬件芯片上的;会比稳定也会快很多。

15. 为什么使用http而不是直接使用TCP传输。

答:

  • 这个是mjpeg流传输方案中,最常使用的就是HTTP;有比较成熟的方案可以参考。
  • 因为TCP是数据流,很有可能存在粘包现象。因此需要自定义分割符。HTTP能够使用context-length-方便对数据进行分割和处理。当然自定义字段也可以但是,未必有HTTP考虑的周全,而且需要客户端和服务器两边的配合,不适合客户端使用ffmpeg的初衷。
  • 而且使用HTTP简单灵活,技术比较成熟;配合浏览器就能实现一个简单的跨平台客户端。方便进行调试和验证。可以使用浏览器,快速定位错误。
  • 并且浏览器底层使用的是ffmjpeg作为流媒体的接收;和客户端基本一致。现有的HTTP成熟的开源代码和框架很多;可以有很多的参考。另外也是希望可以和浏览器结合。毕竟客户端安装比较麻烦。
  • 为了后期的错误处理,和进一步的图像处理结果的传输。还是会在TCP的基础之上再定义一个简单协议;既然如此,为什么不用HTTP。

16. 你是怎么使用监听者模式的;为什么这样使用

答:

  • 这里是受到Qt中信号和槽的启发。可以创建一个监听者函数队列,在完成摄像头图片数据的读取之后,进行进一步的处理和分发。
  • 摄像头的camera为了防止I/O争用是使用的单例模式创建,为了提供后期的多个使用需要,因此在这里使用监听者模式。每个creama类会有以监听者链类,它和普通的video listener都拥有相同的监听函数,不过是遍历左右链表并,执行对应的OnNewImage函数。
  • 中间使用了shared共享指针,避免了图像对象的重复拷贝消耗。监听者只有在需要的时候才会拷贝到自己的内存中

17. 为什么要使用无锁环形缓冲队列

答:

  • 因为摄像头,在定义的时候,摄像头线程和读取线程是分开的,所以为了防止两边的读写速度不匹配,设置的缓冲区。
  • 更为重要的原因是,当摄像头不支持jpeg格式的图像数据的时候,对图像进行压缩编码的;图像处理本身就是高度的数据并行。因此进行单帧的流水线处理比较消耗资源,也比较浪费时间。使用buffer可以支持openmp或者cuda进行并行的图像处理。
  • 但是其实在后期这里出现了一些问题。当进行实时的视频传输时,总是会发现掉帧的情况。是因为客户端设置的读取速度和摄像头本身设置的速度不匹配,当缓冲区满了之后,写者就不进行写入了,造成中间存在掉帧现象。后面更改为写者优先的强制写入;这个问题才得以解决。
  • 后面在老师的要求下,取消了缓冲区,直接再camera和网络中间的连接者之间设置了一个单帧buffer使用锁来进行保护。保证无论客户端的请求频率是多少,都能保证获取到的是最新的图片。最后的效果竟然和原来差不多。因该是,同时在线的客户端比较少,索引影响不大,也再次证明了,还是需要根据自己的需求来,不要过度设计。
  • 将无锁缓冲区留给后期的图像处理,不输出处理后的视频流了。再次说明理解需求和实际应用场景的问题,不要想的太多。

18. 无锁环形缓冲区是怎么设计的?强制写入时怎么回事

答:

  • 使用一个数组模板作为数据存放点。使用原子数据定义队头和队尾。
  • 在存取的时候,使用c++的std::memory_order_requirestd::memory_order_release队投头和队尾的有序性,先将需要的tail或者head指针保存到本地,再进行数值的拷贝和释放,最后再更改变量。使得整个操作有序。避免了使用锁的巨大消耗(感觉没有太大效果)。
  • 强制写入就是在写入时,检测对头和队为是否相同,如果相同,就移动一次队尾指针,然后再进行写入。

19. 网络通信模块是怎么设计的?

答:

  • 网络通信模块,参考了muduo 开源库,使用Reactor模型,自己实现了一个简单的网络通信。使用事件循环机制,每个线程创建一个event loop和channel指定监听的socket文件描述符和监听的事件。线程池初始化之后,每个线程都开始不停的自旋;并执行函数队列中的函数。
  • 主线程event loop 中会有一个 poller,channel,来进行epoll的监听。poller将event loop中的中的channel中的监听队列事件,并,对于每次的新连接都,产生一个socket fd和channel的map,方便epoll_wait时,进行快速的查找。加入到epoll中,再使用epoll_wait等待监听队列,每次检测到读写。
  • 其中Accepter才是真正监听的队列,只有它使用epoll绑定了listen,转换了被动监听函数。所监听的事件为可读和可写。当接收到一个连接时, 创建一个TCPconnet类,每个connet绑定了读取和写入的回调函数。回调函数时在Tcpsever中创建的。再从Event loop线程池中选择一个线程,将TCP的connectEstablished和conn进行绑定,调用runInLoop,并更新channel中的回调函数,将channel添加到主线程的监听集合中,channel会有 一个指针指向连接的Tcpconncet,每个loop循环调用epoll_wait,当事件触发时,就将epoll wait中返回的事件集合转化为channel并,查找是否是属于当前线程的channel,是就更新当前的活动事件,查找对应的函数,并执行执行回调函数。,是直接调用绑定的函数,不是就不管;将函数添加到event loop的执行队列。在event loop的下一次循环中执行。每个TCPconnect中设置了读写buffer,进行写入,防止数据量巨大时,因不能进行读写而中断。
  • 每个TCP连接建立时,会在epoll中存在记录,即fd和channel。每个线程拥有自己的channel。循环调用poll。检测自己的channel是否被激活,如果激活了就执行回调函数,注意每个channel中包含tcpconnect 指针,因此可以检索到fd。
  • loop由thread创建,一个thread,一个loop 。channel由loop创建。每个TCPconnect创建时,会设置可读写事件,并调用channel_->enableReading();然后顺序调用loop_->updateChannel(channel_);,poller_->updateChannel(channel_);将channel注册监听的事件并将其添加到channel队列中;更新index;
  • 关键类:event loop,thread, channel,acceptor,poller;sever;poller为acceptor提供多路复用封装。TCP sever主线程执行acceptor函数,创建TCPconnect; channel连接TCPconnect和poller。将监听事件和写入poller。event loop 与thread结合,产生线程和循环主体。并且在循环主体中创建channel。为TCPconnect提供事件监听的channel支持。

20. HTTP时怎么设计的:

答:

  • HTTP其实是在TCP的基础之上,添加了HTTP的解析。并设计了reponse进行结构体的封装,避免每次大量重复的字符串凭拼接和写入。
  • 队HTTP server设置了函数映射map,创建handler帮助实现队对不同的请求路径的注册和处理。

21. 在项目中遇到了哪些困难,时怎么解决的;

答:因为我之前大概做了两个项目,不同的项目遇到的困难不同;

  • 做监控显示客户端的时候,
    • 遇到的最大的困难时功能需求的多样以及,可参考资料较少。需要不断的google并查询资料,看代码。客户端中的地图数据,其实设计到很多GIS的知识,而且Qt的location地图组件并不成熟,因此自己查看相关源代码和文档,自定义地图拼接数据,同时由于国内标准坐标系和星火坐标系的偏差,定位坐标的转换和不同地图之间的偏移,也比较难以计算。最终在高德地图的开源amap组件中找到源代码。需要你能够快速的了解新的知识和善于查找资料。
    • 客户端同时需要和硬件做一些交互;但是客户端的显示屏幕是定制的,其中的屏幕控制很难办,没有开源的解决方案;基本没有博客可以参考;只要到官网上查看文档和代码示例
    • 客户端主要还是对于需求的不清楚和不明晰;老师很多时候也不知道具体要做成什么样子。尤其时GUI设计这块;要不断的跟老师沟通,进行修改。
  • 流媒体服务器的时候,主要是技术问题和设计问题。
    • 一开始的时候,是直接使用ffmpeg进行rtsp的播放流。但是延迟比较大,大概3s左右。想到应该是做帧间压码花费的时间太多了。因此继续查找资料,看有没有压缩较少的方案。
    • 查找资料后,发现是mjpeg流。没有做帧间压缩,而且摄像头支持的话,可以不用转码压缩,但是ffmpeg对其支持较少。而且老师要求能够在树莓派等嵌入式中能够运行,因此自己就开始了一个轻量级的mjpeg图片传输服务器的工作。
    • 之后基本能够满足要求,但是发现在运行久之后会存在卡帧的情况。这个是因为长时间的文件流,mjpeg浏览器或者客户端的ffmjpeg需要对缓存队列进行一次清理。所以将服务器端建立连接之后的主动发送,改为了由客户端发起的被动请求方式,这样,每次只是请求单张图片。客户端还能更改请求频率。降低消耗。
    • 但是,又存在问题了;当两边的请求帧率相差太大时,会出现掉帧的情况。最后发现时设计之初的环形缓冲区的锅。因为读写速度不匹配,造成队列满后,无法写入最新数据,中间的帧就被丢掉了。因此将写入,改为强制写入,并减少缓冲区的大小。给老师说了之后,最后老师,直接让放弃缓冲区。改为一个单独的帧来存储数据。并用锁来保护。这样在并发量较少时终于基本解决问题,并将环形缓冲区作为后面图像处理的并行设计。+
    • 上述环形缓冲区的事,说明不要一开始就将软件设计的很好;先处理需求。不要想当然的进行设计。过度设计时要不得的。

22. 看你用过FFmpeg开发,简单介绍一下,说一下你得编解码模块的流程

答:主要流程是,使用初始化,ffmpeg的网络模块和和编解码模块;指定网络模块监听的url;然后查找网络流的对应信息和对应的视频格式。根据视频格式,选择查找对应的解码器;然后设置解码器的解码参数;如解码的帧率等;接下来就是打开解码器,并循环调用av_read_frame将网络包解压出来;并使用sws_scale将图片缩放对应的RGB格式。通过Qt的信号发送出去。交给显示来显示图片。(视频和视频帧:FFMPEG解码套路整理)

  • 主要流程如下:

ffmpeg解码流程

  • 主要的流程如下:

    • 解码Step1. 连接和打开视频流连接和打开视频流必然是后续进行解码的关键,该步骤对应的API调用为:

      1. int avformat_network_init(void):初始化网络流模块
      2. int avformat_open_input(AVFormatContext** ps, const char* filename, AVInputFormat* fmt, AVDictionary ** options) avformat_open_input()官方说法是“打开并读取视频头信息”,该函数较为复杂,笔者还没有完全吃透他的每一行源码,大致了解其功能为AVFormatContext内存分配。如果为视频文件,会探测其封装格式并将视频源装入内部buffer中;如果是网络流视频,会创建socket等工作连接视频获取其内容,装入内部buffer中。最后读取视频头信息。源码深入阅读的话,笔者推荐雷神的FFmpeg源代码简单分
    • 解码Step2. 定位视频流数据:查找对应的视频流和音频流对应的格式

      1. int avformat_find_stream_info(AVFormatContext** ic, AVDictionary ** options) avformat_find_stream_info()进一步解析该视频文件信息,主要是指AVFormatContext结构体的AVStream。从雷神的FFmpeg源代码简单分析:avformat_find_stream_info()文章可以了解到,该函数内部已经做了一套完整的解码流程,获取了多媒体流的信息。请注意,一个视频文件中可能会同时包括视频文件和音频文件等多个媒体流,这也就解释了为什么后续还要遍历AVFormatContextstreams成员(类型是AVStream)做对应的解码。
    • 解码Step3. 准备解码器codec

      • 准备解码器的步骤包括:寻找合适的解码器 -> 拷贝解码器(optiona)-> 打开解码器。
        1. 使用avcodec_find_decoder(enum AVCodecID id)查找合适的解码器。这里的id由上一步的AVStream中的成员变量codecpar->codec_id确定。
        2. 拷贝解码器 - AVCodecContext* avcodec_alloc_context3(const AVCodec* codec)int avcodec_parameters_to_context(const AVCodec* codec, const AVCodecParameters* par) avcodec_alloc_context3()创建了AVCodecContext,而avcodec_parameters_to_context()才真正执行了内容拷贝。avcodec_parameters_to_context()是新的API,替换了旧版本的avcodec_copy_context()
        3. 打开解码器 - avcodec_open2(AVCodecContext* avctx, const AVCodec* codec, AVDictionary ** options) 源码阅读还是推荐雷神的FFmpeg源代码简单分析:avcodec_open2(),该函数主要服务于解码器,包括为其分配相关变量内存、检查解码器状态等。
    • 解码Step4. 解码

      • 解码的核心是重复进行取包、拆包解帧的工作,这里说的包是FFMPEG非常重要的数据结构之一
        1. 使用av_read_frame(pFormatCtx, packet)读取包,返回读取到包的数量,小于0,表示读取完毕,关闭就行了
        2. avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);将包读取到准备好的pFrame中进行数据的解压。
        3. 使用sws_scale进行图片格式的转换。
        4. 将数据拷贝到QImage中,并使用信号发送图片。

23. 这边的主要技术栈是java/go/python,语言的切换你怎么来准备?

首先,本身研究生主要使用的是c++,但是原来java也学过,并做过课程设计,基础还在。c++转向java的自身还是有的,自己也在准备转向java。因为自己网络和http以及操作系统的基础,语言基本都是相同的,已经有了C++的基础,再学习java会很快,主要还是放在java语言本身的深入,和配套对应的常用开发框架spring 等的熟悉,主要思路还是和原来的C++学习路线差不多,先基本语法,标准库标准库里面肯定有很多优秀的设计与实现,再并发编程,再深入理解虚拟机,师兄去看《Java核心技术》;《JAVA编程思想》;《深入理解Java虚拟机》等。同时自己学习Spring,MyBatis等;试着自己做一个后台服务器项目,并进一步学习MySQL,redis;当然这个是我自己的想法。您有什么比较好的建议吗?

24. 未来的职业规划,自己的优缺点说一下。

未来,个人还是希望,主攻后台开发方向;同时专注于微服务,高并发和分布式架构的东西,感觉那个非常有意思。对业务的分解,集群的搭建与设计等。但是因为学校里面做这个的很少,没有人带,所以非常想要进入像XX这样的一流公司,学习进一步提升自己的能力,最后能够自己参与实现一个完整的大型后台开发的流程;最后还是希望自己能够独立设计一个合格的后台架构。对于我而言,我的优点在于基础知识良好,而且个人比较喜欢看看代码,写代码。平时再学校的时候每天看代码,早上10点到晚上10点。学习新东西是一件很快乐的事;可以持续不断的去学习,也有后台开发的相关经验,对网络和HTTP有所了解;但是对于数据库和缓存已经分布式相关的东西知识储备不够,需要进一步学习;希望xxxx能够提供这个平台。

25 你的项目中使用了哪些设计模式

主要使用了监听者模式 在摄像头中,使用监听者链的模式来进行数据的被动监听和分发。

单例模式:摄像头创建时,为了避免多个线程对摄像头源的争用,使用单例模式保证每个摄像头数据源的唯一性。

策略模式:使用策略模式,根据http请求路径来选择对应的请求处理函数。

26 你的项目中多线程编程是怎么使用的

主要的研究方向是数字图像处理和计算机视觉;

1. 一分钟的自我介绍

您好,我叫敬方;现在正在四川大学计算机学院就读研究生;研究生阶段主要使用语言是c++,之前主要的项目是老师所带领的一个校园无人公交车的项目;我主要在里面负责车辆监控客户端和轻量级的HTTP图像采集传输服务器。后期的主要精力还是在轻量图像传输服务器,也由此开始网络编程和后台开发感兴趣。之后有幸进入阿里实习,学习到了软件开发流程和软件方案设计方法,从事了一点相关的开发工作;进一步了解到了web服务上层架构状况。不过因为实习时间较短,学习不够深入,希望能够进入xxx进一步进行学习。

2. 菜鸟网络

2013年成立,阿里、顺丰、三通一达(申通、圆通、中通、韵达)等共同组建“菜鸟网络科技有限公司”;主要是使用互联网将原本各自独立的今昔联通起来;努力打造遍布全国的开放式、社会化物流基础设施和平台,建立网络零售额的智能骨干网络。菜鸟网络方面表示,中国智能骨干网要在物流的基础上搭建一套开放、共享、社会化的基础设施平台。菜鸟的长板就是数据,不仅有客户、商家、消费者的数据,还有物流信息路由的数据。凭借这些数据,菜鸟做的是物流订单的聚合工作。发达国家的仓储自动化平均普及率达到80%,而我国目前仅为20%。

简单概括一下,阿里成立菜鸟,既为了补自己的物流短板,也是为了给整个物流行业“赋能”,提升整个物流行业的服务水平。打通物流骨干网络和毛细血管,提供智慧供应链服务。提高物流效率,降低社会物流成本,提升消费者的物流体验;创造更大的利润空间。

建设整个物流行业的数字化基础设置,搭建面向未来的、基于新零售的物流供应链解决方案,打造全球化的物流网络。

自成立起,菜鸟的使命就是:全国24小时,全球72小时必达。

主要工作:

  • 第一件事是开发电子面单。
  • 智能仓储和智能配送:方便快速发单。
  • 搭建仓储和末端网络:菜鸟搭建了最后一公里菜鸟驿站+自提柜的网络,既能够方便用户在自己方便的时候取件,也能够大幅减小了快递员的工作强度和单件派送时间。

四川学校中的自提点,使用非常方便快捷。

主要业务是经营中台,对业务进行分析,业务中间件等。