网络基础 -- 应用层HTTP协议

目录

应用层(TCP/IP协议中的应用层/HTTP协议)

HTTP协议

URL -- 统一资源定位符

UrlEncode / UrlDecode

HTTP协议格式

概述 

HTTP 请求消息Request / 响应消息Response

首行

请求(Request)首行

请求(Request)首行

头部 

正文 

实现一个简单的HTTP服务器


应用层(TCP/IP协议中的应用层/HTTP协议)

应用层 : 程序员自己编写程序, 应用程序之间如何进行数据沟通(数据发送的格式, 以及接收到数据之后如何解析)的约定, 也称之为应用层协议.  它由程序员自己制定,  程序员可以自定义协议, 称为私有协议, 也可以选择一些现成的非常好用的知名协议.

序列化 : 将数据对象按照指定的协议, 即按约定的格式持久化存储或格式化数据传输

反序列化 : 持久化存储的数据或网络数据传输的数据按照指定协议解析出数据对象化得过程

来看个私有协议的例子

网络计算器的例子 : 客户端将要计算的两个数字以及运算符发送给服务端, 有服务端进行计算, 最终返回结果.

方案一 : 

  • 客户端发送一个形如 "1+1" 的字符串
  • 这个字符串有两个操作数, 都是整型
  • 两个整型之间有一个字符是运算符吗只能是是 '+' 
  • 数字和运算符之间没有空格

这种方式相对来说是非常麻烦的, 因为将一串字符解析成我们想要的结果并不是那么的简洁, 所以我们来看下面的结构化传输

方案二 :

  • 定义结构体来表示我们需要出传输的数据
  • 发送时将结构体按照字符串发送, 接收到的数据再按照相同的规则将这一串字符串转换为结构体
    (因为在内存中都是以二进制传输的, 所以传啥都是传二进制, 解析时就可以还原)
  • 这个过程就是序列化和反序列化的过程

核心代码

#定义格式化数据
typedef struct{
    int num1;
    int num2;
    char c;//运算符
}data;

#服务端
data buf;
recv(ser_fd, &buf, sizeof(buf), 0);
if(buf.c == '+'){
    cout << buf.num1 << buf.c << buf.num2 << " = " << buf.num1 + buf.num2 <<endl;
}

#客户端
data buf;
cout << "请依次输入num1, num2\n";
cin >> buf.num1;
cin >> buf.num2;
buf.c ='+';
send(cli_fd, &buf, sizeof(buf), 0);

序列化的方法还有json序列化, jsio序列化用途广泛, 还衍生了阿里的fastjson, 美团的MSON, 谷歌的GSON等更加优秀的转码工具. protobuf序列化是谷歌的一款开源项目,性能好,效率高,并且支持多种语言,例如:java,C++,python等. 优点:转码性能高,支持多语言.  缺点:中文文档少,使用相对复杂.

接下来再来看知名协议, http协议


HTTP协议

HTTP协议(Hyper Text Transfer Protocol, 超文本传输协议) 的缩写, 是一种简单的请求-响应协议, 是应用层协议. 通常运行在TCP之上, 是万维网 (WWW, World Wide Web) 数据通信的基础,  所有的 WWW 文件都必须遵守这个标准.

HTTP的发展是由蒂姆·伯纳斯·李 于1989年在欧洲核子研究组织(CERN)所发起. HTTP的标准制定由万维网协会(World Wide Web Consortium,W3C) 和互联网工程任务组 (Internet Engineering Task Force,IETF) 进行协调, 最终发布了一系列的RFC, 其中最著名的是1999年6月公布的 RFC 2616, 定义了HTTP协议中现今广泛使用的一个版本—— HTTP 1.1.

2014年12月, 互联网工程任务组 (IETF) 的Hypertext Transfer Protocol Bis(httpbis)工作小组将 HTTP/2 标准提议递交至IESG进行讨论, 于2015年2月17日被批准.  HTTP/2 标准于2015年5月以RFC 7540正式发表, 今后将取代HTTP 1.1成为HTTP的实现标准 .

URL -- 统一资源定位符

平时我们所说的 "网址" 其实就是URL(Uniform Resource Locator, 统一资源定位符)

                  

来看个栗子 :
 

