当前位置: 首页 > news >正文

基于Tcp协议的应用层协议定制

前言:本文默认读者已掌握 TCP 协议相关网络接口知识,将聚焦于应用层协议的设计与剖析,有关底层通信机制及业务逻辑部分仅作简要概述,不再展开详述。

目录

服务器

一、通信

二、协议

1.序列化与反序列化

2. 封包与解包

三、业务

客户端

四、源码


        本文将基于TCP协议构建一个网络计算器服务。业务逻辑相对弱化一些,目的是完整演示服务端开发的核心流程。而把重点放在应用层协议的设计过程,包括请求/响应报文结构定义、数据传输机制等关键实现细节;同时讲解Socket通信层的工程化封装,通过抽象连接建立、数据收发、资源管理等基础操作,构建可扩展的网络通信模块。本文形成的协议设计方法论与组件封装方案,可直接复用于各类TCP服务端开发场景。

服务器

框架设计

        TCP协议作为面向字节流的传输层协议,其不维护报文边界的特点可能导致粘包/半包问题。为此我们需要设计应用层协议,通过报文头部标识、数据完整性校验等机制确保可靠通信。若您对以上内容很懵这很正常,到下文解决自定义协议时会细讲。

首先我们梳理本文要完成的核心文件及功能,如下:

1.通信

  • TcpServer.hpp:服务器相关的类以及类方法的实现——主要完成通信功能。
  • TcpServer.cc:服务器主函数(main)的实现——对服务器接口的调用,即启动服务器。
  • TcpClient.cc:客户端主函数(main)的实现——启动客户端,并与服务器通信。

2.协议

  • Protocol.hpp:自定义协议,完成序列化、反序列化、封包,解包等。

3.业务

  • NetCal.hpp:网络计算器的实现。

一、通信

        由于C++标准库未原生提供网络通信支持,而网络编程中连接管理、数据收发等底层操作虽遵循固定模式却存在大量重复劳动,因此我们将首先实现一个高内聚的Socket封装类。该模块通过抽象TCP通信的核心流程,统一处理连接建立维护、收发数据等基础功能,为后续业务开发提供稳定可靠的通信基础设施。

        这里我们使用模板方法模式,即基类Socket大部分方法都是纯虚方法,让Tcp协议类和Udp类作为子类进行继承。

创建Socket.hpp文件,实现类的声明,如下:

static const int gbacklog = 8;//允许8个客户端连接
namespace SocketMoudule
{class Socket{public:virtual void SocketOrDie() = 0; //打开网络文件virtual void BindOrDie(uint16_t) = 0; //绑定端口virtual void ListenOrDie(int) = 0; //监听virtual shared_ptr<Socket> Accept(InetAddr *) = 0; //接收请求virtual void Close() = 0;    //关闭网络文件virtual int Recv(string *) = 0;    //收数据virtual int Send(string &) = 0;    //发数据virtual void Connect(const std::string &, uint16_t) = 0; //与服务器建立连接public:void BuildTcpServerMoudule(uint16_t port, int backlog = gbacklog){//初始化SocketOrDie();BindOrDie(port);ListenOrDie(backlog);}void BuildUdpServerMoudule(uint16_t port){void SocketOrDie();void BindOrDie(port);}};//Tcp协议通信class TcpSocket : public Socket{public:private:int _socketfd;};//Udp协议通信class UdpSocket : public Socket{public:private:};
}

其中InetAddr类是对sockaddr_in等相关信息的封装,如下:

class InetAddr
{
public:InetAddr() {}InetAddr(sockaddr_in &peer): _addr(peer){_port = ntohs(peer.sin_port);char buffer[32];inet_ntop(AF_INET, &peer.sin_addr, buffer, sizeof(peer));_ip = buffer;}InetAddr(uint16_t port): _port(port), _ip(to_string(INADDR_ANY)){_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = INADDR_ANY;}InetAddr(uint16_t port, string ip): _port(port), _ip(ip){_addr.sin_family = AF_INET;//主机序->网络序_addr.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);//_addr.sin_addr.s_addr = inet_addr(_ip.c_str());}string tostring_port(){return to_string(_port);}string tostring_ip(){return _ip;}bool operator==(InetAddr addr){return _port == addr._port && _ip == addr._ip;}sockaddr *getaddr(){return (sockaddr *)&_addr;}socklen_t getlen(){return sizeof(_addr);}string stringaddr(){return tostring_ip() + ":" + tostring_port() + " ";}private:uint16_t _port;string _ip;sockaddr_in _addr;
};

为什么要用OrDie后来缀命名呢?

        OrDie:源自C/C++开发中的assert_or_die()等函数命名范式,表示关键资源必须成功初始化,否则程序应立即终止的严格错误处理策略。常见于Google等公司的内部代码库(如ParseOrDie()),形成了一种防御性编程的文化符号

表示:"宁可程序立刻崩溃暴露问题,也不允许在错误状态下苟延残喘"。


        注:以上接口基本没人能一次性想出来,想出来了参数也不一定设对,而应该在开发过程中根据需求或发现问题,而进行添加或修改。

        函数具体实现很简单,一一调用对应的网络接口即可,这里就不再讲解,文末会给出源码。

完成TcpServer.hpp文件

        创建TcpServer类,它主要完成打开网络文件,端口绑定,监听,接收请求。这些功能我们都在Socket中封装了,我们实例化出TcpSocket对象,然后对它的接口就行调用即可。

所以TcpServer类成员,要包含两个成员变量。

  • unique_ptr<Socket> _listensockptr:指向一个TcpSocket对象。
  • 回调数据处理方法:这个成员到后文再设计。

        在实际中会有很多客户端与服务器进行连接,通常需要并发的处理客户端需求,可以使用多进程、多线程、线程池、进程池等。这里我们就做简单一点,使用多进程完成并发功能。

        而子进程默认情况下是需要父进程进行等待的,这就会造成主进程阻塞,无法接收其他客户的请求,和单执行流没区别了。基于这样的问题有两种解决方法:

  1. SIGCHIL信号(17号)的处理方法设为默认。
  2. 主进程创建子进程a后再创建孙子进程b,此时a退出,让b进程去完成任务,b的父亲是a,a退出b进程成为了孤儿进程,会交给系统管理。主进程就不用管了。

如果不理解第1点,可以看一下文章,然后锁定到特殊信号的SIGCHIL信号进行学习:

Linux信号的诞生与归宿:内核如何管理信号的生成、阻塞和递达?_内核是如何产生信号的-CSDN博客z

这里我们用方法2解决,如下:

using namespace SocketMoudule;
class TcpServer
{
public:TcpServer(uint16_t port): _listensockptr(make_unique<TcpSocket>()){//进行初始化,同时启动服务器_listensockptr->BuildTcpServerMoudule(_port);Start();}void Start(){while(true){InetAddr addr;auto sock = _listensockptr->Accept(&addr);if(sock == nullptr) continue;LOG(Level::INFO)<<addr.stringaddr()<<"accept success...";pid_t pid = fork();if(pid < 0){LOG(Level::FATAL)<<"fork fail";exit(FORK_ERRO);}if(pid == 0){if(fork()>0)exit(SUCCESS);//回调方法//......sock->Close();exit(SUCCESS);}else{sock->Close();}}}
private:unique_ptr<Socket> _listensockptr;//回调函数
};

        说明:FORK_ERRO、SUCCESS是退出码,本质是枚举类型,在文件Common.hpp中定义,LOG是我写的一个打印日志的接口, 大家把它当作cout理解就行,当然需要日志源码的可以私信我。

二、协议

TCP协议作为面向字节流的传输层协议,可能导致粘包/半包问题。

粘包:发送方连续发送多个独立数据包,接收方可能一次性读取到合并数据。比如发送6和7,接收到67。

半包:发送方传输大尺寸数据包,接收方首次读取到部分数据。比如发送hello,接收到he和llo。

1.序列化与反序列化

粘包问题

        对于粘包问题,可以在数据之间添加一些标识符来区分它们,比如": ",当接收到完整的数据包后根据这些标识符就可以区分出它们。这个操作就是序列化和反序列化

数据包以什么格式传递是通信双方(服务器和客户端)约定好的,即协议。序列化和反序列化就是协议的一部分。

注意:TCP协议和UDP协议是传输层协议,这里解决粘包/半包问题的是应用层协议,不要混淆。

        我们的业务网络计数器,需要客户端传入两个运算对象和一个运算符。而服务器给客户端返回的是一个运算结果,和一个错误码(用来标识运算结果是否有效,比如除0或其他非法操作需要标识错误)。即有两类数据

        在文件Protocol中创建两个类,Request和Response分别对这两类数据进行序列化和反序列化,如下:

class Request
{
public:Request() {}Request(int x, int y, char oper): _x(x), _y(y), _oper(oper){}//序列化string Serialize();//反序列化void DeSerialize(string &message);
private:int _x;int _y;char _oper;
};
class Response
{
public:Response() {}Response(int result, int code): _result(result), _code(code){}string Serialize();void DeSerialize(string &message);private:int _result;int _code;
};

        序列化和反序列化的具体操作我们不用自己做,我们使用Json::Value,它是 JsonCpp库(最流行的C++ JSON处理库之一)的核心数据类型。

JsonCpp库的安装:

ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

Jsoncpp 提供了多种方式进行序列化方法,如下:

方法优点缺点适用场景
FastWriter体积最小无定制能力机器间数据传输
StyledWriter可读性强性能较差调试/配置文件
StreamWriterBuilder可定制性强,官方推荐配置稍复杂所有生产环境
直接写文件流适合大文件处理需管理文件流持久化存储

这里我们简单一点使用FastWrite,数据是以key:value的方式存储。如下:

string Serialize()
{Json::Value data;data["x"] = _x;data["y"] = _y;data["oper"] = _oper;Json::FastWriter writer;return writer.write(data);
}

如果传入 8,7,* 被序列化为: 

{"oper":42,"x":7,"y":8}

 反序列化:

        Json::Reader用来把字符串转化为Json::Value类型,再从Json::Value中提取到各个元素,这个过程和反序列化很类似,其中要指明数据类型,如.asInt()。反序列化相当于对Request成员变量初始化。

代码示例:

void DeSerialize(string &message)
{Json::Value data;Json::Reader reader;reader.parse(message, data);_x = data["x"].asInt();_y = data["y"].asInt();_oper = data["oper"].asInt();
}

对于Response同样,如下:

class Response
{
public:Response() {}Response(int result, int code): _result(result), _code(code){}string Serialize(){Json::Value data;data["result"] = _result;data["code"] = _code;Json::FastWriter writer;return writer.write(data);}void DeSerialize(string &message){Json::Value data;Json::Reader reader;reader.parse(message, data);_result = data["result"].asInt();_code = data["code"].asBool();}
private:int _result;int _code;
};

2. 封包与解包

半包问题

解决了粘包问题,但我们还需要知道能够判断报文是否完整,即处理半包问题。

        对于半包问题,这里我们选择在数据包前加上一个报头,这个报头存储的是这个数据包有效载荷的长度,然后报头与数据包用“\r\n”区分开,在报文尾加“\r\n”用来区分下一个报文。这样的话我们可以通过报头知道这个报文一个有多长,然后去看报文有没有到达对应的长度,如果是则是完整的,如果不是,则就是不完整。这个过程我们称为封包和解包

我们创建一个类Protocol来封装 封包、解包、请求处理、获取响应、请求构建等。

  • 封包:添加报头,即有效载荷的长度,用“\r\n”与报文分开。方便接收方识别报文的完整性。
  • 解包:判断报文的完整性,并移除报头。
  • 请求构建:对数据进行序列化,封包。
  • 获取响应:接收数据,解包,反序列化。
  • 请求处理:接收数据,解包,反序列化,业务处理(回调),序列化,封包,发送。

数据的处理实质就是传入一个Request,得到一个Response,所以我们定义一个函数类型:

  • using func_t = function<Response(Request &)>
const string sep = "\r\n";
using func_t = function<Response(Request &)>;
class Protocol
{
public:Protocol() {};Protocol(func_t func): _func(func){}string EnCode(string &jsonstr);//封包bool DeCode(string &buffer, string *package);//解包string BuildRequestString(int x, int y, char oper);//构建请求报文void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr);//请求处理bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp);//获取响应
private:func_t _func;
};
  • GetRequest:给服务器用的,即请求处理(业务处理),涉及数据收发,所以传入Socket指针和客户端地址信息InetAddr。
  • GetResponse:给客户端使用,用来获取数据处理结果,涉及数据接收,所以传入客户端的Socket指针,输出型参数buffer(缓冲区)和rsp。注意因为接收到的报文可能不完整,不能一次取到报文,所以需要缓冲区来保留数据。

还记得在TcpServer里我们缺少的成员变量数据处理函数吗?现在我们知道它是谁了,即:

  • void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr);

在TcpServer.hpp中声明一个类型:

  • using func_t = function<void(shared_ptr<Socket> &sock, InetAddr)>;

然后添加成员变量func_t _func,并在构造函数的参数列表进行初始化。

 EnCode

封包 = 报文长度+“\r\n”+报文+“\r\n”。如下:

string EnCode(string &jsonstr)
{size_t len = jsonstr.size();return to_string(len) + sep + jsonstr + sep;
}

DeCode

  1. 从缓冲区找到标识符“\r\n”,如果找不到,说明报文不完整,返回false。
  2. 从缓冲区找到标识符“\r\n”后,提取报头并算出完整报文的长度。如果大于缓冲区长度,说明缓冲区不够一个完整报文的长度,返回false。
  3. 走到这里说明能取到一个完整的报文,然后把有效载荷提取出来,为方便下次提取,删除缓冲区一个报文的长度。返回true。

如下:

bool DeCode(string &buffer, string *package)
{int pos = buffer.find(sep);if (pos == string::npos)return false;string lenStr = buffer.substr(0, pos);int lenTarget = lenStr.size() + stoi(lenStr) + 2 * sep.size();if (buffer.size() < lenTarget)return false;*package = buffer.substr(pos + sep.size(), stoi(lenStr));buffer.erase(0, lenTarget);return true;
}

BuildRequestString

构建请求报文,即对数据进行序列化和封包,如下:

string BuildRequestString(int x, int y, char oper)
{Request req(x, y, oper);string json_str = req.Serialize();return EnCode(json_str);
}

 GetRequest

