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"; //正文
运行,在浏览器输入本机地址。我们看到了服务器发来的响应。
完整代码
#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;
}