Linux 深入理解网络编程:应用层自定义协议、序列化、TCP 粘包与 Socket 封装
Linux 网络编程中的核心概念,包括应用层协议的自定义设计、数据的序列化与反序列化过程。针对 TCP 粘包问题,提出了基于报头长度分隔符的解决方案,并展示了使用 JSON 进行数据转换的代码实现。最后通过模板方法模式封装了 Socket 类,实现了 TCP 和 UDP 操作的统一接口管理,提升了网络编程的优雅性与可维护性。

Linux 网络编程中的核心概念,包括应用层协议的自定义设计、数据的序列化与反序列化过程。针对 TCP 粘包问题,提出了基于报头长度分隔符的解决方案,并展示了使用 JSON 进行数据转换的代码实现。最后通过模板方法模式封装了 Socket 类,实现了 TCP 和 UDP 操作的统一接口管理,提升了网络编程的优雅性与可维护性。

程序员编写的网络程序,旨在解决实际问题并满足日常需求,均运行在应用层。
根据前面的知识,我们知道协议是一种'约定'。
Socket API 的接口在读写数据时,默认按'字符串'方式发送接收。如果我们要传输一些'结构化的数据'怎么办呢?
协议的真正概念:其实,协议就是双方约定好的结构化数据!

举个例子:例如我们需要实现一个服务器版的加法器。客户端需要把要计算的两个加数发过去,由服务器进行计算,最后再把结果返回给客户端。
约定方案:
这就是序列化和反序列化!
向上通过反序列化读取消息,向下通过序列化包装消息。而 TCP/UDP 不关心发送的是什么,都按照字符串进行传输!