        得到并处理请求,对我们刚才写的方法进行组合,即接收数据,解包,反序列化,业务处理,序列化,封包,发送数据。如下:

void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr)
{while (true){string json_package; //接收数据int n = sock->Recv(&json_package);if (n == 0){LOG(Level::INFO) << "client " << addr.tostring_ip() << " exit";break;}else if (n < 0){LOG(Level::WARING) << "Recv fail";break;}else{// 解报包string json_str;while (DeCode(json_package, &json_str)){// 反序列化Request req;req.DeSerialize(json_str);// 业务处理Response resp = _func(req);// 序列化string send_str = resp.Serialize();// 加报头send_str = EnCode(send_str);// 发送sock->Send(send_str);}}}
}

注意把解包过程写成循环,因为一次性也有可能读到多个完整报文,需要把它们都读取出来。

GetResponse

接收数据,解包,反序列化,最后数据是通过输出型参数带回的。 

bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp)
{while (true){int n = sock->Recv(buffer);if (n == 0){LOG(Level::WARING) << "server exit";return false;}else if (n < 0){LOG(Level::WARING) << "client Recv fail";return false;}else{// 解包string json_str;if (!DeCode(*buffer, &json_str)) continue;// 反序列化rsp->DeSerialize(json_str);return true;}}
}

三、业务

        业务处理部分大家可以自行设定,即实现一个function<Response(Request &)>类型的函数,最好封装一个类来维护。这里做一个简单的计算器来充当一个,如下:

class NetCal
{
public:Response Execute(Request &req);
private:
};

具体实现在文末源码给出。 

TcpServer.cc

做完上面的一切我们就可以来完成服务器主函数main了。

        首先需要程序外部传入端口号,所以main函数需要传入命令行参数。需要检测格式的正确性。   

        其次我们创建业务类对象,协议类对象,通信类对象,并把回调方法一层一层的往下传,如下:

int main(int argc, char *argv[])
{if (argc != 2){LOG(Level::FATAL) << "Usage server port";exit(USAGE_ERRO);}// 务业unique_ptr<NetCal> nc(make_unique<NetCal>());// 协议unique_ptr<Protocol> pt(make_unique<Protocol>([&](Request &req) -> Response{ return nc->Execute(req); }));// 通通信unique_ptr<TcpServer> ts(make_unique<TcpServer>(stoi(argv[1]), [&](shared_ptr<Socket> sock, InetAddr addr){ pt->GetRequest(sock, addr); }));return 0;
}

客户端

  1. 同样的需要传入命令行参数来指定服务器的IP和端口号,需要检查格式。
  2. 创建Socket类对象,打开网络文件和与服务器进行连接。
  3. 创建协议对象和用来接收返回结果的缓冲区。
  4. 做一个死循环,进行构建请求,发送请求,接收响应,输出结果。如下:
inline void GetDatafromstdin(int *x, int *y, char *oper)
{cout << "Please Enter x:";cin >> *x;cout << "Please Enter oper:";cin >> *oper;cout << "Please Enter y:";cin >> *y;
}
int main(int argc, char *argv[])
{if (argc != 3){LOG(Level::FATAL) << "Usage serever's ip and port";exit(USAGE_ERRO);}//构建Socket类,并创建套接字,与服务器连接。shared_ptr<Socket> client = make_shared<TcpSocket>();client->SocketOrDie();client->Connect(argv[1], stoi(argv[2]));//创建协议类对象unique_ptr<Protocol> ptl = make_unique<Protocol>();string buffer;while (true){//读取输入并构建请求int x, y;char oper;GetDatafromstdin(&x, &y, &oper);string send_str = ptl->BuildRequestString(x, y, oper);//发送请求client->Send(send_str);//接收响应Response rsp;if (!ptl->GetResponse(client, &buffer, &rsp))break;//结果展示rsp.ShowResult();}return 0;
}

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!💕💕74c0781738354c71be3d62e05688fecc.png

四、源码

TcpServer.hpp

#pragma once
#include <iostream>
#include <functional>
#include <sys/types.h>
#include <unistd.h>
#include <memory>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
using namespace my_log;
using namespace SocketMoudule;
using ioservice_t = function<void(shared_ptr<Socket> &socket, InetAddr &addr)>;
class TcpServer
{
public:TcpServer(uint16_t port, ioservice_t service):_service(service),_listensockptr(make_unique<TcpSocket>()){_listensockptr->BuildTcpServerMoudule(port);Start();}void Start(){while(true){InetAddr addr;auto sock = _listensockptr->Accept(&addr);if(sock == nullptr) continue;LOG(Level::INFO)<<addr.stringaddr()<<"accept success...";pid_t pid = fork();if(pid < 0){LOG(Level::FATAL)<<"fork fail";exit(FORK_ERRO);}if(pid == 0){if(fork()>0)exit(SUCCESS);_service(sock,addr);sock->Close();exit(SUCCESS);}else{sock->Close();}}}private:ioservice_t _service;unique_ptr<Socket> _listensockptr;
};

TcpServer.cc

#include "TcpServer.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Protocol.hpp"
#include "NetCal.hpp"
using namespace SocketMoudule;
int main(int argc, char *argv[])
{if (argc != 2){LOG(Level::FATAL) << "Usage server port";exit(USAGE_ERRO);}// 务业unique_ptr<NetCal> nc(make_unique<NetCal>());// 协议unique_ptr<Protocol> pt(make_unique<Protocol>([&](Request &req) -> Response{ return nc->Execute(req); }));// 通通信unique_ptr<TcpServer> ts(make_unique<TcpServer>(stoi(argv[1]), [&](shared_ptr<Socket> sock, InetAddr addr){ pt->GetRequest(sock, addr); }));return 0;
}

TcpClient.cc

#include <iostream>
#include <memory>
#include "Common.hpp"
#include "Socket.hpp"
#include "Log.hpp"
#include "Protocol.hpp"
using namespace my_log;
using namespace SocketMoudule;
inline void GetDatafromstdin(int *x, int *y, char *oper)
{cout << "Please Enter x:";cin >> *x;cout << "Please Enter oper:";cin >> *oper;cout << "Please Enter y:";cin >> *y;
}
int main(int argc, char *argv[])
{if (argc != 3){LOG(Level::FATAL) << "Usage serever's ip and port";exit(USAGE_ERRO);}shared_ptr<Socket> client = make_shared<TcpSocket>();unique_ptr<Protocol> ptl = make_unique<Protocol>();client->SocketOrDie();client->Connect(argv[1], stoi(argv[2]));string buffer;while (true){int x, y;char oper;GetDatafromstdin(&x, &y, &oper);string send_str = ptl->BuildRequestString(x, y, oper);client->Send(send_str);Response rsp;if (!ptl->GetResponse(client, &buffer, &rsp))break;rsp.ShowResult();}return 0;
}

Socket.hpp

#pragma once
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace my_log;
static const int gbacklog = 8;
namespace SocketMoudule
{class Socket{public:virtual void SocketOrDie() = 0;virtual void BindOrDie(uint16_t) = 0;virtual void ListenOrDie(int) = 0;virtual shared_ptr<Socket> Accept(InetAddr *) = 0;virtual void Close() = 0;virtual int Recv(string *) = 0;virtual int Send(string &) = 0;virtual void Connect(const std::string &, uint16_t) = 0;public:void BuildTcpServerMoudule(uint16_t port, int backlog = gbacklog){SocketOrDie();BindOrDie(port);ListenOrDie(backlog);}void BuildUdpServerMoudule(){void SocketOrDie();void BindOrDie();}};class TcpSocket : public Socket{public:TcpSocket(int socketfd = -1): _socketfd(socketfd){}virtual void SocketOrDie() override{_socketfd = socket(AF_INET, SOCK_STREAM, 0);if (_socketfd < 0){LOG(Level::FATAL) << "Socket fail";exit(SOCKET_ERRO);}LOG(Level::INFO) << "socket success";}virtual void BindOrDie(uint16_t port) override{InetAddr addr(port);int n = bind(_socketfd, addr.getaddr(), addr.getlen());if (n < 0){LOG(Level::FATAL) << "bind fail";exit(BIND_ERRO);}LOG(Level::INFO) << "Bind success";}virtual void ListenOrDie(int backlog) override{int n = listen(_socketfd, backlog);if (n < 0){LOG(Level::FATAL) << "Listen fail";exit(LISTEN_ERRO);}LOG(Level::INFO) << "Listen success";}virtual shared_ptr<Socket> Accept(InetAddr *addr) override{// 为什么不直接用addr,因为构造不了IP。socklen_t len = sizeof(sockaddr_in);sockaddr_in peer;int n = accept(_socketfd, (sockaddr *)&peer, &len);if (n < 0){LOG(Level::WARING) << addr->tostring_ip() << "accept fail";return nullptr;}*addr = InetAddr(peer);return make_shared<TcpSocket>(n);}virtual void Close() override{close(_socketfd);}virtual int Recv(string *out) override{char buffer[1024];int n = read(_socketfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';*out += buffer;}return n;}virtual int Send(string &message) override{return write(_socketfd, message.c_str(), message.size());}virtual void Connect(const std::string &ip, uint16_t port) override{InetAddr addr(port, ip);int n = connect(_socketfd, addr.getaddr(), addr.getlen());if (n < 0){LOG(Level::FATAL) << "connect fail";exit(CONNECT_ERRO);}LOG(Level::INFO) << "connect success";}private:int _socketfd;};class UdpSocket : public Socket{public://......private:int _socketfd;};
}

InteAddr.hpp

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
using namespace std;
class InetAddr
{
public:InetAddr() {}InetAddr(sockaddr_in &peer): _addr(peer){_port = ntohs(peer.sin_port);char buffer[32];inet_ntop(AF_INET, &peer.sin_addr, buffer, sizeof(peer));_ip = buffer;}InetAddr(uint16_t port): _port(port), _ip(to_string(INADDR_ANY)){_addr.sin_family = AF_INET;_addr.sin_port = htons(_port);_addr.sin_addr.s_addr = INADDR_ANY;}InetAddr(uint16_t port, string ip): _port(port), _ip(ip){_addr.sin_family = AF_INET;//主机序->网络序_addr.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);//_addr.sin_addr.s_addr = inet_addr(_ip.c_str());}string tostring_port(){return to_string(_port);}string tostring_ip(){return _ip;}bool operator==(InetAddr addr){return _port == addr._port && _ip == addr._ip;}sockaddr *getaddr(){return (sockaddr *)&_addr;}socklen_t getlen(){return sizeof(_addr);}string stringaddr(){return tostring_ip() + ":" + tostring_port() + " ";}private:uint16_t _port;string _ip;sockaddr_in _addr;
};

Protocol.hpp

#pragma once
#include <iostream>
#include <jsoncpp/json/json.h>
#include <string>
#include <functional>
#include "Socket.hpp"
#include "InetAddr.hpp"
using namespace SocketMoudule;
using namespace std;
class Request
{
public:Request() {}Request(int x, int y, char oper): _x(x), _y(y), _oper(oper){}string Serialize(){Json::Value data;data["x"] = _x;data["y"] = _y;data["oper"] = _oper;Json::FastWriter writer;cout<<writer.write(data);return writer.write(data);}void DeSerialize(string &message){Json::Value data;Json::Reader reader;reader.parse(message, data);_x = data["x"].asInt();_y = data["y"].asInt();_oper = data["oper"].asInt();}int X() { return _x; }int Y() { return _y; }char Oper() { return _oper; }private:int _x;int _y;char _oper;
};
class Response
{
public:Response() {}Response(int result, int code): _result(result), _code(code){}string Serialize(){Json::Value data;data["result"] = _result;data["code"] = _code;Json::FastWriter writer;return writer.write(data);}void DeSerialize(string &message){Json::Value data;Json::Reader reader;reader.parse(message, data);_result = data["result"].asInt();_code = data["code"].asBool();}void ShowResult(){cout << "result[" << _result << "]:code[" << _code << "]" << endl;}int Result() { return _result; }bool Code() { return _code; }void SetResult(int ret) { _result = ret; }void SetCode(int f) { _code = f; }private:int _result;int _code;
};
const string sep = "\r\n";
using func_t = function<Response(Request &)>;
class Protocol
{
public:Protocol() {};Protocol(func_t func): _func(func){}string EnCode(string &jsonstr){size_t len = jsonstr.size();return to_string(len) + sep + jsonstr + sep;}bool DeCode(string &buffer, string *package){int pos = buffer.find(sep);if (pos == string::npos)return false;string lenStr = buffer.substr(0, pos);int lenTarget = lenStr.size() + stoi(lenStr) + 2 * sep.size();if (buffer.size() < lenTarget)return false;*package = buffer.substr(pos + sep.size(), stoi(lenStr));buffer.erase(0, lenTarget);return true;}void GetRequest(shared_ptr<Socket> &sock, InetAddr &addr){while (true){string json_package; //?int n = sock->Recv(&json_package);if (n == 0){LOG(Level::INFO) << "client " << addr.tostring_ip() << " exit";break;}else if (n < 0){LOG(Level::WARING) << "Recv fail";break;}else{// 解报包string json_str;while (DeCode(json_package, &json_str)){// 反序列化Request req;req.DeSerialize(json_str);// 处理Response resp = _func(req);// 序列化string send_str = resp.Serialize();// 加报头send_str = EnCode(send_str);// 发送sock->Send(send_str);}}}}bool GetResponse(shared_ptr<Socket> &sock, string *buffer, Response *rsp){while (true){int n = sock->Recv(buffer);if (n == 0){LOG(Level::WARING) << "server exit";return false;}else if (n < 0){LOG(Level::WARING) << "client Recv fail";return false;}else{// 解包string json_str;if (!DeCode(*buffer, &json_str)) continue;// 反序列化rsp->DeSerialize(json_str);return true;}}}string BuildRequestString(int x, int y, char oper){Request req(x, y, oper);string json_str = req.Serialize();return EnCode(json_str);}private:func_t _func;
};

NetCal.hpp

#pragma once
#include "Common.hpp"
#include "Protocol.hpp"
class NetCal
{
public:Response Execute(Request &req){Response resp(0, 0);switch (req.Oper()){case '+':resp.SetResult(req.X() + req.Y());break;case '-':resp.SetResult(req.X() - req.Y());break;case '*':resp.SetResult(req.X() * req.Y());break;case '/':if (req.Y() == 0)resp.SetCode(1);elseresp.SetResult(req.X() / req.Y());break;case '%':if (req.Y() == 0)resp.SetCode(2);elseresp.SetResult(req.X() % req.Y());default:resp.SetCode(3);break;}return resp;}
private:
};

相关文章:

基于Tcp协议的应用层协议定制

前言&#xff1a;本文默认读者已掌握 TCP 协议相关网络接口知识&#xff0c;将聚焦于应用层协议的设计与剖析&#xff0c;有关底层通信机制及业务逻辑部分仅作简要概述&#xff0c;不再展开详述。 目录 服务器 一、通信 二、协议 1.序列化与反序列化 2. 封包与解包 三、业…...

Flink反压问题解析

一、什么是反压(Backpressure)? 反压(Backpressure) 是流处理系统中的一种流量控制机制。当下游算子处理速度低于上游数据生产速度时,系统会向上游传递压力信号,迫使上游降低数据发送速率,避免数据堆积和系统崩溃。 Flink 通过动态反压机制实现这一过程,但其副作用是…...

C语言中结构体的字节对齐的应用

一、字节对齐的基本原理 计算机的内存访问通常以固定大小的块&#xff08;如 4 字节、8 字节&#xff09;为单位。若数据的内存地址是块大小的整数倍&#xff0c;称为 自然对齐。例如&#xff1a; int&#xff08;4 字节&#xff09;的地址应为 4 的倍数。 double&#xff08…...

大规模数据同步后数据总条数对不上的系统性解决方案:从字段映射到全链路一致性保障

一、引言 在数据同步&#xff08;如系统重构、分库分表、多源整合&#xff09;场景中&#xff0c;“本地数据一致&#xff0c;生产环境条数对不上”是典型痛点。问题常源于并发处理失控、数据库性能瓶颈、字段映射错误、缓存脏数据等多维度缺陷。本文结合实战经验&#xff0c;…...

美团Java后端二面面经!

场景题是面试的大头&#xff0c;建议好好准备 Q. [美团]如何设计一个外卖订单的并发扣减库存系统&#xff1f; Q.[美团]为啥初始标记和重新标记需要STW&#xff1f; Q.[美团]骑手位置实时更新&#xff0c;如何保证高并发写入&#xff1f; Q.[美团]订单表数据量过大导致查询…...

35-疫苗预约管理系统(微服务)

技术&#xff1a; RuoYi框架 后端: SpringBootMySQLspringCloudnacosRedis 前端: vue3 环境&#xff1a; Idea mysql maven jdk1.8 用户端功能 1.首页:展示疫苗接种须知标语、快速预约模块 2.疫苗列表:展示可接种的疫苗 3.预约接种: 用户可进行疫苗预约接种 修改预约时间 …...

Ext JS模拟后端数据之SimManager

Ext.ux.ajax.SimManager 是 Ext JS 框架中用于拦截 Ajax 请求并返回模拟数据的核心工具,适用于前后端分离开发、原型验证或独立测试场景。它通过配置灵活的规则和模拟处理器(Simlet),帮助开发者在不依赖真实后端的情况下完成前端功能开发。 simlets 是simulated servers的…...

BT169-ASEMI无人机专用功率器件BT169

编辑&#xff1a;ll BT169-ASEMI无人机专用功率器件BT169 型号&#xff1a;BT169 品牌&#xff1a;ASEMI 封装&#xff1a;SOT-23 批号&#xff1a;最新 引脚数量&#xff1a;3 特性&#xff1a;单向可控硅 工作温度&#xff1a;-40℃~150℃ BT169单向可控硅&#xff…...

4月26日星期六今日早报简报微语报早读

4月26日星期六&#xff0c;农历三月廿九&#xff0c;早报#微语早读。 1、广州多条BRT相关线路将停运&#xff0c;全市BRT客运量较高峰时大幅下降&#xff1b; 2、国务院批复&#xff1a;同意在海南全岛等15地设立跨境电商综合试验区&#xff1b; 3、我国首次实现地月距离尺度…...

如何将 sNp 文件导入并绘制到 AEDT (HFSS)

导入 sNp 文件 打开您的项目&#xff0c;右键单击 “Result” 绘制结果 导入后&#xff0c;用户可以选择它进行打印。请参阅下面的示例。要点&#xff1a;确保从 Solution 中选择它。...

Shell脚本-for循环应用案例

在Shell脚本编程中&#xff0c;for循环是一种强大的工具&#xff0c;用于处理重复性任务。无论是批量处理文件、遍历目录内容还是简单的计数任务&#xff0c;for循环都能提供简洁而有效的解决方案。本文将通过几个实际的应用案例来展示如何使用for循环解决具体的编程问题。 案…...

MATLAB基础应用精讲-【基础知识篇】发布和共享 MATLAB 代码

目录 MATLAB发布代码---生成文档pdf 分节符对发布文件的分节 实时脚本 Matlab workspace与m脚本数据共享 发布和共享 MATLAB 代码 在实时编辑器中创建和共享实时脚本 发布 MATLAB 代码文件 (.m) 添加帮助和创建文档 发布 MATLAB 代码文件 (.m) 可创建包括您的代码、注释…...

Shell脚本-while循环语法结构

在Shell脚本编程中&#xff0c;while循环是一种重要的流程控制语句&#xff0c;它允许我们重复执行一段代码&#xff0c;直到指定的条件不再满足为止。与for循环不同&#xff0c;while循环通常用于条件驱动的迭代&#xff0c;而不是基于列表或范围的迭代。本文将详细介绍Shell脚…...

Java基础第四章、面向对象

一、成员变量 示例&#xff1a; 二、JVM内存模型 类变量就是静态变量 三、构造方法 默认构造方法、定义的构造方法(不含参数、含参数) 构造方法重载&#xff1a; this关键字 this关键字应用&#xff1a;对构造方法进行复用&#xff0c;必须放在第一行 四、面向对象的三大特征 1…...

【基础IO上】复习C语言文件接口 | 学习系统文件接口 | 认识文件描述符 | Linux系统下,一切皆文件 | 重定向原理

1.关于文件的预备知识 1.1 文件的宏观理解 广义上理解&#xff0c;键盘、显示器等都是文件&#xff0c;因为我们说过“Linux下&#xff0c;一切皆文件”&#xff0c;当然我们现在对于这句话的理解是片面的&#xff1b;狭义上理解&#xff0c;文件在磁盘上&#xff0c;磁盘是一…...

linux离线部署open-metadata

OpenMetadata 环境及离线资源关闭防火墙禁止防火墙关闭 SELinux 创建用户安装JDK安装mysql安装Elasticsearch安装open-metadata 环境及离线资源 系统&#xff1a;CentOS Linux release 7.9.2009 (Core) JDK&#xff1a;17 Mysql&#xff1a; 8.0 OpenMetadata&#xff1a;1.6.…...

Exposure Adjusted Incidence Rate (EAIR) 暴露调整发病率:精准量化疾病风险

1. 核心概念 1.1 传统发病率的局限性 1.1.1 公式与定义 传统发病率公式为新发病例数除以总人口数乘以观察时间。例如在某社区观察1年,有10例新发病例,总人口1000人,发病率即为10/10001=0.01。 此公式假设所有个体暴露时间和风险相同,但实际中个体差异大,如部分人暴露时间…...

信令与流程分析

WebRTC是h5支持的重要特征之一&#xff0c;有了它&#xff0c;不再需要借助音视频相关的客户端&#xff0c;直接通过浏览器的Web页面就可以实现音视频聊天功能。 WebRTC项目是开源的&#xff0c;我们可以借助WebRTC&#xff0c;构建自己的音视频聊缇娜功能。无论是前端JS的Web…...

声音分离人声和配乐base,vocals,drums -从头设计数字生命第6课, demucs——仙盟创梦IDE

demucs -n htdemucs --two-stemsvocals 未来之窗.mp3 demucs -n htdemucs --shifts5 之.mp3demucs -n htdemucs --shifts5 -o wlzcoutspl 未来之窗.mp3 伴奏提取人声分离技术具有多方面的重大意义&#xff0c;主要体现在以下几个领域&#xff1a; 音乐创作与制作 创作便利…...

Chrmo手动同步数据

地址栏输入 chrome://sync-internals分别点击这2个按钮即可触发手动同步...

【Dify系列教程重置精品版】第1课 相关概念介绍

文章目录 一、Dify是什么二、Dify有什么用三、如何玩转Dify?从螺丝刀到机甲战士的进阶指南官方网站:https://dify.ai github地址:https://github.com/langgenius/dify 一、Dify是什么 Dify(D​​efine + ​​I​​mplement + ​​F​​or ​​Y​​ou)。这是一款开源的大…...

【HTTP通信:生活中的邮局之旅】

HTTP通信&#xff1a;生活中的邮局之旅 HTTP通信就像是现代社会的邮政系统&#xff0c;让信息能够在互联网的城市间穿梭。下面我将用邮局比喻和图表来解释这个过程&#xff0c;以及它在现代应用中的重要性。 HTTP通信的旅程图解 #mermaid-svg-gC3zCsPpsFcq3sy3 {font-family:…...

Operating System 实验二 内存管理实验

目录 实验目标: 实验设备: 实验内容: (1)验证FIFO和Stack LRU页面置换算法 【代码(注释率不低于30%)】 【实验过程(截图)】 【结论】 (2)分别用FIFO和Stack LRU页置换算法,自己设定一个页面引用序列,绘制页错误次数和可用页帧总数的曲线并对比(可用Excel绘…...

深入解析YOLO v1:实时目标检测的开山之作

目录 YOLO v1 算法详解​ ​1. 核心思想​ ​2. 算法优势​ ​3. 网络结构&#xff08;Unified Detection&#xff09;​​ ​4. 关键创新​ ​5. 结构示意图&#xff08;Fig1&#xff09;​ Confidence Score 的计算​ 类别概率与 Bounding Box 的关系​ 后处理&…...

windows作业job介绍

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、作业job是什么&#xff1f;二、使用步骤1.代码示例 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; winapi网站&#xff1a; h…...

POLARIS土壤相关数据集

POLARIS相关数据集属于杜克大学&#xff08;Duke University&#xff09;土木与环境工程系&#xff08;CEE&#xff09;的水文学研究团队。该团队有三个总体主题&#xff1a;1&#xff09; 改善地球系统模型中地表异质性的表示&#xff0c;2&#xff09; 利用环境数据来描述在陆…...

【Harmony OS】组件

目录 组件概述 组件常用属性 系统内置组件 Text TextArea 多行文本输入框组件 TextInput 文本输入框 Button Image 图片组件&#xff0c;支持本地图片和网络图片 Radio 单选框 Checkbox 复选框 Blank 空白填充组件 Divider 分隔符 PatternLock 图案密码锁组件 Prog…...

找出字符串中第一个匹配项的下标

题目&#xff1a;28. 找出字符串中第一个匹配项的下标 给你两个字符串 haystack 和 needle&#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 haystack 的一部分&#xff0c;则返回 -1。 …...

专家系统的知识获取、检测与组织管理——基于《人工智能原理与方法》的深度解析

前文我们已经了解了专家系统的基本概念和一般结构&#xff0c;系统中有专业的知识才是专家系统的关键&#xff0c;接下来对专家系统中的知识是如何获取、检测、组织和管理的进行探讨。 1.专家系统的基本概念&#xff1a;专家系统的基本概念解析——基于《人工智能原理与方法》…...

BUUCTF-[GWCTF 2019]re3

[GWCTF 2019]re3 查壳&#xff0c;64位无壳 然后进去发现主函数也比较简单&#xff0c;主要是一个长度校验&#xff0c;然后有一个mprotect函数&#xff0c;说明应该又是Smc&#xff0c;然后我们用脚本还原sub_402219函数处的代码 import idc addr0x00402219 size224 for …...

基准指数选股策略思路

一种基于Python和聚宽平台的量化交易策略&#xff0c;主要包含以下内容&#xff1a; 1. 导入必要的库 - 导入jqdata和jqfactor库用于数据获取和因子计算。 - 导入numpy和pandas库用于数据处理。 2. 初始化函数 - 设置基准指数为沪深300指数。 - 配置交易参数&#xff0c;如使用…...

【阿里云大模型高级工程师ACP习题集】2.5 优化RAG应用提升问答准确度(⭐️⭐️⭐️ 重点章节!!!)

习题集 【单选题】在RAG应用的文档解析与切片阶段,若遇到文档类型不统一,部分格式的文档不支持解析的问题,以下哪种解决方式不可行?( ) A. 开发对应格式的解析器 B. 转换文档格式 C. 直接忽略该类型文档 D. 改进现有解析器以支持更多格式 【多选题】在选择向量数据库时,…...

【torch\huggingface默认下载路径修改】.cache/torch/ 或 .cache/huggingface

问题 服务器的硬盘空间是有限的&#xff0c;系统上的固态硬盘空间又比较小&#xff0c;在跑深度学习模型的时候经常有默认下载权重的操作&#xff0c;不管是torch或者huggingface&#xff0c;如果不加管理&#xff0c;所有的权重都放在home/user/.cache 里面&#xff0c;迟早会…...

SpringBoot 常用注解大全

SpringBoot 常用注解大全 一、核心注解 1. 启动类注解 SpringBootApplication&#xff1a;组合注解&#xff0c;包含以下三个注解 Configuration&#xff1a;标记该类为配置类EnableAutoConfiguration&#xff1a;启用自动配置ComponentScan&#xff1a;组件扫描 2. 配置相…...

【器件专题1——IGBT第2讲】IGBT 基本工作原理:从结构到特性,一文解析 “电力电子心脏” 的核心机制

IGBT&#xff08;绝缘栅双极型晶体管&#xff0c;Insulated Gate Bipolar Transistor&#xff09;作为现代电力电子领域的核心器件&#xff0c;其工作原理融合了 MOSFET 的高效控制优势与 BJT 的大功率处理能力。本文从物理结构、导通 / 关断机制、核心特性等维度&#xff0c;深…...

再谈String

1、字符串常量池 1.1 创建对象的思考 下面是两种创建字符串对象的代码 public static void main1(String[] args) {String s1 "hello";String s2 "hello";System.out.println(s1 s2);//trueString s3 new String("hello");String s4 new …...

语音合成之五语音合成中的“一对多”问题主流模型解决方案分析

语音合成中的“一对多”问题主流模型解决方案分析 引言“一对多”指的是什么&#xff1f;优秀开源模型的方法CosyvoiceSparkTTSLlaSA TTSVITS 引言 TTS系统旨在模仿人类的自然语音&#xff0c;但其核心面临着一个固有的挑战&#xff0c;即“一对多”问题 。这意味着对于给定的…...

嵌入式:Linux系统应用程序(APP)启动参数及其规则详解

在 systemd 的服务单元文件中&#xff0c;[Service] 部分用于定义服务的启动、停止、重启等操作&#xff0c;以及服务的运行环境和参数。以下是 [Service] 部分常见参数及其规则的详细介绍&#xff1a; 服务类型相关参数 **Type** **作用**&#xff1a;指定服务的启动类型&…...

25%甘油(灭菌)保存菌液以及10%甘油(普通)保存蛋白的原理及操作-实验操作系列-010

01 甘油保菌实验原理 1. 渗透压调节 甘油作为渗透压调节剂&#xff0c;能显著降低水的结冰温度&#xff0c;防止低温环境中细菌细胞内冰晶的形成。冰晶会破坏细胞膜&#xff0c;从而损伤细胞的完整性。甘油能够减少冰晶的生成&#xff0c;维持细胞结构的稳定&#xff0c;保护…...

影楼精修-手部青筋祛除算法解析

注意&#xff1a;本文样例图片为了避免侵权&#xff0c;均使用AIGC生成&#xff1b; 手部青筋祛除科普 手部青筋祛除是影楼精修中一个非常精细的工作&#xff0c;需要较高的修图技巧&#xff0c;目前市面上很少有自动化的青筋祛除功能的&#xff0c;而像素蛋糕目测是第一个做到…...

【时时三省】Python 语言----函数

山不在高,有仙则名。水不在深,有龙则灵。 ----CSDN 时时三省 1,函数概念 为了实现某种功能而组织的语句集合 定义格式: def 函数名([参数])函数体def add_num(a,b):c = a + bprint(c)add_num(11, 22) def 是定义函数的关键字。定义函数时,需要注意以下问题: 1,不需要说…...

蜜罐管理和数据收集服务器:Modern Honey Network (MHN)

一、Modern Honey Network (MHN)介绍 Modern Honey Network (MHN) 是一个集中化的蜜罐管理和数据收集服务器。它旨在简化蜜罐的部署和管理&#xff0c;并提供一个简洁的 Web 界面来查看捕获的数据。 1、主要功能: 集中化管理: 通过一个中心服务器管理多个蜜罐传感器。快速部…...

关于hbaseRegion和hbaseRowKey的一些处理

我遇到了什么问题&#xff1f; 我的habse一共有三台服务器&#xff0c;其中一台忙的要死&#xff0c;另外两台吃瓜看戏&#xff0c;我的业务都在其中一个服务器上&#xff0c;导致数据的读写瓶颈。 先说一下hbase的概况&#xff0c;有一个整体了解&#xff0c;我们再讲原因。…...

exec和spawn

前言 需求&#xff1a;做一个electron应用&#xff0c;用node打开exe软件&#xff0c;打开后返回成功与否&#xff0c;打开的软件不会随electron应用的关闭而关闭 exec exec 第一个参数为要运行的command命令&#xff0c;参数以空格分隔。 child_process.exec(command[, opti…...

【C到Java的深度跃迁:从指针到对象,从过程到生态】第四模块·Java特性专精 —— 第十三章 异常处理:超越C错误码的文明时代

一、错误处理的范式革命 1.1 C错误处理的黑暗时代 C语言通过返回值传递错误状态&#xff0c;存在系统性缺陷&#xff1a; 典型错误处理模式&#xff1a; FILE* open_file(const char* path) { FILE* f fopen(path, "r"); if (!f) { return NULL; // 错误信息…...

AD相同网络的铜皮和导线连接不上

出现这样的情况是不是很烦恼&#xff0c;明明是相同的网络连接不上&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f; 直接修改铜皮属性&#xff08;选择所有相同这个选项&#xff09; 这样就可以连接上了...

驱动开发硬核特训 · Day 21(下篇): 深入剖析 PCA9450 驱动如何接入 regulator 子系统

&#x1f4d8; 一、设备树视角&#xff1a;PCA9450 是如何声明的&#xff1f; 设备树中定义了 PCA9450 芯片通过 I2C 总线挂载&#xff0c;并描述了多个 regulator 通道&#xff1a; &i2c1 {pmic25 {compatible "nxp,pca9450c";reg <0x25>;regulators …...

消息队列mq在Mlivus Cloud向量数据库中的关键配置与最佳实践

作为《向量数据库指南》的作者和大禹智库高级研究员,我在30多年的向量数据库实战中深刻认识到:消息队列(MQ)作为现代向量数据库架构的"神经系统",其配置优化直接决定了系统的吞吐量、稳定性和扩展性。本文将基于Mlivus Cloud这一领先的向量数据库平台,深入剖析…...

常见网络安全攻击类型深度剖析(四):跨站脚本攻击(XSS)——分类、漏洞利用与前端安全防护

常见网络安全攻击类型深度剖析&#xff08;四&#xff09;&#xff1a;跨站脚本攻击&#xff08;XSS&#xff09;——分类、漏洞利用与前端安全防护 在Web应用安全中&#xff0c;跨站脚本攻击&#xff08;Cross-Site Scripting, XSS&#xff09;是攻击者利用浏览器漏洞&#x…...

临床试验中安全性估计策略与应用

1. 安全性估计概述 1.1 安全性估计的定义与重要性 1.1.1 安全性估计的定义 安全性估计旨在准确评估药物或干预措施的安全性特征,涵盖不良事件的发生率、严重程度及与治疗的因果关系等关键要素。 依据 ICH E9(R1) 指南,需明确定义目标人群、伴发事件处理方式及分析策略,为药…...