本章重点
理解应用层的作用,初识http协议
理解传输层的作用,深入理解tcp的各项特性和机制
对整个tcp/ip协议有系统的理解
对tcp/ip协议体系下的其他重要协议和技术有一定的了解
学会使用一些网络问题的工具和方法
目录
1.应用层
2.协议概念
3. 网络计算器
4. 序列化和反序列化
5. 协议定制
6. 数据处理
7. 网络函数封装
8. 服务端
9. 客户端
10.结果示例
11. json序列化
12. 添加条件选项
13.再看七层模型
1. 应用层
实际解决问题,满足日常需求的网络程序都在应用层
2. 协议概念
协议是一种“约定”,socket api的接口,在读写数据时,都是按“字符串”的方式来发送接收的,如果我们要传输一些结构化的数据,怎么办?
tcp也称作传输控制协议(什么时候发,发多少,出错了怎么办),传输层是在os内部实现的,是os网络模块部分,将数据交给tcp实际上就是交给os,由于os决定数据的发送,那么收上来的数据就不能完全确定了,有可能是完整的,也有可能是多个报文,或者一部分。所以为了成功的发送和解析报文,应用层就需要协议约定好数据的格式,确定数据的完整性,如果长度不符,就不处理
3. 网络计算器
需要实现一个服务器的计算器,把客户端两个数发过去,然后由服务器计算,最后把结果返回给客户端
约定方案
约定方案一:
客户端发送一个形如“1+1”的字符串
这个字符串有两个操作数,都是整形
两个数字之间会有一个字符是运算符
数字和运算符之间没有空格
。。。
这种情况如果一次性发送了四五组数据,无法区分是一个还是几个报文
约定方案二:
定义结构体表示需要交互的信息
发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则将把字符串串转换回结构体
// proto.h 定义通信的结构体
typedef struct Request {int a;int b;
} Request;
typedef struct Response {int sum;
} Response;
// client.c 客户端核心代码
Request request;
Response response; scanf("%d,%d", &request.a, &request.b);
write(fd, request, sizeof(Request));
read(fd, response, sizeof(Response));
// server.c 服务端核心代码
Request request;
read(client_fd, &request, sizeof(request));
Response response;
response.sum = request.a + request.b;
write(client_fd, &response, sizeof(response));
直接发送结构体,结构体两边类型一样,这种方法可以,但是不同的设备结构体的对齐方式可能不一样,导致同一个结构体大小不一样。出问题后非常难调试,都是二进制的。如果一出性发好多个,也不好区分一个个报文,不过os内部是这样实现的,各种情况都考虑了
只要保证一端发送,另一端能够正确解析,这种约定就是应用层协议。为了更稳定,可以定下面的约定
4. 序列化和反序列化
上面的约定方式都有不足之处。最终的约定方式以qq消息举例,需要的结构体包含消息内容,昵称,发送时间,将这三个字符串组合为一个字符串发送给客户,客户收到后又重新转换为结构体,解析为发送时间+昵称+内容的结构,双方的内容结构是相同的。也实现了简单的分层,上面负责结构的规划组成序列,下面负责将序列化的数据完整发送和接收,为了方便解析和发送,还需要规定报文之间的分隔。这样序列化和反序列化方便网络收发
5. 协议定制
协议分两个类,一个是请求类,一个是响应类,请求方用计算类,生成计算式,两个操作数一个操作符,所以成员变量两个int为操作数,一个char操作符。将数据序列化为“x 操作符 y”的结构发送,对方收到后还要提供反序列化为计算类来计算结果
结算结果放到结果类,两个成员变量,一个是int的结果,一个是int的返回码,表明结果可不可信。也要提供序列化和反序列化功能,将结果和返回码改变为“结果 返回码”的字符串格式
上面的数据可以用来发送了,但如果客户一次性发了很多个数据,或者一个报文也不满足。如何区分每个报文?所以必须为发出的数据添加报头,报头格式定为“长度\n报文\n”,有添加报头也要解析报头
protocol.hpp
#pragma once
#include <string>//分隔符
const std::string black_sep = " ";
const std::string protocol_sep = "\n";//解决报文外部格式//len\n正文\nstd::string encode(std::string& message){std::string package = std::to_string(message.size());package += protocol_sep;package += message;package += protocol_sep;return package;}//len\na + b\nbool decode(std::string& message, std::string* content){std::size_t pos = message.find(protocol_sep);if (pos == std::string::npos){return false;}std::string len_str = message.substr(0, pos);std::size_t len = std::stoi(len_str);std::size_t total_len = len_str.size() + len + 2;//检查长度if (message.size() < total_len){return false;}*content = message.substr(pos + 1, len);//earse 移除报文message.erase(0, total_len);return true;}class Request
{
public:Request(){}Request(int a, int b, char oper){_num1 = a;_num2 = b;_op = oper;}//a + bbool serialize(std::string* out){//构建报文有效载荷std::string str;str += std::to_string(_num1);str += black_sep;str += _op;str += black_sep;str += std::to_string(_num2);*out = str;return true;}//a + bbool deserialize(std::string& in){//astd::size_t left = in.find(black_sep);if (left == std::string::npos){return false; }std::string part_a = in.substr(0, left);// bstd::size_t right = in.rfind(black_sep);if (right == std::string::npos){return false; }std::string part_b = in.substr(right + 1);//+if (left + 2 != right){return false;}_op = in[left+1];_num1 = std::stoi(part_a);_num2 = std::stoi(part_b);return true;}void debugprint(){cout << "新请求构建完成:" << _num1 << _op << _num2 << endl;}public:int _num1;int _num2;char _op;
};class Response
{
public:Response(){}Response(int res, int cod){_result = res;_code = cod;}//1000 0bool serialize(std::string* out){string str = std::to_string(_result);str += black_sep;str += std::to_string(_code);*out = str;return true;}//1000 0bool deserialize(std::string& in){std::size_t pos = in.find(black_sep);if (pos == std::string::npos){return false;}std::string left = in.substr(0, pos);std::string right = in.substr(pos + 1);_result = std::stoi(left);_code = std::stoi(right);return true;}void debugprint(){cout << "结果响应完成,result:" << _result << ",code:" << _code << endl;}public:int _result;int _code; //0可信,否则表明对应的错误
};
6. 数据处理
有了协议就可以实现数据处理,计算结果并封装的类
枚举各种计算错误的情况,操作符等其他问题用OTHER
计算函数传入上面的请求类,返回结果响应类。根据操作符进行不同的运算
数据处理函数将收到的字符串内容转换为请求类,调用计算函数得到结果,并对结果封包返回字符串用来发送
servercal.hpp
#pragma once
#include "protocol.hpp"enum
{DIVZERO = 1,MODZERO,OTHER_OPER
};class ServerCal
{
public:ServerCal(){}Response CalculatorHelp(const Request& req){Response res(0, 0);switch (req._op){case '+':res._result = req._num1 + req._num2;break;case '-':res._result = req._num1 - req._num2;break;case '*':res._result = req._num1 * req._num2;break;case '/':if (req._num2 == 0){res._code = DIVZERO;}else{res._result = req._num1 / req._num2;}break;case '%':if (req._num2 == 0){res._code = MODZERO;}else{res._result = req._num1 % req._num2;break;}default:res._code = OTHER_OPER;break;}return res;}std::string Calcluator(std::string& package){std::string content;bool r = decode(package, &content);if (!r){return "";}Request req;r = req.deserialize(content);if (!r){return "";}req.debugprint();content = "";Response res = CalculatorHelp(req);res.debugprint();res.serialize(&content);content = encode(content); // len\n正文\nreturn content;}~ServerCal(){}
};
7. 网络函数封装
将服务器的socket常用功能封装为scoke类,成员sockfd为socket函数的返回值,提供返回sockfd的函数
Socket.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include "log.hpp"enum
{SOCKERR = 1,BINDERR,LISERR
};Log lg;
const int backlog = 5;
class Sock
{
public:Sock(){}void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){lg.logmessage(fatal, "socket error");exit(SOCKERR);}}void Bind(uint16_t port){struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(port);int bret = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if (bret < 0){lg.logmessage(fatal, "bind error");exit(BINDERR);}}void Listen(){int lret = listen(_sockfd, backlog);if (lret < 0){lg.logmessage(fatal, "listen error");exit(LISERR);}}int Accept(string* clientip, uint16_t* clientport){sockaddr_in peer;socklen_t len = sizeof(peer);int newfd = accept(_sockfd, (sockaddr*)&peer, &len);if (newfd < 0){lg.logmessage(warning, "accept error");return -1;}char ipstr[64];inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));*clientip = ipstr;*clientport = ntohs(peer.sin_port);return newfd;}bool Connect(const string ip, const uint16_t port){sockaddr_in peer;memset(&peer, 0, sizeof(peer));peer.sin_family = AF_INET;inet_pton(AF_INET, ip.c_str(), &peer.sin_addr);peer.sin_port = htons(port);int cret = connect(_sockfd, (const struct sockaddr*)&peer, sizeof(peer));if (cret == -1){lg.logmessage(warning, "connect error");return false;}return true;}void Close(){close(_sockfd);}int Fd(){return _sockfd;}~Sock(){}
public:int _sockfd;
};
8. 服务端
服务端和通用服务器一样,accept收到连接请求后,创建子进程提供服务,因为数据可能不是一次性接收完的,所以recbuff不断加上读到的内容。同时有可能有多个报文,所以收到数据后进入循环处理,将字符串交给函数模板对象,就是上面的数据处理函数。
数据处理函数的返回值
因为可能收到的数据不满足一个报文,所以这个函数里多条判断,报文解析不成功都会返回空,调用得到的字符串内容为空时跳出继续读取。解析成功后返回结果将内容发送给客户端
解析报文时有多条判断,先找\n,找到说明有数据的长度,然后根据长度获取内容,检查数据长度和计算的长度符不符合。不满足上面条件的不予处理,否则说明数据长度符合,移除解析了的内容
server.hpp
#pragma once
#include <string>
#include <signal.h>
#include <functional>
#include "log.hpp"
#include "Socket.hpp"using namespace std;
using func_t = std::function<std::string(std::string &package)>;
class server
{
public:server(uint16_t port, func_t fun):_port(port), _fun(fun){}void init(){//创建套接字_listensocket.Socket();_listensocket.Bind(_port);_listensocket.Listen();lg.logmessage(info, "init server done");}void start(){signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);int cnt = 1;while (true){string ip;uint16_t port;int sockfd = _listensocket.Accept(&ip, &port);if (sockfd < 0){continue;}lg.logmessage(info, "get a new link %d", sockfd);if (fork() == 0){_listensocket.Close();string inbuff_stream;// 提供服务while (true){char buff[1280];ssize_t n = read(sockfd, buff, sizeof(buff));if (n > 0){buff[n] = 0;lg.logmessage(debug, "\n%s", buff);inbuff_stream += buff;while (true){string echo = _fun(inbuff_stream);if (echo.empty()){break;}lg.logmessage(debug, "缓冲区\n%s", inbuff_stream.c_str());lg.logmessage(debug, "结果\n%s", echo.c_str());cout << "次数:" << cnt++ << endl;write(sockfd, echo.c_str(), echo.size());}}else if (n == 0){break;}else{break;}}exit(0);}close(sockfd);}}~server(){}
private:Sock _listensocket;uint16_t _port;func_t _fun;
};
server.cc
#include <unistd.h>
#include "server.hpp"
#include "servercal.hpp"
//#include "protocol.hpp"int main()
{ServerCal cal;uint16_t port = 8000;server *tsvp = new server(port, std::bind(&ServerCal::Calcluator, &cal, std::placeholders::_1));tsvp->init();daemon(0, 0);tsvp->start();return 0;
}
std::bind绑定函数和第一个参数
9. 客户端
客户端创建连接,成功生成5次随机的数字和运算符,赋值给请求类,封装后发送,收到内容后用结果类解析
client.cc
#include <time.h>
#include <unistd.h>
#include <assert.h>
#include "Socket.hpp"
#include "protocol.hpp"int main()
{srand(time(NULL));uint16_t port = 8000;string ip = "106.54.46.147";struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(ip.c_str());server.sin_port = htons(port);const string opers = "+-*/%";Sock socket;socket.Socket();bool r = socket.Connect(ip, port);if (!r)return 1;int cnt = 1;while (cnt <= 5){cout << "=============第" << cnt << "次测试...." << "============" << endl;string package;int x = rand() % 100;int y = rand() % 100 + 1;char op = opers[rand() % opers.size()];Request req(x, y, op);req.debugprint();req.serialize(&package);package = encode(package);write(socket._sockfd, package.c_str(), package.size());char buff[1024];int n = read(socket._sockfd, buff, sizeof(buff));string inbuff_stream;if (n > 0){buff[n] = 0;inbuff_stream += buff;string content;bool r = decode(inbuff_stream, &content);assert(r);Response resp;r = resp.deserialize(content);assert(r);resp.debugprint();}cout << "=======================================" << endl;sleep(1);cnt++;}
}
日志
#pragma once
#include <stdarg.h>
#include <iostream>
#include <stdio.h>
#include <cstring>
#include <time.h>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>using namespace std;#define info 0
#define debug 1
#define warning 2
#define ERROR 3
#define fatal 4#define screen 1
#define onefile 2
#define classfile 3#define path "log.txt"class Log
{
public:Log(int style = screen){printstyle = style;dir = "log/";}void enable(int method){printstyle = method;}const char *leveltostring(int level){switch (level){case 0:return "info";break;case 1:return "debug";break;case 2:return "warning";break;case 3:return "error";break;case 4:return "fatal";break;default:return "none";break;}}void printlog(int level, const string &logtxt){switch (printstyle){case screen:cout << logtxt;break;case onefile:printonefile(path, logtxt);break;case classfile:printclassfile(level, logtxt);break;}}void logmessage(int level, const char *format, ...){time_t t = time(0);tm *ctime = localtime(&t);char leftbuff[1024];sprintf(leftbuff, "[%s]%d-%d-%d %d:%d:%d:", leveltostring(level), ctime->tm_year + 1900,ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);char rightbuff[1024];va_list s;va_start(s, format);vsprintf(rightbuff, format, s);va_end(s);char logtext[2048];sprintf(logtext, "%s %s\n", leftbuff, rightbuff);//printf(logtext);printlog(level, logtext);}void printonefile(const string& logname, const string& logtxt){int fd = open(logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd < 0){return;}write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printclassfile(int level, const string &logtxt){//log.txt.infostring filename = dir + path;filename += ".";filename += leveltostring(level);printonefile(filename, logtxt);}~Log(){};private:int printstyle;string dir; //分类日志,放入目录中
};// int sum(int n, ...)
// {
// int sum = 0;
// va_list s;
// va_start(s, n);// while (n)
// {
// sum = sum + va_arg(s, int);
// n--;
// }// return sum;
// }
10. 结果示例
可以加入守护进程,在初始化服务器后开启守护
11. json序列化
上面的序列化和反序列化功能都是字符串处理,比较麻烦。有一种数据交换格式json,有序列化和反序列化的功能,可以用json代替。用条件编译试试json发送数据
如果没有先安装
sudo yum install -y jsoncpp-devel
序列化
先创建json里的通用变量,Json::Value,以键值对的方式赋值,key和value
再用一个通用变量嵌套一个value类
调用序列化功能生成字符串打印,有两种风格,style内容换行易读
反序列化
创建value变量,创建read对象,调用parse功能解析内容
利用key-value格式提取内容,嵌套类型定义value变量获取,再提取一次
输出结果
修改protocol文件
用#ifdef #else #endif的格式条件编译,json方式写在else的情况里
Request
Json::Value root;root["x"] = _num1;root["y"] = _num2;root["op"] = _op;Json::FastWriter w;*out = w.write(root);return true;
Json::Value root;Json::Reader r;r.parse(in, root);_num1 = root["x"].asInt();_num2 = root["y"].asInt();_op = root["op"].asInt();return true;
Response
Json::Value root;root["res"] = _result;root["code"] = _code;Json::FastWriter w;*out = w.write(root);return true;
Json::Value root;Json::Reader r;r.parse(in, root);_result = root["res"].asInt();_code = root["code"].asInt();return true;
示例
序列化的工具还有protobuf,二进制,更注重效率,可读性不如json,适用于内部使用
12. 添加条件选项
$号可以引入定义的常量,#会注释后面的内容
13. 再看七层模型
会话层的维护是通过server创建子进程提供服务的,接到链接就创建一个会话
表示层就是上面定义的协议,就是结构体字符串等固有的数据格式,网络标准格式就是序列化添加报头这些动作后的数据
应用层就是处理数据的计算器功能
所以表示层和会话层应用层很难在os实现,根据不同的场景有不同的格式和功能,内容也会有文字声音图像等都有可能,无法在os全部实现
如果客户端连上一直不发数据,会占用资源,可以对时间进行判断,超时直接挂掉