前言
前些天用socket底层代码写了客户端发送数据到服务端的代码,C++构建HTTP客户端发送数据(POST方式),这些天写了个比较简单的服务端作为上一篇的姊妹篇,算是对上一篇中偷懒使用FLASK框架的小补充。
目前代码已上传github
HTTP服务端构建思路
一. 初始化socket对象
(1 )网络编程中的重要一步,必不可少的一步,创建Socket对象
//Create Socket
this->socket_fd = socket(AF_INET, SOCK_STREAM,0);
//其中AF_INET是socket地址类型的协议,这里AF_INET对应的地址类型是IPV4;此外还有AF_INET6,对应的是IPV6.
//SOCK_STREAM:一般来说HTTP传输都是数据流的,所以写HTTP底层的socket类型一般都是SOCK_STREAM
//第三个参数是传输协议,0代表着TCP协议,HTTP都是通常基于TCP的这就众所周知了。(大佬们别杠我HTTP3,我工地来的)
(2) 接下来要把socket对象的文件描述符fd,与一串地址相互绑定
struct sockaddr_in serveraddr;
serveraddr.sin_addr.s_addr = htons(INADDR_ANY);
serveraddr.sin_port = htons(this->port);
serveraddr.sin_family = AF_INET; //地址协议还是IPV4
//INADDR_ANY就是表示本机的所有IP,表示所有网卡ip的地址。
//htons函数目的是将主机字节序转换为传输过程中的网络字节序
/*
param1: 要绑定的socket对象的文件描述符fd
param2:要绑定的地址
param3:给这地址分多大空间,一般使用sizeof
return 0 : bind success
return -1: bind failed;
*/
int iRet_bind = bind(this->socket_fd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
(3) 在调用socket(),bind()之后会调用listen()来监听这个socket,如果这个客户端这时调用connect()发出连接请求,服务端就会收到这个请求。
/*
param1: 监听的socket对象的fd
param2:监听队列的长度
关于这个监听队列的长度,它是这样的,在TCP三次握手建立连接过程中,当服务端收到来自客户端的第一次握手的时候,会将这个TCP连接对应的客户端socket对象的fd放入到listen队列中。
*/
int iRet_listen = listen(this->socket_fd, MAX_LISTEN);
初始化完成,开始下一步的接收数据和处理
二.准备监听请求
(1)服务端执行了socket(), bind(), listen()之后,就会监听指定的socket地址,此时若TCP三次握手完成,则服务端会调用accept()函数,把当前客户端的socket_fd从listen队列中取出来,同时得到客户端的ip地址和对应端口。
struct sockaddr clientAddr;
socklen_t size = sizeof(struct sockaddr);
memset((char *)&clientAddr, 0, sizeof(struct sockaddr));
/*
param1: 服务端当前的监听Socket对象的文件描述符fd,即上面初始化Socket对象时得到的fd
param2: 客户端的地址信息,包括ip和port
param3:给这块地址多大空间
return:返回的是TCP连接socket_fd,也称为client_fd,这是新生成的,和监听fd不一样
*/
int iRet_clifd = accept(lisfd, &clientAddr, &size);
三.处理数据
因为我举例子的是post方式,所以要对发送来的httpheader和data进行解析,一般发来的数据的过程是这样的:
1.服务端先接收httpheader。
2.服务端返回100响应给客户端,客户端收到100响应。
3.客户端发送数据主体data给服务端。
//处理方式大同小异,get和post都是传输方式,其实并无区别。
POST /test HTTP/1.1\r\n
Accept: */*\r\n
Accept-Language: cn\r\n
User-Agent: Mozilla/4.0\r\n
Host: 127.0.0.1:8522\r\n
Cache-Control: no-cache\r\n
Connection: Keep-Alive\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 14\r\n
\r\n
(1)先获得发送的方式是post还是get
int pos=0;
const char* recvMessage = recv_buffer;
for (;;pos++) //看看是GET还是POST方式
{
if(*(recvMessage+pos) == ' ') break;
}
char method[4]={0};
strncpy(method, recvMessage,pos);
this->requestMethod = method;
(2) 继续解析
int parseHeader(const char* recvMessage, int pos)
{
/*
提取router
*/
const char* c_router = recvMessage+pos+1;
for (;;pos++)
{
if(*(recvMessage+pos+1) == ' ') break;
}
strncpy(this->router, c_router, pos-4); //把router提取出来
// std::cout<< this->router <<std::endl;
/*
提取httpversion
*/
int current_pos = pos+1;
for (;;)
{
if(*(recvMessage+pos-2) == '\r' && *(recvMessage+pos-1) == '\n') break;
pos++;
}
strncpy(this->http_version, recvMessage+current_pos+1, pos-current_pos-3);
// std::cout << this->http_version << std::endl;
/*
提取 Content_Length
*/
int pos_contLength = count(recvMessage, "Content-Length: ") + strlen("Content-Length: ");
int size_contLength=0;
for (;; )
{
if(*(recvMessage+pos_contLength+size_contLength+2) == '\n' && *(recvMessage+pos_contLength+size_contLength+1) == '\r') break;
size_contLength++;
}
char contentLength[128] = {0};
strncpy(contentLength, recvMessage+pos_contLength, size_contLength+1);
this->content_length = atoi(contentLength);
/*
提取connection状态
*/
int pos_connection = count(recvMessage, "Connection: ") + strlen("Connection: ");
int size_connection = 0;
for (;; )
{
if(*(recvMessage+pos_connection+size_connection+2) == '\n' && *(recvMessage+pos_connection+size_connection+1) == '\r') break;
size_connection++;
}
strncpy(this->connection, recvMessage+pos_connection, size_connection+1);
return pos;
}
这里是解析httpheader的方法,如果还需要获取其他,也是大致的思路。
(3)得到对应的router,就可以根据不同的router执行不同的函数了,假设我的router是“/upload_image”,data是存储这base64格式的图片数据的json对象。
if(strcmp(this->requestMethod, "POST") == 0)
{
if(strcmp(router , "/upload_image")==0)
{
upload_image(content);
if(strcmp(this->connection, "Close")==0 || strcmp(this->connection, "close")==0) close(this->clifd);
}
else
{
std::cerr << TAG_ERROR << "The request router isn't exit!" <<std::endl;
}
}
else if(strcmp(this->requestMethod,"GET")==0)
{
std::cout << "call func() for GET func" <<std::endl;
}
else
{
std::cerr << TAG_ERROR << "Unkown request!" <<std::endl;
return ;
}
return ;
接下来是接收数据主体内容content
1.收到httpheader后,服务端向客户端发送100响应,表示客户端可以发送接下来的数据了
try{
nlohmann::json reponse_100;
reponse_100["status_code"] = SEND_YOUR_CONTENT;
reponse_100["message"] = "'Start to send content!'";
// send(this->clifd, )
send(this->clifd, reponse_100.dump().c_str(), reponse_100.dump().size(),0);
}
catch (nlohmann::json::exception err) {
std::cerr << "[ERROR] exception id: " << err.id <<
" message: " << err.what() << std::endl;
}
2.接收数据主体
//分配一个buffer空间存储即将接收的数据
char tmp_content_buffer[CONTENT_RECV_BUFFER_SIZE]={0};
char* content = (char*)malloc(MULTI_ADD_TO_CONTENT);
memset(content, 0, MULTI_ADD_TO_CONTENT);
//因为要考虑到接收数据的能力问题,所以当数据大小超过接收窗口的大小就分多次接收
if(content_length <= CONTENT_RECV_BUFFER_SIZE){
recv(this->clifd, content, HEADER_RECV_BUFFER_SIZE, 0);
}else
{
//记录下需要接收的次数
int total_times = (this->content_length / CONTENT_RECV_BUFFER_SIZE) + ((this->content_length % CONTENT_RECV_BUFFER_SIZE > 0) ? 1 : 0);
for (int i = 1; i <= total_times; i++)
{
int recv_size;
if(i == total_times) //最后一次接收
{
int last_buffer_size = (this->content_length % CONTENT_RECV_BUFFER_SIZE);
char last_recv_buffer[last_buffer_size]={0};
recv_size = recv(this->clifd, last_recv_buffer, last_buffer_size, 0);
strncat(content, last_recv_buffer,last_buffer_size);
// std::cout<< last_recv_buffer;
}else
{
recv_size = recv(this->clifd, tmp_content_buffer, CONTENT_RECV_BUFFER_SIZE, 0);
strncat(content, tmp_content_buffer,CONTENT_RECV_BUFFER_SIZE);
// std::cout<< tmp_content_buffer;
}
std::cout << TAG_INFO << "Current / Total: " << i << " / " << total_times
<< ", Recv Size: " << recv_size << " Byte(s)." << std::endl;
}
}
四.反馈执行后的响应信息给客户端
这里用的是上传图片的例子,
void myHttpServer::HttpServer::upload_image(const char* content)
//Create return json object
nlohmann::json return_j;
if(strlen(content)==0) //数据为空
{
return_j["status_code"] = JSON_DATA_IS_NULL;
return_j["message"] = "'Json data is null.'";
retData(return_j.dump());// retData的代码看下面
return ;
}
std::cout << content << std::endl;
//Get the request json object
nlohmann::json request_j = nlohmann::json::parse(content);
int nums = request_j["img_num"];
if(nums <= 0)
{
return_j["status_code"] = IMG_NUM_IS_ZERO;
return_j["message"] = "'Argument \'img_num\' of Json date is zero.'";
std:: cout << TAG_ERROR << return_j["message"] <<std::endl;
retData(return_j.dump());
return ;
}
for (int i = 0; i < nums; i++)
{
//transfer base64 to img mat
char tmp_img_name[MINIMUM_SIZE]={0};
sprintf(tmp_img_name, "%d_img_base64", i);
std::string b64_img = request_j[tmp_img_name];
cv::Mat res_img;
base64ToImage(b64_img, res_img);
//save img mat
char uuid[37]={0};
char uuid_name[MINIMUM_SIZE*4] = {0};
sprintf(uuid_name, "./UploadImg/%s.jpg", random_uuid(uuid));
cv::imwrite(uuid_name,res_img);
}
return_j["status_code"] = 200;
return_j["message"] = "Upload Success!";
std::cout<< TAG_INFO << "Return content size:" << return_j.dump().size() << std::endl;;
std::cout << TAG_INFO << return_j["message"] << std::endl;
retData(return_j.dump());
return ;
}
inline void retData(std::string return_data_dump)
{
if(return_data_dump.size() == 0){
return ;
}
const char* header_template =
// Request Line
"HTTP/1.1 200 OK\r\n"
"Server: HttpServer 1.0\r\n"
"Connection: Close\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %lu\r\n"
"\r\n"
"%s";
char header[NORMAL_BUFFER_SIZE] = {0};
sprintf(header,header_template, return_data_dump.size(), return_data_dump.c_str());
int send_size = send(this->clifd, header, NORMAL_BUFFER_SIZE,0);
if(send_size < 0){
std::cerr << TAG_ERROR << "Send size:" << send_size << std::endl;
}
}
大致流程是这样的,代码已上传到github,后期再慢慢补充线程池(已加)和epoll的代码(已加)上去