【Linux 网络】基于TCP的Socket编程:通过协议定制,实现网络计算器
为了让我们的代码结构划分更加清晰,我们先对部分功能进行封装
Socket封装
对Socket中经常需要使用到的接口进行封装。 采用模板方法模式进行设计,通过子类继承并重写父类方法来实现。
Socket.hpp
代码语言:javascript
AI代码解释
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;
const int BACK = 16;
namespace SocketModule
{
// 模板方法模式
// 基类大部分函数都是虚函数
class Socket
{
public:
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t prot) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
virtual void ConnectOrDie(InetAddr &addr) = 0;
virtual ssize_t Recv(string &buffer) = 0;
virtual int Send(const std::string &message) = 0;
virtual void Close() = 0;
void BuildTcpSocket(uint16_t prot, int backlog = BACK)
{
SocketOrDie();
BindOrDie(prot);
ListenOrDie(backlog);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket() {}
TcpSocket(int fd)
: _sockfd(fd)
{
}
void SocketOrDie() override
{
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(SOCK_ERR);
}
}
void BindOrDie(uint16_t prot) override
{
InetAddr addr(prot);
int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(BIND_ERR);
}
}
void ListenOrDie(int backlog) override
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(LISTEN_ERR);
}
}
// 客户端
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
if (fd < 0)
{
LOG(LogLevel::ERROR) << "accept error";
exit(ACCP_ERR);
}
// 将客户端信息带出
InetAddr addr(peer);
*client = addr;
// accept成功后返回一个新的套接字,用于服务
// 可以直接返回fd,也可以返回新对象(的指针)
return std::make_shared<TcpSocket>(fd);
}
void ConnectOrDie(InetAddr &addr) override
{
int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
if (ret < 0)
{
LOG(LogLevel::ERROR) << "connect error";
exit(CONN_ERR);
}
}
ssize_t Recv(string &buffer) override
{
char str[1024];
ssize_t n = recv(_sockfd, str, sizeof(str), 0);
if (n)
{
// 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
// 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
// 所以,我们必须自己显示的加上!
// 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
// 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
// 最终导致json串解析的时候失败!
str[n] = 0;
buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
}
return n;
}
int Send(const std::string &message) override
{
return send(_sockfd, message.c_str(), message.size(), 0);
}
void Close() override
{
close(_sockfd);
}
int Sockfd()
{
return _sockfd;
}
private:
int _sockfd;
};
}
协议定制
协议定制主要分为:序列化与反序列化、编码与解码
序列化与反序列化
在之前的文章中,我们曾讲过:协议在实现上,其本质是结构体!大家都遵守协议,那么涉及网络通信的数据都必须以结构体的形式存放!
而序列化,就是将“结构体”变成“字节流”!虽然协议的本质是结构体,网络通信的数据都是用结构体存放的,但是想要把数据通过网络发送给对方,其数据就必须是“字节流”。
将结构体转化为“字节流”有两种主流流派:
流派一:
直接发送结构体本身!既发送二进程数据。
很高效,很简单,速度快!但是!这就意味着发送方和接收方都必须是C++环境,并且结构体的内布局必须一模一样!
不推荐!
流派二:
结构化数据文本化!
直接将结构体的数据按照一定规则,进行文本化。仅发送文本、接收文本。接收的文件再按照规则进行反序列化!再次得到结构化数据。
推荐!
我们这里采用结构化数据文本化!那么具体改如何做呢?通过Json库实现!
认识JsonCpp库
JsonCpp库是C++中最通用的JSON处理库之一。主要功能就是进行序列化、反序列化。
序列化 (Serialization):将结构化数据,打包成一个标准的JSON字符串
反序列化 (Deserialization):把收到是JSON字符串,解析回结构化数据
使用样例:
由于JsonCpp库的第三方库,编译时一定要带路径:g++ -o @^ -l jsoncpp
代码语言:javascript
AI代码解释
#include <iostream>
#include <unistd.h>
#include <string>
#include <jsoncpp/json/json.h> //头文件注意!!
using namespace std;
// 序列化
int main()
{
// Json::Value 对象
Json::Value root;
root["one"] = "yi";
root["two"] = "er";
root["three"] = "san";
string str = root.toStyledString();
cout << str << endl;
// Json::FastWriter 对象,不添加额外空格、换行符
Json::FastWriter fast;
str = fast.write(root);
cout << str << endl;
// Json::StreamWriterBuilder
Json::StreamWriterBuilder stream;
std::unique_ptr<Json::StreamWriter> writer(stream.newStreamWriter());
stringstream ss;
writer->write(root, &ss);
cout << ss.str() << endl;
}
代码语言:javascript
AI代码解释
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
{
"one" : "yi",
"three" : "san",
"two" : "er"
}
{"one":"yi","three":"san","two":"er"}
{
"one" : "yi",
"three" : "san",
"two" : "er"
}
代码语言:javascript
AI代码解释
// 反序列化
int main()
{
// string str = {"\" one ":\" yi ",\" three ":3,\" two ":" er \""};
string str = "{\"name\":\"张三\", \"age\":30, \"city\":\"北京\"}";
Json::Value root;
Json::Reader read;
read.parse(str, root);
cout << root["name"].asString() << endl;
cout << root["city"].asString() << endl;
cout << root["age"].asInt() << endl;
}
代码语言:javascript
AI代码解释
hyc@hyc-alicloud:~/linux/序列与反序列/json使用样例$ ./a
张三
北京
30
通过使用JsonCpp库,我们就可以实现序列化与反序列化了!
编码与解码
为什么要编码与解码
封包与解包是为了解决 TCP 的“粘包”和“半包”问题,明确消息的边界。 如果说序列化是为了让数据能“顺着网线爬过去”,那么编码就是为了让接收方知道“这一坨数据爬到哪里才算是个头”。
根本原因:TCP是面向字节流的,因此TCP发送数据,是不能像UDP一样保证其数据是完整的数据报。它的传输方式像流水一样,而不是像快递包裹一样。因为 TCP 这种“流水”特性,在接收端会发生两种灾难性的情况:粘包:当发送方快速的发送了多条消息时,在底层这多条数据会被全部交给发送缓冲区中,由OS负责进行发送!
更多推荐



所有评论(0)