本文代码仓库地址2023_04_22_Calculator_TCPSocket · hepburn0504_yyq/LinuxClass - 码云 - 开源中国 (gitee.com)
TCP协议的通信流程
之前写的客户端和应用端的代码是基于应用层的代码。TCP是基于确认应答来保证单向可靠性的
TCP是面向连接的通信协议,在双方通信之前,要先进行3次握手(SYN、SYN/ACK、ACK)才能建立连接,谁调用connect就是谁先发起连接,3次握手完成后,才能把这个连接从内核拿到应用层;断开连接,需要4次挥手(FIN、ACK、FIN、ACK)
服务端TCP:服务端应用层:
listenfd = sock(),【让系统分配一个文件描述符】;
bind(listenfd, 服务器地址端口),【即绑定listenfd的源地址,目标地址任意】;
listen(listenfd, 连接队列长度),【使listenfd 成为监听描述符(揽客的)】;
connfd = accept(listenfd, 客户端地址端口),【阻塞等待客户端连接】;
当客户端来连接的时候,OS底层经过3次握手,把连接建立好之后,给了应用层,accept才会返回,系统会分配新的描述符connfd和客户端通信;
read(connfd, buf, size),【阻塞等待客户端数据请求】
再谈传输控制协议
TCP和UDP都是传输控制协议。调用send/write函数是把数据拷贝到发送缓冲区里,i调用recv/read函数是把数据从接收缓冲区中拷贝到字符串里。这一类IO接口,本质都是拷贝函数。
那数据什么时候发送到网络中呢?由传输层的协议TCP/UDP协议决定,什么时候发送、一次发多少、出错了怎么办都是由传输协议来决定的。
应用层协议
序列化和反序列化
序列化:就是将对象转化成字节序列的过程。
反序列化:就是将字节序列转化成对象的过程。
网络可以直接传输数据,但是无法直接传输对象,故需要在传输前序列化,传输完成后反序列化成对象。所以所有可在网络上传输的对象都必须是可序列化的。
对象(如结构体等)是为了方便上层用户使用。
只要保证一端发送时构造的数据,在另一端能够正确的进行解析,就是ok的。这种约定, 就是 应用层协议protocol。
应用层自定协议
//Protocol.hpp
#pragma once
#include <string>
#include <cstring>
namespace ns_protocol
{
// 分隔符为空格
// 分隔符长度
#define SPACE " "
#define SPACELEN strlen(SPACE)//要用strlen,不能用sizeof
class Request
{
public:
// 序列化
std::string Serialize()
{
// 1.我们自己写的协议 "num1+空格+op+空格+num2"
std::string pkg;
pkg = std::to_string(_num1);
pkg += SPACE;
pkg += _op;
pkg += SPACE;
pkg += std::to_string(_num2);
return pkg;
}
// 反序列化
bool Deserialize(const std::string &pkg)
{
// 从字节流中提取数据,初始化结构体
// 根据分隔符提取数据
// 找第1个空格
std::size_t left = pkg.find(SPACE);
if (std::string::npos == left)
{
// 非法字符串,不认识这个格式的字节序列
return false;
}
// 找第2个空格
std::size_t right = pkg.rfind(SPACE);
if (std::string::npos == right)
{
// 非法字符串,不认识这个格式的字节序列
return false;
}
// 正常提取数据 "123+空格+456"
_num1 = atoi(pkg.substr(0, left).c_str());
_num2 = atoi(pkg.substr(right + SPACELEN).c_str());
if (left + SPACELEN > pkg.size())
return false;
else
_op = pkg[left + SPACELEN];
return true;
}
public:
Request() {}
Request(int x, int y, char op)
: _num1(x), _num2(y), _op(op)
{
}
~Request() {}
public:
int _num1; // 第一个操作数
int _num2; // 第二个操作数
char _op; // 运算符 '+' '-' '*' '/' '%'
};
class Response
{
public:
// 序列化 "code+空格+result"
std::string Serialize()
{
std::string pkg;
pkg = std::to_string(_code);
pkg += SPACE;
pkg += std::to_string(_result);
return pkg;
}
// 反序列化
bool Deserialize(const std::string &pkg)
{
// 根据分隔符提取数据
// 找第1个空格
std::size_t pos = pkg.find(SPACE);
if (std::string::npos == pos)
{
// 非法字符串,不认识这个格式的字节序列
return false;
}
// 正常提取数据 "code+空格+result"
_code = atoi(pkg.substr(0, pos).c_str());
_result = atoi(pkg.substr(pos + SPACELEN).c_str());
return true;
}
public:
Response() {}
Response(int result, int code)
: _result(result), _code(code)
{
}
~Response() {}
public:
int _result; // 计算结果
int _code; // 计算结果的状态码,0正常,1除 2模0,-1非法操作符
};
std::string Recv(int sock)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
if (s > 0)
{
return buffer;
}
}
void Send(int sock, const std::string &pkg)
{
send(sock, pkg.c_str(), pkg.size(), 0);
}
}
这个是简陋版本的应用层自定协议。其中还有很多小细节容易出bug,要注意不要踩坑。
如何保证Recv()函数中读到的buffer是一个完善的请求呢?(如客户端发了很多次消息,一开始服务端被阻塞着,然后一次性读取的情况)
上面讲了传输控制协议,在进行网络通信的时候,这里的代码是由TCP协议来控制何时发送/一次性发多少/出错了怎么办这些事情。即发送的次数和接收的次数是无关的,这就是面向字节流的。(比如数据是10字节,但是发送缓冲区的大小是2字节,要send5次,接收缓冲区的大小是5字节,要recv2次)。
所以,为了保证读取的数据是一个完整的包,就要有开始标识和结束标识,故应该将我们自己写的协议修改为 “本次数据大小+/r+/n+num1+空格+op+空格+num2+/r+/n”。
// 格式化报文数据
std::string Encode(std::string &s)
{
std::string pkg = std::to_string(s.size());
pkg += SEP;
pkg += s;
pkg += SEP;
return pkg;
}
// 解析报文数据 本次数据大小length+/r+/n+num1+空格+op+空格+num2+/r+/n
std::string Decode(std::string &buffer)
{
std::size_t pos = buffer.find(SEP);
// 如果找不到"/r/n",说明此次读取的报文不完整
if (std::string::npos == pos)
return "";
// 能读到一个分隔符SEP,再继续
// 先取到协议里的数据长度
int length = atoi(buffer.substr(0, pos).c_str());
// 正文长度= buffer的长度 - length - 第1个分隔符长度SEPLEN - 第2个分隔符长度SEPLEN
int content_size = buffer.size() - pos - SEPLEN - SEPLEN;
if (content_size >= length)
{
// 说明至少具有一个合法完整的报文
// 移除标识
buffer.erase(0, pos + SEPLEN);
// 取到正文
std::string pkg = buffer.substr(0, length);
// 再移除标识
buffer.erase(0, length + SEPLEN);
return pkg;
}
else
{
// 如果没有,就返回空
return "";
}
}
// 返回值是bool类型,表示本次读取成功/失败
// 协议形式2:本次数据大小+/r+/n+num1+空格+op+空格+num2+/r+/n
bool Recv(int sock, std::string *out)
{
char buffer[1024];
bool ret = false;
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
*out += buffer; // 注意这里是+=,会将字符加在out末尾
ret = true;
}
else if (0 == s)
{
// 表示对方关闭连接
std::cout << "客户端关闭连接" << std::endl;
}
else
{
// 表示读取错误
std::cout << "读取错误" << std::endl;
}
return ret;
}
void Send(int sock, const std::string &pkg)
{
int n = send(sock, pkg.c_str(), pkg.size(), 0);
if (n < 0)
{
std::cout << "发送错误" << std::endl;
}
}
使用现成的json格式
class Request
{
public:
// 序列化
std::string Serialize()
{
Json::Value root;
root["num1"] = _num1;
root["num2"] = _num2;
root["op"] = _op;
// 2.序列化
Json::FastWriter fw;
return fw.write(root);
}
// 反序列化
bool Deserialize(const std::string &pkg)
{
Json::Value root;
Json::Reader r;
r.parse(pkg, root);
_num1 = root["num1"].asInt();
_num2 = root["num2"].asInt();
_op = root["op"].asInt();
}
public:
int _num1; // 第一个操作数
int _num2; // 第二个操作数
char _op; // 运算符 '+' '-' '*' '/' '%'
}
class Response
{
public:
// 序列化
std::string Serialize()
{
Json::Value root;
root["code"] = _code;
root["result"] = _result;
Json::FastWriter fw;
return fw.write(root);
}
// 反序列化
bool Deserialize(const std::string &pkg)
{
Json::Value root;
Json::Reader r;
r.parse(pkg, root);
_code = root["code"].asInt();
_result = root["result"].asInt();
}
public:
int _result; // 计算结果
int _code; // 计算结果的状态码,0正常,1除 2模0,-1非法操作符
}
捕捉SIGPIPE信号
建议在客户端和服务端的主线程处加上信号捕捉的代码,如果某一端关闭了套接字,再调用send函数就直接出错,容易找不到出错原因。加上这一句方便排查出错原因。
一般服务器都是要忽略SIGPIPE信号的。因为对一个对端已经关闭的socket调用两次write,第二次将会生成SIGPIPE信号, 该信号默认结束进程,就会结束服务器,这当然是我们不希望看到的。
-----------------客户端-----------------
static void handler(int signo)
{
std::cout << "捕捉到一个信号:" << signo << std::endl;
}
int main()
{
// 如果是sockfd被关闭了,还向该文件描述符中做写入动作,就会捕捉到13号信号
signal(SIGPIPE, handler);
//服务器端应写成
signal(SIGPIPE,SIG_IGN);
}
-----------------服务器端-----------------
static void handler(int signo)
{
std::cout << "捕捉到一个信号:" << signo << std::endl;
}
int main()
{
//服务器端应忽略SIGPIPE信号
signal(SIGPIPE, SIG_IGN);
}
守护进程的添加
让服务器可以一直接受用户输入