本章重點
理解應用層的作用,初識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全部實現
如果客戶端連上一直不發數據,會占用資源,可以對時間進行判斷,超時直接掛掉