本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 % z" q/ u" R+ u1 t& f- d! l. `
& j- m; y7 A& |/ ?+ G1 N) B

/ D" g5 r+ e0 J, G* a/ W作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
5 W7 M$ V; Y/ `. D7 w9 h7 g
7 i- A) M* l2 N! T2 R本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
. h6 z/ M2 ^7 S2 }* n/ m
. F( T, `( b! a8 {9 T两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 . ]1 P- P; }% P' U
一、HTTP网页服务器
2 D. K8 |1 f" \/ I7 K先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
0 U: x8 ]% z8 V! H- O3 N& Z - {
7 B. K0 `8 }+ M1 c l( O - struct sockaddr_in servaddr;- O# d+ k! O3 Q4 b# F: P$ Q- c, x
- socklen_t addrsize = sizeof(struct sockaddr);: O& O) p& G7 g) {/ e2 k
- bzero(&servaddr , sizeof(servaddr));% Y9 |2 p. {8 H/ f4 z
- servaddr.sin_family = AF_INET;2 O& x6 V: C |. c* y9 U+ E
- servaddr.sin_addr.s_addr = inet_addr(ip);
2 b6 G6 c% Y& q5 k3 U - servaddr.sin_port = htons(port);
u- E: D7 I( z2 R7 D9 f - int ret;
/ g9 y0 B3 ? L, M - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1); H% F" H$ v3 S9 q, O" {( j
- {0 h( l. P3 R7 y+ y0 X/ Q
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
5 K% A& ^; W$ @& ? Z ] - return -1;
) Z: |- @) Z1 | - }* P% @, A$ Q2 k. V! c x
- int on = 1;6 H3 o# q0 {) z& J) b8 w' s b
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)% {0 t# g( `6 H
- {
, b3 @1 D2 P! a - printf("setsockopt error\n");
* M- E; F' R8 [. T7 d" U3 d - }9 I# Q8 H: f5 C" ?
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
* G+ ~6 h S d, ?" w1 W - if(ret == -1)
/ h/ {, o, ]% ]. E" N8 S - {
' V; {2 o- M! @/ k7 H - printf("Tcp bind faiLED!\n");- J) j, g$ p r. R) j1 K2 P
- return -1;
[3 X$ r' v' K$ g3 t* ? - }
/ M$ M/ Q9 K& k" g - if(listen(*socket_found , 5) == -1)
6 O" }% P, i* y3 L# }) |7 Z7 a - {6 f. r* ?: L. [$ {1 ^
- printf("Listen failed!\n");+ D0 M0 k( I# l3 ?; @& m
- return -1;
0 S8 p7 ^2 L1 T3 S5 f* O5 \ - }
8 j! I3 p. H; I' Q: t* x. A# c: V/ G - return 0;
8 r& Q G8 m, K6 {$ }" t - }
复制代码其中setsockopt()函数是可选的,一般只用于规避socket()函数的建立错误。 建立了TCP服务器后,返回的socklen_t型实参在后面的HTTP网页服务器中需要用到。 HTTP网页服务器所属的TCP操作是需要另起轮询线程来让客户端进行accept()握手操作的,accept()之前的listen()倒是只需要执行一次即可,accept()握手操作和recv()接收操作需要创建一个死循环线程: - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
& s" F: u$ `* S' S! j' l* I - void * Thread_TCP_Web_Recv(void *arg)
. M4 p0 W6 w8 l4 i- l3 p4 { - {
4 P7 r8 [+ e: f# Z# [ - 。。。
$ w4 Z& u8 X. @) q1 l - while(1)
- k' b8 H: l* }9 ]" R m7 c7 n. q - {2 ^# y& R, I5 _( z: H$ |0 g/ v
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);' h g/ e$ H! f3 Q) e8 Q( p
- printf("fd_socket_conn = accept()\n");1 N9 r# C. Y) V, X: |
- 。。。: J/ I& l/ f% g2 f8 x( K
- recv(fd_socket_conn , recvbuf , 1000 , 0);% f/ O u, A, C$ {6 o
- }% J' T! n. f: r2 S5 ^2 n
- 。。。
: l( R8 }8 z8 K- T, E - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
2 j8 k) k- a1 _ - pic_tmpbuffer = pic.tmpbuffer;
4 ]5 ^8 i3 D# _% `. i2 d - pic.tmpbytesused = buff.bytesused;: ~$ I. Q2 g! ?4 m" f9 h' [
- pic_tmpbytesused = pic.tmpbytesused;
( I$ U2 q. n& |( h f. y - pthread_cond_broadcast(&pct);# |" S5 }) I0 B+ ]! ?: X. p
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
( T; y/ a7 \+ O$ r, I0 T - pthread_cond_t pct;
! R/ U3 n. c$ Q6 L5 z - int main(int argc, char* argv[])
# b5 U7 a5 j+ [2 Y. N - {0 w# V; _+ }& t" c2 h! r" q- c
- ...7 O. e+ k+ p ]' T( H% h! n
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
) M @& t# V/ u4 _. L. w. k" N. w - pthread_mutex_init(&pmt , NULL);
2 F+ d1 U% c+ P( [ - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);( O( z9 O N7 C/ k
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
$ G+ B; G; n7 Z: C& T - ...
6 b' O& d$ F& l" y% I) n - while(1)4 I5 s) {' B# N4 W4 X* ?/ E
- {
( ]9 T2 B* ^$ V; d( J - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
" c ` a! x E7 G - ...
^; G6 W' Z$ a3 H. F/ k - }
; z' P5 T9 ?3 q+ O' o/ w' G - ...7 A2 f- S* ?, B, M! y0 {
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">6 @ R# B% `1 i8 v$ V, s8 r) z( L
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \( ~2 c( g; P! {
- "Server: MJPG-Streamer/0.2\r\n" \
' O# v$ o# {" {; B& }* Y - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
9 x2 _/ t2 R; j, k2 s. H7 u - "Pragma: no-cache\r\n" \7 |$ u' A% }# U0 o; E S
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"# Z' v7 T. b8 b% j
- #define BOUNDARY "boundarydonotcross"
" E% {0 v' c4 m3 w1 ^, j, h - printf("preparing header\n");0 a2 W) b) R `# u" n4 C; W( O
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
' h" Z3 p# o- X5 p" a - "Access-Control-Allow-Origin: *\r\n" \
4 f8 _* L8 y. }9 X - STD_HEADER \
+ s' b @3 T3 `4 V) D) b' _ T - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
6 F' A* F! o" C8 g' b; I - "\r\n" \1 ^3 x. K* e( S
- "--" BOUNDARY "\r\n");
, ^# g& m4 D4 T. C - if(write(fd, buffer, strlen(buffer)) < 0)
, h$ \+ p: q) T, y- L1 J. p1 O7 ] - {0 H* j0 h. B p( r
- free(frame);+ K( ~1 {+ f& q1 n# K
- return;7 Y# Q' T$ h- R* m8 W5 K
- }
复制代码发送完HTTP标准头之后,就需要发送内容头(Content-Type),这处的Content-Type为image/jpeg,同样,HTTP标准协议里面image支持的类型远不止jpeg一种,发送完内容头之后就是正文和boundary结尾,这样帧完整的HTTP头发送到指定的TCP GET地址,就会在浏览器中显示刚刚发送的图片: - <pre class="prettyprint lang-cpp" style="box-sizing: border-box; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 16px; white-space: pre-wrap; line-height: 1.38462; color: rgb(51, 51, 51); word-break: break-all; background-color: rgb(245, 245, 245); border: 0px; border-radius: 4px; vertical-align: baseline;"> sprintf(buffer, "Content-Type: image/jpeg\r\n" \8 e7 z: J4 `5 D2 ?
- "Content-Length: %d\r\n" \' N: d5 B; Z9 v. F2 G, t8 Z( h
- "X-Timestamp: %d.%06d\r\n" \
6 d" y G' L3 S, k! \ - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);" V( x- A- @- f! E. V6 _6 U
- printf("sending intemdiate header\n");
" E! Z) p/ y. ]8 ]# \ - if(write(fd, buffer, strlen(buffer)) < 0)
0 c1 |# ` F0 m& ?# D* ^& R - break;
( B/ S& m7 [) L9 m4 f, U4 z, W - printf("sending frame\n");
0 X: z0 {& _1 N$ @0 i8 c: N# b0 {) { - if(write(fd, frame, frame_size) < 0)& q$ `9 E3 @* Y- }; R/ l4 N
- break;$ a6 L( P( ?; ~1 y, F9 b" u
- printf("sending boundary\n");
- A2 Q1 ]- s; i" ^9 u% {. p) Q4 ~/ N - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
; M, ~6 w8 j" u - if(write(fd, buffer, strlen(buffer)) < 0)
( D/ W* c P9 l9 k% o - break;</pre><p style="box-sizing: border-box; border: 0px; font-size: 16px; vertical-align: baseline; line-height: 26px; color: rgb(51, 51, 51); font-family: " helvetica="" neue",="" helvetica,="" tahoma,="" arial,="" "microsoft="" yahei",="" "hiragino="" sans="" gb",="" "wenquanyi="" micro="" hei",="" sans-serif;"=""></p>
复制代码另外需要说明的是,TCP服务器线程在发送MJPEG流的时候是死循环发送的,因此TCP客户端在发送完GET指令之后,就会收到TCP服务器循环发送的图像缓存,TCP客户端会陷入忙等待状态无法再对外发送任何GET或者POST指令,从客户端使用者角度来看的效果就是网页一直在等待。 
. w( m* N$ C5 Q" Q8 M- q) j 4 {' k3 T# n3 ?/ l2 d y; w) j v
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
! P: @/ l0 ~5 ~4 M- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)( G4 b# G/ s; R2 j
- {. `* Y3 L9 q( h2 ?2 N
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
! z- a3 S3 b+ y8 G. X - if(*socket_found == (~0))
, ~5 E( G# y0 {8 l% S - {# v7 t, F/ K% ^
- printf("Create udp send socket failed!\n");/ |) g1 W* P% o [! i4 p
- return -1;
/ N# B! A0 m, Q - }6 ?9 J o# |# M5 U( Z
- addr->sin_family = AF_INET;
5 H6 i# z1 E4 l: a$ C1 o' V - addr->sin_addr.s_addr = inet_addr(ip);+ I* q% h4 v& x
- addr->sin_port = htons(port);7 P$ n) \- n! ~* ~
- memset(addr->sin_zero, 0, 8);
- M% W1 g w4 g: a, ] - return 0;
/ x! E) p s7 w8 Z - }
复制代码 3 E9 u2 S. r; l, t1 l8 y: b
" L* z B) W' f
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可: H% s7 Z& I' h! v" B2 K! F) [
1 M i3 l% m+ o5 I
& S; v6 g7 N" E4 y. g& I, t6 z3 b( _- while(fend > 0)
& v" V" K0 x4 }' Q - {
: a% _# D8 H" @: e - memset(picture.data , 0 , sizeof(picture.data));
4 r' w( E) q. U6 F$ [ - fread(picture.data , UDP_FRAME_LEN , 1, fp);
& _: k( v* M5 O, d$ w' O7 q - if(fend >= UDP_FRAME_LEN)0 J6 u9 m. ~) h( B! d! ]
- {
! q @- ~" w& G0 f/ {$ } - picture.length = UDP_FRAME_LEN;/ |7 D, l: A- ?, @& i& c/ b
- picture.fin = 0;. P; q; {5 v* @7 \( H4 E
- }$ Z9 k O, p: s# ?
- else
: C: }: y1 f6 I+ F& P - {2 g0 r( b% k: B# H) z: h& \' P
- picture.length = fend;) p- G* B* s8 @* I
- picture.fin = 1;, @" k9 G$ a0 c7 c9 m
- }$ o2 J& x$ d2 a
- //printf("sendbytes = %d \n",sendbytes);
6 ~) l! m. K/ v - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
9 S3 M: D2 ?2 O9 v - if(sendbytes == -1)
' _5 J) v2 j0 \" s) j - {
\7 ?3 s7 D' s8 L" [ - printf("Send Picture Failed!d\n");0 ~# w/ M P0 I& N0 \3 N4 \
- return -1;# ?- ` ]9 M) U/ z$ l$ @
- }
3 X" A$ _1 m& B; N# E) ` - else2 F# y& s, E1 d! H u
- {5 ~4 X, U: W# V$ O9 Q/ c
- fend -= UDP_FRAME_LEN;
/ C, W( R6 x0 t% ~1 _. v' o - }2 m5 R0 \" w6 G
- }
复制代码
3 L2 ] ?3 M+ ]+ Y$ P
# ^& k& f! N/ a5 g" Y( a# ^% O
9 g* q3 o) B0 X' `7 Y8 E( D. |7 K$ s+ i2 {" }7 X
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |