1. 再谈 "协议"
协议是一种 "约定",在读写数据时, 都是按 "字符串" 的方式来发送接收的.
但是这里我们会遇到一些问题:
如何确保从网上读取的数据是否是完整的,区分缓冲区中的由不同客户端发来的数据
2. 网络版计算器
举例:我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
约定方案一:
客户端发送一个形如"1+1"的字符串;
这个字符串中有两个操作数, 都是整形;
两个数字之间会有一个字符是运算符, 运算符只能是 + ;
数字和运算符之间没有空格;
结果:不可取,无法判断从网上读取的数据是否是完整的
约定方案二:
定义结构体来表示我们需要交互的信息;
发送数据时将这个结构体按照一个规则转换成字符串(这个过程叫做 "序列化"), 接收到数据的时候再按照相同的规则把字符串转化回结构体;(这个过程叫做“反序列化”)
自定义序列化,反序列化
#include<iostream> #include<sys/socket.h> #include<sys/types.h> #include<netinet/in.h> #include<arpa/inet.h> #include<string> #include<memory> #include<unistd.h> using namespace std;
#include"head.hpp" class sock { public:sock(uint16_t port = 8080):_port(port){}void Init(){fd = socket(AF_INET,SOCK_STREAM,0);if(fd < 0){cout << "socket fail" << endl;exit(1);}}void Bind(){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(_port);addr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(addr);int n = bind(fd,(struct sockaddr*)&addr,len);if(n < 0){cout << "bind fail" << endl;exit(2);}}void Listen(){int n = listen(fd,0);if(n < 0){cout << "listen fail" << endl;exit(3);}}void Accept(int &client_fd){sockaddr_in addr;socklen_t len = sizeof(addr);client_fd = accept(fd,(struct sockaddr*)&addr,&len);if(client_fd < 0){cout << "accept fail" << endl;}else{cout << "get a new link ..." << endl;}}int Connect(const uint16_t& port,const string &s){sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(s.c_str());socklen_t len = sizeof(addr);int n = connect(fd,(struct sockaddr*)&addr,len);if(n < 0){cout << "connect fail" << endl;exit(4);}return fd;} private:int fd;uint16_t _port; };
#include<string> string package(const string &content) {//len\ncontent\nint n = content.size();string s = to_string(n);s += '\n';s += content;s += '\n';return s; } string prase(string &package) {//len \n content \nsize_t pos = package.find('\n');if(pos == string :: npos){return nullptr;}int len_package = package.size();string l = package.substr(0,pos);int len = stoi(package.substr(0,pos));if(l.size() + 2 + len > len_package){return nullptr;}string s;s = package.substr(pos + 1,len);package.erase(0,l.size() + 2 + len);return s;} struct protocol_client {protocol_client(int x = 0,int y = 0,char op = '+'):_x(x),_y(y),_op(op){}string serialize(){// _x _op _ystring s;s += to_string(_x);s += " ";s += _op;s += " ";s += to_string(_y);return s;}void deserialize(const string &s){int left = s.find(' ');if(left == string :: npos){cout << "protocol_client : find fail" << endl;}int right = s.rfind(' ');if(right == string :: npos){cout << "protocol_client : find fail" << endl;}if(left + 2 != right){cout << "protocol_client : deserialize fail" << endl;return ;}_x = stoi(s.substr(0,left));_y = stoi(s.substr(right + 1));_op = s[left + 1];}int _x;int _y;char _op; }; struct protocol_server {protocol_server(int result = 0,int code = 0):_code(code), _result(result){}string serialize(){//_result _codestring s;s += to_string(_result);s += " ";s += to_string(_code);return s;}void deserialize(const string &s){size_t pos = s.find(" ");if(pos == string :: npos){cout << "protocol_client : find fail" << endl;}_result = stoi(s.substr(0,pos));_code = stoi(s.substr(pos + 1));}int _code; int _result; };
#include"server.hpp" int main() {unique_ptr<server> sv(new server());sv->start();sv->run();return 0; }
#include "sock.hpp" #include"protocol.hpp" class server { public:server(){}void start(){svr.Init();svr.Bind();svr.Listen();}void run(){int client_fd;svr.Accept(client_fd);string e;while(true){//读端char buff[1024];ssize_t n = read(client_fd, buff, sizeof(buff));if (n < 0){cout << "server : read fail" << endl;}buff[n] = 0;e += buff;string s = prase(e);protocol_client sv;sv.deserialize(s);//计算int result,code = 0;switch(sv._op){case '+':result = sv._x + sv._y;break;case '-':result = sv._x - sv._y;break;case '*':result = sv._x * sv._y;break;case '/':if(sv._y == 0){code = 1;}else{result = sv._x / sv._y;}break;default:code = 2;}protocol_server ss(result,code);s = ss.serialize();s = package(s);//写端n = write(client_fd,s.c_str(),s.size());if(n < 0){cout << "server : write fail" << endl;}}} private: sock svr; };
#include"sock.hpp" #include"protocol.hpp" void usage() {cout << "x + y = ? " << endl; }int main() {uint16_t port = 8080;sock client;client.Init();int fd = client.Connect(port,"1.94.49.66");string e;while(true){usage();int x,y;char op;//写端cout << "please enter: x > ";cin >> x;cout << "please enter: op > ";cin >> op;cout << "please enter: y > ";cin >> y;protocol_client sv(x,y,op);string s = sv.serialize();s = package(s);ssize_t n = write(fd,s.c_str(),s.size());if(n < 0){cout << "client : write fail" << endl;}//读端char buff[1024];n = read(fd,buff,sizeof(buff));if(n < 0){cout << "client : read fail" << endl;}buff[n] = 0;e += buff;s = prase(e); protocol_server ss;ss.deserialize(s);cout << "result : " << ss._result << " code : " << ss._code << endl;cout << "-------------------------------------------------------------" << endl << endl;}return 0; }
json类
#include<jsoncpp/json/json.h> int main() {Json::Value root;root["x"] = 100;root["y"] = 200;root["op"] = '+';root["dect"] = "this is a + oper";Json::FastWriter w;string res = w.write(root);cout << res << endl;return 0; }
Json::Value v; Json::Reader r; r.parse(res,v); int x = v["x"].asInt(); int y = v["y"].asInt(); char op = v["op"].asInt(); string dect = v["dect"].asString();
3. HTTP协议
(一)认识 HTTP协议
应用层协议是我们程序员自己定的,但实际上, 已经有一些现成的, 又非常好用的应用层协议, 供我们直接参考使用,如 HTTP(超文本传输协议) 就是其中之一.
(二)认识URL
平时我们说 "网址" 就是URL
注意:
- 像网站这种,一般默认会添加协议方案名,且协议方案名对应唯一一个端口号,所以即使不写端口号,也没关系(如:http 对应端口号 80,https 对应端口号 443)
- web根目录不一定是linux系统下的根目录,具体指什么,完全由服务器那边解释
(三)urlencode(编码) 和 urldecode(解码)
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现
所以,如果某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下(这个过程叫编码):
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
编码解码工具:
URL 编码解码 - Codeeeee 在线小工具
4. HTTP协议格式
注意:
- 上图对应格式是打印出来的格式,实际上,发送到网络上的是整个字符串,是连在一起的
- http也是一种协议,它也需要将数据序列化,反序列化
代码
5. HTTP的方法
其中最常用的就是 GET方法 和 POST方法
注意:
- GET 方法 和 POST 方法都可以传参
- GET 方法的参数是在 url 内的,通过 url 提交的,POST 方法的参数是在正文内
- POST 方法 比 GET 方法更加私密
GET 方法:
POST 方法:
6. HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found,一般是客户端要访问的文件不存在), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
注意:
- 3XX代表的重定向有两种:一种是临时移动,另一种是长期移动
- 长期移动:一旦设置了永久重定向,所有后续对原始URL的请求都应该被自动转发到新的URL,而且这个重定向是长期有效的;临时移动:表明资源的移动是暂时的。 客户端在接收到临时重定向响应后,会临时使用新的URL,但在将来的某个时间点,对原始URL的请求可能会恢复为直接访问原始资源,而不是被重定向
HTTP常见Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
注意:
- Content-Type : 指明访问的路径是什么类型(如:.html : 网页 ; .jpg :jpg格式的图片 ;.png : png格式的图片 ...... )
- Cookie : Cookie文件可能是内存级的(储存在内存里),也可能是文件级的(储存在磁盘里)
- cookie文件的内容保存多久,一般是由浏览器决定的,如果内容没有了,则下一次访问对应服务器,仍要进行验证
- http协议默认是无状态的
- http对登入用户的会话保持功能
7. 长连接和短连接
- 即使是一个网页,也可能包含很多元素,每一个元素,都要进行一次http请求,即建立一个tcp连接,就会发生多个请求和响应,这就是长连接
- 只有一次请求和响应,连接就断开的,就是短连接