------------------------------------------------------------
author:hjjdebug
date: 2024年 09月 08日 星期日 09:28:15 CST
description: 最简单的http_post 客户端c代码
------------------------------------------------------------
背景: 一个视频录制项目,要求每秒中向服务器汇报当前视频bit率,音频bit率,
起初用的是Qt 的QNetworkAccessManager 对象, 版本QT5.14.0, 它每post一次状态,就会有4处内存泄漏
为88*3+8=272 bytes, 啊!我怎么知道的? 我有照妖镜看着它呢.并不是deleteLater 所能解决的.
虽然泄漏并不严重,但每小时近1M, 每天23.5M,还是不爽!
所以决定放弃qt 对象, 自己写 http_post 客户端. 下面转入正题.
------------------------------------------------------------
http_post 请求有2个要素,
第一个是请求的url.
第二个是你要post 的内容.
请求的url 会被分割为IP,端口,资源位置.
然后url 连同post 的内容message 按照http协议一块打包成request.
最后用tcp 把request 向服务器发送,再从服务器接受响应就可以了.
------------------------------------------------------------
具体还可以划分为以下步骤:
//分割url,提取出host,port,resouce, 参考SplitUrl()
//与服务器建立连接
//构建http 请求,将host:port resource连同post 的内容message 按照http协议一块打包成request.
//发送请求
//接受响应
//关闭套接字
这也是程序主流程:
if(!SplitUrl(url,host,&port,resource)) return 1;
if(!ConnectToServer(host, port)) return 1;
request = ConstructRequest(resource,host, port, message);
bool res=send(request.c_str());
res=receive();
close(sockfd);
具体实现请参考代码:
#include <arpa/inet.h> // for inet_pton
#include <netinet/in.h> // for sockaddr_in, htons
#include <stdio.h> // for printf, perror, snprintf, NULL
#include <stdlib.h> // for exit, atoi
#include <string.h> // for memset, strcpy, strlen, strcmp
#include <sys/socket.h> // for AF_INET, connect, recv, send, socket, SOCK_S...
#include <sys/time.h> // for gettimeofday, timeval
#include <unistd.h> // for close
#include <string> // for string
using namespace std;
int sockfd;
//const char *url="http://192.168.6.50:89/api/daemon/record/v1/channel/";
bool SplitUrl(const char *url, char *host, int *port, char *resource)
{
const char *p;
char buf[64];
char *dst;
//查找协议
for(p=url,dst=buf;*p!=0;p++,dst++)
{
if(*p==':'){p++;break;}
*dst=*p;
}
*dst=0;
if(strcmp(buf,"http")!=0) return false;
p++;
p++; //跳过<//>
//填充ip
dst=buf;
for(;*p!=':'&&*p!='/';p++)
{
*dst++=*p;
}
*dst=0;
strcpy(host, buf);
if(*p==':')
{
p++;
//计算port
dst=buf;
for(;*p!='/';p++)
{
*dst++=*p;
}
*dst=0;
*port=atoi(buf);
}
strcpy(resource,p);
return true;
}
bool ConnectToServer(char *host, int port)
{
//创建tcp 套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
printf("创建socket失败\n");
exit(1);
}
printf("-> 创建socket成功\n");
//建立连接
struct sockaddr_in serv_addr;
memset(&serv_addr, '0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port); // HTTP 默认端口为 80
// 将IP地址从点分十进制转换为网络字节顺序
if(inet_pton(AF_INET, host, &serv_addr.sin_addr) <= 0)
{
printf("IP addr format error\n");
exit(1);
}
// 连接到远端服务器
int connectid = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
if (connectid == -1) {
printf("连接远端服务器失败\n");
exit(1);
}
printf("-> 连接远端服务器成功\n");
return true;
}
// resource 是url资源地址
std::string ConstructRequest(char *resource, char *host, int port,const char *message)
{
//构建http 请求
char request[1024];
int msg_len=strlen(message);
snprintf(request, 1024,
"POST %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"\r\n"
"%s",resource,host,port,msg_len,message);
//这里std::string会自动分配内存
std::string str(request);
return str;
}
bool send(const char *request)
{
int request_len=strlen(request);
//发送请求
int total = 0;
int bytesleft = request_len;
while(total < request_len) {
int n = send(sockfd, request+total, bytesleft, 0);
if (n == -1) { // 处理错误
perror("send error");
return false;
}
total += n;
bytesleft -= n;
}
return true;
}
string receive()
{
std::string str;
std::string nullStr;
char response[1024];
memset(response, 0, sizeof(response));
int n,size;
int remain,left,block_size,read_size;
char *p;
//接受什么时候停止呢? 数据量小(post 的响应),一次就可完全接受
//数据量大时,可以一直接受,直到对方关闭
//较好的办法是依据协议,根据数据块长度来关闭
n = recv(sockfd, response, sizeof(response)-1, 0);
if (n < 0) {perror("receive error"); return nullStr;}
p=strstr(response,"\r\n\r\n");//后边跟block_size
p+=4;
sscanf(p, "%x", &block_size); //根据协议,获取块协议大小,此例是0x2c
for(size=0;*p!='\r';p++)
{
size++; //计算长度描述本身所占大小,此例2c就是2个byte
}
//p+size+2-response 是协议头长度,n是本次读取的长度,相减为本次读取的数据体长度
remain=block_size-(n-(p+size+2-response)); //计算剩余数据体字节数,为0或负数表示已读完
printf("n is %d,remain:%d\n",n,remain);
response[n]=0;
str+=response;
left=remain;
read_size=left>(int)sizeof(response)-1 ? (int)sizeof(response)-1 : left;
while(read_size>0)
{
n =recv(sockfd, response, read_size, 0);
if (n < 0) {perror("receive error"); return nullStr;}
// printf("n is %d\n",n);
response[n]=0;
str+=response;
left-=n;
read_size=left>(int)sizeof(response)-1 ? (int)sizeof(response)-1 : left;
}
return str;
}
long getTimeUs()
{
struct timeval tv;
gettimeofday(&tv,NULL);
long res=tv.tv_sec*1000000+tv.tv_usec;
return res;
}
int http_post(const char *url, const char *message) // 0 成功
{
char host[16];
int port=80;
char resource[256];
string request;
for(int i=0;i<1;i++) //循环,仅为测试
{
// resource 是url 资源位置,例如:/api/daemon/record/v1/channel/100,服务器会用它
if(!SplitUrl(url,host,&port,resource)) return 1;
printf("connet begin at %ld\n",getTimeUs());
if(!ConnectToServer(host, port)) return 1;
printf("connet end at %ld\n",getTimeUs());
//土洋结合,用上了c++的类,它自己管理内存,还是挺方便的
// string request;
//如果request 是括号内定义的局部变量,
//出了大括号,request 会被析构,在这个for循环中,它会被析构n次
//如果request 是括号外定义的变量,
//在等号赋值时,这个request会先被析构(此时释放内存),再被赋值
//就是说虽然流程大体一样,但释放内存的时机不同.
//局部变量是在出作用域时被释放,虽然它在=赋值时也调用析构,但已无内存可释放
//非括号内局部变量是在等号赋值时释放内存的.
request = ConstructRequest(resource,host, port, message);
//这个send 是我们写的send,就是其名称是send后还和参数组合共同构建一个函数名称
//不同于函数内的那个send(属于c函数的send),
//编译器是分得清的.不会报错.
bool res=send(request.c_str());
printf("send result:%s\n",res==true? "ok":"fail");
printf("receiv begin at %ld\n",getTimeUs());
string recv_str=receive();
printf("receiv end at %ld\n",getTimeUs());
printf("recv:%s\n",recv_str.c_str());
//关闭套接字
close(sockfd);
}
return 0;
}
//测试服务器地址及发送的消息块
const char *url="http://192.168.6.50:89/api/daemon/record/v1/channel/100";
const char *message=R"--({"audioBit": 195, "videoBit": 7014})--";
int main()
{
http_post(url, message);
return 0;
}
//如果你要拿来用,改一下url及message 即可,当然也可以放到命令行上,就是小意思了.
//看一下程序执行结果.
./http_post
connet begin at 1727229556873479
-> 创建socket成功
-> 连接远端服务器成功
connet end at 1727229556874010
send result:ok
receiv begin at 1727229556874089
n is 296,remain:-5
receiv end at 1727229556876663
recv:HTTP/1.1 200
Date: Wed, 25 Sep 2024 01:59:15 GMT
Server: Apache/2.4.59 (codeit) OpenSSL/3.0.13+quic
Vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
2c
{"code":400,"errorMsg":"没有这个任务"}
0
//结果分析
//远程服务器是局域网中的一个IP
创建连接用时4010-3479=531us=0.5ms
发送用时: 4089-4010=79us=0.07ms (可忽略)
接收用时: 6663-4089=2574us=2.6ms