目录
前言
什么是应用层?
再谈"协议"
什么是序列化和反序列化
网络版计算器
整体流程实现
Sock.hpp的实现
TcpServer.hpp的实现
Protocol.hpp的实现
CalServer.cc的编写
CalClient.cc的编写
整体代码
前言
本章是属于TCP/UDP四层模型中的第一层 应用层相关的内容。主要介绍了序列化与反序列化的应用,本章代码居多,主要是在代码中体现出序列化与反序列化,希望可以耐心阅读,坚持到底一定会有所收获。
什么是应用层?
- 在网络编程中,应用层是网络协议栈中的最高层。它负责定义网络应用程序与网络通信的接口和规范,主要关注数据的格式、交互方式和应用层协议的定义。
应用层协议是在应用程序之间进行通信时所使用的规则和约定。它定义了应用程序如何打包、发送、接收和解析数据。应用层协议可以基于不同的传输协议(如TCP、UDP)来提供不同的服务,例如文件传输、电子邮件、网页浏览、即时通信等。
应用层协议通常在应用程序中实现,为应用程序提供了一组函数、API(例如我们前面使用的socket相关接口)或库,使得应用程序能够通过网络与其他应用程序进行通信。开发者可以使用这些功能来处理数据的编码、解码、发送和接收等操作,同时也可以根据需要定义自己的应用层协议。
简单来说就是我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.
再谈"协议"
协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢.
例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
这里有两种方案:
约定方案一:
- 客户端发送一个形如"1+1"的字符串;
- 这个字符串中有两个操作数, 都是整形;
- 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
- 数字和运算符之间没有空格;
约定方案二:
- 定义结构体来表示我们需要交互的信息;
- 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
- 这个过程叫做 "序列化" 和 "反序列化"
什么是序列化和反序列化
序列化是将内存中的结构化数据(结构体)转换为字节序列的过程。通过序列化,可以将数据结构转换为一系列字节,使其可以被保存到磁盘或通过网络传输。序列化的结果是一个字节流或二进制数据,可以在需要时进行持久化存储或传输给其他系统。
反序列化是将字节流或二进制数据转换回原始结构化数据(结构体)的过程。通过反序列化,可以将序列化后的数据重新还原为内存中的数据结构,以便在程序中进行处理或使用。
序列化和反序列化通常用于在分布式系统中进行数据传输、存储和进程间通信。例如,在网络通信中,发送方将数据结构序列化为字节流,通过网络发送给远程主机,然后在远程主机上进行反序列化,以还原数据并进行处理。
网络版计算器
整体流程实现
这个简易版的网络程序是客户端发送类似与“1+2”,“1-2”,“1/2”..,然后服务器返回对应的结果和状态码,代表结果的正确与否。
我们编写的整体流程是:先对socket接口做封装成Sock类,提供Socket接口,Bind接口,Listen接口,Accept接口以及Connect接口.
然后我们再对服务器做相关的封装,封装成TcpServer类,构造函数用于初始化服务器,该类对外提供BindService(用于绑定服务)接口,Excute(用来执行对应的回调函数)接口,Start(用于服务器的启动并创建线程运行对应的服务)等接口.
然后是协议的定制,通信双方(服务端和客户端必须同时遵守这一套规定),共有两个类,分别为Request类和Response类,每个类中分别有两个接口 Serialization(序列化)与Deserialization(反序列化),用于格式的转化。
最后再分别编写服务端和客户端即可,
Sock.hpp的实现
这一个文件主要是封装socket的相关接口,到时候我们使用相关socket接口时,只需要调用Sock类中的相关接口即可.
第一个是Socket函数,封装了socket,并得到一个套接字,将该套接字返回,代码如下:
int Socket()
{
// 1.创建套接字
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create sock success, listensock: %d", listensock);
return listensock;
}
第二个是Bind接口,同样地我们首先需要创建一个sockaddr_in结构体,然后填入相关的数据,这个前几章已经说过好多次了,便不再详细说明了,然后调用bind绑定。
int Bind(int sock, uint16_t port, string ip = "0.0.0.0")
{
// 2.bind
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;//使用的协议簇为IPv4
local.sin_port = htons(port);//填入端口号
local.sin_addr.s_addr = INADDR_ANY;//填入ip地址
if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0)
{
logMessage(FATAL, "bind error", errno, strerror(errno));
exit(3);
}
}
第三个是Listen接口,用于对客户端请求的监听,内部只需要调用listen接口并判断即可.
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(FATAL, "listen error", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "init server success");
}
第四个是Accept接口,和上一章所说的一样,先建立一个sockaddr_in结构体,用于接收保存客户端的相关信息(ip和端口等等),同时会得到一个新的套接字,返回这个套接字即可。
int Accept(int listensock, string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof src;
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error", errno, strerror(errno));
return -1;
}
if (port)
*port = ntohs(src.sin_port);
if (ip)
*ip = inet_ntoa(src.sin_addr);
return servicesock;
}
最后一个是Connect接口,和之前的用法一样,先创建一个sockaddr_in结构体,然后填入客户端本身的相关信息,最后调用connect函数接口.
bool Conncect(int sock,string server_ip, uint16_t server_port)
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr=inet_addr(server_ip.c_str());
if(connect(sock,(struct sockaddr*)&server,sizeof server) == 0) return true;
else {perror("connect"); return false; }
}
TcpServer.hpp的实现
这个类中首先要有这3个成员:listensock_用于监听的套接字,Sock sock,上面的Sock类的一个对象,用于接口的调用,vector<func_t> func_,用于存储相关的服务. func_t是一个函数指针.
定义如下:
using func_t = function<void(int)>;
首先是构造函数,需要对服务器进行初始化,包括创建套接字,绑定与监听,如下:
TcpServer(const uint16_t &port, const string &ip = "0.0.0.0")
{
listensock_ = sock_.Socket();
sock_.Bind(listensock_, port, ip);
sock_.Listen(listensock_);
}
然后是BindService,用于将绑定某一个服务,将其加入到func_即可.
void BindService(func_t func)
{
func_.push_back(func);
}
接下来是Excute函数,用于执行func_中的方法:
void Excute(int sock)
{
for(auto & f : func_)
{
f(sock);
}
}
然后是Start函数,用于服务器的启动,调用Accept等待客户端的连接,当有客户端连接后,创建一个新的线程,并调用对应的回调函数,于此同时,我们新建一个ThreadData类,用于存储该线程的相关信息,包括该线程的套接字和调用该线程的this指针.如下:
void Start()
{
for (;;)
{
string clientip;
uint16_t clientport;
int sock = sock_.Accept(listensock_, &clientip, &clientport);
if (sock == -1)
continue;
logMessage(NORMAL, "Create new link success, sock: %d", sock);
pthread_t tid;
ThreadData* td= new ThreadData(sock,this);
pthread_create(&tid, nullptr, ThreadRoutine, td);
}
}
ThreadData类:
class ThreadData
{
public:
ThreadData(int sock,TcpServer* server):sock_(sock),server_(server)
{}
~ThreadData(){}
public:
int sock_;
TcpServer* server_;
};
回调函数ThreadRoutine
static void *ThreadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
td->server_->Excute(td->sock_);
close(td->sock_);
return nullptr;
}
Protocol.hpp的实现
这个类主要是对发送方和应答方进行一个协议的定制,方便发送和解析数据。
所以一共两个类,Request类和Response类.
- 先来看Request类,即请求方。该类主要是对请求数据的序列化和反序列化.
- 共有3个类成员:x_,y_,op_,分别代表第一个操作、第二个操作数、和符号.
第一个接口Serialization序列化,它每次需要把结构化数据转化成字符串形式,我们规定形式为
“x_ op_ y_”,所以代码如下:
string Serialization()
{
// 1.自主实现 "x_ op_ y_\r\n"
string str;
str = to_string(x_);
str += SPACE;
str += op_;
str += SPACE;
str += to_string(y_);
return str;
}
第二个接口是请求方 Deserialization反序列化的实现,需要将字符串数据转化成结构化的数据,首先我们要解析字符串,分别提取出x,y和op,然后赋值给这个类成员(x_,y_,op_),便成功的反序列化了.
bool Deserialization(const string &str)
{
//提取x的第一个字符
size_t left = str.find(SPACE);
if (left == string::npos)
return false;
//提取y的第一个字符
size_t right = str.rfind(SPACE);
if (right == string ::npos)
return false;
//得到整数x
x_ = atoi(str.substr(0, left).c_str());
//得到整数y
y_ = atoi(str.substr(right + SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
}
-
Response类
- 该类主要是对处理结果的序列化和反序列化,类成员有result_和code_,分别代表结果和结果状态码.
它的序列化形式我们规定为“result_ code_”,所以代码如下:
// code_ result
string Serialization()
{
string s;
s = to_string(code_);
s += SPACE;
s += to_string(result_);
return s;
}
反序列化逻辑和Request类一样。分别提取,然后赋值给类成员:
bool Deserialization(const string& s)
{
size_t pos = s.find(SPACE);
if(pos == string::npos)
{
return false;
}
code_ = atoi(s.substr(0,pos).c_str());
result_ = atoi(s.substr(pos+SPACE_LEN).c_str());
return true;
}
于此同时,还封装了两个函数,Recv和Send.
实现分别如下:
string Recv(int sock)
{
//tcp是面向字节流的
char inbuffer[1024];
ssize_t s = recv(sock, inbuffer, sizeof inbuffer, 0);
if (s > 0)
{
inbuffer[s] = '\0';
return inbuffer;
}
else if(s == 0)
{
cout << "client quit" << endl;
}
else
{
cout << "recv error" << endl;
}
return "";
}
void Send(int sock, const string str)
{
send(sock, str.c_str(), str.size(), 0);
}
到这里我们基本工作就完成了一大半,接下来是服务端和客户端的编写
CalServer.cc的编写
这是服务端的代码,首先我们都是要忽略SIGPIPE信号的,这是为了避免进程在写入已关闭的套接字时触发SIGPIPE信号而终止。
例如,当一个客户端与服务器建立连接后,但在向客户端发送响应前,客户端已经关闭了连接。如果服务器端没有忽略SIGPIPE信号,那么在尝试向已关闭的连接写入数据时,服务器进程将会被终止,这显然是不希望看到的行为。
signal(SIGPIPE, SIG_IGN);
然后我们需要绑定一个服务,我们要进行计算,所以假设服务 方法是calculator,该函数首先要从调用Recv读取到请求,如果读取结果不为空的话,我们将读取到的数据进行反序列化成结构化的数据(Request类),然后再将这个结构化的数据传送到calculatorHelp()函数进行计算,再次得到一个结构化的数据(Response类),最后再对这个数据进行序列化成字符串形式,再Send回请求的客户端中.
static Response calculatorHelp(Request &req)
{
Response resp(0, 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.code_ = 1;
else
resp.result_ = req.x_ / req.y_;
break;
case '%':
if (req.y_ == 0)
resp.code_ = 2;
resp.result_ = req.x_ % req.y_;
break;
default:
resp.code_ = 3;
break;
}
return resp;
}
void calculator(int sock)
{
while (true)
{
string str = Recv(sock); // 在这里我们读到了一个请求
if (!str.empty())
{
Request req;
req.Deserialization(str); // 反序列化
Response resp = calculatorHelp(req);
string respString = resp.Serialization(); // 对计算结果进行序列化
Send(sock, respString);
}
else
{
break;
}
}
}
int main()
{
//....
server->BindService(calculator);
//....
}
一切完成后,我们在Start启动服务器即可.
server->Start();
CalClient.cc的编写
客户端编写就比较简单了,先创建套接字,然后获取到用户输入的ip和port,然后进行Connect,连接完成后,提示用户输入数据,输入完成后,将数据序列化,然后发送给服务器,然后接收服务器返回的数据,再将其反序列化即可。
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
Sock sock;
int sockfd = sock.Socket();
cout << sockfd << endl;
if (!sock.Conncect(sockfd, serverip, serverport))
{
cerr << "connect error" << endl;
exit(2);
}
while (true)
{
Request req;
cout << "Please Enter x# ";
cin >> req.x_;
cout << "Please Enter y#" ;
cin >> req.y_;
cout << "Please Enter operator# ";;
cin >> req.op_; ;
string s = req.Serialization();
Send(sockfd, s);
string r = Recv(sockfd);
Response resp;
resp.Deserialization(r);
cout << "code: " << resp.code_ << endl;
cout << "result: " << resp.result_ << endl;
sleep(1);
}
return 0;
}
代码效果图:
整体代码
如果你不想仔细看,直接拷贝下面的代码也可以运行,然后可以自己研究:
- Sock.hpp
#pragma once #include <iostream> #include <stdlib.h> #include <assert.h> #include <unistd.h> #include <string.h> #include <memory> #include <pthread.h> #include <signal.h> #include <cstring> #include <ctype.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include "log.hpp" using namespace std; class Sock { public: const static int gbacklog = 20; Sock(){} int Socket() { // 1.创建套接字 int listensock = socket(AF_INET, SOCK_STREAM, 0); if (listensock < 0) { logMessage(FATAL, "%d:%s", errno, strerror(errno)); exit(2); } logMessage(NORMAL, "create sock success, listensock: %d", listensock); return listensock; } int Bind(int sock, uint16_t port, string ip = "0.0.0.0") { // 2.bind struct sockaddr_in local; memset(&local, 0, sizeof local); local.sin_family = AF_INET;//使用的协议簇为IPv4 local.sin_port = htons(port);//填入端口号 //local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str()); local.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&local, sizeof local) < 0) { logMessage(FATAL, "bind error", errno, strerror(errno)); exit(3); } } void Listen(int sock) { // 3.因为TCP是面向连接的,意味着当我们正式通信的时候,需要先建立连接 if (listen(sock, gbacklog) < 0) { logMessage(FATAL, "listen error", errno, strerror(errno)); exit(3); } logMessage(NORMAL, "init server success"); } // const string& 输入型参数 // string* 输出型参数 // string& 输入输出型参数 int Accept(int listensock, string *ip, uint16_t *port) { struct sockaddr_in src; socklen_t len = sizeof src; int servicesock = accept(listensock, (struct sockaddr *)&src, &len); if (servicesock < 0) { logMessage(ERROR, "accept error", errno, strerror(errno)); return -1; } if (port) *port = ntohs(src.sin_port); if (ip) *ip = inet_ntoa(src.sin_addr); return servicesock; } bool Conncect(int sock,string server_ip, uint16_t server_port) { struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(server_port); server.sin_addr.s_addr=inet_addr(server_ip.c_str()); // cout << server.sin_port << " " << server.sin_addr.s_addr << endl; if(connect(sock,(struct sockaddr*)&server,sizeof server) == 0) return true; else {perror("connect"); return false; } } ~Sock() { } };
- TcpServer.hpp
#pragma once #include "Sock.hpp" #include <vector> #include <functional> #include <pthread.h> namespace ns_TcpServer { class TcpServer; using func_t = function<void(int)>; class ThreadData { public: ThreadData(int sock,TcpServer* server):sock_(sock),server_(server) {} ~ThreadData(){} public: int sock_; TcpServer* server_; }; class TcpServer { private: static void *ThreadRoutine(void *args) { pthread_detach(pthread_self()); ThreadData* td = static_cast<ThreadData*>(args); td->server_->Excute(td->sock_); close(td->sock_); return nullptr; } public: TcpServer(const uint16_t &port, const string &ip = "0.0.0.0") { listensock_ = sock_.Socket(); sock_.Bind(listensock_, port, ip); sock_.Listen(listensock_); } void BindService(func_t func) { func_.push_back(func); } void Excute(int sock) { for(auto & f : func_) { f(sock); } } void Start() { for (;;) { string clientip; uint16_t clientport; int sock = sock_.Accept(listensock_, &clientip, &clientport); if (sock == -1) continue; logMessage(NORMAL, "Create new link success, sock: %d", sock); pthread_t tid; ThreadData* td= new ThreadData(sock,this); pthread_create(&tid, nullptr, ThreadRoutine, td); } } ~TcpServer() { if (listensock_ >= 0) close(listensock_); } private: int listensock_; Sock sock_; vector<func_t> func_; }; }
- Protocol.hpp
#pragma once #include <iostream> #include <string> #include <string.h> #include <sys/types.h> #include <sys/socket.h> using namespace std; namespace ns_protocol { #define SPACE " " #define SPACE_LEN strlen(SPACE) class Request { public: string Serialization() { // 1.自主实现 "x_ op_ y_\r\n" string str; str = to_string(x_); str += SPACE; str += op_; str += SPACE; str += to_string(y_); return str; } bool Deserialization(const string &str) { size_t left = str.find(SPACE); if (left == string::npos) return false; size_t right = str.rfind(SPACE); if (right == string ::npos) return false; x_ = atoi(str.substr(0, left).c_str()); y_ = atoi(str.substr(right + SPACE_LEN).c_str()); if (left + SPACE_LEN > str.size()) return false; else op_ = str[left + SPACE_LEN]; return true; } public: Request(int x, int y, char op) : x_(x), y_(y), op_(op) { } Request() { } ~Request() {} public: int x_; int y_; char op_; }; class Response { public: // code_ result string Serialization() { string s; s = to_string(code_); s += SPACE; s += to_string(result_); return s; } bool Deserialization(const string& s) { size_t pos = s.find(SPACE); if(pos == string::npos) { return false; } code_ = atoi(s.substr(0,pos).c_str()); result_ = atoi(s.substr(pos+SPACE_LEN).c_str()); return true; } public: Response(int res, int code) : result_(result_), code_(code) { } Response() { } ~Response() {} public: int result_; // 计算结果 int code_; // 计算结果状态码 }; string Recv(int sock) { //tcp是面向字节流的 char inbuffer[1024]; ssize_t s = recv(sock, inbuffer, sizeof inbuffer, 0); if (s > 0) { inbuffer[s] = '\0'; return inbuffer; } else if(s == 0) { cout << "client quit" << endl; } else { cout << "recv error" << endl; } return ""; } void Send(int sock, const string str) { send(sock, str.c_str(), str.size(), 0); } }
- CalServer.cc
#include "TcpServer.hpp" #include "Protocol.hpp" #include <memory> #include <signal.h> using namespace ns_TcpServer; using namespace ns_protocol; using namespace std; static void usage(string proc) { cout << "\n Usage: " << proc << "port\n" << endl; } static Response calculatorHelp(Request &req) { Response resp(0, 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.code_ = 1; else resp.result_ = req.x_ / req.y_; break; case '%': if (req.y_ == 0) resp.code_ = 2; resp.result_ = req.x_ % req.y_; break; default: resp.code_ = 3; break; } return resp; } void calculator(int sock) { while (true) { string str = Recv(sock); // 在这里我们读到了一个请求 if (!str.empty()) { Request req; req.Deserialization(str); // 反序列化 Response resp = calculatorHelp(req); string respString = resp.Serialization(); // 对计算结果进行序列化 Send(sock, respString); } else { break; } } } //./calServer port int main(int argc, char *argv[]) { if (argc != 2) { usage(argv[0]); exit(1); } //一般经验:server在编写的时候,要有较为严谨的判断逻辑 //一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法访问问题 signal(SIGPIPE, SIG_IGN); unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1]))); server->BindService(calculator); server->Start(); }
- CalClient.cc
#include <iostream> #include "Sock.hpp" #include "Protocol.hpp" using namespace std; using namespace ns_protocol; static void usage(string proc) { cout << "\n Usage: " << proc << " serverIp serverPort\n" << endl; } //./clinet server_ip server_port int main(int argc, char *argv[]) { if (argc != 3) { usage(argv[0]); exit(1); } string serverip = argv[1]; uint16_t serverport = atoi(argv[2]); Sock sock; int sockfd = sock.Socket(); cout << sockfd << endl; if (!sock.Conncect(sockfd, serverip, serverport)) { cerr << "connect error" << endl; exit(2); } while (true) { Request req; cout << "Please Enter x# "; cin >> req.x_; cout << "Please Enter y#" ; cin >> req.y_; cout << "Please Enter operator# ";; cin >> req.op_; string s = req.Serialization(); Send(sockfd, s); string r = Recv(sockfd); Response resp; resp.Deserialization(r); cout << "code: " << resp.code_ << endl; cout << "result: " << resp.result_ << endl; sleep(1); } return 0; }