一.应用层
我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
二.再谈 "协议"
协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?(序列化和反序列化)
序列化:将结构体数据变成字符串的过程
为什么需要序列化:方便网络的发送和接受
反序列化:将字符串变回结构体数据的过程为什么需要反序列化:方便上层应用程序正常使用数据
三.网络版计算器
客户端发送需计算的数据,服务器进行计算并返回结果.
Sock.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"
class Sock
{
private:
const static int gbacklog = 20;
public:
Sock() {}
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, listensock: %d", listensock);
return listensock;
}
void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
}
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
// 一般经验
// const std::string &: 输入型参数
// std::string *: 输出型参数
// std::string &: 输入输出型参数
int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
return -1;
}
if(port) *port = ntohs(src.sin_port);
if(ip) *ip = inet_ntoa(src.sin_addr);
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
else return false;
}
~Sock() {}
};
这个文件封装sock套接字有关接口,方便直接在代码中使用,就没有什么好解释的,,就是把套接字有关的接口进行了封装。
Log.hpp:
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./calculator.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
//FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
//fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
//fclose(fp);
}
这个就是一个普通的日志文件,用来关注此时的运行状态。
TcpServer.hpp:
#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>
namespace ns_tcpserver
{
using func_t = std::function<void(int)>;
class TcpServer;
class ThreadData
{
public:
ThreadData(int sock, TcpServer *server) : sock_(sock), server_(server)
{
}
~ThreadData()
{
}
public:
int sock_;
TcpServer *server_;
};
class TcpServer
{
public:
static void* ThreadRoutine(void* args)
{
pthread_detach(pthread_self());
ThreadData* td=static_cast<ThreadData*>(args);
td->server_->Excute(td->sock_);
close(td->sock_);
return nullptr;
}
public:
TcpServer(const uint16_t port,const std::string ip="")
{
listensock_=sock_.Socket();
sock_.Bind(listensock_,port,ip);
sock_.Listen(listensock_);
}
//bind服务
void BindService(func_t func)
{
func_.push_back(func);
}
//执行任务
void Excute(int sock)
{
for(auto f: func_)
{
f(sock);
}
}
void Start()
{
for(;;)
{
uint16_t clientport;
std::string clientip;
int sock=sock_.Accept(listensock_,&clientip,&clientport);
if(sock==-1)
continue;
pthread_t tid;
ThreadData* td=new ThreadData(listensock_,this);
pthread_create(&tid,nullptr,ThreadRoutine,td);
}
}
~TcpServer()
{
if(listensock_>=0)
{
close(listensock_);
}
}
private:
int listensock_;
Sock sock_;
std::vector<func_t> func_;
};
}
这个文件,首先把内容放在一个ns_tcpserver的命名空间里面。
TcpServer:
类成员:首先这个类的成员有三个,listensock_这个用来存储套接字,sock_是一个套接字有关的类,func_是一个用来存储任务的数组。
TcpServer:构造函数,用于创建套接字,bind对应的ip,port,并且进行listen。
~TcpServer:析构函数,用于关闭套接字
BindService:bind一个服务
Excute:执行一个对应的服务任务
Start:首先获取连接,然后创建一个线程去执行对应的任务
ThreadRoutine:线程的回调函数,第一步分离线程,然后执行对应的服务任务
ThreadData:
类成员:这个类有两个成员,sock_存储套接字,server是一个TcpServer的类
这个类的作用是用来方便线程执行对应的服务任务
CalServer.cc:
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>
using namespace ns_tcpserver;
using namespace ns_protocol;
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " port\n"
<< std::endl;
}
static Response calculatorHelper(const Request &req)
{
Response resp(0, 0, req.x_, req.y_, req.op_);
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 '/':
if (0 == req.y_)
resp.code_ = 1;
else
resp.result_ = req.x_ / req.y_;
break;
case '%':
if (0 == req.y_)
resp.code_ = 2;
else
resp.result_ = req.x_ % req.y_;
break;
default:
resp.code_ = 3;
break;
}
return resp;
}
void calculator(int sock)
{
std::string inbuffer;
while (true)
{
// 1. 读取成功
bool res = Recv(sock, &inbuffer); // 在这里我们读到了一个请求?
if (!res)
break;
// std::cout << "begin: inbuffer: " << inbuffer << std::endl;
// 2. 协议解析,保证得到一个完整的报文
std::string package = Decode(inbuffer);
if (package.empty())
continue;
// std::cout << "end: inbuffer: " << inbuffer << std::endl;
// std::cout << "packge: " << package << std::endl;
logMessage(NORMAL, "%s", package.c_str());
// 3. 保证该报文是一个完整的报文
Request req;
// 4. 反序列化,字节流 -> 结构化
req.Deserialized(package); // 反序列化
// 5. 业务逻辑
Response resp = calculatorHelper(req);
// 6. 序列化
std::string respString = resp.Serialize(); // 对计算结果进行序列化
// 7. 添加长度信息,形成一个完整的报文
// "length\r\ncode result\r\n"
// std::cout << "respString: " << respString << std::endl;
respString = Encode(respString);
// std::cout << "encode: respString: " << respString << std::endl;
// 8. send这里我们暂时先这样写,多路转接的时候,我们再来谈发送的问题
Send(sock, respString);
}
}
// void handler(int signo)
// {
// std::cout << "get a signo: " << signo << std::endl;
// exit(0);
// }
// ./CalServer port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// 一般经验:server在编写的时候,要有较为严谨性的判断逻辑
// 一般服务器,都是要忽略SIGPIPE信号的,防止在运行中出现非法写入的问题!
// signal(SIGPIPE, SIG_IGN);
MyDaemon();
std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));
server->BindService(calculator);
server->Start();
// Request req(123, 456, '+');
// std::string s = req.Serialize();
// std::cout << s << std::endl;
// Request temp;
// temp.Deserialized(s);
// std::cout << temp.x_ << std::endl;
// std::cout << temp.op_ << std::endl;
// std::cout << temp.y_ << std::endl;
return 0;
}
这个是服务端,首先来说一下主函数,首先调用MyDaemon(),然这个进程变成守护进程,这个在后面再仔细说,然后就是创建一个TcpServer对象,然后给他BindService一个服务,然后调用Start(),然后启动服务端。
calculator:这个函数就是对应服务,第一步首下就是要从对应客户端读取数据,然后进行协议解析,然后反序列化,再按正常逻辑计算结果即可,最后序列化和添加报文,然后发送出去即可。
为什么需要协议解析以及添加报文:因为tcp是字节流式的,收到的数据可能并不完整,就是并不是一个数据,有可能是半个,也有可能是一个半,因为send/write这类接口是把数据写到缓冲区,其他的事情都归Tcp管,因为TCP是字节流式的,所以发送次数和接收次数,没有关系,不像udp发多少次就接受多少次。
Makefile:
.PHONY:all
all:client server
client:CalClient.cc
g++ -o $@ $^ -std=c++11
server:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf client server
CalClient.cc:
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
using namespace ns_protocol;
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " serverIp serverPort\n"
<< std::endl;
}
// ./client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
Sock sock;
int sockfd = sock.Socket();
if (!sock.Connect(sockfd, server_ip, server_port))
{
std::cerr << "Connect error" << std::endl;
exit(2);
}
bool quit = false;
std::string buffer;
while (!quit)
{
// 1. 获取需求
Request req;
std::cout << "Please Enter # ";
std::cin >> req.x_ >> req.op_ >> req.y_;
// 2. 序列化
std::string s = req.Serialize();
// std::string temp = s;
// 3. 添加长度报头
s = Encode(s);
// 4. 发送给服务端
Send(sockfd, s);
// 5. 正常读取
while (true)
{
bool res = Recv(sockfd, &buffer);
if (!res)
{
quit = true;
break;
}
std::string package = Decode(buffer);
if (package.empty())
continue;
Response resp;
resp.Deserialized(package);
std::string err;
switch (resp.code_)
{
case 1:
err = "除0错误";
break;
case 2:
err = "模0错误";
break;
case 3:
err = "非法操作";
break;
default:
std::cout << resp.x_ << resp.op_ << resp.y_ << " = " << resp.result_ << " [success]" << std::endl;
break;
}
if(!err.empty()) std::cerr << err << std::endl;
// sleep(1);
break;
}
}
close(sockfd);
return 0;
}
CalClient.cc这个是客户端:
main():第一步接收数据(端口号和IP),然后创捷套接字,并且获取连接,然后进入一个正常的死循环,先是从键盘获取数据,然后序列化添加报头,发送给服务器,然后再从服务器读取数据,进行协议解析和反序列化,最后回显到显示器即可。
Daemon.hpp:
#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void MyDaemon()
{
// 1. 忽略信号,SIGPIPE,SIGCHLD
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 不要让自己成为组长
if (fork() > 0)
exit(0);
// 3. 调用setsid
setsid();
// 4. 标准输入,标准输出,标准错误的重定向,守护进程不能直接向显示器打印消息
int devnull = open("/dev/null", O_RDONLY | O_WRONLY);
if(devnull > 0)
{
dup2(0, devnull);
dup2(1, devnull);
dup2(2, devnull);
close(devnull);
}
}
Daemon.hpp:作用是让一个进程变成守护进程
接下来解释一下什么是守护进程:
守护进程:
1.全部都是在前台运行的
2..前台进程:和终端关联的进程,前台进程。
3.任何xshell登陆,只允许一个前台进程和多个后台进程
4.进程除了有自己的pid,ppid,还有一个组ID
5.在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash -→>可以用匿名管道来进行通信,而同时被创建的多个进程可以成为一个进程组的概念,组长一般是第一个进程
6.任何一次登陆,登陆的用户,需要有多个进程(组)来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。我们把给用户提供服务的进程或者用户自己启动的所有的进程或者服务,整体都是要属于一个叫做会话的机制中的。
7.如何将自己变成自成会话呢? 调用setsid0
8. setsid要成功被调用,必须保证当前进程不是进程组的组长,怎么保证我不是组长呢?调用fork()
9.守护进程不能直接向显示器打印消息一旦打印,会被暂停、终止
10.如何杀死守护进程,使用9号信号杀死
Protocol.hpp:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>
namespace ns_protocol
{
#define MYSELF 0
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP) // 不能是sizeof!
class Request
{
public:
// 1. 自主实现 "length\r\nx_ op_ y_\r\n"
// 2. 使用现成的方案
std::string Serialize()
{
#ifdef MYSELF
std::string str;
str = std::to_string(x_);
str += SPACE;
str += op_; // TODO
str += SPACE;
str += std::to_string(y_);
return str;
#else
Json::Value root;
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
// "x_ op_ y_"
// "1234 + 5678"
bool Deserialized(const std::string &str)
{
#ifdef MYSELF
std::size_t left = str.find(SPACE);
if (left == std::string::npos)
return false;
std::size_t right = str.rfind(SPACE);
if (right == std::string::npos)
return false;
x_ = atoi(str.substr(0, left).c_str());
y_ = atoi(str.substr(right + SPACE_LEN).c_str());
if (left + SPACE_LEN > str.size())
return false;
else
op_ = str[left + SPACE_LEN];
return true;
#else
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
public:
Request()
{
}
Request(int x, int y, char op) : x_(x), y_(y), op_(op)
{
}
~Request() {}
public:
// 约定
// x_ op y_ ? y_ op x_?
int x_; // 是什么?
int y_; // 是什么?
char op_; // '+' '-' '*' '/' '%'
};
class Response
{
public:
// "code_ result_"
std::string Serialize()
{
#ifdef MYSELF
std::string s;
s = std::to_string(code_);
s += SPACE;
s += std::to_string(result_);
return s;
#else
Json::Value root;
root["code"] = code_;
root["result"] = result_;
root["xx"] = x_;
root["yy"] = y_;
root["zz"] = op_;
Json::FastWriter writer;
return writer.write(root);
#endif
}
// "111 100"
bool Deserialized(const std::string &s)
{
#ifdef MYSELF
std::size_t pos = s.find(SPACE);
if (pos == std::string::npos)
return false;
code_ = atoi(s.substr(0, pos).c_str());
result_ = atoi(s.substr(pos + SPACE_LEN).c_str());
return true;
#else
Json::Value root;
Json::Reader reader;
reader.parse(s, root);
code_ = root["code"].asInt();
result_ = root["result"].asInt();
x_ = root["xx"].asInt();
y_ = root["yy"].asInt();
op_ = root["zz"].asInt();
return true;
#endif
}
public:
Response()
{
}
Response(int result, int code, int x, int y, char op)
: result_(result), code_(code), x_(x), y_(y), op_(op)
{
}
~Response() {}
public:
// 约定!
// result_? code_? code_ 0? 1?2?3?
int result_; // 计算结果
int code_; // 计算结果的状态码
int x_;
int y_;
char op_;
};
// 临时方案
// 调整方案2: 我们期望,你必须给我返回一个完整的报文
bool Recv(int sock, std::string *out)
{
// UDP是面向数据报:
// TCP 面向字节流的:
// recv : 你怎么保证,你读到的inbuffer,是一个完整完善的请求呢?不能保证
// "1234 + 5678" : 1234 +
// "1234 + 5678" : 1234 + 5678 123+99
// "1234 "
// 必须是:"1234 + 5678"
// 单纯的recv是无法解决这个问题的,需要对协议进一步定制!
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); // 9\r\n123+789\r\n
if (s > 0)
{
buffer[s] = 0;
*out += buffer;
}
else if (s == 0)
{
// std::cout << "client quit" << std::endl;
return false;
}
else
{
// std::cout << "recv error" << std::endl;
return false;
}
return true;
}
void Send(int sock, const std::string str)
{
// std::cout << "sent in" << std::endl;
int n = send(sock, str.c_str(), str.size(), 0);
if (n < 0)
std::cout << "send error" << std::endl;
}
// "length\r\nx_ op_ y_\r\n..." // 10\r\nabc
// "x_ op_ y_\r\n length\r\nXXX\r\n"
std::string Decode(std::string &buffer)
{
std::size_t pos = buffer.find(SEP);
if(pos == std::string::npos) return "";
int size = atoi(buffer.substr(0, pos).c_str());
int surplus = buffer.size() - pos - 2*SEP_LEN;
if(surplus >= size)
{
//至少具有一个合法完整的报文, 可以动手提取了
buffer.erase(0, pos+SEP_LEN);
std::string s = buffer.substr(0, size);
buffer.erase(0, size + SEP_LEN);
return s;
}
else
{
return "";
}
}
// "XXXXXX"
// "123\r\nXXXXXX\r\n"
std::string Encode(std::string &s)
{
std::string new_package = std::to_string(s.size());
new_package += SEP;
new_package += s;
new_package += SEP;
return new_package;
}
}
protocal.hpp:这个文件里面主要包含了序列化和反序列化以及协议解析和添加报头
首先把内容封装在ns_protocal这个命名空间里面
Requst和Response这两个类分别是存储初始数据和结果,然后在里面各写了一份序列化和反序列化
Recv:就是把读取数据封装了一下
Send:就是把发送数据封装了
Decode:协议解析
Encode:添加报头