欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:C++【智能指针】
前言
在正式代码开始前,会有一些前提知识引入
目录
- 👉🏻序列化和反序列化
- 👉🏻三次握手和四次挥手
- 👉🏻一些概念知识
- 全双工
- TCP和UDP对比
- send和recv
- 👉🏻TCP通信-使用相同结构化字段传输数据(但未真正的序列化和反序列化)
- Protocol.hpp(协议内容)
- Socket.hpp(封装socket通信的功能)
- TcpServer.hpp(封装服务端功能)
- TcpServerMain.cc
- TcpClientMain.cc
- 实现效果
👉🏻序列化和反序列化
在网络应用层中,序列化(Serialization)和反序列化(Deserialization)是将数据转换为可在网络上传输的格式,并从网络接收的数据恢复为本地数据结构的过程。
🌈序列化(Serialization):
序列化是将数据对象转换为一系列字节的过程,以便在网络上传输或存储到磁盘上。序列化的目标是创建一个可以被发送到其他系统或进程,并且能够被正确解释和还原的数据表示形式。在网络应用中,常见的序列化格式包括JSON(JavaScript Object Notation)、XML(eXtensible Markup Language)、Protocol Buffers、MessagePack等。
序列化的过程通常涉及以下步骤:
- 选择序列化格式:根据数据的特性和需求选择合适的序列化格式。
- 定义数据结构:确定要序列化的数据对象的结构,并为其定义序列化规则。
- 将数据转换为字节流:根据所选的序列化格式,将数据对象转换为字节流。
- 传输或存储:将序列化后的字节流发送到网络或者存储到磁盘上。
🌈反序列化(Deserialization):
反序列化是将序列化后的数据流转换回原始数据对象的过程。在接收到网络传输的数据后,需要对其进行反序列化以还原成原始数据对象。
反序列化的过程通常包括以下步骤:
- 接收数据流:从网络或者磁盘读取序列化后的数据流。
- 解析数据:根据所选的序列化格式,解析字节流并将其转换为数据对象。
- 数据还原:根据序列化规则和数据结构,将解析后的数据转换为原始数据对象。
- 应用数据:将还原后的数据对象用于应用程序的后续处理。
序列化和反序列化在网络通信中扮演着重要的角色,它们允许不同系统之间以统一的方式进行数据交换,同时也提供了数据传输的可靠性和可扩展性。
📒 一个小故事理解序列化和反序列化
故事标题:糖果工厂的序列化奇遇
一天,糖果工厂的老板决定向全球各地拓展市场,他决定使用一种特殊的糖果序列化器来包装他的糖果,以确保它们在长途运输中保持新鲜和美味。
序列化器(Serializationizer)是一台神奇的机器,它可以将任何形状、口味的糖果转换成一种特殊的串口糖果,这种串口糖果可以轻松地传输到世界各地,并在需要时还原为原始的糖果。
老板向工厂的工程师们解释了他的计划,然后开始了序列化器的操作。首先,他们把一袋五彩斑斓的糖果放进了序列化器中,它发出了一声“嘟噜嘟噜”的声音,然后从另一端输出了一串光滑而有序的串口糖果。
工程师们快乐地向老板展示他们的成果,老板也很满意。于是,他们将这些串口糖果装进了特殊的包装盒,准备发往全球各地的客户。
然而,一名新来的工程师在整理文件时不小心碰到了序列化器的控制台,他误触了一个按钮,导致序列化器的设置发生了改变。
于是,下一个批次的糖果被转换成了一种奇怪的形状,颜色也变得混乱不堪。这些串口糖果被送到了全球各地,但客户们收到后都表示了不满,称他们从未见过如此奇特的糖果。
老板赶紧调查了原因,发现了新工程师的失误。他们及时纠正了序列化器的设置,重新开始了正常的生产。这次,他们确保了每个糖果被正确序列化,而不是变成了像乱七八糟的串口糖果。
结局: 糖果工厂重新获得了客户的信任,全球各地的人们再次享用到了美味的糖果。而那位新工程师也从这个经历中学到了重要的教训:在操作序列化器时一定要小心,否则可能会引发一场糖果灾难!
👉🏻三次握手和四次挥手
当建立和终止TCP连接时,通常会执行三次握手(Three-Way Handshake)和四次挥手(Four-Way Handshake)的过程,以确保通信的可靠性和正确性。
🌈三次握手(Three-Way Handshake):
-
客户端发送同步序列号(SYN)报文:
- 客户端首先向服务器发送一个带有SYN标志的报文,表示客户端请求建立连接,并选择一个初始的序列号(Sequence Number)。
-
服务器确认连接请求:
- 服务器收到客户端的SYN报文后,会发送一个带有SYN和ACK标志的报文,表示同意建立连接,并确认收到了客户端的连接请求,并选择自己的初始序列号。
-
客户端确认连接:
- 客户端收到服务器的SYN+ACK报文后,会发送一个带有ACK标志的报文给服务器,表示确认连接建立。
这样,客户端和服务器之间的TCP连接就建立起来了,可以开始进行数据传输。
🌈 四次挥手(Four-Way Handshake):
-
客户端发送关闭连接请求:
- 客户端发送一个带有FIN(结束)标志的报文给服务器,表示客户端不再发送数据,但仍愿意接收数据。
-
服务器确认关闭请求并关闭数据传输:
- 服务器收到客户端的FIN报文后,会发送一个带有ACK标志的报文给客户端,表示确认关闭请求,并停止向客户端发送数据,但仍可以接收数据。
-
服务器发送关闭连接请求:
- 服务器发送一个带有FIN标志的报文给客户端,表示服务器也准备关闭连接。
-
客户端确认关闭请求并关闭连接:
- 客户端收到服务器的FIN报文后,会发送一个带有ACK标志的报文给服务器,表示确认关闭请求,并关闭连接。
这样,客户端和服务器之间的TCP连接就完全关闭了。四次挥手的过程中,双方都可以发送数据,并且在关闭连接后都不能再发送数据。
📒 一个小例子理解三次握手和四次挥手
好的,让我们用一种有趣的方式来理解三次握手和四次挥手。
🫱🏻🫲🏻 三次握手(Three-Way Handshake):
想象一下你和朋友约好去吃披萨。这里有个名叫小明的朋友(客户端)和一个叫披萨店的地方(服务器)。
-
小明: “嗨,披萨店老板!我想要一份披萨!”(发送SYN)
-
披萨店老板: “好的,小明,你想要什么口味的披萨?”(发送SYN+ACK)
-
小明: “我想要意大利香肠披萨!”(发送ACK)
现在,小明和披萨店之间建立了连接,披萨店知道了小明的口味,准备开始制作披萨。
🤲🏻 四次挥手(Four-Way Handshake):
披萨终于做好了,大家都吃得很开心,然后就是结束这次美好的披萨时光。
-
小明: “披萨店老板,谢谢你的披萨,我不想再点了!”(发送FIN)
-
披萨店老板: “不客气,小明,欢迎下次再来!披萨店休息了!”(发送ACK)
-
披萨店老板: “好了,披萨店打烊了,我们关门了!”(发送FIN)
-
小明: “明白了,披萨店老板,再见!”(发送ACK)
这样,小明和披萨店之间的交流就结束了,披萨店可以关门休息了,而小明也满足地离开了。
👉🏻一些概念知识
全双工
全双工(Full Duplex)是指数据通信系统中能够同时实现双向通信的能力,即在同一时间点上可以同时进行发送和接收数据的操作。这种模式下,通信双方能够同时进行双向数据传输,而不需要等待对方完成发送或接收操作。
在全双工通信中,发送和接收数据的通道是完全独立的,彼此之间互不干扰。这意味着通信双方可以在不同的频率或者不同的频道上同时进行通信,而不会造成碰撞或数据丢失。
全双工通信通常用于需要高速、实时双向数据传输的场景,比如电话通话、视频会议、网络通信等。相比于半双工通信(Half Duplex),全双工通信具有更高的通信效率和更低的延迟,因为它允许发送和接收数据同时进行,而不需要等待切换操作。
在网络通信中,全双工模式通常通过使用不同的通信频率(如Wi-Fi、蓝牙等无线通信)、不同的通信信道(如以太网的双绞线)或者使用不同的时隙(如时分多址技术)来实现。这种模式在现代通信技术中被广泛应用,为用户提供了更流畅、更高效的通信体验。
TCP和UDP对比
TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的网络传输协议,它们在数据传输时有着不同的特点和适用场景:
-
连接性:
- TCP是面向连接的协议,它在通信双方建立连接后才能进行数据传输,确保数据的可靠性和顺序性。
- UDP是无连接的协议,通信双方无需建立连接即可直接发送数据,因此不保证数据的可靠性和顺序性。
-
可靠性:
- TCP提供可靠的数据传输,通过序号、确认和重传机制来确保数据的完整性和可靠性,保证数据不会丢失或损坏。
- UDP不提供可靠性保证,数据包可能会丢失、重复或者乱序,因此在一些实时性要求高、但对数据完整性要求较低的场景下使用较多。
-
流量控制(面向字节流)和拥塞控制:
- TCP通过流量控制和拥塞控制机制来调节数据传输速率,以避免网络拥塞和数据丢失。
- UDP不提供流量控制和拥塞控制,数据传输速率由发送方直接决定,可能会导致网络拥塞。
-
适用场景:
- TCP适用于需要可靠数据传输和顺序传输的场景,如文件传输、网页浏览、电子邮件等。
- UDP适用于实时性要求高、但对数据完整性要求较低的场景,如音频和视频流、在线游戏、实时通信等。
-
开销:
- TCP的头部开销较大,包含了序号、确认、窗口大小等信息,因此在传输小量数据时可能会存在较大的开销。
- UDP的头部开销较小,只包含了源端口、目标端口、长度和校验和等基本信息,因此在传输小量数据时开销较小。
总的来说,TCP提供了可靠的数据传输和顺序传输,适用于对数据完整性要求高的场景;而UDP提供了更快速的数据传输和更低的开销,适用于实时性要求高、但对数据完整性要求较低的场景。选择使用哪种协议取决于具体的应用需求和性能要求。
send和recv
当编写网络程序时,常用的函数之一是send
和recv
,它们通常用于在TCP连接上发送和接收数据。
🍓send 函数:
- 功能: 用于在已建立的连接上发送数据。
- 语法:
send(socket, data, flags)
socket
:指定发送数据的套接字。data
:要发送的数据。flags
:指定发送操作的可选标志。
🍓recv 函数:
- 功能: 用于从已建立的连接上接收数据。
- 语法:
recv(socket, buffersize, flags)
socket
:指定接收数据的套接字。buffersize
:指定接收缓冲区的大小。flags
:指定接收操作的可选标志。
这两个函数在TCP编程中非常常见,它们允许程序在客户端和服务器之间进行双向通信。
🍴 与sendto 函数和 recvfrom 函数的区别:
- sendto 函数: 用于在无连接的套接字上发送数据。通常用于UDP套接字。
- 它需要指定目标地址和端口。
- recvfrom 函数: 用于从无连接的套接字上接收数据。通常用于UDP套接字。
- 它返回发送数据的源地址和端口。
在网络编程中,write和read函数通常用于TCP套接字,因此它们也是基于已连接的。
总的来说,send
和recv
函数适用于TCP连接,而sendto
和recvfrom
函数适用于UDP套接字。前者是基于连接的,后者是无连接的。
👉🏻TCP通信-使用相同结构化字段传输数据(但未真正的序列化和反序列化)
代码目录:
Protocol.hpp(协议内容)
#pragma once
#include<iostream>
#include<memory>
using namespace std;
class Request
{
public:
Request()
{}
Request(int x,int y,char op)
:_data_x(x),_data_y(y),_oper(op)
{}
void Inc()
{
_data_x++;
_data_y++;
}
void Debug()
{
cout<<"_data_x: "<<_data_x<<endl;
cout<<"_data_y: "<<_data_y<<endl;
cout<<"_oper: "<<_oper<<endl;
}
private:
int _data_x;
int _data_y;
char _oper;//操作数
};
class Response
{
public:
Response()
{}
Response(int result,int code)
:_result(result),_code(code){
}
private:
int _result;
int _code;
};
//工厂模式,建造类设计模式,直接返回指针对象
class Factory
{
public:
shared_ptr<Request> BuildRequest()
{
shared_ptr<Request> req = make_shared<Request>();
return req;
}
shared_ptr<Request> BuildRequest(int x,int y,char op)
{
shared_ptr<Request> req = make_shared<Request>(x,y,op);
return req;
}
shared_ptr<Response> BuildResponse()
{
shared_ptr<Response> resp = make_shared<Response>();
return resp;
}
shared_ptr<Response> BuildResponse(int result,int code)
{
shared_ptr<Response> resp = make_shared<Response>(result,code);
return resp;
}
};
Socket.hpp(封装socket通信的功能)
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define Convert(addrptr) ((struct sockaddr*)addrptr)
using namespace std;
namespace Net_Work
{
const static int defaultsockfd = -1;
const int backlog = 5;
enum
{
SocketError = 1,
BindError,
ListenError,
};
//封装一个基类:Socket接口类
class Socket
{
public:
virtual ~Socket(){}
virtual void CreateSocketOrDie() = 0;//创建一个套接字
virtual void BindSocketOrDie(uint16_t port) = 0;//套接字进行绑定网络信息
virtual void ListenSocketOrDie(int backlog) = 0;//进行监听
virtual Socket* AcceptConnection(string * peerip,uint16_t* peerport)=0;//接收连接,并返回一个新的套接字
virtual bool ConnectServer(string& peerip,uint16_t peerport)=0;//连接服务端
virtual int GetSockFd() = 0;//返回套接字描述符
virtual void SetSockFd(int sockfd) = 0;//
virtual void CloseSocket() = 0;//关闭套接字
public:
void BuildListenSocketMethod(uint16_t port,int backlog)//创建一个监听服务
{
//1.创建套接字
CreateSocketOrDie();
//2.套接字进行绑定网络信息
BindSocketOrDie(port);
//3.开始监听
ListenSocketOrDie(backlog);
}
bool BuildConnectSocketMethod(string& serverip,uint16_t& serverport)//创建一个连接服务
{
//1.创建套接字
CreateSocketOrDie();
return ConnectServer(serverip,serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockFd(sockfd);
}
};
//实现Tcp套接字
class TcpSocket:public Socket
{
public:
TcpSocket(int sockfd = defaultsockfd )
:_sockfd(sockfd)
{
}
~TcpSocket(){}
/
void CreateSocketOrDie() override//创建一个套接字
{
_sockfd = socket(AF_INET,SOCK_STREAM,0);
if(_sockfd<0)
exit(SocketError);
}
void BindSocketOrDie(uint16_t port) override//套接字进行绑定网络信息
{
//本地网络信息初始化
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;//服务端的ip由本地随机绑定
local.sin_port = htons(port);
//开始绑定
int n = bind(_sockfd,Convert(&local),sizeof(local));
if(n<0) exit(BindError);
}
void ListenSocketOrDie(int backlog) override//进行监听
{
int n = listen(_sockfd,backlog);
if(n<0) exit(ListenError);
}
Socket* AcceptConnection(string * peerip,uint16_t* peerport)override//接收连接
{
struct sockaddr_in peer;//用来存储客户端的地址信息
socklen_t len = sizeof(peer);
int newsockfd = accept(_sockfd,Convert(&peer),&len);
if(newsockfd<0)
return nullptr;
*peerport = ntohs(peer.sin_port);//网络序列本地化
*peerip = inet_ntoa(peer.sin_addr);
Socket* s = new TcpSocket(newsockfd);
return s;
}
bool ConnectServer(string& serverip,uint16_t serverport)override//连接服务端
{
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());//ip网络字节序化,4字节
int n = connect(_sockfd,Convert(&server),sizeof(server));
if(n==0)
return true;
else
return false;
}
int GetSockFd() override//返回套接字描述符
{
return _sockfd;
}
void SetSockFd(int sockfd) override//
{
_sockfd = sockfd;
}
void CloseSocket() override//关闭套接字
{
if(_sockfd>defaultsockfd)
close(_sockfd);
}
private:
int _sockfd;
};
};
TcpServer.hpp(封装服务端功能)
#pragma once
#include"Socket.hpp"
#include<pthread.h>
#include<functional>
using func_t = function<void(Net_Work::Socket* sockp)>;
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer* tcp_this,Net_Work::Socket* sockp)
:_this(tcp_this),_sockp(sockp)
{}
public:
TcpServer* _this;//TcpServer的指针对象
Net_Work::Socket* _sockp;//套接字指针对象
};
class TcpServer
{
public:
TcpServer(uint16_t port,func_t handler_request)
:_port(port),_listensocket(new Net_Work::TcpSocket()),_hanlder_request(handler_request)
{
_listensocket->BuildListenSocketMethod(_port,Net_Work::backlog);//开启监听事务
}
static void * ThreadRun(void* args)//因为pthread_create要求方法参数中的参数必须只有一个void*
//所以必须变为静态,否则成员函数第一个参数默认隐式为this指针
{
//因为执行的是多线程,这里我们也没有封装线程的自动回收
//所以为了不发生线程阻塞,我们要让当前线程与主线程分离,不影响主线程,并且自己做完任务自己回收
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
td->_this->_hanlder_request(td->_sockp);//执行_hanlder_request方法
td->_sockp->CloseSocket();//关闭accept的新套接字
delete td->_sockp;//销毁指针
delete td;
return nullptr;
}
void Loop()
{
while(true)
{
string peerip;
uint16_t peerport;
Net_Work::Socket* newsocket = _listensocket->AcceptConnection(&peerip,&peerport);//接收客户端信息
if(newsocket==nullptr) continue;
cout<<"获取一个新连接,sockfd:"<<newsocket->GetSockFd()<<"client info: "<<peerip<<" "<<peerport<<endl;
//用完后关闭newsocket
//newsocket->CloseSocket();
//使用多线程进行处理任务
pthread_t tid;
ThreadData* td = new ThreadData(this,newsocket);
pthread_create(&tid,nullptr,ThreadRun,td);//线程创建并执行相对应任务
}
}
~TcpServer()
{
delete _listensocket;
}
private:
uint16_t _port;
Net_Work::Socket* _listensocket;
public:
func_t _hanlder_request;//request执行方法
};
TcpServerMain.cc
#include"Protocol.hpp"
#include"Socket.hpp"
#include"TcpServer.hpp"
#include<memory>
using namespace Net_Work;
void HandlerRequest(Socket* sockp)
{
while(true)
{
struct Request req;//用来存储客户端发来的需求信息
recv(sockp->GetSockFd(),&req,sizeof(req),0);//接收
req.Debug();//打印信息
}
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
cout << "Usage : " << argv[0] << " port" << std::endl;
return 0;
}
uint16_t localport = stoi(argv[1]);
unique_ptr<TcpServer> svr (new TcpServer(localport,HandlerRequest));//unique_ptr只能支持移动构造
svr->Loop();//server开始不断获取新连接
return 0;
}
TcpClientMain.cc
#include"Protocol.hpp"
#include"Socket.hpp"
int main(int argc,char* argv[])
{
if(argc != 3)
{
cout << "Usage : " << argv[0] << " serverip serverport" << std::endl;
return 0;
}
string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
Net_Work::Socket* s = new Net_Work::TcpSocket();
if(!s->BuildConnectSocketMethod(serverip, serverport))
{
cerr << "connect " << serverip << ":" << serverport << " failed" << std::endl;
}
cout << "connect " << serverip << ":" << serverport << " success" << std::endl;
unique_ptr<Factory> factory = make_unique<Factory>();//创建一个工厂对象指针(后续可以生产需求和回应),工厂只能有一个,所以用unique_ptr指针
shared_ptr<Request> req = factory->BuildRequest(10,20,'+');
while(true)
{
req->Inc();
send(s->GetSockFd(),&(*req),sizeof(*req),0);//将需求信息发送给服务端
sleep(1);
}
s->CloseSocket();//关闭套接字
return 0;
}
实现效果
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长