总言
主要内容:演示socke套接字编程(TCP模式),介绍序列化和反序列化,并进行演示(json版本达成协议编写、守护进程介绍)。
文章目录
- 总言
- 4、基于套接字的TCP网络程序
- 4.0、log.hpp
- 4.1、version1.0:echo服务器(单进程单线程模式)
- 4.1.1、成员变量与构造、析构
- 4.1.2、初始化服务器:InitServer()
- 4.1.2.1、socket、bind
- 4.1.2.2、listen
- 4.1.3、启动服务器:Start()
- 4.1.3.1、accept
- 4.1.4、该部分整体框架:
- 4.1.4.1、tcp_server.hpp
- 4.1.4.2、tcp_server.cc
- 4.1.4.3、telnet 指令
- 4.2、version2.0 && version2.1 (多进程版)
- 4.2.1、version2.0:采用信号捕捉达成非阻塞等待
- 4.2.1.1、tcp_server.hpp
- 4.2.1.2、tcp_client.cc:connect函数介绍
- 4.2.2、version2.1:采用孤儿进程达成非阻塞等待
- 4.2.2.1、tcp_server.hpp
- 4.4、version3.0(多线程版)
- 4.4.1、tcp_server.hpp
- 4.5、version4.0(线程池版)
- 4.5.1、tcp_server.hpp
- 4.6、TCP协议通讯流程
- 5、序列化和反序列化(应用层·一)
- 5.1、基本情况介绍
- 5.2、网络版本的计算器NetCal编写(version1.0:自定义版协议)
- 5.2.4、Sock.hpp && TcpServer.hpp
- 5.2.4.1、Sock.hpp
- 5.2.4.2、TcpServer.hpp
- 5.2.5、CalServer.cc服务端
- 5.2.3、Protocol.hpp:定制的协议
- 5.2.6、CalClient.cc客户端
- 5.3、网络版本的计算器NetCal编写(version2.0:json版协议)
- 5.3.1、守护进程
- 5.3.1.1、问题引入
- 5.3.1.2、如何做到:setsid、daemon
- 5.3.1.3、Daemon.hpp
- 5.3.1.4、log.hpp
- 5.3.2、使用json完成序列化
- 5.3.2.1、基本使用介绍
- 5.3.3、改动NetCal
- 5.3.3.1、主要部分
- 5.3.3.2、整体Protocol.hpp
4、基于套接字的TCP网络程序
4.0、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"
};
// 完整的日志功能,至少有 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)//const char *format, ... 可变参数
{
// #ifndef DEBUG_SHOW
// if(level== DEBUG) return;
// #endif
//标准部分:固定输出的内容
char stdBuffer[1024];
time_t timestamp = time(nullptr);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
//自定义部分:允许用户根据自己的需求设置
char logBuffer[1024];
va_list args; //定义一个va_list对象
va_start(args, format);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args); //相当于 args == nullptr
printf("%s%s\n", stdBuffer, logBuffer);
}
4.1、version1.0:echo服务器(单进程单线程模式)
先来完善tcp_server.hpp
的整体逻辑:
4.1.1、成员变量与构造、析构
有了UDP的基础,此处的框架搭建也相同。
class TcpServer
{
public:
TcpServer(uint16_t port, const std::string& ip="")
:_port(port),_ip(ip), _linstensock(-1)
{ }
~TcpServer()
{ }
// 初始化服务器
bool InitServer()
{
}
// 启动服务器
void Start()
{
}
private:
uint16_t _port;//端口号
std::string _ip;//IP
int _listensock;//套接字
};
4.1.2、初始化服务器:InitServer()
4.1.2.1、socket、bind
说明:初始化服务器,这里用法和UDP类似,区别在于socket时,第二参数类型填入的是SOCK_STREAM
。
socket
:
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
The socket has the indicated type, which specifies the communication semantics. Currently defined
types are:
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-
band data transmission mechanism may be supported.
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
bind
:
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
相关演示如下:
// 1、创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAM
if (_listensock < 0)
{
logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);
// 2、绑定
struct sockaddr_in localaddr;
bzero(&localaddr, 0);
localaddr.sin_family = AF_INET;
localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换
localaddr.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0)
{
logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));
exit(3);
}
logMessage(DEBUG, "bind, 绑定成功. ");
4.1.2.2、listen
1)、相关函数介绍
说明: 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接。而作为服务器,为了确保客户端能够随时享有通讯需求,服务器需要保持在等待被连接的状态。
listen
:该函数可将套接字sockfd
状态设置为监听状态,并且最多允许有backlog
个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,这里设置不会太大。
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
DESCRIPTION
listen() marks the socket referred to by sockfd as a passive socket, that is, as a
socket that will be used to accept incoming connection requests using accept(2).
The sockfd argument is a file descriptor that refers to a socket of type
SOCK_STREAM or SOCK_SEQPACKET.
The backlog argument defines the maximum length to which the queue of pending con‐
nections for sockfd may grow. If a connection request arrives when the queue is
full, the client may receive an error with an indication of ECONNREFUSED or, if
the underlying protocol supports retransmission, the request may be ignored so
that a later reattempt at connection succeeds.
相关参数:
socket
:即创建套接字时的返回值
backlog
:关于该参数在后续TCP协议时详细解释,这里我们只需要先使用即可。通常设置如下:
const static int gbacklog = 20; //不能太大、也不能太小
返回值:
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
2)、建立链接
使用演示:这里我们对监听的结果做了一下判断。
//3、监听
if (listen(_listensock, gbacklog) < 0)
{
logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");
相关演示结果:
4.1.3、启动服务器:Start()
4.1.3.1、accept
说明:三次握手完成后,服务器调用accept()
接受连接,如果此时时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
DESCRIPTION
The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the
first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected
socket, and returns a new file descriptor referring to that socket. The newly created socket is not in the listening
state. The original socket sockfd is unaffected by this call.
演示如下:accept
之后就是正常的通讯流程。
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if(servicesock < 0)
{
logMessage(ERROR,"accept, 获取链接失败, servicesock: %d ",servicesock);
continue;//本次失败了,结束此次循环即可,可下一次重新获取连接
}
4.1.4、该部分整体框架:
4.1.4.1、tcp_server.hpp
#ifndef _TCP_SERVER_HPP
#define _TCP_SERVER_HPP
#include<iostream>
#include<string>
#include<cstring>
#include<memory>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"log.hpp"
#define SIZE 1024
// 业务函数:服务器用于处理业务,可根据需求自定义
// 【version1: echo版服务器】:服务端打印从客户端读取到的数据,并将其原封不动返回给客户端
static void service(int servicesock, const std::string &clientip, const uint16_t &clientport)
{
char server_buffer[SIZE];
while (true)
{
// a、读取客户端发来的数据
ssize_t s = read(servicesock, server_buffer, sizeof(server_buffer) - 1);
if (s > 0)
{
server_buffer[s] = '\0'; //\0的ASCII码是0
std::cout << clientip << ":" << clientport << "# " << server_buffer << std::endl;
}
else if (s == 0)
{
logMessage(NORMAL, "read, %s:%d 退出。", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read, 读取失败, %d:%s", errno, strerror(errno));
break;
}
// b、将读取到的结果返回
write(servicesock, server_buffer, strlen(server_buffer));
}
}
class TcpServer
{
private:
const static int gbacklog = 20;//listen中的参数设置
public:
TcpServer(uint16_t port, const std::string& ip="")
:_port(port),_ip(ip),_listensock(-1)
{ }
~TcpServer()
{ }
// 初始化服务器
bool InitServer()
{
// 1、创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAM
if (_listensock < 0)
{
logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);
// 2、绑定
struct sockaddr_in localaddr;
bzero(&localaddr, 0);
localaddr.sin_family = AF_INET;
localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换
localaddr.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0)
{
logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));
exit(3);
}
logMessage(DEBUG, "bind, 绑定成功. ");
//3、监听
if (listen(_listensock, gbacklog) < 0)
{
logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");
return true;
}
// 启动服务器
void Start()
{
// 网络通讯角度:作为一款网络服务器,永远不退出的!
// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
while (true)
{
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if(servicesock < 0)
{
logMessage(ERROR,"accept, 获取链接失败, servicesock: %d ",servicesock);
continue;//本次失败了,结束此次循环即可,可下一次重新获取连接
}
// 2、开始进行通讯服务
// 2.1、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);
logMessage(DEBUG, "accept, 成功获取连接, servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);
// 2.2、根据需求处理客户端数据(服务端的业务处理)
// version1: echo版服务器
service(servicesock, client_ip, client_port);
// 2.3、通讯结束,关闭套接字。
close(servicesock);
}
}
private:
uint16_t _port;//端口号
std::string _ip;//IP
int _listensock;//套接字
};
#endif
4.1.4.2、tcp_server.cc
#include"tcp_server.hpp"
//使用手册
void Usage(std:: string proc)
{
std::cout << "\nUsage: "<< proc << " port\n"<< std::endl;
}
//服务端启动: ./tcp_server port
int main(int argc, char*argv[])
{
//1、检测命令行参数是否正确
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);//说明:命令行参数为字符串,port端口号需要整型
//2、使用智能指针管理服务器
std::unique_ptr<TcpServer> server(new TcpServer(port));
//3、初始化和启动服务器
server->InitServer();
server->Start();
return 0;
}
4.1.4.3、telnet 指令
演示结果一:先测试看看我们的代码能否成功通过。
演示结果二:接下来,验证以下这种单进程模式的缺陷,服务器始终只能为一个客服端提供服务,当有多个客服端同时连接时,后者处于阻塞状态。
现象观察:
原因解释:
基于上述分析,我们提出了以下方案,用以解决服务端只一次能够处理一个客户端的问题。
4.2、version2.0 && version2.1 (多进程版)
说明: 采用多进程的方式解决问题。让父进程接收连接,让子进程处理业务,其中,需要解决父进程等待子进程的问题。
4.2.1、version2.0:采用信号捕捉达成非阻塞等待
4.2.1.1、tcp_server.hpp
说明:仍旧是演示echo版服务器(service函数不变),这里只是使用了多进程,用以让服务端能够同时服务多个客服端。
改动的部分在start函数中。这里使用了信号捕捉的方式,让父进程达成非阻塞式等待子进程:signal(SIGCHLD, SIG_IGN);
(这种处理在Linux下的含义有在 信号章节 讲述过)。
// 启动服务器
void Start()
{
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
// 网络通讯角度:作为一款网络服务器,永远不退出的!
// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
while (true)
{
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);
continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接
}
// 2、开始进行通讯服务
// 2.1、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);
logMessage(DEBUG, "accept, 成功获取连接, servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);
// 2.2、根据需求处理客户端数据(服务端的业务处理)
// ——————————【version2:多进程版本】———————————//
pid_t pd = fork();
assert(pd >= 0);
if (pd == 0) // 对子进程:处理业务
{
close(_listensock); // 子进程用不到监听套接字,可以关掉(子进程能够继承父进程打开的文件及其fd)
service(servicesock, client_ip, client_port);
exit(0); // 正常退出
}
// 对父进程:继续循环接收客户端的连接请求
close(servicesock); // 父进程用不到accept提供的套接字,可以关掉(对子进程无影响/文件描述符是有限资源,有上限)
// ————————————————————————————————————————————//
}
}
使用talnet
验证当前版本的tcp_server.hpp
:
4.2.1.2、tcp_client.cc:connect函数介绍
1)、相关函数介绍
connect
:客户端不需要像服务端一样手动bind
,同时也不需要accept
接收连接,但客户端需要有链接别人的能力,可以通过此函数达成。
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
DESCRIPTION
The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr.
The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address space of
the socket sockfd; see socket(2) for further details.
If the socket sockfd is of type SOCK_DGRAM then addr is the address to which datagrams are sent by default, and the only
address from which datagrams are received. If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this call attempts to
make a connection to the socket that is bound to the address specified by addr.
Generally, connection-based protocol sockets may successfully connect() only once; connectionless protocol sockets may use
connect() multiple times to change their association. Connectionless sockets may dissolve the association by connecting to
an address with the sa_family member of sockaddr set to AF_UNSPEC (supported on Linux since kernel 2.2).
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
2)、代码演示
相关代码:
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SIZE 1024
// 使用手册
void Usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}
// 启动方式:udp_client server_ip server_port
int main(int argc, char *argv[])
{
// 1、获取命令行传入的端口号、IP:注意将其转换为对应的类型
// a、检测命令行参数是否正确
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// b、获取服务端端口号、IP
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2、创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "client: 创建套接字失败。 socket: " << sock << std::endl;
exit(2);
}
// 3、客户端需要有连接服务端的能力(PS:客户端不需要bind,一般由OS自动分配端口号)
// a、准备工作:
struct sockaddr_in serveraddr;
bzero(&serveraddr, 0);
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(server_port);//端口号:需要进行网络字节序的转换
serveraddr.sin_addr.s_addr = inet_addr(server_ip.c_str());//IP:
// b、建立连接
if(connect(sock, (struct sockaddr*)&serveraddr, sizeof serveraddr) < 0)
{
std::cerr << "client: connect建立连接失败。" << std::endl;
exit(3);
}
std::cout << "client: connect建立连接成功。" << std::endl;
// 4、链接成功,即可通讯
while(true)
{
//a、客户端向服务端发送数据
std::string message;
std::cout << "client-请输入# ";
std::getline(std::cin,message);
send(sock, message.c_str(), message.size(),0);
//b、客户端接收服务端传回的数据
char client_buff[SIZE];
ssize_t s = recv(sock,client_buff,sizeof(client_buff)-1,0);
if(s > 0)//成功接收到数据
{
client_buff[s] = 0;
std::cout << "client-服务端响应:" << client_buff << std::endl;
}
}
return 0;
}
演示结果如下:
4.2.2、version2.1:采用孤儿进程达成非阻塞等待
4.2.2.1、tcp_server.hpp
说明: 仍旧是演示echo版本服务器、多进程模式。只是这次不采用信用捕捉,而是使用孤儿进程的方式达成非阻塞等待。
写法如下: 在子进程中再创建子进程,由于子进程关闭,子子进程会成为孤儿进程,被1号进程领养。孤儿进程退出的时候,由OS自动回收孤儿进程!
// 启动服务器
void Start()
{
// 网络通讯角度:作为一款网络服务器,永远不退出的!
// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
while (true)
{
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);
continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接
}
// 2、开始进行通讯服务
// 2.1、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);
logMessage(DEBUG, "accept, 成功获取连接, servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);
// 2.2、根据需求处理客户端数据(服务端的业务处理)
// ——————————【version2.1:多进程版本】———————————//
pid_t pd = fork();
assert(pd != -1);
if( pd == 0)//子进程
{
close(_listensock);//子进程:关闭不必要的套接字
if(fork() == 0)// 在子进程中再fork子进程,得到子子进程(孙子进程)
{
service(servicesock, client_ip, client_port);
exit(0);// 孙子进程,由于子进程关闭,其成为孤儿进程,OS领养, OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
}
exit(0);//关闭子进程,会导致子子进程变成孤儿进程
}
//对父进程:
waitpid(pd, nullptr, 0);
close(servicesock);//父进程:关闭不必要的套接字
// —————————————————————————————————————————————//
}
}
这里也可以使用if(fork() > 0)
来判断,写法无区别。
//version2.1 -- 多进程版
pid_t id = fork();
if(id == 0)
{
// 子进程
close(_listensock);
if(fork() > 0)//子进程本身
exit(0); //子进程本身立即退出
// 孙子进程称为孤儿进程,OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程!
service(servicesock, client_ip, client_port);
exit(0);
}
// 父进程
waitpid(id, nullptr, 0); //不会阻塞!
close(servicesock);
演示结果如下:
4.4、version3.0(多线程版)
4.4.1、tcp_server.hpp
1)、准备工作
1、为了让新线程执行业务处理,需要设置回调函数。这里我们将其设置在了TcpServer
类中,并将其置为静态成员函数。(注意,线程结束时要关闭文件描述符和释放空间)
PS:此部分涉及的函数在多线程中有学习。
class TcpServer
{
private:
//【version3:多线程版本】: 为新线程提供的回调函数,设置为静态成员函数是因为非静态成员函数有默认参数this,不符合回调函数的格式要求
static void* threadRoutine(void* args)
{
线程分离:结束服务,若不捕捉(捕捉属于阻塞式的,我们要求服务端不能阻塞等待),
//则需要对线程进行分离pthread_detach(pthread_self());以避免系统层面的内存泄漏
pthread_detach(pthread_self());
threadData* data = static_cast<threadData *>(args);//C++11中的类型转换
service(data->sock, data->ip, data->port);
// 线程结束时,需要关闭文件描述符、并释放new出来的空间
close(data->sock);
delete data;
return nullptr;
}
//……
}
2、void*args
参数设置:使用类可提供更多选择性。
//【version3:多线程版本】:要在新线程中调用server,要将其需要函数参数设置进void*args中。这里使用类来完成。
struct threadData
{
int sock;
std::string ip;
uint16_t port;
};
TcpServer
类中需要改动的仍旧是start()
部分,代码如下:
// 启动服务器
void Start()
{
//signal(SIGCHLD, SIG_IGN); // 【version2.1:多进程版本】对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
// 网络通讯角度:作为一款网络服务器,永远不退出的!
// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
while (true)
{
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);
continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接
}
// 2、开始进行通讯服务
// 2.1、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);
logMessage(DEBUG, "accept, 成功获取连接, servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);
// 2.2、根据需求处理客户端数据(服务端的业务处理)
// ——————————【version3:多线程版本】———————————//
// a、准备工作
threadData* td = new threadData;//在堆上
td->sock = servicesock;
td->ip = client_ip;
td->port = client_port;
pthread_t pid;
// b、创建线程
pthread_create(&pid, nullptr, threadRoutine, (void*)td);
//PS:注意这里不需要close(servicesock);因为主线程和新线程共享资源
// —————————————————————————————————————————————//
}
}
演示结果:
4.5、version4.0(线程池版)
这里我们借用了之前写过的线程池库,详细见:多线程章节。
4.5.1、tcp_server.hpp
该线程库大体逻辑不变,只是需要改动任务对象,将其换成我们所需要的部分(即task.hpp中封装的_func实际传入的是回调函数,task.hpp中需要的成员变量是为了满足该_func函数。)
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"
// typedef std::function<void (int , const std::string &, const uint16_t &)> func_t; //写法一
using func_t = std::function<void (int , const std::string &, const uint16_t &, const std::string &)>; //写法二
class Task
{
public:
Task(){}//无参构造
Task(int sock, const std::string ip, uint16_t port, func_t func)//构造
: _sock(sock), _ip(ip), _port(port), _func(func)
{}
void operator ()(const std::string &name)
{
_func(_sock, _ip, _port, name);
}
public:
int _sock;
std::string _ip;
uint16_t _port;
func_t _func;
};
需要改动部分:service业务处理函数,新增了参数,用以辅佐观察(实则该函数可根据需求调节内容)
// 【version4:echo版服务器】:和上面的相同,只是为了显示是哪个新线程执行的业务处理,新增了参数
static void service(int servicesock, const std::string &clientip, const uint16_t &clientport, const std::string &threadname)
{
char server_buffer[SIZE];
while (true)
{
// a、读取客户端发来的数据
ssize_t s = read(servicesock, server_buffer, sizeof(server_buffer) - 1);
if (s > 0)
{
server_buffer[s] = '\0'; //\0的ASCII码是0
std::cout << threadname << "| " << clientip << ":" << clientport << "# " << server_buffer << std::endl;
}
else if (s == 0)
{
logMessage(NORMAL, "read, %s:%d 退出。", clientip.c_str(), clientport);
break;
}
else
{
logMessage(ERROR, "read, 读取失败, %d:%s", errno, strerror(errno));
break;
}
// b、将读取到的结果返回
write(servicesock, server_buffer, strlen(server_buffer));
}
close(servicesock);// c、结束,需要将文件描述符关闭
}
新增了成员变量std::unique_ptr<ThreadPool<Task>> _pthreadpool;
,所以,构造函数中的初始化列表也需要改动。其它改动部分仍旧在Start()
中。
class TcpServer
{
private:
const static int gbacklog = 20; // listen中的参数设置
public:
TcpServer(uint16_t port, const std::string &ip = "") //在构造时,初始化其它变量时也要初始化线程库
: _port(port), _ip(ip), _listensock(-1),_pthreadpool(ThreadPool<Task>::getThreadPool())
{
}
~TcpServer()
{
close(_listensock);
}
// 初始化服务器
bool InitServer()
{
// 1、创建套接字
_listensock = socket(AF_INET, SOCK_STREAM, 0); // 这里填入SOCK_STREAM
if (_listensock < 0)
{
logMessage(ERROR, "socket, 创建套接字失败: %s-%d ", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "socket, 创建套接字成功,sock: %d", _listensock);
// 2、绑定
struct sockaddr_in localaddr;
bzero(&localaddr, 0);
localaddr.sin_family = AF_INET;
localaddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 字节序转换
localaddr.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr *)&localaddr, sizeof localaddr) < 0)
{
logMessage(ERROR, "bind, 绑定失败,%d:%s", errno, strerror(errno));
exit(3);
}
logMessage(DEBUG, "bind, 绑定成功. ");
// 3、监听
if (listen(_listensock, gbacklog) < 0)
{
logMessage(ERROR, "listen, 监听失败,%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(DEBUG, "linsten, 监听成功, 初始化套接字完成。");
return true;
}
// 启动服务器
void Start()
{
_pthreadpool->run();//【version4】:在启动服务器时,一并将线程池中的线程启动(创建)
// 网络通讯角度:作为一款网络服务器,永远不退出的!
// OS角度:服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
while (true)
{
// 1、获取连接
// 1.1、准备工作:用于后续从网络中读取客户端的IP、端口号
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(clientaddr);
// 1.2、连接
int servicesock = accept(_listensock, (struct sockaddr *)&clientaddr, &len);
if (servicesock < 0)
{
logMessage(ERROR, "accept, 获取链接失败, servicesock: %d ", servicesock);
continue; // 本次失败了,结束此次循环即可,可下一次重新获取连接
}
// 2、开始进行通讯服务
// 2.1、获取客户端端口号、IP
uint16_t client_port = ntohs(clientaddr.sin_port); // uint16_t htons(uint16_t hostshort);
std::string client_ip = inet_ntoa(clientaddr.sin_addr); // char *inet_ntoa(struct in_addr in);
logMessage(DEBUG, "accept, 成功获取连接, servicesock: %d, client: [%s:%d] .", servicesock, client_ip.c_str(), client_port);
// 2.2、根据需求处理客户端数据(服务端的业务处理)
// ——————————【version4:线程池版本】———————————//
// a、主线程派发任务对象:将客服端当作一个任务对象,创建后放入线程池中,后续由线程池中的新线程来执行。
Task t(servicesock, client_ip, client_port, service);
_pthreadpool->pushTask(t);
// ———————————————————————————————————————————//
}
}
private:
uint16_t _port; // 端口号
std::string _ip; // IP
int _listensock; // 套接字
std::unique_ptr<ThreadPool<Task>> _pthreadpool; // 【version4】:指向线程池的指针
};
演示结果如下:
4.6、TCP协议通讯流程
5、序列化和反序列化(应用层·一)
5.1、基本情况介绍
1)、应用层与应用层协议
应用层说明: 我们之前写的一个个解决实际问题、满足日常需求的网络程序,都是在应用层进行的。在之前几个小节的socket套接字编,都是属于应用层的开发(我们只是使用了传输层包装出来的接口而已),且这些套接字只是演示了数据收发过程,并非实际涉及协议。
应用层协议: 协议是一种“约定”。通常,只要保证一端发送时构造的数据, 在另一端能够正确的进行解析, 这种“约定”就是应用层协议。例如,socket api的接口在读写数据时, 就是按 “字符串” 的方式来发送、接收数据的。
2)、序列化和反序列化
基本介绍:数据类型可以是字节流数据,也可以是结构化数据。通常,前者应用于网络传输,后者应用于上层业务。
序列化:把对象转换为字节序列的过程,称为对象的序列化。
反序列化:把字节序列恢复为对象的过程,称为对象的反序列化。
思考问题:为什么要做这种序列化和反序列化的处理?
5.2、网络版本的计算器NetCal编写(version1.0:自定义版协议)
目的说明:
①从编码角度介绍什么是序列化、什么是反序列化。
②手动定制协议→成熟的协议使用:这里只是针对当前场景自定义协议,这种完全自己写的协议会极大概率存在各种问题缺陷,且若要对Cal中的协议做扩展,需要改动很大, 而使用别人提供的成熟方案,相对而言还是能简单很多。因此,此部分内容学习主要是为了理解协议定制的整体流程。
5.2.4、Sock.hpp && TcpServer.hpp
5.2.4.1、Sock.hpp
说明:该文件只是对sock函数接口进行了一次封装,以便在客户端、服务端两个.cc
文件中调用。
相关代码如下:
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
class Sock
{
private:
const static int gbacklog = 20; // listen中参数设置:详细将在后续介绍
public:
// 构造
Sock(){}; // 无参构造
// 析构
~Sock(){};
// 创建套接字:int socket(int domain, int type, int protocol);
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "socket:创建套接字失败。%d:%s", errno, strerror(errno));
exit(2); // 退出
}
logMessage(NORMAL, "socket:创建套接字成功, listensock:%d", listensock);
return listensock; // 将套接字返回给TcpServer中的成员函数_listensock
}
// 绑定:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
void Bind(int sock, uint16_t port, const std::string& ip = "0.0.0.0")
{
// 准备工作:sockaddr结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port); // 对端口号:需要转换为网络字节序
inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 对ip:点分十进制风格-->网络字节序+四字节
// 绑定:
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind:绑定失败。%d:%s", errno, strerror(errno));
exit(3); // 退出
}
logMessage(NORMAL, "bind: 绑定成功。");
}
// 监听:int listen(int sockfd, int backlog);
void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(FATAL, "listen:监听失败。%d:%s", errno, strerror(errno));
exit(4); // 退出
}
logMessage(NORMAL, "listen:监听成功。");
}
// 获取连接:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int Accept(int listensock, std::string *ip, uint16_t *port) // 后两个*为输出型参数:这里的作用是将accept接收到的ip、port返回给TcpServer。
{
// 准备工作:用于接收源IP、源端口号
struct sockaddr_in src;
memset(&src, 0, sizeof(src));
src.sin_family = AF_INET;
socklen_t len = sizeof(src);
// 获取连接
int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicesock < 0)
{
logMessage(FATAL, "accept:接收失败。%d:%s", errno, strerror(errno));
exit(5);
}
logMessage(NORMAL, "accept:接收成功。servicesock:%d", servicesock);
if (ip) // 判空:获取源IP
*ip = inet_ntoa(src.sin_addr); // 四字节+网络字节序--->主机字节序+点分十进制
if (port) // 判空:获取源端口号
*port = ntohs(src.sin_port); // 网络字节序--->主机字节序
return servicesock;
}
// int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bool Connect(int sock, const uint16_t &port, const std::string &ip)
{
// 准备工作
struct sockaddr_in aim;
bzero(&aim, sizeof(aim));
aim.sin_family = AF_INET;
aim.sin_port = htons(port); // 主机字节序--->网络字节序
aim.sin_addr.s_addr = inet_addr(ip.c_str()); // 主机字节序+点分十进制风格--->网络字节序+四字节
// 连接
if (connect(sock, (struct sockaddr *)&aim, sizeof(aim)) < 0)
{
logMessage(FATAL, "connect:连接失败。%d:%s", errno, strerror(errno));
return false;
}
logMessage(NORMAL, "connect:连接成功。");
return true;
}
};
5.2.4.2、TcpServer.hpp
说明: 这里我们使用的仍旧是TCP网络通信,相关内容的编写在之前章节演示过,只是在其基础上封装了接口。
代码如下: 使用的是线程版本的服务端。
1、这里新增了两个函数,BindService
是用于绑定业务处理函数,可在CalServer.cc中根据需求实现,并在启动服务器前将其绑定。
2、Excute
是提供给新线程的,用于执行业务函数。服务端能够处理的业务不止一种,因此可以使用vector<func_t>
存储。如需要还可以使用unordered_map<std::string, func_t>
,为每个函数附带名称,形成键值对。
#pragma once
#include "Sock.hpp"
#include <vector>
#include <functional>
#include <pthread.h>
namespace ns_tcpserver // 命名空间:
{
class TcpServer; // 声明
struct ThreadData
{
// 构造
ThreadData(int sock, TcpServer *server)
: sock_(sock), server_(server)
{ }
// 析构
~ThreadData()
{ }
int sock_;
TcpServer *server_; // 为了直接传递this指针,方便回调函数调用整个类成员及函数
};
using func_t = std::function<void(int)>;
// PS:这里使用的是多线程版本的服务端
class TcpServer
{
private:
static void *ThreadRoutine(void *args) // 类中成员函数:为了满足线程回调函数的参数需求,设置为静态成员函数
{
// 线程分离:为了服务端不阻塞式等待,需要对线程分离
pthread_detach(pthread_self());
// 解析args参数
ThreadData *pdata = static_cast<ThreadData *>(args);
// 调用任务处理函数
pdata->server_->Excute(pdata->sock_);
// 线程处理完任务后,需要关闭套接字,释放申请出来的空间
logMessage(NORMAL,"新线程执行完毕,关闭套接字:%d",pdata->sock_);
close(pdata->sock_);
delete pdata;
return nullptr;
}
public:
// 构造:初始化服务器
TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0")
{
// 创建套接字
listensock_ = sock_.Socket();
// 绑定套接字
sock_.Bind(listensock_, port, ip);
// 监听套接字
sock_.Listen(listensock_);
}
// 析构
~TcpServer()
{
if (listensock_ >= 0)
close(listensock_); // 关闭套接字
}
// 绑定服务:提供给TcpServer.cc,将服务器的业务处理函数插入到vector数组中
void BindService(func_t func)
{
func_.push_back(func);
}
// 新线程执行业务处理函数
void Excute(int sock)
{
for(auto& f : func_)
{
f(sock);
}
}
// 启动服务器
void Start()
{
// while死循环:服务器启动后,主线程一直在运行,不断接收客户端请求,并派发线程对其进行处理。
while (true)
{
// 连接
uint16_t client_port; // 客服端端口号
std::string client_ip; // 客户端IP
int servicesock = sock_.Accept(listensock_, &client_ip, &client_port); // 输出型参数,调用accept函数获取客户端的端口号和IP
if (servicesock == -1) // 连接失败,重新连接
continue;
logMessage(NORMAL, "连接成功, 可以开始业务处理。servicsock:%d", servicesock);
// 派发新线程进行业务处理
pthread_t tid;
ThreadData *pdata = new ThreadData(servicesock, this);
pthread_create(&tid, nullptr, ThreadRoutine, (void *)pdata);
}
}
private:
int listensock_; // 套接字
Sock sock_; // 将套接字封装:用于调用相关socket函数
std::vector<func_t> func_; // 服务端用于业务处理的函数:这里使用了vector将函数存储,表示服务器提供的处理业务可能有多种
};
}
5.2.5、CalServer.cc服务端
相关代码:
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " port\n"
<< std::endl;
}
ns_protocol::Response calculatorHelper(const ns_protocol::Request &req)
{
// 根据op选项进行计算
ns_protocol::Response resp(0, 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 '/':
if (req.y_ == 0) // 除零错误,需要设置状态码
resp.code_ = 1;
else
resp.result_ = req.x_ / req.y_;
break;
case '%':
if (req.y_ == 0) // 模零错误,需要设置状态码
resp.code_ = 2;
else
resp.result_ = req.x_ % req.y_;
break;
default: // 输入错误,需要设置状态码
resp.code_ = -1;
break;
}
return resp; // 返回结果(响应:结构体对象)
}
void calculator(int sock) // 服务端用于提供业务的函数
{
std::string buffer;//临时缓冲区:用于存储recv读取上来的请求
while (true)
{
// Recv读取请求(客户端经过网络传输发来的数据,属于字节流数据,即客户端在发送前会做序列化处理)
bool ret = ns_protocol::Recv(sock, buffer);
if(!ret) //读取失败:直接结束
break;
// 解析协议,判断读取到的是否是完整的报文。
std::string package = ns_protocol::Decode(buffer);
if(package.empty())//报文不完整
continue;//继续重新读取,直到读取到完整的报文
logMessage(NORMAL,"本次请求: %s",package.c_str());
// 当读取到完整的报文后,将请求反序列化(字节流→结构化,服务端要进行上层业务处理,使用的是结构化的数据,所以要进行反序列化)
ns_protocol::Request req;
req.Deserialized(package);
// 进行业务处理,获取结构化存储的结果(结构化数据)
ns_protocol::Response resp = calculatorHelper(req);
// 将结果序列化(结构化→字节流,服务端要将处理后的结果通过网络传输返回给客户端,需要进行序列化处理)
std::string result_string = resp.Serialize();
// 对序列化后的结果添加长度信息,形成完整的报文
result_string = ns_protocol::Encode(result_string);
logMessage(NORMAL,"本次响应: %s",result_string.c_str());
// 返回客户端响应
ns_protocol::Send(sock, result_string);
}
}
// 启动:./CalServer port
int main(int argc, char *argv[])
{
// 检查命令行参数,获取命令行端口号
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
//signal(SIGPIPE,SIG_IGN);//此句在自定义daemon中写过
//让进程守护进程化
MyDaemon();
// 创建服务器
std::unique_ptr<ns_tcpserver::TcpServer> server(new ns_tcpserver::TcpServer(atoi(argv[1])));
// 绑定业务处理函数:这里是网络计算器
server->BindService(calculator);
// 启动服务器
server->Start();
return 0;
}
5.2.3、Protocol.hpp:定制的协议
以下为协议定制时的相关测试:
//测试:用于测试序列化、反序列化·自定义写法是否正确
// 1.1、测试序列化
ns_protocol::Request req(1234, 5678, '+');
std::string str = req.Serialize();
std::cout << "Request-Serialize: " << str << std::endl;
std::cout<< std::endl;
// 1.2、测试反序列化
ns_protocol::Request req2;
req2.Deserialized(str);
std::cout << "Request-Deserialize: " << std::endl;
std::cout << "x: " << req2.x_ << " y: " << req2.y_ << " op: " << req2.op_ << std::endl;
std::cout<< std::endl;
// 2.1、测试序列化
ns_protocol::Response resp(6012,0);
std::string str2 = resp.Serialize();
std::cout << "Response-Serialize: " << str2 << std::endl;
std::cout<< std::endl;
// 2.2、测试反序列化
ns_protocol::Response resp2;
resp2.Deserialized(str2);
std::cout << "Response-Deserialize: " << std::endl;
std::cout << "code: " << resp2.code_ << " result: " << resp2.result_ << std::endl;
std::cout<< std::endl;
这里我们使用条件编译来完成,先使用自定义方案理解,之后会调整为成熟方案。如下:
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
// 这里的协议是用于服务网络版计算器的
namespace ns_protocol
{
// 控制条件编译:自定义方案 && 现成方案
#define MYSELF 1
// 处理分隔符:这里使用的是空格,定义成宏方便根据需求修改
#define SPACE " "
#define SPACE_LINE strlen(SPACE)
// 加入数据长度,并使用特殊字符(\r\n)区分各段
#define SEP "\r\n"
#define SEP_LINE strlen(SEP)
/// 请求:结构体对象 ///
class Request
{
public:
// 构造
Request(){};
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
// 对请求进行序列化(结构化数据→字节流数据)
std::string Serialize() // 将x_、y_、op_
{
#ifdef MYSELF
// version1: "x_[空格] op_[空格] y_"
std::string str;
str = std::to_string(x_); // 先将对应的运算数转换为字符类型:例如32-->"32"。这里注意与ASCII中值为32的字符区别
str += SPACE; // 中间以我们设置的间隔符分割(为了反序列化时能够提取每部分)
str += op_; // op_本身就是char类型
str += SPACE;
str += std::to_string(y_);
return str;
#else
std::cout << "TODO" << std::endl;
#endif
}
// 对请求进行反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str) // 获取x_、y_、op_
{
#ifdef MYSELF
//----------------------------------
// version1: "x_[空格] op_[空格] y_" 根据分隔符提取有效数放入结构化对象中
// 例如:"1234[空格]+[空格]5678"
// a、找左运算数
std::size_t left_oper = str.find(SPACE);
if (left_oper == std::string::npos) // 没找到
return false;
// b、找右运算数
std::size_t right_oper = str.rfind(SPACE);
if (right_oper == std::string::npos) // 没找到
return false;
// c、提取运算数,赋值给结构化对象成员
x_ = atoi((str.substr(0, left_oper)).c_str()); // string substr (size_t pos = 0, size_t len = npos) const;
y_ = atoi((str.substr(right_oper + SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过
if (left_oper + SPACE_LINE > str.size())
return false;
else
op_ = str[left_oper + SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]
return true;
//----------------------------------
#else
std::cout << "TODO" << std::endl;
#endif
}
public:
int x_; // 左运算数
int y_; // 右运算数
char op_; // 运算符
};
/// 响应:结构体对象 ///
class Response
{
public:
// 构造函数
Response(int result, int code)
: result_(result), code_(code)
{
}
Response() {}
// 析构函数
~Response() {}
// 对响应序列化(结构化数据→字节流数据)
std::string Serialize()
{
#ifdef MYSELF
// version1:"code_ [空格] result_"
// 例如:"0[空格]6912"
std::string str;
str = std::to_string(code_);
str += SPACE;
str += std::to_string(result_);
return str;
#else
std::cout << "TODO" << std::endl;
#endif
}
// 对响应反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str)
{
#ifdef MYSELF
//----------------------------------
// version1:"code_ [空格] result_"
// 例如:"0[空格]6912"
// a、找分隔符
std::size_t pos = str.find(SPACE);
if (pos == std::string::npos) // 没找到
return false;
// b、获取状态码
code_ = atoi((str.substr(0, pos)).c_str());
// c、获取计算结果
result_ = atoi((str.substr(pos + SPACE_LINE)).c_str());
return true;
//----------------------------------
#else
std::cout << "TODO" << std::endl;
#endif
}
public:
int result_; // 计算结果
int code_; // 状态码:用于判断结果是否正常
};
// 从网络中读取
bool Recv(int sock, std::string &out_buffer)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据
if (s > 0) // 读取成功
{
buffer[s] = 0; // 文件尾字符\0
out_buffer += buffer; // 使用+=,输出到out_buffer中,这样在多次读取的情况下能保证数据连续性
}
else if (s == 0)
{
std::cout << "recv:quit." << std::endl;
return false;
}
else
{
std::cout << "recv error." << std::endl;
return false;
}
return true;
}
// 向网络中发送
void Send(int sock, const std::string &str)
{
ssize_t s = send(sock, str.c_str(), str.size(), 0);
if (s < 0)
{
std::cout << "send error" << std::endl;
}
}
// length\r\nx_[空格]op_[空格]y_\r\n
// 实际正文部分:x_[空格]op_[空格]y_,后续需要对正文部分序列化
std::string Decode(std::string &buffer)
{
// 找有没有\r\n
std::size_t pos = buffer.find(SEP);
if (pos == std::string::npos) // 没找到,说明本次读取/接收到的报文不完整,需要继续读取/接收
return "";
// 执行到此,说明确实有\r\n,但不代表数据完整。此时需要提取length值,与实际正文做比较,判断是否读取到一个完整的报文
int length_size = atoi(buffer.substr(0, pos).c_str()); // 获取长度信息
int remain_size = buffer.size() - pos - 2 * SEP_LINE; // 获取剩余长度
if (remain_size >= length_size) // 说明此时缓冲区buffer中存在一个完整的报文,可以提取。
{
// 举例:【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】
// string& erase (size_t pos = 0, size_t len = npos);
buffer.erase(0, pos + SEP_LINE); // 移除缓冲区中的length\r\n,即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】
std::string str = buffer.substr(0, length_size); // 获取length长度的字串:即【XXXXXXXXX】
buffer.erase(0, length_size + SEP_LINE); // 移除缓冲区中正文及尾随的\r\n,即【length\r\nXXXXXX\r\n 】
return str;
}
else
return ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。
}
// 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n
// 例如:123 * 456 --->9\r\n123 * 456\r\n
std::string Encode(std::string &s)
{
// 1、获取正文长度
std::string package = std::to_string(s.size());
// 2、加上SEP分隔符
package += SEP;
// 3、加上正文
package += s;
// 4、加上SEP分隔符
package += SEP;
return package;
}
}
5.2.6、CalClient.cc客户端
相关代码如下:实际也可根据需求,将读取、发送分线程执行。
#include <iostream>
#include "Sock.hpp"
#include "Protocol.hpp"
#include "Log.hpp"
static void Usage(const std::string &process)
{
std::cout << "\nUsage: " << process << " server_ip server_port\n"
<< std::endl;
}
//./CalClient server_ip server_port
int main(int argc, char *argv[])
{
// 检查命令行参数,获取服务端端口号、IP地址
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_port, server_ip) < 0)
{
std::cerr << "client:连接失败" << std::endl;
exit(2);
}
// 与服务端通讯
bool quit = false;
std::string buffer;
while (!quit)
{
std::cout << std::endl;
std::cout << "----------------------" << std::endl;
// 创建一个请求
ns_protocol::Request req;
std::cout << "Please Enter # ";
std::cin >> req.x_ >> req.op_ >> req.y_;
logMessage(DEBUG, "请求结果为, x:%d, y:%d, op:%c", req.x_, req.y_, req.op_);
// 将请求序列化
std::string send_str = req.Serialize();
logMessage(DEBUG, "序列化结果为,%s", send_str.c_str());
// 添加长度信息
send_str = ns_protocol::Encode(send_str);
logMessage(DEBUG, "添加长度信息后, %s", send_str.c_str());
// 发送给服务端
ns_protocol::Send(sockfd, send_str);
// 从服务端读取结果
while (true)
{
// 读取响应结果
bool ret = ns_protocol::Recv(sockfd, buffer);
if (!ret) // 读取失败
{
printf("DEBUG: 获取响应失败,退出。\n");
quit = true; // 退出循环,关闭sockfd
break;
}
// 对响应解析:是否获取到完整报文
std::string package = ns_protocol::Decode(buffer);
if (package.empty()) // 说明本次接收到的报文不完整,继续读取
{
logMessage(DEBUG, "报文不完整,继续读取。");
continue;
}
logMessage(DEBUG, "读取到完整报文, %s\n", package.c_str());
// 到此步骤,获取到了完整报文,可以反序列化
ns_protocol::Response resp;
resp.Deserialized(package);
// 显示结果
std::string err;
switch (resp.code_)
{
case 1:
err = "除零错误";
break;
case 2:
err = "模零错误";
break;
case -1:
err = "非法操作";
break;
default:
std::cout << "result: " << resp.result_ << std::endl;
break;
}
if (!err.empty()) // 显示错误信息
std::cout << "code: " << err << std::endl;
break;
}
}
close(sockfd);
return 0;
}
5.3、网络版本的计算器NetCal编写(version2.0:json版协议)
5.3.1、守护进程
5.3.1.1、问题引入
说明一:什么是前台进程?
在 Linux 中,前台进程是指当前正在运行的进程,它与用户交互并占用终端。当用户在终端中输入命令时,该命令所启动的进程就是前台进程。前台进程会占用终端,直到它执行完毕或者被中断(例如按下 Ctrl+C)。在前台进程运行期间,用户可以通过键盘输入命令或者发送信号来与进程交互。
守护进程全部都是在前台运行的。 任何xshell登陆,只允许一个前台进程和多个后台进程。
说明二:进程有自己的pid、 ppid、组ID
问题说明:退出登录,不同的shell有不同处理。当我们启动服务端,其为前台进程进入同一个会话中,若关闭shell退出登录,该服务端进程可能会随着会话结束而被杀掉,那么此时用户端就无法访问到服务端了。即,不符合服务端一直运行的需求。
为了解决这个问题,引入守护进程。自成一个会话的进程,即守护进程。
5.3.1.2、如何做到:setsid、daemon
1)、setsid
man 2 setsid
:
NAME
setsid - creates a session and sets the process group ID
SYNOPSIS
#include <unistd.h>
pid_t setsid(void);
DESCRIPTION
setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new ses‐
sion, the process group leader of the new process group, and has no controlling terminal. The process group ID and session ID of the
calling process are set to the PID of the calling process. The calling process will be the only process in this new process group and in
this new session.
RETURN VALUE
On success, the (new) session ID of the calling process is returned. On error, (pid_t) -1 is returned, and errno is set to indicate the
error.
ERRORS
EPERM The process group ID of any process equals the PID of the calling process. Thus, in particular, setsid() fails if the calling
process is already a process group leader.
说明:setsid
要成功被调用,必须保证当前进程不是进程组的组长。因此可以通过创建子进程的方式,保证当前进程不是进程组的组长。
2)、daemon
man daemon
:系统中有相关函数,可以为我们做到让一个进程成为守护进程。
NAME
daemon - run in the background
SYNOPSIS
#include <unistd.h>
int daemon(int nochdir, int noclose);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
daemon(): _BSD_SOURCE || (_XOPEN_SOURCE && _XOPEN_SOURCE < 500)
DESCRIPTION
The daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system dae‐
mons.
If nochdir is zero, daemon() changes the calling process's current working directory to the root directory ("/"); otherwise, the current
working directory is left unchanged.
If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made to
these file descriptors.
RETURN VALUE
(This function forks, and if the fork(2) succeeds, the parent calls _exit(2), so that further errors are seen by the child only.) On
success daemon() returns zero. If an error occurs, daemon() returns -1 and sets errno to any of the errors specified for the fork(2) and
setsid(2).
说明:虽然系统提供了相关函数,但一般项目里倾向于自定义编写相关代码。即在Linux中正确的写一个让进程守护进程化的代码。
5.3.1.3、Daemon.hpp
1)、dev/null
ls /dev/null
:dev/null是一个特殊的设备文件,该文件接收到的任何数据都会被丢弃,也无法从该文件中读取到任何数据。因此,其被称为文件黑洞,也被成为位桶(bit bucket)。
2)、相关编写
#pragma once
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void MyDaemon()
{
// 1. 忽略信号,SIGPIPE,SIGCHLD
signal(SIGPIPE, SIG_IGN); // sighandler_t signal(int signum, sighandler_t handler);
signal(SIGCHLD, SIG_IGN);
// 2. 不要让自己成为组长
if (fork() > 0)
exit(0); // 将父进程退出,那么此时运行的就是子进程,其不会成为进程组的组长
// 3. 调用setsid,该函数能够创建会话并设置进程组id
setsid(); // pid_t setsid(void);
// 4. 标准输入,标准输出,标准错误的重定向
int devnull_fd = open("/dev/null", O_RDONLY | O_WRONLY); // int open(const char *pathname, int flags);
if(devnull_fd > 0)//文件打开成功
{
dup2(devnull_fd, 0);
dup2(devnull_fd, 1);
dup2(devnull_fd, 2);
close(devnull_fd);
}
}
注意这里dup2
的使用:
3)、结果演示
5.3.1.4、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
//日志的标准部分:日志等级、时间
char stdBuffer[1024];
time_t timestamp = time(nullptr);
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);
将日志写到显示器上
//printf("%s%s\n", stdBuffer, logBuffer);
// 将日志写到文件中:
FILE *fp = fopen(LOGFILE, "a");
fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
fclose(fp);
}
关于log:可根据自己的需求做调整,这里只是简单的使用演示。(实际我们对CalCilent.cc和CalServer.cc都使用了同一个log.hpp,为了方便日志观察可分别建立两个不同的log,这里只是简单举例log在这种场景编写下的作用)
5.3.2、使用json完成序列化
5.3.2.1、基本使用介绍
1)、安装与编译
JsonCpp是一款开源的C++库,专用于解析和生成JSON格式的数据。相关文档:JsonCpp。
安装指令:sudo yum install jsoncpp-devel
。
2)、jsoncpp中主要的类
Json::Value
:可以表示所有支持的类型,如:int , double ,string , object, array
等.
Json::Reader
:将文件流或字符串创解析到Json::Value
中,主要使用parse函数。Json::Reader的构造函数还允许用户使用特性Features来自定义Json的严格等级。
Json::Writer
:与Json ::Reader
相反,将Json::Value
转换成字符串流等,Writer类是一个纯虚类,并不能直接使用。在此我们使用 Json::Writer
的子类:Json::FastWriter
(将数据写入一行,没有格式),Json::StyledWriter
(按json格式化输出,易于阅读)。
3)、相关使用演示
验证代码:
#include <string>
#include <iostream>
#include <jsoncpp/json/json.h>
int main()
{
// 假设有三个变量:
int a = 19;
double b = 3.14;
char c = '*';
// 使用json存储:
Json::Value root; // 定义一个万能对象:Json::Value,用来表示Json中的任何一种value抽象数据类型
root["a"] = a;
root["b"] = b;
root["c"] = c;
Json::Value sub;
sub["s1"] = "hello";
sub["s2"] = "json";
root["sub"] = sub; // 对象中放入对象(套娃使用
// 序列化:
Json::StyledWriter writer1;
std::string str1 = writer1.write(root);
Json::FastWriter writer2;
std::string str2 = writer2.write(root);
// 两种方式的结果演示:前者会做一些字段分隔处理,方便查看。一般直接使用可用后者。
std::cout << str1 << std::endl;
printf("\n\n");
std::cout << str2 << std::endl;
// 反序列化:
Json::Value buffer1;
Json::Value buffer2;
Json::Reader reader;
reader.parse(str1, buffer1);
reader.parse(str1, buffer2);
std::cout << buffer1["a"].asInt() << std::endl;
std::cout << buffer1["b"].asDouble() << std::endl;
std::cout << buffer1["c"].asInt() << std::endl;
return 0;
}
结果演示:
5.3.3、改动NetCal
基于上述,我们对之前的协议做出修改,使用json来完成序列化和反序列化。
5.3.3.1、主要部分
对请求:使用json版本的序列化和反序列化如下:
class Request
{
public:
// 构造
Request(){};
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
// 对请求进行序列化(结构化数据→字节流数据)
std::string Serialize() //
{
// 序列化
Json::Value root; // 定义一个万能对象,将需要的键值对存入
root["x"] = x_;
root["y"] = y_;
root["op"] = op_;
Json::FastWriter writer; // 进行序列化并将结果返回
return writer.write(root);
}
// 对请求进行反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str)
{
// 反序列化
Json::Value root;
Json::Reader reader;
reader.parse(str, root); // 对str进行反序列化,将结果取出
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
}
public:
int x_; // 左运算数
int y_; // 右运算数
char op_; // 运算符
};
对响应:使用json版的序列化和反序列化如下。
class Response
{
public:
// 构造函数
Response(int result, int code)
: result_(result), code_(code)
{
}
Response() {}
// 析构函数
~Response() {}
// 对响应序列化(结构化数据→字节流数据)
std::string Serialize()
{
// 序列化
Json::Value root;
root["code"] = code_;
root["result"] = result_;
Json::FastWriter writer;
return writer.write(root); // 将序列化后的结果返回
}
// 对响应反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str)
{
// 反序列化
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
code_ = root["code"].asInt();
result_ = root["result"].asInt();
return true;
}
public:
int result_; // 计算结果
int code_; // 状态码:用于判断结果是否正常
};
演示结果如下:
5.3.3.2、整体Protocol.hpp
以下是完整的代码实现:方便观察总览。
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
// 这里的协议是用于服务网络版计算器的
namespace ns_protocol
{
// t控制条件编译:自定义方案 && 现成方案
// #define MYSELF 1
// 处理分隔符:这里使用的是空格,定义成宏方便根据需求修改
#define SPACE " "
#define SPACE_LINE strlen(SPACE)
// 加入数据长度,并使用特殊字符(\r\n)区分各段
#define SEP "\r\n"
#define SEP_LINE strlen(SEP)
/// 请求:结构体对象 ///
class Request
{
public:
// 构造
Request(){};
Request(int x, int y, char op)
: x_(x), y_(y), op_(op)
{
}
// 对请求进行序列化(结构化数据→字节流数据)
std::string Serialize() // 将x_、y_、op_
{
#ifdef MYSELF
// version1: "x_[空格] op_[空格] y_"
std::string str;
str = std::to_string(x_); // 先将对应的运算数转换为字符类型:例如32-->"32"。这里注意与ASCII中值为32的字符区别
str += SPACE; // 中间以我们设置的间隔符分割(为了反序列化时能够提取每部分)
str += op_; // op_本身就是char类型
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
}
// 对请求进行反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str) // 获取x_、y_、op_
{
#ifdef MYSELF
//----------------------------------
// version1: "x_[空格] op_[空格] y_" 根据分隔符提取有效数放入结构化对象中
// 例如:"1234[空格]+[空格]5678"
// a、找左运算数
std::size_t left_oper = str.find(SPACE);
if (left_oper == std::string::npos) // 没找到
return false;
// b、找右运算数
std::size_t right_oper = str.rfind(SPACE);
if (right_oper == std::string::npos) // 没找到
return false;
// c、提取运算数,赋值给结构化对象成员
x_ = atoi((str.substr(0, left_oper)).c_str()); // string substr (size_t pos = 0, size_t len = npos) const;
y_ = atoi((str.substr(right_oper + SPACE_LINE).c_str())); // 注意这里右运算符需要将[空格]跳过
if (left_oper + SPACE_LINE > str.size())
return false;
else
op_ = str[left_oper + SPACE_LINE]; // 提取运算符时也要注意跳过分隔符[空格]
return true;
//----------------------------------
#else
// 反序列化
Json::Value root;
Json::Reader reader;
reader.parse(str, root); // 对str进行反序列化,将结果取出
x_ = root["x"].asInt();
y_ = root["y"].asInt();
op_ = root["op"].asInt();
return true;
#endif
}
public:
int x_; // 左运算数
int y_; // 右运算数
char op_; // 运算符
};
/// 响应:结构体对象 ///
class Response
{
public:
// 构造函数
Response(int result, int code)
: result_(result), code_(code)
{
}
Response() {}
// 析构函数
~Response() {}
// 对响应序列化(结构化数据→字节流数据)
std::string Serialize()
{
#ifdef MYSELF
// version1:"code_ [空格] result_"
// 例如:"0[空格]6912"
std::string str;
str = std::to_string(code_);
str += SPACE;
str += std::to_string(result_);
return str;
#else
// 序列化
Json::Value root;
root["code"] = code_;
root["result"] = result_;
Json::FastWriter writer;
return writer.write(root); // 将序列化后的结果返回
#endif
}
// 对响应反序列化(字节流数据→结构化数据)
bool Deserialized(const std::string &str)
{
#ifdef MYSELF
//----------------------------------
// version1:"code_ [空格] result_"
// 例如:"0[空格]6912"
// a、找分隔符
std::size_t pos = str.find(SPACE);
if (pos == std::string::npos) // 没找到
return false;
// b、获取状态码
code_ = atoi((str.substr(0, pos)).c_str());
// c、获取计算结果
result_ = atoi((str.substr(pos + SPACE_LINE)).c_str());
return true;
//----------------------------------
#else
// 反序列化
Json::Value root;
Json::Reader reader;
reader.parse(str, root);
code_ = root["code"].asInt();
result_ = root["result"].asInt();
return true;
#endif
}
public:
int result_; // 计算结果
int code_; // 状态码:用于判断结果是否正常
};
// 从网络中读取
bool Recv(int sock, std::string &out_buffer)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 阻塞式从网络中读取字节流数据
if (s > 0) // 读取成功
{
buffer[s] = 0; // 文件尾字符\0
out_buffer += buffer; // 使用+=,输出到out_buffer中,这样在多次读取的情况下能保证数据连续性
}
else if (s == 0)
{
std::cout << "recv:quit." << std::endl;
return false;
}
else
{
std::cout << "recv error." << std::endl;
return false;
}
return true;
}
// 向网络中发送
void Send(int sock, const std::string &str)
{
ssize_t s = send(sock, str.c_str(), str.size(), 0);
if (s < 0)
{
std::cout << "send error" << std::endl;
}
}
// length\r\nx_[空格]op_[空格]y_\r\n
// 实际正文部分:x_[空格]op_[空格]y_,后续需要对正文部分序列化
std::string Decode(std::string &buffer)
{
// 找有没有\r\n
std::size_t pos = buffer.find(SEP);
if (pos == std::string::npos) // 没找到,说明本次读取/接收到的报文不完整,需要继续读取/接收
return "";
// 执行到此,说明确实有\r\n,但不代表数据完整。此时需要提取length值,与实际正文做比较,判断是否读取到一个完整的报文
int length_size = atoi(buffer.substr(0, pos).c_str()); // 获取长度信息
int remain_size = buffer.size() - pos - 2 * SEP_LINE; // 获取剩余长度
if (remain_size >= length_size) // 说明此时缓冲区buffer中存在一个完整的报文,可以提取。
{
// 举例:【length\r\nXXXXXXXXX\r\nlength\r\nXXXXXX\r\n】
// string& erase (size_t pos = 0, size_t len = npos);
buffer.erase(0, pos + SEP_LINE); // 移除缓冲区中的length\r\n,即【XXXXXXXXX\r\nlength\r\nXXXXXX\r\n 】
std::string str = buffer.substr(0, length_size); // 获取length长度的字串:即【XXXXXXXXX】
buffer.erase(0, length_size + SEP_LINE); // 移除缓冲区中正文及尾随的\r\n,即【length\r\nXXXXXX\r\n 】
return str;
}
else
return ""; // 说明本次读写缓冲区中报文不完整,需要继续读写。
}
// 返回一个带有长度信息的完整报头:实际正文部分为 x_[空格]op_[空格]y_,需要为其添加长度信息,变为length\r\nx_[空格]op_[空格]y_\r\n
// 例如:123 * 456 --->9\r\n123 * 456\r\n
std::string Encode(std::string &s)
{
// 1、获取正文长度
std::string package = std::to_string(s.size());
// 2、加上SEP分隔符
package += SEP;
// 3、加上正文
package += s;
// 4、加上SEP分隔符
package += SEP;
return package;
}
}