目录
1.10一个概念:协议定制 和 序列化和反序列化是工作在不同阶段的
2.3简易版本的状态机--分割输入字符函数ParseLine
补充概念:三次握手,四次挥手
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
调用socket, 创建文件描述符;
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
再谈协议
协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
结构化的数据:
接下来手写一个简单协议
网络版计算器
准备工作
因为代码相关性的问题,这里的相关代码就直接复用上篇文章的了
简单的TCP网络程序·线程池(后端服务器)_tcp线程池_清风玉骨的博客-CSDN博客
参考下图稍作修改
makefile
cc=g++
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f calclient calserver
log.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
// 定义五种不同的信息
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3 // 一种不影响服务器的错误
#define FATAL 4 // 致命错误
const char *to_levelstr(int level)
{
switch (level) // 这里直接return了
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
// 获取前缀信息[日志等级] [时间戳/时间] [pid]
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
// 获取内容
char logcontent[NUM];
va_list arg;
va_start(arg, format); // 因为压栈是反过来的,所以直接使用左边那个参数就行了
vsnprintf(logcontent, sizeof(logcontent), format, arg); // 第三个参数是格式, 第四个就是初始化好的可变参数
std::cout << logprefix << logcontent << std::endl; // 我们只需要日志显示就好了,其余的不需要
}
calServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万
class TcpServer
{
public:
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 创建socket文件套接字对象 -- 流式套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
// 2.bind绑定自己的网路信息 -- 注意包含头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
void start()
{
for (;;) // 一个死循环
{
// 4. server 获取新链接
// sock 和client 进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
continue;
}
logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // 因为支持可变参数了
// 这里直接使用多进程版的代码进行修改
// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
pid_t id = fork();
if (id == 0) // 当id为 0 的时候就代表这里是子进程
{
// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
close(_listensock);
//if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
// serviceIO(sock); 暂时不需要
close(sock);
exit(0);
}
// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
// close(sock);
// father
// 那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
// 且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
// 一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了
// 不需要等待了 version 2
pid_t ret = waitpid(id, nullptr, 0);
if(ret > 0)
{
logMessage(NORMAL, "wait child success"); // ?
}
}
}
~TcpServer() {}
private:
int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
uint16_t _port;
};
} // namespace server
calServer.cc
#include "calServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
tsvr->start();
return 0;
}
calClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
TcpClient(const std::string &serverip, const uint16_t &port)
: _sock(1), _serverip(serverip), _serverport(port)
{
}
void initClient()
{
// 1. 创建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 客户端也可以有日志,不过这里就不再实现了,直接打印错误
std::cout << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
// 3. 要不要listen? -- 不需要!客户端不需要建立链接
// 4. 要不要accept? -- 不要!
// 5. 要什么? 要发起链接!
}
void start()
{
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());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string msg;
while (true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串, 截至目前
buffer[n] = 0;
std::cout << "Server回显# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
~TcpClient()
{
if(_sock >= 0) close(_sock); //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
calClient.cc
#include "calClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}
// ./tcpclient serverip serverport 调用逻辑
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
至此准备工作完成
服务端
新建文件与接口
Protocol.hpp
协议的初步定制与初始化
#pragma once
#include <iostream>
class Request
{
public:
Request():x(0), y(0), op(0) // 初始化
{}
public:
// "x op y" 规定请求一定是这样的格式,并且x操作数一定在前,y操作数在后
int x;
int y;
char op; // 操作符可能有除0之类的错误,所以需要处理
};
class Response
{
public:
Response():exitcode(0), result(0)
{}
public:
int exitcode; // 0: 计算成功,!0表示计算失败,具体是多少,定好标准,类似与错误码
int result; // 计算结果
};
1.0服务端的一个流程
1.1创建一个回调方法
1.2保证你读到的消息是【一个】完整的请求
1.3类Request序列化和反序列化
注意1
注意修正地方,这部分是在测试代码中发现的错误,上图中可能有未修改过来的,以此图为基准
calServer.hpp
1.4接口handlerEnter修改1
注意这边还有个无参构造,不然会报错
calServer.cc
1.5接口cal业务逻辑操作
1.6接口handlerEnter修改2
protocol.hpp
1.7类Response中序列化和反序列化工作
注意2
注意修正地方,这部分是在测试代码中发现的错误,上图中可能有未修改过来的,以此图为基准
1.8接口enLength增加有效载荷
calServer.hpp
1.9发送已经序列化的数据
新接口send
当然我们对于字符流的写入可以直接使用write
1.10一个概念:协议定制 和 序列化和反序列化是工作在不同阶段的
这里的协议定制指的是定制x,y,op和exitcode,result,enLength函数这些东西属于定制协议,其他的像serialize,deserialize这些接口就属于序列化和反序列化
protocol.hpp
1.11recvRequest读取请求
注意:后面更名为recvPackage
1.12deLength删除有效载荷并得到正文部分
1.13inbuffer放到循环外部去
测试1阶段 -- 编译测试
至此客户端的代码基本完成,现在编译代码是可以通过的
注意这里的类名为了防止混淆就行了修改
有关此次的TcpServer全部修改为CalServer
有关此次的TcpClient全部修改为CalClient
客户端
calClient.hpp
2.0客户端的发送与接收
2.1简单的打印说明
测试2阶段 -- 功能基本完善测试
2.3简易版本的状态机--分割输入字符函数ParseLine
测试阶段3 -- 功能完善
2.4人为制造一个错误
测试阶段4 -- 当传输的数据不完整的时候(额外测试)
序列化和反序列化可以用现成的
注意这里指的是序列化和反序列化,这个是一个复杂的体力活,并且现在有很多成熟的解决方案可以供我们使用,没必要花费精力去做序列化和反序列化,当然协议我们还是可以自己写的
Jasoncpp的安装
判断jsoncpp是否安装成功
直接安装的是动态库
makefile修改1
cc=g++
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp #-DMYSELF
.PHONY:clean
clean:
rm -f calclient calserver
如果程序中有第三方库的使用,就在编译的时候要加入这一句,以代码为基准
json
条件编译修改protocol.hpp
注意事项:这两只负责添加删除报头,和序列化反序列化没关系
json的库包含
json是一个kye-val的格式
使用json修改protocol
因为代码的耦合度不高,可以直接修改
使用json测试
条件编译,不使用自己写的
条件编译,使用自己写的
makefile修改2
cc=g++
LD=#-DMYSELF
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
.PHONY:clean
clean:
rm -f calclient calserver
当需要使用自己写的时候就把 # 去掉即可
序列化和反序列化自定义协议完成
源码
makefile
cc=g++
LD=#-DMYSELF
.PHONY:all
all:calserver calclient
calclient:calClient.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
calserver:calServer.cc
$(cc) -o $@ $^ -std=c++11 -ljsoncpp ${LD}
.PHONY:clean
clean:
rm -f calclient calserver
calClient.cc
#include "calClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " serverip serverport\n\n"; // 命令提示符
}
// ./tcpclient serverip serverport 调用逻辑
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<CalClient> tcli(new CalClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
calClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "protocol.hpp"
#define NUM 1024
class CalClient
{
public:
CalClient(const std::string &serverip, const uint16_t &port)
: _sock(1), _serverip(serverip), _serverport(port)
{
}
void initClient()
{
// 1. 创建socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
// 客户端也可以有日志,不过这里就不再实现了,直接打印错误
std::cout << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind? 要的! 但是不需要显示bind,这里的client port要让OS自定!
// 3. 要不要listen? -- 不需要!客户端不需要建立链接
// 4. 要不要accept? -- 不要!
// 5. 要什么? 要发起链接!
}
void start()
{
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());
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "socket connect error" << std::endl;
}
else
{
std::string line;
std::string inbuffer;
while (true)
{
std::cout << "mycal>>> ";
std::getline(std::cin, line); // 输入类似于 1 + 1
Request req = ParseLine(line); // 对输入的数据进行分割
// 先简单测试一个该功能
// Request req(10,10,'+'); 这里是进行程序从测试,防止大改,先让它运行起来,这里就模拟出一个请求样例
std::string content;
req.serialize(&content); // 序列化
std::string send_string = enLength(content); // 添加长度
send(_sock, send_string.c_str(), send_string.size(), 0); // bug? 暂时不管
std::string package, text;
// "content_len"\r\n"exitcode result"\r\n 我们会拿到一个序列化的数据
if(!recvPackage(_sock, inbuffer, &package)) continue; // 没有成功就继续
if(!deLength(package, &text)) continue; // 去掉多余的,成功就会得到"exitcode result",否则继续
// "exitcode result" 到这里就text里面就是一个正文部分了
Response resp;
resp.deserialize(text);
std::cout << "exitCode: " << resp.exitcode << std::endl;
std::cout << "result: " << resp.result << std::endl;
}
}
}
Request ParseLine(const std::string &line)
{
// 简易版本的状态机
// "1+1" "123*456" "12/0"
int status = 0; // 0:操作符之前,1:碰到了操作符,2: 操作符之后
int i = 0;
int cnt = line.size(); // 这里的line不包括\n,因为getline的性质,不会把斜杠录进来
std::string left, right;
char op;
while (i < cnt)
{
switch(status)
{
case 0:
{
if(!isdigit(line[i])) // 判断是不是数字字符,不是说明是操作符
{
op = line[i];
status = 1;
}
else left.push_back(line[i++]); // string支持一个字符一个字符的push_back
}
break;
case 1:
i++; // 操作符直接下一位,在状态0的时候就处理了
status = 2;
break;
case 2:
right.push_back(line[i++]);
break;
}
}
std::cout << std::stoi(left) << " " << std::stoi(right) << " " << op << std::endl;
return Request(std::stoi(left), std::stoi(right), op);
}
~CalClient()
{
if(_sock >= 0) close(_sock); //不写也行,因为文件描述符的生命周期随进程,所以进程退了,自然也就会自动回收了
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
calServer.cc
#include "calServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " local_port\n\n"; // 命令提示符
}
// req: 里面一定是我们的处理好的一个完整的请求对象
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入(IO), 序列化和反序列化等任何细节
bool cal(const Request &req, Response &resp)
{
// req已经有结构化完成的数据啦,你可以直接使用
resp.exitcode = 0;
resp.result = 0;
switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x - req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/': // switch case 里面也可以放花括号
{
if (req.y == 0)
resp.exitcode = DIV_ZERO; // 除0错误
else
resp.result = req.x / req.y;
}
break;
case '%':
{
if (req.y == 0)
resp.exitcode = MOD_ZERO; // 模0错误
else
resp.result = req.x % req.y;
}
break;
default:
resp.exitcode = OP_ERROR;
break;
}
return true;
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<CalServer> tsvr(new CalServer(port));
tsvr->initServer();
tsvr->start(cal); // 传入方法
return 0;
}
calServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
#include "protocol.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5; // 10、20、50都可以,但是不要太大比如5千,5万
// const Request &req: 输入型
// Request & resp: 输出型
typedef std::function<bool(const Request &req, Response &resp)> func_t; // 回调函数
// 我们把它放到类外处理,保证解耦
void handlerEnter(int sock, func_t func)
{
std::string inbuffer; // 放到循环的外部
while (true)
{
// 1. 读取: "content_len"\r\n"x op y"\r\n
// 1.1 你怎么保证你读到的消息是【一个】完整的请求?
std::string req_text; // 输出型参数,这个模型是多执行流多进程版的,所以我们等的起,我们可以一直等到【一个】完整的请求被读上了来
// 1.2 我们保证,我们req_text里面一定是一个完整的请求: "content_len"\r\n"x op y"\r\n
if (!recvPackage(sock, inbuffer, &req_text))
return; // 和协议有关,放到protocol里面去
std::cout << "带报头的请求: \n" << req_text << std::endl;
std::string req_str; // 创建一个变量用来装东西
if (!deLength(req_text, &req_str))
return;
std::cout << "去掉报头的正文: \n" << req_str << std::endl;
// 2. 对请求Request, 反序列化
// 2.1 得到一个结构化的请求对象
Request req;
if (!req.deserialize(req_str))
return; // 如果反序列化失败就直接返回
// 3. 计算处理, req.x, req.op, req.y --- 业务逻辑
// 3.1 得到一个结构和的响应
Response resp;
func(req, resp); // req的处理结果,全部放入到了resp。回调是不是不回来了? 不是!
// 4.对响应Response, 进行序列化
// 4.1 得到了一个"字符串"
std::string resp_str;
resp.serialize(&resp_str); // 因为步骤3已经完成了业务逻辑处理,并且把结构化的数据填充完成,这里就可以直接使用了
std::cout << "计算完成, 序列化响应: " << resp_str << std::endl;
// 5. 然后我们再发送响应
// 5.1 构建成为一个完整的报文
std::string send_string = enLength(resp_str);
std::cout << "构建完成完整的响应\n" << send_string << std::endl;
send(sock, send_string.c_str(), send_string.size(), 0); // 其实这里的发送也是有问题的,不过后面再说
}
}
class CalServer
{
public:
CalServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 创建socket文件套接字对象 -- 流式套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 第三个参数默认 0
if (_listensock < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
// 2.bind绑定自己的网路信息 -- 注意包含头文件
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 这里有个细节,我们会发现当我们接受数据的时候是不需要主机转网路序列的,因为关于IO类的接口,内部都帮我们实现了这一功能,这里不帮我们做是因为我们传入的是一个结构体,系统做不到
local.sin_addr.s_addr = INADDR_ANY; // 接受任意ip地址
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// 3. 设置socket 为监听状态 -- TCP与UDP不同,它先要建立链接之后,TCP是面向链接的,后面还会有“握手”过程
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面再填这个坑
{
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
void start(func_t func)
{
for (;;) // 一个死循环
{
// 4. server 获取新链接
// sock 和client 进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next"); // 这个不影响服务器的运行,用ERROR,就像张三不会因为没有把人招呼进来就不干了
continue;
}
logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // 因为支持可变参数了
// 这里直接使用多进程版的代码进行修改
// version 2 多进程版(2) -- 注意子进程会继承父进程的一些东西,父进程的文件操作符就会被子进程继承,
// 即父进程的文件操作符那个数字会被继承下来,指向同一个文件,但是文件本身不会被拷贝一份
// 也就是说子进程可以看到父进程创建的文件描述符sock和打开的listensock
pid_t id = fork();
if (id == 0) // 当id为 0 的时候就代表这里是子进程
{
// 关闭不需要的文件描述符 listensock -- 子进程不需要监听,所以我们要关闭这个不需要的文件描述符
// 即使这里不关,有没有很大的关系,但是为了防止误操作我们还是关掉为好
close(_listensock);
// if(fork()>0) exit(0); 解决方法1:利用孤儿进程特性
// serviceIO(sock); 暂时不需要
handlerEnter(sock, func); // 保证解耦, 把接口的实现,放到类外
close(sock);
exit(0);
}
// 一定要关掉,否则就会造成文件描述符泄漏,但是这里的关掉要注意了,这里只是把文件描述符的计数-1
// 子进程已经继承过去了,所以这里也可以看做,父进程立马把文件描述符计数-1,只有当子进程关闭的时候,这个文件描述符真正的被关闭了
// 所以后面申请的链接使用的还是这个4号文件描述符,因为计算机太快了
// close(sock);
// father
// 那么父进程干嘛呢? 直接等待吗? -- 显然不能,这样又会回归串行运行了,因为等待的时候会阻塞式等待
// 且这里并不能用非阻塞式等待,因为万一有一百个链接来了,就有一百个进程运行,如果这里非阻塞式等待
// 一但后面没有链接到来的话.那么accept这里就等不到了,这些进程就不会回收了
// 不需要等待了 version 2
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
logMessage(NORMAL, "wait child success"); // ?
}
}
}
~CalServer() {}
private:
int _listensock; // 修改二:改为listensock 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
uint16_t _port;
};
} // namespace server
log.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
// 定义五种不同的信息
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3 // 一种不影响服务器的错误
#define FATAL 4 // 致命错误
const char *to_levelstr(int level)
{
switch (level) // 这里直接return了
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
// 获取前缀信息[日志等级] [时间戳/时间] [pid]
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
// 获取内容
char logcontent[NUM];
va_list arg;
va_start(arg, format); // 因为压栈是反过来的,所以直接使用左边那个参数就行了
vsnprintf(logcontent, sizeof(logcontent), format, arg); // 第三个参数是格式, 第四个就是初始化好的可变参数
std::cout << logprefix << logcontent << std::endl; // 我们只需要日志显示就好了,其余的不需要
}
protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
// 这里是为了当我们后面需要改动分割符的时候这里可以直接修改
#define SEP " "
#define SEP_LEN strlen(SEP) // 不要使用sizeof,会出错
#define LINE_SEP "\r\n" // 这里是为了打印(调试)出来便于观察,我们使用自描述的方式去定制协议前面会添加一个报头表示后面正文部分有多长,
// 当然我们也可以用/r/n来分割,但是使用场景就没有那么完整了,基于可扩展可维护的理论
#define LINE_SEP_LEN strlen(LINE_SEP) // 同理不要使用sizeof
enum
{
OK = 0,
DIV_ZERO,
MOD_ZERO,
OP_ERROR
}; // 正常,除零错误,模0错误,操作符错误
// 增加有效载荷 -- 这里也可以用四个字节来标定大小,不过比较麻烦,就直接使用字符串了
// "x op y" -> "content_len"\r\n"x op y"\r\n
std::string enLength(const std::string &text) // 这两这边就不用引用返回了,不过用不用应该都没有事
{
// std::string text_len = std::to_string(text.size()); 与下面一行合并,注意这里的text.size()还是没有包含\r\n,这两是在这一步添加的
std::string send_string = std::to_string(text.size());
send_string += LINE_SEP;
send_string += text;
send_string += LINE_SEP;
return send_string;
}
// 删除有效载荷
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text) // 输出型参数
{
auto pos = package.find(LINE_SEP);
if (pos == std::string::npos)
return false;
std::string text_len_string = package.substr(0, pos);
int text_len = std::stoi(text_len_string);
*text = package.substr(pos + LINE_SEP_LEN, text_len);
return true;
}
// 没有人规定我们网络通信的时候,只能有一种协议!!
// 我们怎么让系统知道我们用的是哪一种协议呢?
// "content_len"\r\n"协议编号"\r\n"x op y"\r\n -- 我们可以通过加一个协议编号来人系统区分
class Request
{
public:
Request() : x(0), y(0), op(0)
{
}
Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_) // 初始化
{
}
// 1.自己写 -- 目前使用
// 2.用现成的
bool serialize(std::string *out) // 输出型参数
{
#ifdef MYSELF
*out = ""; // c式风格清空
// 结构化 -> "x op y"; 注意这里的空格, 这里不需添加LINE_SEP,因为在添加报头的时候就已经做了
std::string x_string = std::to_string(x);
std::string y_string = std::to_string(y);
// 修正地方111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
*out = x_string;
*out += SEP;
*out += op;
*out += SEP;
*out += y_string;
#else
Json::Value root; // 这是一个万能对象,可以接收任意数据类型
root["first"] = x; // 给x起个名字,这里的x是int类型,它会自动转为字符串,并且所有的类型都是如此
root["second"] = y;
root["oper"] = op;
Json::FastWriter writer; // 序列化
// Json::StyledStreamWriter writer; 和上面的序列化不同的是,这个会比较好看,上面那个内容会一行全部显示出来
*out = writer.write(root); // 返回值是string
#endif
return true;
}
// "x op y\r\n" -- 这里的\r\n会在上面的删除增那边处理
// "x op yyyy" -- 所以这里只需要处理这种情况就行了
bool deserialize(const std::string &in)
{
#ifdef MYSELF
// "x op y\r\n" -> 结构化
auto left = in.find(SEP);
auto right = in.rfind(SEP);
if (left == std::string::npos || right == std::string::npos)
return false; // 没找到分隔符
if (left == right)
return false; // 左右分隔符指向同一个
if (right - (left + SEP_LEN) != 1)
return false; // op操作符不是一个字符
std::string x_string = in.substr(0, left); //[ ) 左闭右开区间 [start, end)
std::string y_string = in.substr(right + SEP_LEN); // 因为后面的\r\n在传进来的时候已经被处理掉,所以这里直接截取到末尾
if (x_string.empty())
return false; // 为空返回错误
if (y_string.empty())
return false;
x = std::stoi(x_string);
y = std::stoi(y_string);
op = in[left + SEP_LEN]; // 直接取到操作符,因为前面判断过了,到这里一定是一个字符
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
x = root["first"].asInt();
y = root["second"].asInt();
op = root["oper"].asInt(); // 这里会比较奇怪,因为op在root里面会呈现字符串的形式,asInt会是int类型,但是又会被转为char类型
#endif
return true;
}
public:
// "x op y" 规定请求一定是这样的格式,并且x操作数一定在前,y操作数在后
int x;
int y;
char op; // 操作符可能有除0之类的错误,所以需要处理
};
class Response
{
public:
Response() : exitcode(0), result(0)
{
}
Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_)
{
}
bool serialize(std::string *out)
{
#ifdef MYSELF
*out = "";
std::string ec_string = std::to_string(exitcode);
std::string res_string = std::to_string(result);
// 修正地方222222222222222222222222222222222222222222222222222222222222222222222222
*out = ec_string;
*out += SEP;
*out += res_string;
#else
Json::Value root;
root["exitcode"] = exitcode;
root["result"] = result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
bool deserialize(const std::string &in)
{
#ifdef MYSELF
// "exitcode result" 同理在这里会拿到这个字符串,后面的/r/n在传进来之前的时候就被去掉了
auto mid = in.find(SEP);
if (mid == std::string::npos)
return false;
std::string ec_string = in.substr(0, mid);
std::string res_string = in.substr(mid + SEP_LEN);
if (ec_string.empty() || res_string.empty())
return false;
exitcode = std::stoi(ec_string);
result = std::stoi(res_string);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in, root);
exitcode = root["exitcode"].asInt();
result = root["result"].asInt();
#endif
return true;
}
public:
int exitcode; // 0: 计算成功,!0表示计算失败,具体是多少,定好标准,类似与错误码
int result; // 计算结果
};
// "content_len"\r\n"x op y"\r\n
bool recvPackage(int sock, std::string &inbuffer, std::string *text) // 把inbuffer当作参数传进来,防止在多线程里面出错,输出型参数
{
char buffer[1024];
while (true)
{
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
inbuffer += buffer;
// 分析处理
auto pos = inbuffer.find(LINE_SEP); // 寻找第一个分隔符
if (pos == std::string::npos)
continue; // 没有一个完整的请求,所以继续读取
std::string text_len_string = inbuffer.substr(0, pos); // 正文长度的子串
int text_len = std::stoi(text_len_string); // 长度就有了
// text_len_string + "\r\n" + text + "\r\n" <= inbuffer.size(); 至少有一个完整的报文,面向字节流读取可能有多个也可能一个都没有
int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
std::cout << "处理前#inbuffer: \n"
<< inbuffer << std::endl;
if (inbuffer.size() < total_len)
{
std::cout << "你输入的消息, 没有严格遵守我们的协议, 正在等待后续的内容, continue" << std::endl;
continue; // 小于说明正文部分还没有全部读取到
}
// 走到这里就说明一定有一个完整的报文了
*text = inbuffer.substr(0, total_len);
inbuffer.erase(0, total_len);
std::cout << "处理后#inbuffer: \n"
<< inbuffer << std::endl;
break;
}
else
return false;
}
return true;
}
/*
我们也可以类似与这样把报文一个个全部塞到一个vector里面去,不过这里就不考虑了,这里只是表示这种方法是可行的
bool recvRequestALL(int sock, std::vector<std::string> *out)
{
std::string line;
while(recvRequest(sock, &line)) out->push_back(line);
}
*/
补充
简单认识http协议
平时我们俗称的 "网址" 其实就是说的 URL
其实 ?在url中很常见,可以在后面再添加一些东西,未来再介绍
未完持续更新中……