网络编程——HTTP协议

目录

一、HTTP协议的应用

大家平时使用电脑经常会通过浏览器来上网冲浪,一般打开浏览器之后需要在地址栏输入网站的域名,比如百度网站的域名是www.baidu.com,如果电脑有网络则可以跳转到对应的网站,这其实是依赖于互联网应用层的HTTP协议实现。

HTTP协议也称为超文本传输协议(Hypertext Transfer Protocol)是一个简单的基于请求-响应的协议,它通常运行在TCP之上,所以属于应用层协议。

浏览器基本都是基于该协议与服务器通信的,HTTP是基于 TCP/IP 协议来传输数据的,超文本的意思是指HTTP协议不只是支持普通数据,还支持传输文件、图片以及具有超链接功能。

想要了解HTTP协议的详细内容,用户可以通过RFC规范中下载HTTP协议标准,具体如下:

HTTP通信实际上就是按照HTTP协议的规范,将TCP数据段进一步封装为HTTP数据包发送给对方,以及将对方发来的HTTP数据包按规范逐次拆解的过程。

二、HTTP请求头部

1.请求行 \r\n 第一部分

2.头部字段1 \r\n 第二部分

3.头部字段2 \r\n

4.\r\n 第三部分

5.请求包体 \r\n 第四部分

可以看到,整个 HTTP 请求头部由请求行、请求头部和请求包体组成,注意HTTP请求头部必须以"\r\n"回车换行符作为结束。

另外,请求行必须以 "\r\n" 结束,请求头部中的每个头部字段也需要使用 "\r\n" 进行分隔!

(1)请求行

(2)首部字段

HTTP 首部字段是构成 HTTP 报文的要素之一。在客户端与服务器之间以HTTP协议进行通信的过程中,无论是请求还是响应都会使用首部字段,它能起到传递额外重要信息的作用。

使用首部字段是为了给浏览器和服务器提供报文主体大小、所使用的语言、认证信息等内容。

可以看到,客户端在发送请求消息的时候必须包含Host字段,该字段指定主机的域名,该字段格式如下:

客户端必须指定Host字段: Host : 主机域名 \r\n 比如 "Host: www.baidu.com\r\n"

三、HTTP请求方法

HTTP协议的请求方法常见的有 GET、HEAD、POST、PUT 等,每种请求方法的规则各不相同:

要注意,HTTP 并非一种强制协议,HTTP 有很高的拓展性,因此在实际应用中,客户端发起请求时所使用的的请求方法是由当时具体提供服务的服务端决定的,并不一定遵循上述约定。

另外,一般向服务器发起请求时需要指定服务器的URL,URL指的是统一资源定位符(英文全称是Uniform Resource Locator),用来表示所请求的资源在服务主机中的路径,格式如下:


四、HTTP协议版本

可以知道,HTTP协议的版本是由主版本号+次版本号组成的,最初的版本是HTTP/0.9,后期逐渐发展出其他版本,比如HTTP/1.0和目前最为主流的HTTP/1.1,但是还存在更高版本的HTTP协议。

HTTP协议的版本格式: HTTP + / + major + '.' + minor 比如HTTP/1.0 HTTP/1.1 HTTP/2.0

因为HTTP协议是基于TCP协议实现的,所以客户端和服务器需要建立连接才可以实现双向通信,HTTP/1.1版本引入了持久连接,就是建立TCP连接之后默认是不关闭,这样TCP套接字可以被多个请求复用。

练习:利用http协议向某个时间服务器/天气服务器发送请求,把请求的数据进行接受并输出。提示:一般需要获取服务器的相关的API接口。 另外,由于使用http协议进行数据请求,http是建立在tco之上,所以需要先利用tcp协议连接到服务器,然后才可以发起请求,

需要知道服务器的IP地址和端口,http端口是固定的,端口是80,服务器IP地址则可以选择利用gethostbyname()获取,也可以选择手动ping。

五、HTTP响应头部

由于HTTP协议是基于请求-响应的协议,所以客户端向服务器发起请求之后,服务器会把相应的返回给客户端,服务器返回的是固定格式的响应报文。

与请求报文类似,响应报文中的状态行、以及响应头部中的各个头部字段之间都用 "\r\n" 相区隔,整个响应头部也以"\r\n"作为结束,响应包体包含了从服务器获取的文件、图片或其他网络资源。

作业:了解HTTP的端口,以及HTTP和HTTPS的区别。

六、HTTP状态编码

服务器响应的数据报文中存在状态码,用户通过状态码可以知道服务器的响应状态,在HTTP标准中有对应的状态码类型,如下:

平时访问浏览器时可能会遇到网页找不到或者资源找不到的情况,一般页面就会显示404错误,其实是由于客户端发起请求的请求报文中存在问题导致的。

当客户端接收到服务器的响应报文之后,可以根据响应报文中的状态码分析可能存在的问题并针对性解决。

七、HTTP基本应用

