探索http原理,使用C++基于socket自实现http

http是最常用的互联网协议。http协议是基于tcp协议的,今天我打算使用C++语言,基于tcp编程自己实现http。适用于linux及mac系统。windows的tcp编程我没使用,但原理都是一样的。如果对网络编程不熟悉的,可以先熟悉一下网络编程。通过此例子,一定会对http协议的理解更上一层楼。

首先得有一个socket套接字。

  int local_fd = socket(AF_INET, SOCK_STREAM, 0);
  if (local_fd == -1)
  {
       cout << "socket error!" << endl;
       exit(-1);
  }
  cout << "socket ready!" << endl;

socket函数是一种可用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源的函数。在头文件sys/socket.h中。

它有三个参数:
1.domain:协议域,常见的协议组用AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE . 协议族决定了socket的地址类型,在通信中必须采用相应的地址。这里我使用了AF_INET,表示使用ipv4。

2.type: 指定socket的类型。主要有流式套接字(SOCK_STREAM)、数据报式套接字(SOCK_DGRAM)。我使用了SOCK_STREAM流式套接字。

3.protocol:协议,常见的协议有IPPROTO_TCP、IPPTOTO_UDP、 IPPROTO_SCTP、IPPROTO_TIPC他们分别对应这TCP传输协议,UDP传输协议,STCP传输协议,TIPC传输协议.当protocol为0时,会自动选择type类型对应的默认协议。因为我第二个参数使用的是SOCK_STREAM,所以这里传0是自动使用tcp协议。

创建好套接字之后,我把它绑定到80端口,因为http的默认端口就是80。当然使用其它端口也可以。只不过使用别的端口的话,我们在浏览器请求时得自己在:后面拼上端口,稍显麻烦。

    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(port);  //绑定端口
    local_addr.sin_addr.s_addr =  INADDR_ANY ; //绑定本机IP地址

    //3.bind(): 将一个网络地址与一个套接字绑定,此处将本地地址绑定到一个套接字上
    int res = bind(local_fd, (struct sockaddr *)&local_addr, sizeof(local_addr));
    if (res == -1)
    {
        cout << "bind error!" << endl;
        exit(-1);
    }
    cout << "bind ready!" << endl;

然后就调用listen函数让我们创建的tcp套接字监听客户端请求。

    listen(local_fd, 10);
    cout << "等待来自客户端的连接...." << endl;

listen的第二个参数千万要注意了,它表示等待连接队列的最大长度。就是说同意时刻最多有10个客户端在等待连接服务器,第11个等待者会被拒绝。这个10并不是表示客户端最大的连接数为10, 实际上可以有很多很多的客户端。

然后我们就可以接收客户端的请求了。我采用了死循环,好像没有写死循环的退出,例子嘛,我也懒得改了。

    while (true)//循环接收客户端的请求
    {
        //5.创建一个sockaddr_in结构体,用来存储客户机的地址
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);
        //6.accept()函数:阻塞运行,直到收到某一客户机的连接请求,并返回客户机的描述符
        int client_fd = accept(local_fd, (struct sockaddr *)&client_addr, &len);
        if (client_fd == -1)
        {
            cout << "accept错误\n"
                 << endl;
            exit(-1);
        }

        //7.输出客户机的信息
        char *ip = inet_ntoa(client_addr.sin_addr);
        cout << "客户机: " << ip << " 连接到本服务器成功!" << endl;

        //8.输出客户机请求的信息
        char buff[1024] = {0};
        int size = read(client_fd, buff, sizeof(buff));
        cout << "Request information:\n"
             << buff << endl;
        cout << size << " bytes" << endl;

        //9.使用第6步accept()返回socket描述符,即客户机的描述符,进行通信。
        write(client_fd, message.c_str(), message.length());//返回message

        //10.关闭sockfd
        close(client_fd);
    }

就是不停的调用accept去接受客户端的连接,accept会阻塞函数的执行,当有tcp客户端连接到来的时候它才会返回。accept函数的返回值是一个新的socket套接字。这个套接字是专门用来和该连接的客户端交互的。每个客户端都会有一个专门的套接字。

我们用这个accept函数返回的新套接字调用read、write函数读取和写入数据,就相当于在和客户端交互了,就跟读写文件一模一样。但是在读写这个套接字的时候需要注意,http的数据是有固定格式的,一定要按照这个格式去读写才可以。

