文章目录
- 应用层
- 初识TCP协议通讯流程
- 定制协议
- 再谈协议
- 网络版本计算器
- Protocal.hpp
- CalServer
- CalClient
- Json的安装
应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
初识TCP协议通讯流程
- 建立链接和断开链接
基于TCP协议,我们需要知道写代码时对应的接口大概在TCP通讯的过程中属于什么样的时间点角色,在TCP协议时详谈。三次握手,四次挥手
listen状态:准备好了,可以进行链接,accept:获取链接,不是创造链接,链接已经在底层创建好了,在应用层调用accept把链接拿上来
connect:1.发起链接请求2.绑定套接字;建立链接,在底层向服务端建立链接请求,在TCP中,采用链接的方案是三次握手的方案,connect会发起三次握手,发起链接请求和真正的建立链接是两码事,建立链接由双方OS自动完成的,为什么自动完成?网络分层中,下三层是OS内部的,用户感知不到。通过客户端调用connect让OS来帮我们把三次握手的工作做完。
而accept是获取链接,链接是已经建立好了的,所以accept并不参与三次握手的任何细节,accept一定是在获取链接前别人把链接做完,既链接建立完。三次握手是OS自己完成的,connect只是发起,accept只是收尾。即使上层不调用accept,三次握手也是能够建立好的。
TCP保证可靠性不是write和read有关系的,由双方OS完成的,后面详谈。
建立链接后面就要断开链接,所以UDP由于不需要建立链接,自然不需要谈论断开链接
而四次挥手的工作都是由双方的OS完成,而我们决定什么时候分手一旦调用系统调用close,用户层就不用管了。
- 理解链接
谈男女朋友时,都会表达自己的爱意,一定有一方主动发起链接,无论如何表达,双方看对眼的概率是极低的。而主动发起链接,是怎么发起的呢?首先,男方先表白,然后女方在做表态,什么时候在一起?男方回答就现在。这就是双方三次握手成功。(虽然现实生活中被拒绝是常态)
建立链接究竟在干什么:记下一些东西
- 什么是建立链接
所谓的建立链接,三次握手根本就是手段,不是目的,为了达到让双方都能记住这一套,一个服务端链接客户端,很多客户端来链接了,意味着很多的客户端来了,OS应该区分清楚,需要把链接管理起来,先描述在组织,需要创建对应的链接数据结构,把所有的链接描述起来,在对其进行管理。所谓的链接就是OS内部创建的链接结构体,包含了在建立链接时对应的属性信息。当有新的链接进来时,每到来一个链接,服务端会构建一个链接对象 ,将所有的链接对象在内部中用特定的数据结构管理起来。这就是链接的建模过程。维护链接是需要成本的。占用内存资源,要用对象进行管理。
断开链接需要四次挥手,断开链接的最终目的毫无疑问就是把建立好的链接信息释放。四次挥手理解:
男女朋友处的非常好,走到了婚姻的殿堂,但是被现实打败了,过不下去啦。然后一方提出离婚,但是你自己说了不算,另一方说好啊,过了一会,对象又说离就离,那我也要离,那么你一看,我也OK。所以断开链接是双方的事情,必须得征求双方的意见。双方在协商,TCP要保证可靠性,你说的话要保证你也听到了,我也知道了,反之也一样。这就是传说中的四次挥手
TCP与UDP对比
可靠传输VS不可靠传输
有连接VS无连接
字节流VS数据报
定制协议
应用层协议的定制
再谈协议
协议是一种约定,socket api的接口,在读写数据时,都是按照字符串的方式来接收的,如果要传输一些”结构化的数据“怎么办呢?
结构化的数据:群里说话的时候除了消息本身,还有头像,昵称时间等等信息 。但是不是一个一个独立的个体,你需要做的把这些消息形成一个报文——打包成一个字符串。
由多变一这个过程就是序列化。经过网络传输后,收到的是一个报文,收到一个报文要的是什么?把一个字符串变成多个字符串,这个过程是反序列化
业务数据发送到网络的时候,先序列化发送,收到的是序列字节流,要先进行反序列化, 然后才能使用
业务协议就是结构体,这样说还是不够的,所以我们要手写一个协议。
应用场景:形成字符串对方收到,收到之后上层来不及接收,对方又发一个,有可能一次全读的,上层如何保证收到的是一个报文?
tcp这里怎么保证收到一个完整的报文
理解业务协议,理解序列化和反序列化。
网络版本计算器
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加减乘除数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端
TCP是面向字节流的,所以明确报文和报文的边界:
TCP是全双工的,如果接收方来不及读,那接收缓冲区就会存在很多数据,读的时候怎么怎么保证读到一个完整的报文:
1.定长2.特殊符号3.自描述方式
序列化、反序列化与定制协议是两码事,是不同阶段的事情,定制协议:报头+有效载荷
Protocal.hpp
自定义协议:
#define SEP " " #define SEP_LEN strlen(SEP) #define LINE_SEP "\r\n" #define LINE_SEP_LEN strlen(LINE_SEP)
复制
请求和响应:Request,Response
Request:x,y,op(“x op y”)x和y是数据,op是操作符,比如1+2
Response:设置了退出码exitcode和结果result()
对请求和响应添加报头,这里设置的报头是长度,enLength(即添加大小,转化成字符串),也就是封装了enLength函数:
//"x op y"->"content_len"\r\n"x op y"\r\n //"exitcode result"->"cotent_len"\r\n"exitcode result"\r\n const std::string enLength(const std::string &text) { std::string send_string = std::to_string(text.size()); send_string += LINE_SEP; send_string += text; send_string += LINE_SEP; return send_string; }
复制
对请求和响应提取报文,只要报文,不要报头,也就是封装了deLength函数:
//"cotent_len"\r\n"exitcode result"\r\n bool deLength(const std::string &package, std::string *text) { auto pos = package.find(LINE_SEP); if (pos == std::string::npos) return false; std::string text_len_string = package.substr(0, pos); int text_len = std::stoi(text_len_string); *text = package.substr(pos + LINE_SEP_LEN, text_len); return true; }
复制
对请求和响应进行序列化和反序列化:对于序列化和反序列化我们可以用Json来进行实现
序列化过程:结构化数据->“x op y”
反序列化过程:“x op y”->结构化数据
Protocal.hpp还提供了recvPackage函数
#define SEP " " #define SEP_LEN strlen(SEP) #define LINE_SEP "\r\n" #define LINE_SEP_LEN strlen(LINE_SEP) enum { OK = 0, DIV_ZERO, MOD_ZERO, OP_ERROR }; //"x op y" --->"content_len"\r\n"x op y"\r\n,添加报头 std::string enLength(const std::string &text) { std::string send_string = std::to_string(text.size()); send_string += LINE_SEP; send_string += text; send_string += LINE_SEP; return send_string; } //"content_len"\r\n"exitcode result"\r\n // 去掉报头,得到"exitcode result" bool deLength(const std::string &package, std::string *text) { auto pos = package.find(LINE_SEP); if (pos == std::string::npos) return false; std::string text_len_string = package.substr(0, pos); // content_len:如“14” int text_len = std::stoi(text_len_string); *text = package.substr(pos + LINE_SEP_LEN, text_len); return true; } class Request { public: Request() : x(0), y(0), op(0) { } Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) { } // 序列化: // 结构化-> "x op y" bool serialize(std::string *out) { #ifdef MYSELF *out = ""; std::string x_string = std::to_string(x); std::string y_string = std::to_string(y); *out = x_string; *out += SEP; *out += op; *out += SEP; *out += y_string; #else Json::Value root; root["first"] = x; root["second"] = y; root["oper"] = op; Json::FastWriter writer; *out = writer.write(root); #endif return true; } // 反序列化化: //"x op y"->结构化 bool deserialize(const std::string &in) { #ifdef MYSELF auto left = in.find(SEP); auto right = in.rfind(SEP); if (left == std::string::npos || right == std::string::npos) return false; if (left == right) return false; if (right - (left + SEP_LEN) != 1) return false; std::string x_string = in.substr(0, left); std::string y_string = in.substr(right + SEP_LEN); if (x_string.empty()) return false; if (y_string.empty()) return false; x = std::stoi(x_string); y = std::stoi(y_string); op = in[left + SEP_LEN]; #else Json::Value root; Json::Reader reader; reader.parse(in, root); x = root["first"].asInt(); y = root["second"].asInt(); op = root["oper"].asInt(); #endif return true; } public: int x; int y; char op; }; class Response { public: Response() : exitcode(0), result(0) { } Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_) { } bool serialize(std::string *out) { #ifdef MYSELF *out = ""; std::string ec_string = std::to_string(exitcode); std::string res_string = std::to_string(result); *out = ec_string; *out += SEP; *out += res_string; #else Json::Value root; root["exitcode"] = exitcode; root["result"] = result; Json::FastWriter writer; *out = writer.write(root); #endif return true; } bool deserialize(const std::string &in) { #ifdef MYSELF auto mid = in.find(SEP); if (mid == std::string::npos) return false; std::string ec_string = in.substr(0, mid); std::string res_string = in.substr(mid + SEP_LEN); if (ec_string.empty() || res_string.empty()) return false; exitcode = std::stoi(ec_string); result = std::stoi(res_string); #else Json::Value root; Json::Reader reader; reader.parse(in, root); exitcode = root["exitcode"].asInt(); result = root["result"].asInt(); #endif return true; } public: int exitcode; int result; }; // 读取报文,保证读取的是一个完整的报文 ,inbuffer由外部传入 // "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op y"\r\n"content_len"\r\n"x op bool recvPackage(int sock, std::string &inbuffer, std::string *text) { char buffer[1024]; while(true) { ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); if(n>0) { buffer[n] = 0; inbuffer+=buffer; auto pos = inbuffer.find(LINE_SEP); if(pos == std::string::npos) continue; std::string text_len_string = inbuffer.substr(0,pos); int text_len =std::stoi(text_len_string); int total_len = text_len_string.size()+2*LINE_SEP_LEN+text_len; std::cout<<"处理前#inbuffer:\n"<<inbuffer<<std::endl; if(inbuffer.size()< total_len) { std::cout<<"你输入的消息,没有遵守所定制的协议,正在等待后续的内容,continue"<<std::endl; continue; } //至少是一个完整的报文 *text = inbuffer.substr(0,total_len); inbuffer.erase(0,total_len); std::cout<<"处理后#inbuffer:\n"<<inbuffer<<std::endl; break; } else return false; } return true; }
复制
对于recvPackage函数我们要保证读到的至少是一个完整的报文
CalServer
服务端代码
//CalServer.hpp namespace server { enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; static const uint16_t gport = 8080; static const int gbacklog = 5; typedef std::function<bool(const Request &req, Response &resp)> func_t; void handlerEntery(int sock,func_t func) { std::string inbuffer; while(true) { //1.读取:"content_len"\r\n"x op y"\r\n //保证读到的消息是一个完整的请求 std::string req_text,req_str; if(!recvPackage(sock,inbuffer,&req_text)) return; std::cout<<"带报头的请求:\n"<<req_text<<std::endl; //去掉报头 if(!deLength(req_text,&req_str)) return; std::cout<<"去掉报头的正文:\n"<<req_str<<std::endl; //2.对请求Request,反序列化 //2.1得到一个结构化的请求对象 Request req; if(!req.deserialize(req_str)) return; //3.计算机处理————业务逻辑 Response resp; func(req,resp); //4.对响应Response,进行序列化 //4.1得到一个"字符串" std::string resp_str; resp.serialize(&resp_str); std::cout<<"计算完成,序列化响应:"<<resp_str<<std::endl; //5.发送响应 std::string send_string = enLength(resp_str); std::cout<<"构建完成完整的响应\n"<<send_string<<std::endl; send(sock,send_string.c_str(),send_string.size(),0); } } class CalServer { public: CalServer(const uint16_t&port = gport):_listensock(-1),_port(port) {} void initServer() { _listensock = socket(AF_INET,SOCK_STREAM,0); if(_listensock<0) { logMessage(FATAL,"create socket error"); exit(SOCKET_ERR); } logMessage(NORMAL,"create socket success:%d",_listensock); struct sockaddr_in local; memset(&local,0,sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0) { logMessage(FATAL,"bind socket error"); exit(BIND_ERR); } logMessage(NORMAL,"bind socket success"); if(listen(_listensock,gbacklog)<0) { logMessage(FATAL,"listen socker error"); exit(LISTEN_ERR); } logMessage(NORMAL,"listen socket success"); } void start(func_t func) { for(;;) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(_listensock,(struct sockaddr*)&peer,&len); if(sock<0) { logMessage(ERROR,"accept error,next"); continue; } logMessage(NORMAL,"accept a new link success,get new sock:%d",sock); pid_t id = fork(); if(id == 0) { close(_listensock); handlerEntery(sock,func); close(sock); exit(0); } close(sock); pid_t ret = waitpid(id,nullptr,0); if(ret>0) { logMessage(NORMAL,"wait child success"); } } } ~CalServer() {} public: int _listensock; uint16_t _port; }; } //CalServer.cc #include "calServer.hpp" #include <memory> using namespace server; using namespace std; static void Usage(string proc) { cout << "\nUsage:\n\t" << proc << " local_port\n\n"; } // req是处理好的完整的请求对象 // resp:根据req进行业务处理,填充resp,不需要管理任何IO,序列化和反序列化 bool cal(const Request &req, Response &resp) { // req已经有结构化的数据 resp.exitcode = OK; resp.result = 0; switch (req.op) { case '+': resp.result = req.x+req.y; break; case '-': resp.result = req.x-req.y; break; case '*': resp.result = req.x*req.y; break; case '/': { if(req.y==0) resp.exitcode = DIV_ZERO; else resp.result = req.x/req.y; } break; case '%': { if(req.y == 0) resp.exitcode = MOD_ZERO; else resp.result = req.x%req.y; } break; default: resp.exitcode = OP_ERROR; break; } return true; } // tcp服务器,启动上和udp server一模一样 // ./tcpserver local_port int main(int argc, char *argv[]) { if (argc != 2) { Usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]); unique_ptr<CalServer> tsvr(new CalServer(port)); tsvr->initServer(); tsvr->start(cal); return 0; }
复制
CalClient
ParseLine:解析,构建一个请求:“1+1”
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include "Protocol.hpp" #define NUM 1024 class CalClient { public: CalClient(const std::string &serverip, const uint16_t &serverport) : _sock(-1), _serverip(serverip), _serverport(serverport) { } void initClient() { // 1. 创建socket _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { std::cerr << "socket create error" << std::endl; exit(2); } } void start() { struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(_serverport); server.sin_addr.s_addr = inet_addr(_serverip.c_str()); if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "socket connect error" << std::endl; } else { std::string line; std::string inbuffer; while (true) { std::cout << "mycal>>> "; std::getline(std::cin, line); // 1+1 Request req = ParseLine(line); // "1+1" std::string content; req.serialize(&content); std::string send_string = enLength(content); send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管 std::string package, text; // "content_len"\r\n"exitcode result"\r\n if (!recvPackage(_sock, inbuffer, &package)) continue; if (!deLength(package, &text)) continue; // "exitcode result" Response resp; resp.deserialize(text); std::cout << "exitCode: " << resp.exitcode << std::endl; std::cout << "result: " << resp.result << std::endl; } } } Request ParseLine(const std::string &line) { // 建议版本的状态机! //"1+1" "123*456" "12/0" int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后 int i = 0; int cnt = line.size(); std::string left, right; char op; while (i < cnt) { switch (status) { case 0: { if(!isdigit(line[i])) { op = line[i]; status = 1; } else left.push_back(line[i++]); } break; case 1: i++; status = 2; break; case 2: right.push_back(line[i++]); break; } } std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl; return Request(std::stoi(left), std::stoi(right), op); } ~CalClient() { if (_sock >= 0) close(_sock); } private: int _sock; std::string _serverip; uint16_t _serverport; }; #include "calClient.hpp" #include <memory> using namespace std; static void Usage(string proc) { cout<<"\nUasge:\n\t"<<proc<<" serverip serverport\n\n"; } int main(int argc,char*argv[]) { if(argc!=3) { Usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t serverport = atoi(argv[2]); unique_ptr<CalClient> tcli(new CalClient(serverip,serverport)); tcli->initClient(); tcli->start(); return 0; }
复制
Json的安装
sudo yum install -y jsoncpp-devel
复制