本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 6 Z9 Z% m! F+ o! P7 ^- Y& N% ~0 g
0 f4 t" \# A) A0 H

5 j$ A) _ J! e. L4 L作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html, T# N5 |: x! M4 W
& r1 {; T- d9 Q; s1 N& P本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 $ \3 q) u4 h0 G) n, D% H8 g
 # N2 s8 C( g: u
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
; E6 B$ Z- P* a一、HTTP网页服务器
) d1 D6 ` x) l" x* N先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
/ U; S6 l0 D& E! F/ r% D3 G - {, O+ W& V- V0 C3 I# y4 c
- struct sockaddr_in servaddr;' t4 [2 r0 L# Y% @; A' \ v
- socklen_t addrsize = sizeof(struct sockaddr);
6 G9 _3 B& s. ?( [ - bzero(&servaddr , sizeof(servaddr));( F; t. W6 h! z0 D1 A. M. l
- servaddr.sin_family = AF_INET;7 V( h, I/ I+ a1 a+ C4 S3 c
- servaddr.sin_addr.s_addr = inet_addr(ip);
% s! p$ S9 @, P8 @8 h7 } - servaddr.sin_port = htons(port);
9 E8 ~- y( [0 N w# \# Z - int ret;
( p o2 L ?. j% A- D' U - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)4 ]* n8 v$ W8 H" n$ d# J
- {2 a" p- R- {" B5 y5 U& T( _
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
% _3 p: ?& C$ i - return -1;/ t, i+ b2 D! F# O& L& }! i
- }
& i' F7 n& D7 P) d+ R0 l0 O: G - int on = 1;
1 g4 W; P0 d4 | a: F" z+ T' l6 a - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)4 H3 D5 X. e T6 \7 E: s; T- \
- {
+ c* K. H8 q2 l `2 e - printf("setsockopt error\n");. v, r1 I3 M5 @- Y: v0 R8 A
- }4 z& V/ b; G# G# i/ B, P7 p
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
?2 _" K5 s& R4 Z) g8 L - if(ret == -1)
' E5 v$ T7 Z& \( v7 e4 N3 e! v Q( X+ w - {- \& Z1 s; A* a/ ^. |
- printf("Tcp bind faiLED!\n");) P: A' \& R" Y5 M6 {' M
- return -1; g9 V9 k U. X( u# j* Q5 [/ n
- }5 s T& p) r3 ?( @& }' ?
- if(listen(*socket_found , 5) == -1)
4 G, ~( U5 b- ?) |$ ^5 l, s4 a L - {
& }+ e3 ^" u9 {4 R4 R3 B. b6 k - printf("Listen failed!\n");
2 ^0 j1 z4 W" u# b - return -1;
& }+ {5 d" w) k `3 I - }
, Q4 `7 y; N- I) t* ?, b* E6 E - return 0;, N6 d$ Q9 ?! k6 C! Q# D% ]( x& ^
- }
复制代码其中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);: K$ |! m5 V# v! x& R* J: A
- void * Thread_TCP_Web_Recv(void *arg)
1 ^! u/ B0 F _( d - {
# j( x) Y5 H7 w - 。。。" T V7 E2 _$ O9 ?" p
- while(1)
" A2 G+ A# \1 c# Z% V - {0 X$ r3 q) ?8 n; n G8 ]1 a
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
- k9 ^ F6 d- z$ X. K, W - printf("fd_socket_conn = accept()\n");
* X+ C/ ]& @4 i0 [( H4 O u - 。。。$ L0 B5 l' \& j, J
- recv(fd_socket_conn , recvbuf , 1000 , 0);* I, K T4 h+ y, V5 w- [
- }
0 W9 r; H6 Z- `9 s - 。。。
0 u) y) b; T/ z9 V9 q! |; W% ~ - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);; O+ H$ X- T4 ]
- pic_tmpbuffer = pic.tmpbuffer;
& [0 u& F0 F; J- H8 J% O - pic.tmpbytesused = buff.bytesused;
L* c" n f4 e" N n - pic_tmpbytesused = pic.tmpbytesused;
$ Y- q) C- j" w- n" a3 @ - pthread_cond_broadcast(&pct);
% T$ M* C; }8 ?$ ~4 R+ A - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;/ u/ W" R# g) a5 h1 M1 L
- pthread_cond_t pct;3 h6 i4 g4 M- Z' w- W2 Z- R7 r
- int main(int argc, char* argv[])
) H5 R _) s8 P8 @ P4 o, L5 R. G - {
. T3 z) R3 O- Y - ...
% z7 I; z {5 a - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
/ c v' s! h& w+ L4 h - pthread_mutex_init(&pmt , NULL);
0 f v* v* r$ f) q; s - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
3 W) B/ a0 j7 g. s - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);0 K6 o- F8 A8 I$ U2 B l. ^: L6 E: F
- ...
% `5 c# D# E0 n - while(1); H# ]% X+ q a% p$ z7 S2 r" _
- {0 S6 A6 F+ I0 s/ V2 W* _- R! f
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
6 {) J1 R1 r% u6 D - ...( @4 x9 x i% M
- }
* H) V2 n# r" h5 Q! ?3 T - ...! h1 x8 a" J$ u& T# g5 O' F3 D
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">6 Z- K& b) P1 S$ D* m4 f/ E4 F
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \$ ~. j5 ^ ^2 ~8 V/ @% x3 \
- "Server: MJPG-Streamer/0.2\r\n" \
2 k v7 B" ~( S A - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
! g; O: r) o4 N6 ^0 N - "Pragma: no-cache\r\n" \! G4 ^/ h: Q5 U, T) A7 {0 d& n
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
9 T( G$ I/ c( ~. `- o7 z% v - #define BOUNDARY "boundarydonotcross"" N' k6 E3 ?% E) r ~' D' Z' x! N
- printf("preparing header\n");. ?. {7 P6 `- m0 c7 |$ S2 J5 q
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
" Z* d3 b0 l K& F' ?, H' [ - "Access-Control-Allow-Origin: *\r\n" \
+ V$ F$ n6 C8 g. w( x - STD_HEADER \& z W) E3 U e$ e- w5 H \- D
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
$ b8 c; x$ r( H" C* E- |6 g! T$ W3 W - "\r\n" \8 j8 G0 d; h4 [3 |/ \7 h: S
- "--" BOUNDARY "\r\n");" U$ q3 M) s# C0 R% O
- if(write(fd, buffer, strlen(buffer)) < 0)8 U+ K" i o, B5 K- c- L# J1 `) t
- {+ a9 f, D, r5 [- {
- free(frame);
8 ?( t2 {$ O+ D S/ H( { B. l - return;
! o/ d+ T$ u! |# q' O: F - }
复制代码发送完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" \
4 Q! n2 `4 V# y" V3 Y - "Content-Length: %d\r\n" \
+ ^' L. b( v {, K4 @/ e) |- ^! t - "X-Timestamp: %d.%06d\r\n" \# F6 ^5 g: D, n
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
0 Z3 Q$ f+ a; S" n. j% I( H - printf("sending intemdiate header\n");0 @: b. g2 H( N- e# x& P
- if(write(fd, buffer, strlen(buffer)) < 0)- v; p3 ~, C" \
- break;$ s& t' u; |8 w7 L$ A2 @
- printf("sending frame\n");2 J: b9 Q' K! J' x: N: X+ ?
- if(write(fd, frame, frame_size) < 0)' D+ t8 c' z( ?# z- ~, r
- break;
% }9 ]! ]7 G( F# m ` - printf("sending boundary\n");' W7 ]6 ~7 G; Q
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ _8 M; x6 V0 ?: m. x
- if(write(fd, buffer, strlen(buffer)) < 0)1 ~. h" s. Z3 d! v0 h' ?
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
( T( F+ z* `7 K3 x1 e. X: ^; B
0 s8 Z4 c6 K. ?/ B0 |; d& {! D二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:8 D5 y# U; y( J
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port): j* {* p, |! W
- {' o0 a" k" E* c; G- _' C2 o
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
' {% [% G! N$ ?3 x+ A+ B - if(*socket_found == (~0))! O) q0 L5 g( s
- {3 `5 a* j3 w$ v5 c, `) x; ~3 ?& m
- printf("Create udp send socket failed!\n");
. s3 _. X9 Y3 a g$ b3 T9 p9 M1 G - return -1;
3 b" C1 I2 F9 { - }
) e3 k5 P6 f9 G5 ^& t3 Y& p - addr->sin_family = AF_INET;
2 ~+ R* I9 d$ _8 H: B - addr->sin_addr.s_addr = inet_addr(ip);
+ `/ |& A, u' Y/ U% _4 \# s1 O - addr->sin_port = htons(port);
$ F. c/ {2 r u8 E; T - memset(addr->sin_zero, 0, 8);
' H5 P: b3 o3 r9 I' |; s3 I5 V8 y - return 0;8 ]4 N0 |/ R4 J5 u
- }
复制代码
5 k/ c5 M" d* l7 h# {9 D
. K& f: Z A e6 f8 L1 x1 F而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:. |' P/ v- v- |) e+ c/ @
- Q5 U1 j! B2 u" \/ O5 ]) ~/ e5 N4 Z/ Q, k0 {
- while(fend > 0); k! C* k* b) u! l \1 Q4 l
- {
8 K3 _8 m: l0 Z6 u% y - memset(picture.data , 0 , sizeof(picture.data));
: \' x! b3 N- S6 R- c# k6 D - fread(picture.data , UDP_FRAME_LEN , 1, fp);
+ N3 \- E$ I. y0 @) j - if(fend >= UDP_FRAME_LEN)
+ [7 c# t! O$ @5 y* D6 r% y# b - {
) \$ k7 `! v3 B! Z - picture.length = UDP_FRAME_LEN;
% R1 x/ F" @6 ~( X - picture.fin = 0;6 x/ \; q ]! \ ~) }9 t' O
- }' ~- Y# r' F1 F- F4 X! ^
- else
# [$ G. ^' j6 t* X) j: Q8 l% a - {
8 D3 G) }/ o* B9 X: H# ?+ p - picture.length = fend;) Y+ z; b* O. I8 j! b# [# M7 i
- picture.fin = 1;& ?8 w" e0 `- X+ N9 o0 U% U0 U8 G9 ^
- }
& B0 ` E4 `3 y- J - //printf("sendbytes = %d \n",sendbytes);
& d2 N6 `( L# q, J6 \& v - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
& U* w- P0 h0 x" k: n - if(sendbytes == -1)
5 G' [& {! @ C }- U5 N - {, E% N9 N* x! O2 _$ p1 |
- printf("Send Picture Failed!d\n");
+ w( D/ z+ K( d3 z/ L - return -1;7 }# [7 K* k$ C' k3 x* K8 [
- }' M9 N5 r& @+ b% R1 t; a% j9 A
- else9 W4 K& T" u( u x1 l8 w1 h7 j
- {
/ ]; n, h, J( X6 {+ D - fend -= UDP_FRAME_LEN;" L$ o" r* {, K3 _
- }
! `) L& _2 E! \. x$ I3 G - }
复制代码
3 K! q3 R8 [/ [$ \5 l i& T
5 \9 d3 K3 P, F! v8 N
0 N/ W6 {8 c- N6 c3 k4 G8 w; ~' `$ ^) j4 u8 a k! Q
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |