【网络编程】Linux自带socket库实现HTTP服务端代码

前言

前些天用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的代码(已加)上去

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值