TCP 能接受全双工的本质原因是因为 TCP 连接各有一对发送缓冲区和接收缓冲区。
TCP 发送数据的本质:将自己的发送缓冲区拷贝到接收方的接收缓冲区中!
通信的本质就是拷贝!!!
read、write、recv、send 本质是拷贝函数!
因为缓冲区中没有数据,阻塞的本质就是用户层在同步!
TCP 设计也是符合生产者消费者模型!因为发送缓冲区和接收缓冲区都是属于操作系统的,所以一定是临界资源!会有多个生产者,多个消费者!而 IO 发生阻塞也就是为了维护同步关系,保证缓冲区的正确使用!
接受的时候,我们还需要解决一个问题,保证我们拿到的是一个完整请求!
举个例子:对方的接收缓冲区写满了,对方一直不读,那么我们的发送缓冲区就积压了很多同样的请求,如果一次性刷新过去,对方就读取到多条信息;又或者只发送了一条请求的一半过去,那么接受方读取就读取一半了,就不可能进行反序列化!这个过程就叫面向字节流!!!
所以怎么保证读取的是一个完整的请求呢???
这就是经典的 TCP 的粘包问题了!
需要我们自定义协议出马了!(协议的再理解)
我们自己规定协议如下:
报文 = 报头 + 有效载荷
"有效载荷的长度"\r\n"有效载荷"\r\n
"len"\r\n"_x _op _y"\r\n -> len: 有效载荷的长度,约定\r\n是分隔符,不参与统计
我们自己利用自定义协议将报文封装起来,方便解码判断是否是一个完整的报文。
编码后的报文:有效载荷的长度 + 分隔符 + 有效载荷 + 分隔符
const std::string SEP = "\r\n"; // 我们把 tcp 中读到的报文,可能读到半个,也可能读到 1 个半个,TCP 粘报问题 // 解决 TCP 的粘报问题
std::string Encode(const std::string &json_str) {
int json_str_len = json_str.size();
std::string proto_str = std::to_string(json_str_len);
proto_str += SEP;
proto_str += json_str;
proto_str += SEP;
return proto_str;
}
根据我们自己定义的协议,分离报头,分隔符后,看是否是一个完整的报头。
如果输入流还有内容,我们只需要从输入流中提取出一个完整的请求,剩余留着下一次处理!
std::string Decode(std::string &inbuffer) {
auto pos = inbuffer.find(SEP);
if (pos == std::string::npos) return std::string();
std::string len_str = inbuffer.substr(0, pos);
if (len_str.empty()) return std::string();
int packlen = std::stoi(len_str);
int total = packlen + len_str.size() + 2 * SEP.size();
if (inbuffer.size() < total) return std::string();
std::string package = inbuffer.substr(pos + SEP.size(), packlen);
inbuffer.erase(0, total);
return package;
}
下面主要是主要代码
bool Serialize(std::string *out) {
// 转换成为字符串
Json::Value root;
root["result"] = _result;
root["code"] = _code;
Json::FastWriter writer;
// Json::StyledWriter writer;
*out = writer.write(root);
return true;
}
bool Deserialize(const std::string &in) {
Json::Value root;
Json::Reader reader;
bool res = reader.parse(in, root);
if (!res) return false;
_result = root["result"].asInt();
_code = root["code"].asInt();
return true;
}
将 socket 系列操作分类封装,设计为基类,派生出 Tcp 和 Udp 两种具体的 Socket!基类都需要进行创建 socket 文件、进行绑定、进入 listen、获取链接、申请链接…由于两种类的操作方式不一致,所以基类只需要进行一个声明就可以,具体实现在派生类中完成!
以后直接调用相应接口即可,非常优雅!TcpSocket 继承 Socket 类,成员变量 sockfd。
通过这些操作的组合,可以进行建立监听链接,建立客户端连接等操作,十分方便!这种设计模式是模版方法设计模式!!!

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
/* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/types.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
// 模版方法模式
namespace socket_ns {
class Socket;
const static int gbacklog = 8;
using socket_sptr = std::shared_ptr<Socket>;
enum { SOCKET_ERROR = 1, BIND_ERROR, LISTEN_ERROR, USAGE_ERROR };
// std::unique_ptr<Socket> listensock = std::make_unique<TcpSocket>();
{
:
= ;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
:
{
();
();
(addr);
();
}
{
();
(addr);
}
};
: Socket {
:
( fd = ) : _sockfd(fd) { }
{
_sockfd = ::(AF_INET, SOCK_STREAM, );
(_sockfd < ) {
(FATAL, );
(SOCKET_ERROR);
}
(DEBUG, , _sockfd);
}
{
local;
(&local, , (local));
local.sin_family = AF_INET;
local.sin_port = (addr.());
local.sin_addr.s_addr = (addr.().());
n = ::(_sockfd, ( sockaddr *)&local, (local));
(n < ) {
(FATAL, );
(BIND_ERROR);
}
(DEBUG, , _sockfd);
}
{
n = ::(_sockfd, gbacklog);
(n < ) {
(FATAL, );
(LISTEN_ERROR);
}
(DEBUG, , _sockfd);
}
{
peer;
len = (peer);
sockfd = ::(_sockfd, ( sockaddr *)&peer, &len);
(sockfd < ) {
(WARNING, );
;
}
*addr = peer;
socket_sptr sock = std::<TcpSocket>(sockfd);
sock;
}
{
server;
(&server, , (server));
server.sin_family = AF_INET;
server.sin_port = (addr.());
server.sin_addr.s_addr = (addr.().());
n = (_sockfd, ( sockaddr *)&server, (server));
(n < ) {
std::cerr << << std::endl;
;
}
;
}
{
opt = ;
::(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, (opt));
}
{
inbuffer[];
n = ::(_sockfd, inbuffer, (inbuffer) - , );
(n > ) {
inbuffer[n] = ;
*out = inbuffer;
}
n;
}
{
n = ::(_sockfd, in.(), in.(), );
n;
}
{
_sockfd;
}
{
(_sockfd > ) ::(_sockfd);
}
:
_sockfd;
};
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online