目前很多企业都推出很多开放的API接口,这些接口一般都是以HTTP协议进行通信,比如天气、物流、AI应用等,用户可以直接调用这些平台的接口来获取数据。并且其数据都是以网络通用的格式封装,一般都是采用JSON格式。

(1)获取天气

可以使用企业提供的天气API接口,比如比较知名的天气相关的API有心知天气,一般需要用户注册平台账号,然后进行个人认证、认证成功之后可以创建应用,得到应用对应的密钥,再通过密钥区访问API接口。




(2)手机验证

一般产品在登录或者注册时,都需要通过手机接受验证码进行验证,一般手机验证都需要和短信服务商合作,一般情况下,一些大平台想要使用短信服务需要企业认证,所以需要寻找一些可以支持个人开发者使用的短信服务平台,比如互亿无线平台。

想要使用短信服务,一般短信都需要签名,可以理解为短信标签,【xx银行】,一般签名需要审核,可以选择使用平台提供的默认签名。


八、练习

1.

c 复制代码
/*
 * Copyright (c)
 * 
 * date: 2025-8-10
 * 
 * author: Charles
 *
 * function name : getTem.c
 *
 * function: 访问新知天气平台,获取实时天气信息
 *
 */
//接口地址
//https://api.seniverse.com/v3/weather/now.json?key=your_api_key&location=beijing&language=zh-Hans&unit=c

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <netdb.h>

#define PORT 80
#define KEY "xxxxxxxxx"