其中目录中 1. 语法是一个超链接, 链接为 : 
https://baike.baidu.com/item/URL%E6%A0%BC%E5%BC%8F/10056474?fr=aladdin#1

登录信息 : 也就是用户名和密码, 由于安全性的原因, 一般已经很少早URL中出现了. 是非必要部分

服务器地址 : 是服务器IP地址的另一种表达方式, 方便人们记忆, 但最终还是要经过域名解析得到服务器的IP地址才能访问服务器. 一个域名只能对应一个IP地址, 但一个IP地址可以对应多个域名. 如我们熟知的 www.baidu.com 的对应的IP 是 14.215.177.39  , 也可以用IP地址作为域名.

端口号 : 端口不是一个URL必须的部分, 如果省略端口部分, 将采用默认端口.

带层次文件路径 :  也叫虚拟目录部分, 从域名后的第一个 "/" 开始到最后一个 "/" 为止, 是虚拟目录部分. 虚拟目录也不是一个URL必须的部分. "/" 开头但并不根目录, 只是服务器中的一个子目录, 因为如果是根目录的话对服务器太过危险.  从域名后的最后一个 "/" 开始到 "?" 为止, 是文件名部分. 如果没有 "?" , 则是从域名后的最后一个 "?" 开始到 "#" 为止, 是文件部分. 如果没有 "?" 和 "#", 那么从域名后的最后一个 "/" 开始到结束, 都是文件名部分. 文件名部分也不是一个URL必须的部分, 如果省略该部分, 则使用默认的文件名.

查询字符串 : 从 "?" 到 "#" 之间, 都是查询字符串部分, 是客户端提交给服务器的数据, 有一个个key=val的键值对组成, 键值对之间用&间隔

片段标识符 : 从#开始到最后, 都是片段标识部分, 即网页上点击跳转的标签, 如上面连接中最后的 #1


UrlEncode / UrlDecode

可以看到, 在URL中, "/", "#", "?", "&" 等字符已经具有了特殊含义, 所以, 提交给服务器的数据中如果有这些字符就会存在歧义, 到底是字符本身还是具有特定意义呢? 这时就有了url编码和url解码, 来解决这个问题

UrlEncode (Url编码)

将字符的每个字节转化为16进制, 例如 : 字符串 "c++" 其中 '+' 是特殊字符转换后为 c2B2B, 在特殊字符转换之后在前面加上%号来标识这是一个特殊字符, c++也就变成了 c%2B%2B 

还有像中文也需要Url编码,   在线编码 : http://tool.chinaz.com/tools/urlencode.aspx

"中文" 这两个字编码后如图 : 

UrlDecode (Url解码)

按照相同的规则, 将url编码后的字符转换为原字符 . 


HTTP协议格式

概述 

HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准. 通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80). 我们称这个客户端为用户代理程序(user agent). 应答的服务器上存储着一些资源,比如HTML文件和图像. 我们称这个应答服务器为源服务器(origin server). 在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel).

尽管TCP/IP协议是传输层最流行的协议簇, 但在HTTP协议中, 并没有规定其下层(传输层)必须用什么协议. 事实上,HTTP可以在任何互联网协议上,或其他网络上实现. HTTP假定其下层协议提供可靠的传输. 因此,任何能够提供这种保证的协议都可以被其使用. 因此也就是其在TCP/IP协议族使用TCP作为其传输层。

通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求. 一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息. 

那么具体客户端是如何向服务端请求或者服务端如何响应客户端呢?


HTTP 请求消息Request / 响应消息Response

Request 和 Response 的格式都是

首行 
头部/报头  
正文 

//在头部与正文之间有一个空格(实际是\r\n), 所以我们看到的应该是下面的格式

首行 
头部/报头  
空格
正文 

     


首行

请求(Request)首行

首行 : 共三条信息, 以空格进行间隔, 以\r\n作为结尾, 具体是:   

            请求方法 URL 协议版本\r\n

我们在浏览器中访问 www.cplusplus.com , 用Fiddler 抓包工具抓包, 如下 :

请求信息如下 :

请求方法: 

序号 方法 描述
1 GET 请求指定的页面信息, 并返回实体主体, 主要用于获取资源
2 HEAD 类似于 GET 请求, 只不过返回的响应中没有具体的内容(没有正文), 用于获取头部/报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件). 数据被包含在请求体中. POST 请求可能会导致新的资源的建立和/或已有资源的修改. 
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容
5 DELETE 请求服务器删除指定的页面
6 CONNECT HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器
7 OPTIONS 允许客户端查看服务器的性能
8 TRACE 回显服务器收到的请求,主要用于测试或诊断
9 PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新

