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\n int n = content.size(); string s = to_string(n); s += '\n'; s += content; s += '\n'; return s; } string prase(string &package) { //len \n content \n size_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 _y string 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 _code string 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连接,就会发生多个请求和响应,这就是长连接
- 只有一次请求和响应,连接就断开的,就是短连接