int main(int argc, char const *argv[])
{   
    if(argc != 2)
    {
        fprintf(stderr, "argc error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
    }
	//1.创建TCP套接字
	int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
	if (tcp_socket == -1)
	{
		fprintf(stderr, "tcp socket error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}

    //获取新知天气域名IP地址
    struct hostent *host = gethostbyname("api.seniverse.com");
    if (host == NULL)
    {
        fprintf(stderr, "gethostbyname error,errno:%d,%s\n",errno,strerror(errno));
        exit(1);
    }

	//4.发起连接请求,等待接受服务器接受连接
	struct sockaddr_in  dest_addr;
	dest_addr.sin_family 		= AF_INET; 						//协议族,是固定的
	dest_addr.sin_port   		= htons(PORT);			//服务器端口,必须转换为网络字节序
	dest_addr.sin_addr.s_addr   = inet_addr(inet_ntoa(*((struct in_addr *)host->h_addr)));//服务器地址 


	int ret = connect(tcp_socket,(struct sockaddr *)&dest_addr,sizeof(dest_addr));
	if (ret < 0)
	{
		fprintf(stderr, "connect error,errno:%d,%s\n",errno,strerror(errno));
		exit(1);
	}
	char buf[128] = {0};

	//5.说明双方建立连接,此时可以接收数据
    char recv_buf[1024] = {0};

    //构造http请求信息https://api.seniverse.com/v3/weather/now.json?key=your_api_key&location=beijing&language=zh-Hans&unit=c

    //请求行:GET URL HTTP/1.1\r\n
    //请求字段:Host: api.seniverse.com\r\n
    //\r\n

    sprintf(recv_buf,
            "GET https://api.seniverse.com/v3/weather/now.json?key=%s&location=%s&language=zh-Hans&unit=c "
            "HTTP/1.1\r\n"
            "Host: api.seniverse.com\r\n"
            "\r\n",
            KEY, argv[1]
    );

    send(tcp_socket, recv_buf, strlen(recv_buf), 0);

    memset(recv_buf, 0, sizeof(recv_buf));
    
	recv(tcp_socket, recv_buf, sizeof(recv_buf), 0);
	printf("%s", recv_buf);
    memset(recv_buf, 0, sizeof(recv_buf));
    recv(tcp_socket, recv_buf, sizeof(recv_buf), 0);
	printf("%s\n", recv_buf);


	return 0;
}

2.

根据刚才讲解的流程,实现通过短信服务平台实现向自己的手机发送短信验证码,另外,尝试区修改短信的模板和签名。

c 复制代码
//接口类型:短信接口
//测试环境:centos7.8 gcc4.8.5
//测试日期:2022-01-26
//使用说明:demo只包含了对接时需要的传参和解析的核心代码,可参考整合到自己的系统中,具体业务细节代码可根据自己的需要进行调整及优化。
//linux下的编译:gcc -o test ./test.c
//linux下的执行:./test

//接口类型:互亿无线触发短信接口,支持发送验证码短信、订单通知短信等。
//账户注册:请通过该地址开通账户http://sms.ihuyi.com/register.html
//注意事项:
//(1)调试期间,请用默认的模板进行测试,默认模板详见接口文档;
//(2)请使用APIID(查看APIID请登录用户中心->验证码短信->产品总览->APIID)及APIkey来调用接口;
//(3)该代码仅供接入互亿无线短信接口参考使用,客户可根据实际需要自行编写;

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

//服务器的域名
#define host "106.ihuyi.com"

//端口号
#define PORT 80

//缓冲区大小
#define BUFSIZE 4096


int main(int argc, char **argv)
{
    int sockfd, ret, i, h,srandnum;
    struct sockaddr_in servaddr;
    char str1[4096], str2[4096], buf[BUFSIZE], *str;
    socklen_t len;
    fd_set t_set1;
    struct timeval tv;

    //创建TCP套接字
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { //创建套接字
        printf("create connect error!\n"); //创建网络连接失败
        exit(0);
    };

    //客户端需要连接服务器 connect
    
    bzero(&servaddr, sizeof(servaddr));     //清空结构体
    servaddr.sin_family = AF_INET;          //使用IPv4地址
    servaddr.sin_port = htons(PORT);        //端口   转换为网络字节序


    if (inet_aton(host, &servaddr.sin_addr) == 0)
    {   
        //解析域名之后的内容
        struct hostent *he;

        //对服务器域名进行解析
        he = gethostbyname(host);
        if (he == NULL)
            return -1;

        //把解析之后的IP存储到结构体中
        memcpy(&servaddr.sin_addr, he->h_addr, sizeof(struct in_addr));
    }

    char ipbuf[128];

    //把IP地址转换为字符串形式的 点分十进制  "xxx.xxx.xxx.xxx"
    strncpy(ipbuf, inet_ntoa(servaddr.sin_addr), 128); //将域名转成IP

    //把转换的目标主机的IP存储到IPv4的结构体变量中
    if (inet_pton(AF_INET, ipbuf, &servaddr.sin_addr) <= 0 ){
            printf("inet_pton error!\n"); //创建网络连接失败
            exit(0);
    };

    //客户端发送连接请求
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
            printf("connect error!\n");
            exit(0);
    }
    //说明服务器接受连接请求
    printf("connect success!\n\n");

    //发送数据
    memset(str2, 0, 4096); //清空数组

    strcat(str2, "account=C06781899&password=xxxxxxxe26bf0f89445&mobile=xxxxxxxxx6&content=您的验证码是:8888。请不要把验证码泄露给其他人。");
    str=(char *)malloc(128);
    len = strlen(str2);
    sprintf(str, "Content-Length: %d\r\n\r\n", len);  //计算待发送的请求包体的长度


    memset(str1, 0, 4096);//清空数组

    //构造POST请求行
    strcat(str1, "POST /webservice/sms.php?method=Submit&format=json HTTP/1.1\r\n");

    //构造Host字段
   
    strcat(str1, "Host: ");
    strcat(str1, host);
    strcat(str1, "\r\n");

    //构造Content-Type字段
    strcat(str1, "Content-Type: application/x-www-form-urlencoded\r\n");

    //构造Content-Length字段
    strcat(str1, str);

    //构造请求包体
    strcat(str1, str2);

    strcat(str1, "\r\n\r\n");

    //输出请求内容  测试
    printf("Request Data: \n%s\n",str1);

    //客户端把请求内容发送服务器
    ret = write(sockfd,str1,strlen(str1));
    if (ret < 0) {
        printf("send data fail!errno:%d, errmsg:'%s'\n\n",errno, strerror(errno));
        exit(0);
    }else{
        printf("send data success, length:%d byte!\n\n", ret);
    }

    //IO多路复用,只不过现在只需要监听一个文件描述符 sockfd 
    FD_ZERO(&t_set1);
    FD_SET(sockfd, &t_set1);

    while(1){
        //sleep(1);
        tv.tv_sec= 0;
        tv.tv_usec= 0;

        //进行select多路复用  监听读就绪状态   该函数目前不会阻塞
        h = select(sockfd+1, &t_set1, NULL, NULL, &tv);

        printf("h:%d sec:%ld usec:%ld\n\n", h, tv.tv_sec, tv.tv_usec);

        if (h == 0) {
            memset(buf, 0, 4096); 
            i= read(sockfd, buf, 4095);   //读取服务器的响应数据
            if (i==0){
                close(sockfd);//关闭套接字
                printf("0.connect close!\n"); //读取数据报文时发现远端关闭
                return -1;
            }

            //输出响应数据
            printf("Response Data: \n%s\n", buf);
            close(sockfd);//关闭套接字
            printf("1.connect close!\n");
            return 1;
        }

        if (h > 0) {
            close(sockfd);//关闭套接字
            printf("2.connect close!\n");
            return -1;
        };

        if (h < 0) {
            close(sockfd);//关闭套接字
            printf("3.connect close!\n"); //在读取数据报文时SELECT检测到异常
            return -1;
        };

        //continue;
        //break;
    }
    close(sockfd);//关闭套接字
    printf("4.connect close!\n");

    return 0;
}


希望各位靓仔靓女点赞,收藏,关注多多支持,我们共同进步,后续我会更新更多的面试真题,你们的支持将是我前进最大的动力