URL : 统一资源定位符 -- 包含像哪个服务器请求哪个资源等信息

协议版本 :  HTTP/0.9  HTTP/1.0  HTTP/1.1  HTTP/2

HTTP/0.9 : 0.9是第一个版本的HTTP协议,已过时。它的组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有协议头,造成了HTTP/0.9协议只支持一种内容,即纯文本. 不过网页仍然支持用HTML语言格式化,同时无法插入图片. 0.9版本只支持短连接, 即一次传输后立即断开

HTTP/1.0 : HTTP协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用。相对于HTTP/0.9 增加了如下主要特性:

  • 请求与响应支持头域
  • 响应对象以一个响应状态行开始
  • 响应对象不只限于超文本
  • 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法
  • 支持长连接(但默认还是使用短连接, 长连接 : 一次连接(TCP三次握手后)可以传输多次), 缓存机制, 以及身份认证 

HTTP/1.1HTTP协议的第三个版本, 是目前使用广泛的协议版本 . HTTP 1.1是目前主流的HTTP协议版本

支持长连接(默认就为长连接), chunked编码传输,字节范围请求,请求流水线, 支持管线化传输等.

chunked(分块)编码 : 该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据)

字节范围请求 : HTTP/1.1支持传送内容的一部分. 比方说, 当客户端已经有内容的一部分, 为了节省带宽, 可以只向服务器请求一部分. 该功能通过在请求消息中引入了range头域来实现, 它允许只请求资源的某个部分. 在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度. 如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content)

请求流水线 : 支持持久连接的客户端可以 "流水线" 处理它的请求(例如 : 发送多个请求, 而不需要等待每个响应). 服务器必须按照接收请求的顺序将其响应发送到这些请求

支持管线化传输 : 管线化机制须通过长连接完成, 管线化传输是将多个HTTP请求 (request) 整批提交的技术, 而在传送过程中不需先等待服务端的回应.

另外还新增这些特性 :
     请求消息和响应消息都应支持主机(Host)头域, 在HTTP/1.0中认为每台服务器都绑定一个唯一的IP地址, 因此,请求消息中的URL并没有传递主机名(hostname). 但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机 (Multi-homed Web Servers), 并且它们共享一个IP地址. 因此, Host头的引入就很有必要了.

     新增了一批请求方法 (Request method) :  OPTIONS,PUT, DELETE, TRACE, CONNECT方法

HTTP/2 : 最新HTTP协议,目前应用还非常少.

主要特点 : 
HTTP/2 最大的特点是, 不会改动HTTP 的语义, HTTP 方法, 状态码, URI 及首部字段等等, 在这些核心概念上一如往常, 却能致力于突破上一代标准的性能限制, 改进传输性能, 实现低延迟和高吞吐量. 而之所以叫2, 是在于新增的二进制分帧层.

服务器端推流: Server Push, 支持服务器主动向客户端推送消息. 目前服务器需要浏览器解析页面后再发送新请求来获取js, css, 图片等资源. HTTP/2为了优化这个开销, 可以提前将这些资源 "推送" 到客户端的缓存中.

随时复位 : HTTP/1.1一个缺点是当HTTP信息有一定长度大小数据传输时, 不能方便地随时停止它, 中断TCP连接的代价是比较昂贵的. 使用HTTP/2的 RST_STREAM将能方便停止一个信息传输, 启动新的信息, 在不中断连接的情况下提高带宽利用效率.

头部压缩 : 现在网页加载是资源密集型的, 一个页面通常有很多资源要加载, 每次请求的头部数据不可忽视 (尤其是Cookies),  加上TCP的Slow Start机制(一种拥塞控制机制)会导致往返次数加大. 压缩可以有效的减少包分组的数量, 从而减少延迟, 尤其是在移动端上. 因为GZIP压缩有安全性隐患, 所以HTTP/2自己实现了一套压缩算法——HPACK.


请求(Request)首行

共三条信息, 以空格进行间隔, 以\r\n作为结尾, 具体是:   

            协议版本 响应状态码 状态码描述\r\n