http请求数据包括请求行,消息头,消息正文三部分,而响应数据同样包括响应行,响应头,响应正文三部分。主要需要的是换行需要用\r\n表示还有就是有和正文之间有一个空行。

这里贴一下我测试时浏览器发来的请求数据

GET / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
sec-ch-ua: \"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\"\r\n
sec-ch-ua-mobile: ?0\r\n
sec-ch-ua-platform: \"macOS\"\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n
Sec-Fetch-Site: none\r\n
Sec-Fetch-Mode: navigate\r\n
Sec-Fetch-User: ?1\r\n
Sec-Fetch-Dest: document\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
Cookie: sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%22182d98f2ee11c1-032ff8224617bd2-1b525635-1296000-182d98f2ee2192d%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22url%E7%9A%84domain%E8%A7%A3%E6%9E%90%E5%A4%B1%E8%B4%A5%22%2C%22%24latest_search_keyword%22%3A%22url%E7%9A%84domain%E8%A7%A3%E6%9E%9

我给客户端回复的响应数据如下

    std::string message ="";
    message+="HTTP/1.1 200 OK\r\n";                                    //响应行
    message+="Content-Type:text/html\r\n";                             //响应头
    message+="server:Tengine \r\n";                                    //响应头
    message+="name:LiaoKun \r\n";                                      //响应头
    message+="\r\n";                                                   //空行
    message+="<html><head>Hello,World!</head></html>\r\n";             //正文

运行,在浏览器输入本机地址。我们看到了服务器发来的响应。

image-20220907112304939

完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>

#define port 80 //监听端口,可以在范围内自由设定
using namespace std;

int main()
{
    std::string message ="";
    message+="HTTP/1.1 200 OK\r\n";                                    //响应行
    message+="Content-Type:text/html\r\n";                             //响应头
    message+="server:Tengine \r\n";                                    //响应头
    message+="name:LiaoKun \r\n";                                      //响应头
    message+="\r\n";                                                   //空行
    message+="<html><head>Hello,World!</head></html>\r\n";             //响应体

    //1.创建一个socket套接字
    int local_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (local_fd == -1)
    {
        cout << "socket error!" << endl;
        exit(-1);
    }
    cout << "socket ready!" << endl;

    //2.sockaddr_in结构体:可以存储一套网络地址(包括IP与端口),此处存储本机IP地址与本地的一个端口
    struct sockaddr_in local_addr;
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(port);  //绑定端口
    local_addr.sin_addr.s_addr =  INADDR_ANY ; //绑定本机IP地址

    //3.bind(): 将一个网络地址与一个套接字绑定,此处将本地地址绑定到一个套接字上
    int res = bind(local_fd, (struct sockaddr *)&local_addr, sizeof(local_addr));
    if (res == -1)
    {
        cout << "bind error!" << endl;
        exit(-1);
    }
    cout << "bind ready!" << endl;

    //4.listen()函数:监听试图连接本机的客户端
    //参数二:监听的进程数
    listen(local_fd, 10);
    cout << "等待来自客户端的连接...." << endl;

    while (true)//循环接收客户端的请求
    {
        //5.创建一个sockaddr_in结构体,用来存储客户机的地址
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);
        //6.accept()函数:阻塞运行,直到收到某一客户机的连接请求,并返回客户机的描述符
        int client_fd = accept(local_fd, (struct sockaddr *)&client_addr, &len);
        if (client_fd == -1)
        {
            cout << "accept错误\n"
                 << endl;
            exit(-1);
        }

        //7.输出客户机的信息
        char *ip = inet_ntoa(client_addr.sin_addr);
        cout << "客户机: " << ip << " 连接到本服务器成功!" << endl;

        //8.输出客户机请求的信息
        char buff[1024] = {0};
        int size = read(client_fd, buff, sizeof(buff));
        cout << "Request information:\n"
             << buff << endl;
        cout << size << " bytes" << endl;

        //9.使用第6步accept()返回socket描述符,即客户机的描述符,进行通信。
        write(client_fd, message.c_str(), message.length());//返回message

        //10.关闭sockfd
        close(client_fd);
    }
    close(local_fd);
    return 0;
}

代码:https://github.com/kunliao/http.git

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值