个人博客网站开发,开发手机端网站模板,碧江网站建设,wordpress国外主题网站模板文章目录 前言一、概念铺垫1.TCP2.全双工 二、网络版本计算器1. 原理简要2. 实现框架代码2.1 封装socket2.2 客户端与服务端2.3 封装与解包2.4 请求与响应2.5 对数据进行处理2.6 主程序逻辑 3.Json的简单使用 总结尾序 前言 在上文我们学习使用套接字的相关接口进行了… 文章目录 前言一、概念铺垫1.TCP2.全双工 二、网络版本计算器1. 原理简要2. 实现框架代码2.1 封装socket2.2 客户端与服务端2.3 封装与解包2.4 请求与响应2.5 对数据进行处理2.6 主程序逻辑 3.Json的简单使用 总结尾序 前言 在上文我们学习使用套接字的相关接口进行了编程因此对网络编程有了一定的认识可是我们之前只是以字符串的形式简单的收发信息如果我们要发送和接受的信息更加复杂比如客户端发送一个结构体服务端要如何接收这个结构体呢 如果说还要对结构体的数据进行处理并返回呢下面就让我们带着这些疑问开始今天的学习吧 说明 每台计算机的结构体的对齐方式可能会有所不同因此不能直接发送结构体。因此要将结构体里的数据要以特定的形式即协议的方式发送和接收。对数据处理后还要以协议的方式发送给客户端从而客户端收到并进行对应的处理。 一、概念铺垫
1.TCP 众所周知TCP是可靠的传输控制协议一般是通过三次握手和四次挥手来保证数据的传输是可靠的。说明下面只是简单的理解后面博主详细讲解的。 三次握手 三次交互建立连接。 四次挥手 断开连接就是要断的干净避免之后一方进行死缠烂打。 2.全双工
所谓的全双工就是服务端和客户端都是可以收消息和发消息的例如UDP和TCP协议都是全双工的。
UDP TCP 理解传输控制协议 对于UDP来说在传输层对于发消息不做控制但是对于收消息如何处理则全权交由UDP决定。对于TCP来说用户只负责将消息发送到发送和接收缓存区但对于消息如何处理则全权由TCP决定。 说明处理一般涉及什么时候传传多少传错了怎么办等等。 从UDP与TCP相比较TCP多了一个发送缓冲区这在一定程度上可以体现TCP的可靠性。 二、网络版本计算器
1. 原理简要 因为我们做的是网络版本的计算器数据格式设定为[ 数据(空格)方法(空格)数据(换行符)]即可而且在网络中我们一般是以字符串的形式进行发送的因此我们还要将整形数据转换为字符串便于之后的解析。数据的封装为了能将一个完整的数据解析出来因此我们应该在数据的前面封装数据的长度当截取数据时我们按照长度截取即可检查是否可获取到一个完整的数据并且长度应与数据分开便于获取这里我们用换行符作为分割符即可。这里实现了数据的封装也就间接的实现了对数据解包。 举一个体现自定义协议的例子比如 [1 1]封装为 [5\n][1 1\n]数据按上面的封装而服务器读取时假如只读取到了[5\n 1 ]通过读取5这个字符串转换为int可以验证读取的报文是否是完整的报文那么数据不是无法进行解包的会直接返回。 因为客户端和服务端都要遵循这种规则即自定义协议是一种约定因此双方都要遵守的因此不存在数据被污染的情况即网络中传输的数据都是符合要求的。因此客户端传输的数据可以被服务端正确的提取提取之后我们要进行解析和处理数据并将处理后的数据以【结果 返回码】返回码用于检查数据是否计算可靠比如1 除 0 无法进行计算设返回码为1表示除0错误。并以上述同样的方式进行封装将封装之后的结果返回给用户进行解析并处理。 2. 实现框架代码
实现服务器和封装socket套接字。对请求和响应分别进行序列化和反序列化。对序列化的数据进行封装与解包。服务器对解析的数据进行处理和返回。
代码框架
2.1 封装socket 在之前我们实现代码时主要目的是为了熟悉系统调用接口熟练使用之后这里我们可以将Socket进行封装(包含客户端与服务端的常用的接口)方便我们之后进行调用
#pragma once#includeiostream
#includestring#includecstring
#includestrings.h
#includeunistd.h//网络相关的头文件。
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includearpa/inet.h//小组件
#includeLog.hppusing std::string;enum FAIL
{CREAT 1,SIP_TO_NIP,BIND,LISTEN,ACCEPT,CONNECT,
};
uint16_t defaultport 8080;
string defaultip 0.0.0.0;
class Sock
{
public:Sock(uint16_t port defaultport,string ip defaultip):_port(port),_ip(ip){}~Sock(){if(_sockfd 0){close(_sockfd);}}//创建套接字void Socket(){_sockfd socket(AF_INET,SOCK_STREAM,0);if(_sockfd 0){lg(CRIT,socket create fail,reason is\%s,errno is %d,strerror(errno),errno);exit(CREAT);}lg(INFORE,sockfd is %d,create success!,_sockfd);}//获取套接字int GetSocket(){return _sockfd; }//绑定void Bind(){sockaddr_in server;memset(server,0,sizeof(server));server.sin_family AF_INET;server.sin_port htons(_port);if(inet_pton(AF_INET,_ip.c_str(),server.sin_addr) ! 1){lg(CRIT,string_ip to inet_ip fail,reason is %s\,errno is %d,strerror(errno),errno);exit(SIP_TO_NIP);}if(bind(_sockfd,(sockaddr*)server,sizeof(server)) -1){lg(CRIT,bind fail,reason is %s,errno \is %d,strerror(errno),errno);exit(BIND);}lg(INFORE,bind success!);}//监听void Listen(){if(listen(_sockfd,_backlog) -1){lg(CRIT,bind fail,reason is %s,errno is\%d,strerror(errno),errno);exit(LISTEN);}lg(INFORE,lisen success!);}//接收连接int Accept(sockaddr_in* client,socklen_t* len){int fd accept(_sockfd,(sockaddr*)client,len);if(fd 0){lg(CRIT,accept fail,reason is %s,\errno is %d,strerror(errno),errno);exit(ACCEPT);}uint16_t port ntohs(client-sin_port);char ip[64] {0};inet_ntop(AF_INET,(client-sin_addr),ip,sizeof(ip) - 1);lg(INFORE,accept success,get a new link,ip is\%s, port is %d,ip,port);return fd;}//连接void Connect(sockaddr_in* server){memset(server,0,sizeof(sockaddr_in));server-sin_family AF_INET;server-sin_port htons(_port);if(inet_pton(AF_INET,_ip.c_str(),\(server-sin_addr)) -1){lg(WARNNING,inet_pton fail,reason is %s\,errno is %d,strerror(errno),errno);return;}int res connect(_sockfd,\(sockaddr*)server,sizeof(sockaddr_in));if(res -1){lg(CRIT,connect fail,reason is %s,\errno is %d,strerror(errno),errno);exit(CONNECT);return;}lg(INFORE,connect success!);}//从指定的套接字文件描述符里面读取数据。string Read(int fd){char buffer[128] {0};ssize_t n read(fd,buffer,sizeof(buffer) - 1);if(n 0){lg(CRIT,read fail,reason is %s,\errno is %d,strerror(errno),errno);sleep(1);return ;}else if(n 0){lg(INFORE,read nothing!);sleep(1);return ;}buffer[n] \0;return buffer;}//向指定的套接字文件描述符里面写数据。int Write(int fd,const string str){ssize_t n write(fd,str.c_str(),str.size());if(n 0){lg(CRIT,write fail,reason is %s,errno \is %d,strerror(errno),errno);sleep(1);return n;}else if(n 0){lg(INFORE,write nothing!);sleep(1);return n;}return n; }void Close(int fd){close(fd);}
private:int _sockfd;uint16_t _port;string _ip;int _backlog 5;//?
};以后我们直接用这个小组件即可不用再手搓系统调用的接口了。 2.2 客户端与服务端 这里我们使用上面封装的socket接口实现的服务端与客户端。
服务端
#pragma once
#includeiostream
#includepthread.h
#includefunctional
#include../Tools/Socket.hpp
#include../Tools/Log.hpp
using cal_t functionstring(string);
class TcpServer;struct ThreadData
{ThreadData(int fd,TcpServer* tp):_fd(fd),_tp(tp){}int _fd;TcpServer* _tp;
};
class TcpServer
{
public:TcpServer(uint16_t port 8080,cal_t cal nullptr):_socket(port),_cal(cal){}~TcpServer(){}void Init(){_socket.Socket();_socket.Bind();_socket.Listen();}static void* Rouetine(void* args){//分离线程pthread_detach(pthread_self());auto thread_ptr static_castThreadData*(args);TcpServer* tp thread_ptr-_tp;int fd thread_ptr-_fd;tp-Server(fd);return nullptr;}void Run(){for(;;){sockaddr_in client;socklen_t len sizeof(client);int fd _socket.Accept(client,len);pthread_t tid;pthread_create(tid,nullptr,Rouetine,\new ThreadData(fd,this));}}void Server(int fd){string mes;for(;;){sleep(10);//收消息string str _socket.Read(fd);//啥也没读到if(str ) break;mes str;//处理消息string ans;string echo_mes;//一次处理一批while((echo_mes _cal(mes)) ! ){ans echo_mes;}//没有读取到整段的报文或者报文为空。int res _socket.Write(fd,ans);if(res 0) break;}_socket.Close(fd);}
private:Sock _socket;cal_t _cal; //这里的cal函数是对接收的消息的处理方法。
};根据上面的信息我们可以大致了解服务器的基本框架 创建套接字绑定套接字监听套接字。接收外面的请求建立连接接收信息。调用处理信息的接口返回处理之后的信息。 因此 我们可以让服务器与处理信息的逻辑进行解耦并且使用封装之后的套接字是很方便的。 客户端
#pragma once
#includeiostream
#includestring#include../Tools/Log.hpp
#include../Tools/protocol.hpp
#include../Tools/Socket.hppusing std::string;
string default_ip 59.110.171.164;
uint16_t default_port 8080;
class TcpClient
{
public:TcpClient(string ip default_ip,uint16_t port default_port):_sock(port,ip){}void Init(){}void Run(){string res;for(;;){_sock.Socket();sockaddr_in server;_sock.Connect(server); int fd _sock.GetSocket();while(true){cout Please Enter;int x,y;char oper;cin x oper y;Request req(x,y,oper);string str req.Serialize();//为了更好的体现自定义协议这里我们多次进行写入。_sock.Write(fd,str);_sock.Write(fd,str);_sock.Write(fd,str);_sock.Write(fd,str);_sock.Write(fd,str);sleep(10);//一次读一批res _sock.Read(fd);Response resq;//一次处理一批while(resq.Deserialize(res));}_sock.Close(fd);}}
private:Sock _sock;
}; 说明这里我们让客户端一次发一批消息处理一批消息服务端一次处理一批消息发一批消息这样更加能够体现自定义协议的功能。 2.3 封装与解包
//.....char space ;
char newline \n;
//解包
string Decode(string str)
{int pos str.find(newline);if(pos string::npos) return ;int len stoi(str.substr(0,pos));int totalsize pos len 2;//如果总的报文的长度大于读取的字符串的长度说明没有一个完整的报文。if(totalsize str.size()){return ;}//将有效载荷截取出来string actual_load str.substr(pos 1,len);//将完整的报文丢弃便于下一次进行读取。str.erase(0,totalsize);return actual_load;
}
//编码
string InCode(const string str)
{//一个完整的报文有效载荷的长度 换行符 有效载荷 换行。string text to_string(str.size()) newline str newline;return text;
}封装数据我们将在报头处封装有效载荷的长度并以换行符作为分割符。解析数据首先要找到有效载荷的长度并检验是否存在一个完整的报文。 2.4 请求与响应
struct Request
{Request(int x, int y, char oper):_x(x), _y(y), _oper(oper){}Request(){}bool Deserialize(string str){cout endl;//首先把字符串的报头和有效载荷进行分离string content Decode(str);if(content ) return false;//解析字符串:字符 空格 字符int left content.find(space);int right content.rfind(space);if (left 1 ! right - 1){//说明是无效的字符return false;}_x stoi(content.substr(0, left));_y stoi(content.substr(right 1));_oper content[left 1];cout 解析的字符串: _x _oper _y endl; cout 待读取的字符串: endl str endl;cout ------------------------------- endl;return true;}string Serialize(){string package;//首先对结构体进行编码//编码格式字符 空格 操作符 空格 字符package to_string(_x) space _oper space\ to_string(_y); //对报文再进行封装package InCode(package);return package;}int _x 0;int _y 0;char _oper 0;//给出一个缺省值避免编译器告警。
};struct Response
{Response(int res, int code):_res(res), _code(code){}Response(){}bool Deserialize(string str){string content Decode(str);if (content ) return false;int pos content.find(space);_res stoi(content.substr(0,pos));_code stoi(content.substr(pos 1));//for debug:cout endl;cout 转换结果: _res _code endl;cout 待读取的字符串 endl str endl;cout ------------------------------- endl;return true;}string Serialize(){string package to_string(_res) space \ to_string(_code);package InCode(package);return package;}int _res 0;int _code 0;//同理。
};Request是客户端对服务器发送的请求要客户端进行序列化服务端进行反序列化并进行解析。Response是服务端对客户端发送的响应要服务端进行序列化客户端进行反序列化并进行解析。 2.5 对数据进行处理
#includeiostream
#include../Tools/Log.hpp
#include../Tools/protocol.hppenum CAL
{DIV_ZERO 1,MOD_ZERO,
};
struct CalHelper
{string Cal(string str){Request req;if(req.Deserialize(str) false) return ;int x req._x;int y req._y;char op req._oper;int res 0, code 0;switch(op){case :res x y;break;case -:res x - y;break;case *: res x * y;break;case /:if(!y){code DIV_ZERO;break;}res x / y;break;case %:if(!y){code MOD_ZERO;break;}res x % y;break;default:break;}return Response(res,code).Serialize();}
};这是服务器对客户端请求的处理包含请求的反序列化和对数据的处理以及结果的序列化。 2.6 主程序逻辑
client.cc
#includeiostream
#includememory
#includeclientcal.hpp
using std::unique_ptr;
void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ ip port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 3){Usage(argv[0]);return 1;}string ip argv[1];uint16_t port stoi(argv[2]);unique_ptrTcpClient cp(new TcpClient(ip,port));cp-Init();cp-Run();return 0;
}server.cc
#includeiostream
#includememory
#includefunctional
#includeserver.hpp
#includeservercal.hpp
using std::unique_ptr;void Usage(char* pragma_name)
{cout endl Usage: pragma_name \ port[8000-8888] endl endl;
}
int main(int argc,char* argv[])
{if(argc ! 2){Usage(argv[0]);return 1;}uint16_t port stoi(argv[1]);CalHelper cal;unique_ptrTcpServer tp(new TcpServer(port,\bind(CalHelper::Cal,cal,placeholders::_1)));//bind是C的一个接口用于封装函数便于使用。//因为cal是库里面的因此要指定作用域并传this指针//绑定参数进而封装出指定类型的函数。tp-Init();tp-Run();return 0;
}bind的使用跳转详见目录 运行结果 这里我们传数据接收数据处理数据都是一批一批的进行的因此可以看见待处理的字符串。 3.Json的简单使用 在上面实现的过程中唯一比较难设计的就是序列化与反序列化的过程上面我们为了进一步的理解所以自己设计但是市面上有一些简单好用的序列化与反序列化工具下面我们介绍一种。 在网络中序列化与反序列化有现成的工具比如json 和 protobuf这两个工具下面我们简单介绍Json的使用。
安装Json库
sudo yum install -y jsoncpp-devel说明 普通用户需要输入root密码并且要添加到系统的信任白名单中所以这里建议直接su命令切到root用户直接安装。 简单使用
test.cc
#includeiostream
#includestring
#includejsoncpp/json/json.h
using namespace std;
int main()
{Json::Value root;Json::StyledWriter writer;//Json::FastWriter writer;//StyleWriter打印起来比较有风格。//FastWrier打印比较紧凑比较省空间。root[x] 1;root[y] 2;root[oper] ;string res writer.write(root);//序列化之后的结果:cout 序列化之后的结果: endl;cout res endl;Json::Value des;Json::Reader r;r.parse(res,des);int x des[x].asInt();int y des[y].asInt();char oper des[oper].asInt();//反序列化的结果cout 反序列化的结果为: endl;cout x oper y endl;return 0;
}编译运行查看结果
g test.cc -stdc11 -ljsoncpp总结
铺垫TCP三次握手四次挥手的概念以及理解全双工。实现了自定义协议(封装报头) 序列化与反序列化的 网络版本的计算器。介绍了Json工具的基本使用。 了解自定义协议之后我们将在下篇认识现成的应用层协议之Http。
尾序
我是舜华期待与你的下一次相遇