接着上面的例子, 响应信息如下:

协议版本: 还是上面所说的HTTP/0.9  HTTP/1.0  HTTP/1.1  HTTP/2

响应状态码 : 告诉客户端针对这次请求, 是一个什么样的处理结果, 共有五大类

HTTP状态码分类
分类 分类描述
1xx 信息,服务器收到请求,需要请求者继续执行操作
2xx 成功,操作被成功接收并处理
3xx 重定向,需要进一步的操作以完成请求
4xx 客户端错误,请求包含语法错误或无法完成请求
5xx 服务器错误,服务器在处理请求的过程中发生了错误
常见HTTP状态码列表
状态码 状态码英文 中文描述
200 OK 请求成功. 一般用于GET与POST请求
301 Moved Permanently 永久移动. 请求的资源已被永久的移动到新URI, 返回信息会包括新的URI, 浏览器会自动定向到新URL. 今后任何新的请求都应使用新的URI代替
302 Found 临时移动. 与301类似. 但资源只是临时被移动.客户端应继续使用原有URI
303 See Other 查看其它地址. 与301类似. 使用GET和POST请求查看
400 Unauthorized 客户端请求的语法错误,服务器无法理解
404 Not Found 服务器无法根据客户端的请求找到资源(网页).通过此代码,网站设计人员可设置 "您所请求的资源无法找到" 的个性页面
500 Internal Server Error 服务器内部错误,无法完成请求
502 Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求

响应状态码描述 : 对于状态码的解释

并不固定, 我们可以根据具体的问题设置具体的解释, 一般都用状态码的英文名称

更多状态码, 戳链接( ̄︶ ̄)↗ : https://www.runoob.com/http/http-status-codes.html


头部 

由一个个 key: val 键值对组成, 键值对以 \r\n作为结尾.

           key: val\r\n
           key: val\r\n
           key: val\r\n 
           key: val\r\n 

还是接着上面的例子 , 下面的图片是请求报文 : 

头部字段 (key): 值(val)

Connection -- 本次请求是否是长连接, 是 val=keep-Alive, 不是长连接val=close
Content-Length -- 描述正文的数据类型, 让对端知道处理多大的数据
Content-Type -- 描述正文数据的类型, 好让对端知道正文数据如何处理, 如上图中的text/html, 即需要接收html类型数据, 编码是
                           UTF-8
Content-Language -- 用来说明访问者希望采用的语言或语言组合, 但正文是什么语音并不由Content-Language决定
                                    例如中文是: zh-CN
User-Agent -- 简称UA, 是一种向访问网站提供客户端所使用的浏览器类型及版本, 操作系统及版本, 浏览器内核, 等信息的标识.
                        UA可以伪装.

Accept -- 告诉对端我能接收处理什么样的数据, Accept: text/html
Accept-Language -- 表示浏览器所支持的语言类型, 如 Accept-Language: zh-CN, zh;q=0.9  表示浏览器支持简体中文和繁体中
                                   文, 简体中文优先,  q是权重系数, 范围 [0, 1], q 值越大, 请求越倾向于获得其“;”之前的类型表示的内容, 若没
                                   有指定 q 值, 则默认为1, 若被赋值为0, 则用于提醒服务器哪些是浏览器不接受的内容类型.

Referer -- 在百度中搜谷歌, 然后点击谷歌的链接, 就能抓取到如下, 请求信息, 可以看到, 谷歌是通过百度搜索进入的, 谷歌赶紧给
                 百度打了5毛钱, 买网页广告的人就知道自己广告买的值不值了, Referer 是referrer拼写错误的, 但也就这样沿用下来来, 
                    
Transfer_Encoding -- 传输编码, Transfer-Encoding: chunked 表示分块传输, 在每个块前添加16进制的块大小(字节), 最后以
                                     0\r\n\r\n结束, 主要用于传输比较大的数据或未知大小的数据
