在远程监控领域,MJPG码流传输虽然是一个经典话题,但实际落地时往往需要掌握一些关键技巧。最常见的两种实现方案——基于浏览器的HTTP网页服务器以及基于UDP的上位机,各有特点。本文以飞凌嵌入式OKMX8MP-C开发板为例,系统环境为Linux5.4.70+Qt5.15.0,详细梳理这两种MJPG流传输方案的完整实现流程。

先来看两种方式的差异。UDP上位机传输效率较高,代码编写也比较顺手,但客户端必须安装特定软件,跨平台兼容性是个难题。HTTP网页服务器则恰恰相反——客户端仅需一个浏览器,无论电脑、手机、平板,还是不同操作系统,都能轻松访问,省去了软件安装的麻烦。不过HTTP传输效率相对偏低,而且服务器端需要处理更复杂的HTTP协议。两种方案各有短板,对于嵌入式开发者来说,两者都需要熟练掌握。
一、HTTP网页服务器
首先拆解HTTP方式的实现。开发板端需要完成的首要任务是建立TCP服务器。参考下面这段代码:
int TCP_Server_Found(socklen_t* socket_found , char* ip , int port) {
struct sockaddr_in servaddr;
socklen_t addrsize = sizeof(struct sockaddr);
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(ip);
servaddr.sin_port = htons(port);
int ret;
IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
printf("Create socket error: %s (errno :%d)n",strerror(errno),errno);
return -1;
}
int on = 1;
if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
printf("setsockopt errorn");
}
ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
if(ret == -1)
{
printf("Tcp bind faiLED!n");
return -1;
}
if(listen(*socket_found , 5) == -1)
{
printf("Listen failed!n");
return -1;
}
return 0;
}
这里有一个实用小细节:setsockopt()并非强制要求,主要用来规避套接字建立时可能出现的常见异常。TCP服务器建立完成后,返回的socklen_t类型实参在后续流程中会用到。
接下来,HTTP服务器的TCP操作需要单独开启一个轮询线程,以便客户端发起握手。注意listen()只需执行一次,而accept()和recv()必须放在死循环中:
pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
void * Thread_TCP_Web_Recv(void *arg) {
。。。
while(1) {
fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
printf("fd_socket_conn = accept()n");
。。。
recv(fd_socket_conn , recvbuf , 1000 , 0);
}
。。。
}
MJPG帧通过Grab操作获取。这里有一个关键点:帧数据在Grab线程中写入,在TCP线程中读取。多线程同时访问共享资源,不加锁必然导致数据错乱。因此每次写入时必须先加锁,写完后及时解锁:
pthread_mutex_lock(&pmt);
pic_tmpbuffer = pic.tmpbuffer;
pic.tmpbytesused = buff.bytesused;
pic_tmpbytesused = pic.tmpbytesused;
pthread_cond_broadcast(&pct);
pthread_mutex_unlock(&pmt);
互斥锁和条件变量在使用前需要先初始化:
pthread_mutex_t pmt;
pthread_cond_t pct;
int main(int argc, char* argv[]) {
...
TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
pthread_mutex_init(&pmt , NULL);
pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
...
while(1)
{
V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
...
}
...
}
发送环节也有讲究。在发送图片数据之前,必须先发送HTTP标准头部,这相当于为后续数据传输铺好道路:
#define STD_HEADER "Connection: closern"
"Server: MJPG-Streamer/0.2rn"
"Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0rn"
"Pragma: no-cachern"
"Expires: Mon, 3 Jan 2000 12:34:56 GMTrn"
#define BOUNDARY "boundarydonotcross"
printf("preparing headern");
sprintf(buffer, "HTTP/1.0 200 OKrn"
"Access-Control-Allow-Origin: *rn"
STD_HEADER
"Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "rn"
"rn"
"--" BOUNDARY "rn");
if(write(fd, buffer, strlen(buffer)) < 0)
{
free(frame);
return;
}
标准头部发送完毕后,紧接着发送内容头部,其中Content-Type设置为image/jpeg。需要注意的是,HTTP协议中图片类型并不只有这一种。内容头部之后跟着图片数据以及boundary结尾标记,这样一帧完整的HTTP数据包就组装完毕,浏览器即可正确显示该图片:
sprintf(buffer, "Content-Type: image/jpegrn"
"Content-Length: %drn"
"X-Timestamp: %d.%06drn"
"rn", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
printf("sending intemdiate headern");
if(write(fd, buffer, strlen(buffer)) < 0)
break;
printf("sending framen");
if(write(fd, frame, frame_size) < 0)
break;
printf("sending boundaryn");
sprintf(buffer, "rn--" BOUNDARY "rn");
if(write(fd, buffer, strlen(buffer)) < 0)
break;
需要说明的是,TCP服务器发送MJPG流时处于死循环状态,客户端在发送GET指令后将持续接收图片数据。此时客户端相当于忙等待,无法再发送其他指令。从用户视角看,网页会一直显示加载状态——这其实是正常的运行现象。
二、UDP上位机
UDP方式的实现则简洁得多。首先创建一个UDP Socket:
int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port) {
*socket_found = socket(AF_INET, SOCK_DGRAM, 0);
if(*socket_found == (~0))
{
printf("Create udp send socket failed!n");
return -1;
}
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr(ip);
addr->sin_port = htons(port);
memset(addr->sin_zero, 0, 8);
return 0;
}
UDP方式发送文件比HTTP省事很多——只需将文件按固定长度切分成帧,逐帧发送即可:
while(fend > 0) {
memset(picture.data , 0 , sizeof(picture.data));
fread(picture.data , UDP_FRAME_LEN , 1, fp);
if(fend >= UDP_FRAME_LEN) {
picture.length = UDP_FRAME_LEN;
picture.fin = 0;
} else {
picture.length = fend;
picture.fin = 1;
}
//printf("sendbytes = %d n",sendbytes);
sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
if(sendbytes == -1) {
printf("Send Picture Failed!dn");
return -1;
} else {
fend -= UDP_FRAME_LEN;
}
}
至此,两种方式的核心代码与实现思路均已清晰呈现。HTTP方式需要处理复杂的HTTP协议头部以及多线程同步问题,而UDP方式则更加简洁高效。在实际项目中究竟选择哪种方案,取决于你的应用场景:如果对跨平台移植性要求较高,建议优先考虑HTTP网页服务器;如果传输效率是主要瓶颈,那么UDP上位机无疑是更合适的选择。