Location -- 响应头指示URL的页面重定向到. 它仅在提供3xx状态响应时才有意义.
Expires -- 表示应该在什么时候认为文档已经过期,从而不再缓存它.
Cookie/Set-Cookie -- 早期的HTTP是短连接, 请求/响应完毕连接则会断开, 就比如我们要剁手网购, 买一个东西就要登录一次, 剁
                 都不快乐了. 对这个问题, 解决方案就是Cookie, 客户登录一次, 服务端为客户端创建一个session(会话), session中保                     存当前的会话信息, 客户端的认证信息等, 存储到数据库中,在登录成功响应时服务端使用Set-Cookie告诉客户端会话
                 id(session_id)是多少. 客户端收到这个响应后, 将Cookie中的信息保存起来. 下一次客户端请求服务器时, 就会自动保存
                 Cookie信息, 读取出来, 通过Cookie发送给服务器, 服务器接收到Cookie之后取出信息, 即取出session_id, 通过这个id在
                 数据库中找到会话信息, 就知道当前这个客户端是谁了
Cookie和Session区别 : Session是服务端为每个客户端建立的会话, 将session_id作为Cookie信息返回给客户端, 也就是说
                  Session保存在服务端.  Cookie保存在客户端. Cookie中保存的Session_id就是服务端得到时可以找到对应的Session
                  从而识别出客户端身份.

更多头部字段的信息, 戳链接( ̄︶ ̄)↗ : https://www.php.cn/manual/view/35521.html

正文 

Request : 客户端向服务端的请求数据

Response : 响应正文, 服务器返回给客户端的数据


实现一个简单的HTTP服务器

我们实现HTTP服务器通常是TCP服务器, 即在传输层使用TCP协议.  在应用层的数据通信采用HTTP协议格式

具体步骤 : 

1. 搭建一个TCP服务器, 等待客户端连接

2. 客户端新连接到来, 服务端新建套接字

3. 通过这个新建套接字接收数据(这个数据是HTTP协议格式的数据)

接收数据的时候如何保证能够接收一个完整的HTTP请求?

   1). 接收HTTP请求头部 : 头部与正文之间以\r\n作为间隔, 最后一个头部信息以\r\n作为结尾, 所以当出现\r\n\r\n时, 就认为头部到
        此结束.
   2). 根据头部信息中的Content-Length确定正文长度, 然后接收具体长度的正文就可以了.

4. 服务器解析客户端请求

5. 根据具体业务请求, 进行处理并响应

封装socket_tcp.hpp

#include<iostream>
#include<string>
#include<unistd.h> //close包含在这个头文件中
#include<netinet/in.h>//地址结构体
#include<arpa/inet.h>//字节序转换接口
#include<sys/socket.h>//套接字接口

using namespace std;
#define BACKLOG 10
#define CHECK_RET(ret){if((ret) == false) return -1;} 

class TcpSocket{
    int m_sockfd;

    void Addr(struct sockaddr_in*, const string&, const uint16_t) const;
public:
    TcpSocket():m_sockfd(-1){}
    bool Socket();
    int GetFd();
    bool Bind(const string& ip, const uint16_t port) const;
    bool Listen(int backlog = BACKLOG) const;
    bool Connect(const string ip, const uint16_t port) const;
    bool Accept(TcpSocket* sock, string* ip = nullptr, uint16_t* port = nullptr) const;
    bool Recv(string* buf) const;
    bool Send(const string& data) const;
    bool Close() const;
};

bool TcpSocket::Socket(){
    m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(m_sockfd < 0){
        perror("socket error");
        return false;
    }
    return true;
}
int TcpSocket::GetFd(){
    return m_sockfd;
}
void TcpSocket::Addr(struct sockaddr_in* addr, const string& ip, const uint16_t port) const{
    addr->sin_family = AF_INET;
    addr->sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &addr->sin_addr.s_addr);
}
bool TcpSocket::Bind(const string& ip, const uint16_t port) const{
    struct sockaddr_in addr;
    Addr(&addr, ip, port);
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = bind(m_sockfd, (struct sockaddr*)&addr, len);
    if(ret < 0){
        perror("bind error");
        return false;
    }
    return true;
}

bool TcpSocket::Listen(int backlog) const{
    int ret = listen(m_sockfd, backlog);
    if(ret < 0){
        perror("listen error");
        return false;
    }
    return true;
}

bool TcpSocket::Connect(const string ip, const uint16_t port) const{
    struct sockaddr_in addr;//定义一个IPv4的地址结构
    Addr(&addr, ip, port);//向这个结构中绑定地址与端口
    socklen_t len = sizeof(struct sockaddr_in);
    int ret = connect(m_sockfd, (struct sockaddr*)&addr, len);
    //将服务端信息描述到socket中, 并向服务端发起连接请求
    if(ret < 0){
        perror("connect error");
        return false;
    }
    return true;
}

bool TcpSocket::Accept(TcpSocket* sock, string* ip, uint16_t* port) const{
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    int newsockfd = accept(m_sockfd, (struct sockaddr*)&cli_addr, &len);
    if(newsockfd < 0){
        perror("accept error");
        return false;
    }
    sock->m_sockfd = newsockfd;
    char str[INET_ADDRSTRLEN];//IPv6用INET6_ADDRSTRLEN
    if(ip != nullptr){
       *ip = inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str));
    }
    if(port != nullptr){
        *port = ntohs(cli_addr.sin_port);
    }
    return true;
}

bool TcpSocket::Recv(string* buf) const{
    char tmp[4096] = { 0 };
    ssize_t ret = recv(m_sockfd, tmp, 4095, 0);
    if(ret < 0){
        perror("recv error");
        return false;
    }
    else if(ret == 0){
        cout << "connection break\n";
        return false;
    }
    buf->assign(tmp, ret);
    return true;
}
bool TcpSocket::Send(const string& data) const{
    size_t slen = 0, ret = 0;
    size_t size = data.size();
    while(slen < size) {
        ret = send(m_sockfd, &data[slen], size - slen, 0);
        if(ret < 0){
            perror("send error");
            return false;                                                  
        }
        slen += ret;                              
    }
    return true;
}
bool TcpSocket::Close()const {
    close(m_sockfd);
    return true;
}

http_ser.cpp

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<sstream>
#include<pthread.h>
#include"socket_tcp.hpp"

#define CHECK_RET_(ret, ptr){if((ret) == false){delete ptr; return NULL;}}

void* thr_start(void* arg) {
    pthread_detach(pthread_self());
    TcpSocket* sock = (TcpSocket*)arg;
    string buf;
    CHECK_RET_(sock->Recv(&buf), sock);
    cout << "http req:\n" << buf;
    string body = "<html><body><h1>Hello World!</h1></body></html>";
    string blank = "\r\n";
    stringstream header;
    header << "Content-Length: " << body.size() << "\r\n";
    header << "Content-Type: text/html\r\n";
    //header << "Location: http://www.baidu.com/\r\n";
    //string first = "HTTP/1.1 302 Found\r\n";
    string first = "HTTP/1.1 200 OK\r\n";
    CHECK_RET_(sock->Send(first), sock);
    CHECK_RET_(sock->Send(header.str()), sock)
    CHECK_RET_(sock->Send(blank), sock);
    CHECK_RET_(sock->Send(body), sock);
    delete sock;
    return NULL;
}
int main(int argc, char* argv[]){
    if(argc != 3){
        cout << "Should input ./ser_tcp [ip] [port]\n";
        return -1;
    }
    TcpSocket ser;
    CHECK_RET(ser.Socket());
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    CHECK_RET(ser.Bind(ip, port));
    CHECK_RET(ser.Listen());
    string buf;
    while(1){
        TcpSocket* newsock = new TcpSocket;
        string ip;
        uint16_t port;
        bool ret = ser.Accept(newsock, &ip, &port);
        if(ret == false){delete newsock;  continue; }//服务端并不会因为一次失败而退出, 而是继续获取下一个连接
        printf("new connection[ip: %s][port: %d]\n", ip.c_str(), port);
        pthread_t tid;
        if(pthread_create(&tid, NULL, thr_start, (void*)newsock)){
            fprintf(stderr, "pthread_create: %s\n", strerror(ret));
            delete newsock;
            return -1;
        }
    }
    return 0;
}

浏览器要想能访问我们虚拟机中的服务器程序, 就需要关闭Linux防火墙, 命令如下 : 

sudo systemctl stop firewalld
sudo systemctl disable firewalld

还需要将虚拟机的网络设置为桥接模式, 在局域网下其他的主机也能访问到 .

电脑浏览器 :

                 

 手机浏览器 :

                                                          

可以看到, 操作系统 是Android 9, 手机型号是 HONOR HLK-AL00, 浏览器是UC. 感情啥都给我传过去了.....

发布了232 篇原创文章 · 获赞 720 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/105033001