Linux套接字编程
- 预备知识
- IP地址和MAC地址
- 套接字结构
- 网络字节序
- UDP套接字编程
- 服务端代码
- 客服端代码
- TCP 套接字
- 守护进程
- 计算器
- 模块1 日志头文件
- 序列化和反序列化
预备知识
IP地址和MAC地址
MAC地址用来在局域网中标识唯一主机
Ip地址用于在广域网中标识唯一主机
(1)IP地址:
IP协议有两个版本 , IPv4 和IPv6. 我们凡是提到 IP 协议 , 没有特殊说明的 , 默认都是指 IPv4 IP 地址是在 IP
协议中 , 用来标识网络中不同主机的地址 ; 对于 IPv4 来说 , IP 地址是一个4字节, 32位的整数; 我们通常也使用
“点分十进制” 的字符串表示IP地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;
(2)MAC地址:
MAC地址是物理网卡硬件地址 :用于识别相邻的两个物理硬件设备,它的大小为:6字节 ①长度为48位, 及6个字节 .
一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
②在出厂时就会设定,不能修改,MAC地址通常是唯一的,它的大小是6字节,用于识别相邻设备,在链路层完成相邻设备之间的数据传输。
(虚拟机中的mac地址不是真实的mac地址, 可能会冲突; 也有些网卡支持用户配置mac地址). ③MAC地址与网络无关
④一台计算机可以有多个MAC地址:一台计算机可以绑定多个网卡,进而可以拥有多个MAC地址。
举例:IP数据报的收发方进行跨网投递时,发送方需利用ARP协议获取
发送方本网段路由器对应端口的MAC地址,因为当需要跨网络进行传递的时候,也就是意味着需要找到该数据包的下一跳的MAC地址,所以认为从发送方出来,首先先到到达本网段的路由器,所以获取本网段的路由器的MAC地址
套接字结构
套接字由IP地址和端口号组成,其中端口号标识唯一进程。
主机间在通信的本质是:在各自的主机上的两个进程在互相交互数据!
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方
IP :确保主机的唯一性
端口号(port):确保该主机上某一个进程的唯一性(则一个进程只能占用一个端口号)
IP:PORT = 标识互联网中唯一的一个进程!——>这两个合起来叫 socket(套接字)(翻译是插座)
网络通信的本质:就是进程间通信! ! !
端口号(port)是传输层协议的内容:
端口号是一个 2字节16位的整数 ;类型是uint16_t
,不过传uint32_t
也可以,最终会截断成uint16_t
。 因为一般1-1023端口属于系统保留端口,这些端口已经分配给一些应用了,所以我们只能使用1024及以上的端口。
端口号是进程的门,如果一个进程有多个门,那我可以接受多路信息。
如果一个端口可以去多个进程,那么就会出问题,端口就变成十字路口了。
注1: 一个端口号只能被一个进程占用(一个进程可以有多个端口号,但一个端口号不可以对应多个进程,只要保证从端口号到进程的数据链路是唯一的 )
注2: Socket客户端的端口是不固定的, Socket服务端的端口是固定的。
解释:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定;
服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了。
思考一下 服务端为什么是固定的? 客服端为什么是动态的?
因为高铁站修好了就不动了,而你可以一直搬家。
理解 “端口号” 和 “进程ID”(端口号的意义)
端口号和进程ID的区别,端口号是进程在网络的户口,进程ID是在操作系统的户口。这样更加方便分层管理。
网络字节序
网络字节序在网络中是大端。可能大家已经忘了大小端,下面我们在介绍一下大小端。
大端存储在网络中是规定的。
网络和主机字节序的转换函数:
为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 , 可以调用以下库函数做网络字节序和主机字节序的转换
uint32_t htonl (uint32_ t hostlong);
——htonl(host to net 主机转网络)
下面是这四个函数的详细讲解:
htonl (host to network long)
功能:将32位无符号整数从主机字节序转换为网络字节序(大端字节序)。
参数:一个32位无符号整数(通常表示IPv4地址或端口号)。
返回值:转换后的32位无符号整数(网络字节序)。
htons (host to network short)
功能:将16位无符号整数从主机字节序转换为网络字节序(大端字节序)。
参数:一个16位无符号整数(通常表示端口号)。
返回值:转换后的16位无符号整数(网络字节序)。
ntohl (network to host long)
功能:将32位无符号整数从网络字节序(大端字节序)转换为主机字节序。
参数:一个32位无符号整数(网络字节序)。
返回值:转换后的32位无符号整数(主机字节序)。
ntohs (network to host short)
功能:将16位无符号整数从网络字节序(大端字节序)转换为主机字节序。
参数:一个16位无符号整数(网络字节序)。
返回值:转换后的16位无符号整数(主机字节序)。
①这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示以32位的长整数为单位从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
②如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
③如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
所以主机必须可具备大小端转换,并且保证发到网络中的数据是大端数据。
UDP套接字编程
接下来我们介绍套接字编程,首先介绍流程:
socket常见API
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
socket的数据类型:sockaddr结构(套接字的地址结构类型定义)
前16位是标志,是数据结构的名字。通用数据类型sockaddr,sockaddr_in和sockaddr_un是sockaddr是他们的统一形式,为了方便传参。
struct sockaddr_ in
——网络套接字,用于网络通信;
structsockaddr_un
——域间套接字,用于UNIX本地通信。
下面我们详细介绍struct sockaddr_in
struct sockaddr_in {
short sin_family; // 地址族,通常为 AF_INET
unsigned short sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IPv4 地址
char sin_zero[8]; // 填充至 struct sockaddr 的大小,通常不用
};
字符串风格的IP地址转为4字节地址 inet_addr
4字节转字符串 inet_ntoa
in_addr_t inet_addr(const char *cp);
网络服务 recvfrom
与 sendto
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
从特定套接字 sockfd中读取数据到缓冲区buf中,buf大小为len,flags设为0——阻塞式读取
src_addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入src_addr中。(src_addr的类型是套接字类型指针struct
sockaddr*,传入的网络套接字类型struct sockaddr_in需要强转成此类型指针 struct sockaddr。)addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)
返回值:返回读到的字节数,错误就返回-1错误码被设置
该接口为阻塞方式接口。接收端收到消息后,就已经知道发送方的套接字,并不需要再次接收。
socket编程三部曲: 1创 2绑 3发
部分细节解释+代码(udp套接字)
易错:1. port_ 端口号是一个 2字节16位的整数,主机转网络要用htons,不能用htonl.16位是短整型
server.sin_port=htons(server_port);
htonl 是转换四字节的,如果你传入一个两字节的数据,它就会自动进行补位,补位前面部分都是零,那这时候经过htonl置换之后,前16位就变成零了,相当于你的程序跑去绑定零端口去了,就会绑定失败。
(1)INADDR_ANY
#define INADDR_ANY ((in_addr_t) 0x00000000)
local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
①INADDR_ANY (这个宏的值就是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法(解释:一般服务器只有一个IP,会自动bind这个IP;如果服务器有多个IP,会自动bind这个服务器的所有的IP——因为如果有两个IP:IP1和IP2,只bind一个IP1,那么只有传给IP1的报文会交给程序,IP2就不会提交报文)
云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 所以这里只能使用INADDR_ANY,如果你是虚拟机就可以bind自己虚拟机的IP,用ifconfig查看IP。
注意:这里inet_addr(ip_.c_str())
当ip_是"0"时 等价于INADDR_ANY,INADDR_ANY 这个宏的值就是0,0是字符串风格还是网络风格无所谓,并且inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序,即 inet_addr(0)=inet_addr(INADDR_ANY)=htonl(INADDR_ANY)
作用是一样的
UDPsocke的创建
1.创 创建UDPsocket文件描述符:
sockfd_=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd_<0)
{
log(Fatal,"socket creat error, sockfd : %d",sockfd_);
exit(SOCKET_ERR);
}
2 绑 绑定之前需要设定好需要被绑定的信息:
接下来我们详细介绍一下sockaddr_in结构体的内部:
服务端代码
这就是他的结构体类型,我开始依次绑定三个信息地址族,端口号,IPV4地址信息。
local.sin_family=AF_INET;//表示我使用IPV4协议族
local.sin_port=htons(port_);//字节序的转换,不管你是什么字节序,在发送时都必须转换为网络字节序
local.sin_addr.s_addr=inet_addr(ip_.c_str());
//字符串风格ip转转ip
inet_addr有两个功能一个是字符串转ip 。
一个是主机序转网络序。
3发 发送消息,其实就是像文件中写入。
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
inet_ntoa(client.sin_addr)
32位IPV4转换为点分十进制IP
ntohs(client.sin_port)
网络序转
刚刚我们写完了服务器,现在我们来描述一下客服端。
客服端可以主动给服务器发消息,所以我们需要知道,客服端到底给谁发以及发什么。
所以我可以用我们之前学的命令行参数,直接给main函数传参。
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
我们可以直接将通信的ip 以及端口号 传进去。组成基本套接字。
客服端代码
客服端 也是1创 2绑 3发
接下来我们继续创建客服端:
1 创 创建端口
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
2 绑 绑定之前先写入IP信息
struct sockaddr_in client;
client.sin_family=AF_INET;
client.sin_port=htons(serverport);//端口号 转换
client.sin_addr.s_addr=inet_addr(serverip.c_str()); // 字符串风格IP转网络字节序整数
socklen_t len =sizeof(client);
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
注意: 服务器需要固定的端口号和ip地址 客服端不需要!旅客可以变,旅馆不能变。
小贴士: 可以通过
netstat -naup
查看端口号
2. 云服务禁止绑定公网IP!
3. 0-1023系统内定了,不能使用。都被固定应用层用了。
- 注意 客服端的端口号并不需要固定,服务器的端口号是固定的,由你选择的协议而定
我们知道,服务器 IP和端口号固定。但是用户端 ip和端口都不需要固定 。我们测试这个服务器的端口号。
我们通过测试发现,系统每次绑定的并不是同一个。
接下来我们附上udp代码:
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
using namespace std;
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;
extern Log lg;
enum{
SOCKET_ERR=1,
BIND_ERR
};
uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;
class UdpServer{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
:sockfd_(0), port_(port), ip_(ip),isrunning_(false)
{}
void Init()
{
// 1. 创建udp socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
if(sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
// 2. bind socket
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
// local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void Run() // 对代码进行分层
{
isrunning_ = true;
char inbuffer[size];
while(isrunning_)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr*)&client, &len);
sockaddr_in clientTmp=(sockaddr_in)client;
string IP(inet_ntoa(clientTmp.sin_addr));
uint16_t p=ntohs(client.sin_port);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n] = 0;
string buffer=inbuffer;
std::string info ;
info="client@:"+buffer;
cout<<"test:"<<p<<endl;
cout<<"test:"<<info<<" client ip:"<<IP<<" client port:"<<(uint16_t)p<<endl;
sendto(sockfd_, info .c_str(), info .size(), 0, (const sockaddr*)&client, len);
}
}
~UdpServer()
{
if(sockfd_>0)
close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
bool isrunning_;
};
#pragma once
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <functional>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <time.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <mutex>
#include<netinet/in.h>
#include<string.h>
#include"Log.hpp"
#include<semaphore.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
int main(int argc, char *argv[])//启动客服端必须告知你要访问的ip 端口等信息
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]); //端口号字符串
int sockfd =socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
cout<<"socker error"<<endl;
return 1;
}
struct sockaddr_in client;
client.sin_family=AF_INET;
client.sin_port=htons(serverport);//端口号 转换
client.sin_addr.s_addr=inet_addr(serverip.c_str()); // 字符串风格IP转网络字节序整数
socklen_t len =sizeof(client);
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
string message;
char buffer[1024];
while (true)
{
cout << "Please Enter@ ";
getline(cin, message);
//std::cout << message << std::endl;
// 1. 数据 2. 给谁发
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&client, len);
struct sockaddr_in temp;
socklen_t lent = sizeof(temp);
ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &lent);
if(s > 0)
{
buffer[s] = '\0';
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
TCP 套接字
再次介绍地址族和套接字族:
地址族(Address Family)
地址族是指网络中主机的地址类型。在网络编程中,地址族决定了套接字所使用的网络通信协议和地址格式。
AF_INET:这是一个常用的地址族,表示使用IPv4协议。IPv4是互联网上使用最广泛的协议之一,它使用32位的IP地址。当你创建一个使用IPv4地址的套接字时,你会使用AF_INET作为地址族参数。
AF_INET6:这个地址族用于IPv6协议。IPv6是IPv4的下一代协议,它使用128位的IP地址,提供了更多的地址空间和其他一些改进。
套接字类型(Socket Type)
套接字类型决定了套接字的工作方式和特性。不同的套接字类型适用于不同的应用场景。
SOCK_STREAM:这是一个面向连接的套接字类型,通常用于TCP协议。它提供了可靠、有序的、基于字节流的通信。
SOCK_DGRAM:这是一个无连接的套接字类型,通常用于UDP协议。它提供了不可靠的、基于数据报的通信。
介绍完之后,我们依然在初始化的时候对,TCP服务器开始三步走
//1 创
sockfd_=socket(AF_INET, SOCK_STREAM,0);
if(sockfd_<0)
{
log(Fatal,"socket is worring");
exit(-1);
}
log(Info,"sockfd is %d",sockfd_);
//2 写
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(port_);
server.sin_addr.s_addr=inet_addr(ip_.c_str());
//3 绑
if(bind(sockfd_, (const struct sockaddr *)&server, sizeof(server))<0)
{
log(Fatal,"bind errno");
exit(-1);
}
if(listen(sockfd_,backlog)<0)
{
log(Fatal,"listen errno");
exit(-1);
}
TCP多了一步 听。TCP是面向链接的,建立链接了才能发。
void Run()
{
log(Info,"Tcp is running");
while(1)
{
//创建新链接 sockfd
struct sockaddr_in client;
socklen_t len =sizeof(client);
int sockfd=accept(sockfd_,(sockaddr*)&client,&len);
// 根据新链接通信
if(sockfd<0)
{
log(Warning,"accecpt is waitting");
continue;
}
char* ipstr=new char[32];
uint16_t clientport =ntohs(client.sin_port);
inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);
log(Info,"get a new link..., sockfd:%d, client ip: %s\n ",sockfd,ipstr);
}
}
inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);
在本接口中输入型参数。用来获取转换后的ip。该函数没有线程不安全的问题。
查看TCP网络服务器情况和端口使用情况 netstat -nltp
注意:TCP是流式套接字,我们用wirte写入,read读取。
TCP服务端的创建
1创 2写 3绑
class Tcpserver
{
public:
Tcpserver(const uint16_t port=DEAFLITPORT,const string ip="0.0.0.0")
:port_(port),sockfd_(0),ip_(ip)
{
}
void Init()//Tcp初始化
{
//1 创
sockfd_=socket(AF_INET, SOCK_STREAM,0);
if(sockfd_<0)
{
log(Fatal,"socket is worring");
exit(-1);
}
log(Info,"sockfd is %d",sockfd_);
//2 写
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(port_);
server.sin_addr.s_addr=inet_addr(ip_.c_str());
//3 绑
if(bind(sockfd_, (const struct sockaddr *)&server, sizeof(server))<0)
{
log(Fatal,"bind errno");
exit(-1);
}
if(listen(sockfd_,backlog)<0)
{
log(Fatal,"listen errno");
exit(-1);
}
}
void Run()
{
int sockfd;
log(Info,"Tcp is running");
while(1)
{
//创建新链接 sockfd
struct sockaddr_in client;
socklen_t len =sizeof(client);
sockfd=accept(sockfd_,(sockaddr*)&client,&len);
// 根据新链接通信
if(sockfd<0)
{
log(Warning,"accecpt is waitting");
continue;
}
char* ipstr=new char[32];
uint16_t clientport =ntohs(client.sin_port);
inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);
log(Info,"get a new link..., sockfd:%d, client ip: %s\n ",sockfd,ipstr);
break;
}
string infomassage ;
while(1)
{
infomassage.clear();
char *str=new char[1024];
ssize_t s = read(sockfd, str, strlen(str));
infomassage=str;
if(s>0)
{
string tmp("revice:");
infomassage+=tmp;
size_t num=write(sockfd,infomassage.c_str(),infomassage.size());
std::cout << "Server Echo>>> " <<infomassage << std::endl;
}
else
{
break;
}
}
}
~Tcpserver()
{
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 任意地址bind 0
uint16_t port_; // 表明服务器进程的端口号
};
后面我们会用一个计算机串联一切知识点。
守护进程
有一种进程他会残留信息造成进程信息,称之为僵尸进程。有一种暖心的进程叫做守护进程。
这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响,并且退出后不会成为僵尸进程。
进程关系图
前后台进程
用户登录时会建立一个会话,会话内部会构建一个前台进程组 和 0个或者多个后台进程组,linux下客户端登录时 会给我们加载bash,bash就是前台进程组。(windows下的注销就是新建立一个会话)前台进程组必须有一个,而且任何时刻只能有一个。
2. 守护进程的创建
守护进程的创建分两步:
- fork创建子进程。
- 父进程退出,并且调用setsid()函数接口。
必做:fork+setsid()——让自己不成为进程组组长+设置自己是一个独立的会话
那我如何不成为组长以便调用setsid呢?——bash中新启动第一个进程一定成为组长,所以你可以成为进程组内的第二个进程。即:常规做法:fork()子进程,子进程就不再是组长进程了,它就可以成功调用setsid(); ————
if(fork() > 0) exit(0) ;
setsid() ;
改守护进程的工作目录,如何更改进程的工作目录?—chdir()
(3)一般守护进程都要做的(必做):
- (不常用做法一)因为守护进程与标准输入,标准输出,标准错误已经没关系了,所以close(0, 1,2) 守护进程获取输入或写入都是和网络有关,不会从键盘获取,不会往显示器输出。(很少有人这样做,因为兼容性不好,会导致代码中的打印代码报错)
守护进程作为后台进程,就不能把自己的输出排放到显示器上。所以,他应该把产生的一切信息写入垃圾箱。
类似于所有Linux下的一个”垃圾桶(文件黑洞)“,凡是从 /dev/null
里面读/写一概被丢弃
推荐做法:打开/dev/null, 并且对 0,1,2 进行重定向!
总结:1.忽略SIGPIPE
2.更改进程的工作目录
3.让自己不要成为进程组组长(必做)
4.设置自己是一个独立的会话(必做)
5.重定向0,1,2(必做)
计算器
该计算器具有
- 多线程(生成消费者模型) 2. 守护进程 3. 打印日志 4. 计算数据四个功能。并且该计算机的协议自己手动实现。使用TCP/IP协议。
模块1 日志头文件
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <fstream> // 如果您打算使用 C++ 的文件流
#include <cstring> // 如果您使用 C 风格的字符串操作
#include <errno.h>
#include <cerrno>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
using namespace std;
#define FILENAME "log.txt"
#define SIZE 1024
class Log
{
public:
Log(int printMethod=Screen,std::string path="./log/")//构造函数
:_path(path),_printMethod(printMethod)
{
}
//错误等级 输入错误等级返回字符串
std::string LevelTostring(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
// 根据 printMethod 的值选择不同的日志输出方式
switch (_printMethod)
{
// 如果 printMethod 为 Screen,则将日志内容输出到屏幕
case Screen:
std::cout << logtxt << std::endl;
break;
// 如果 printMethod 为 Onefile,则将日志内容输出到指定的日志文件中
case Onefile:
printOneFile(FILENAME,logtxt);
break;
// 如果 printMethod 为 Classfile,则根据日志级别和日志内容输出到分类的日志文件中
case Classfile:
printClassFile(level, logtxt);
break;
// 如果 printMethod 的值不是上述任何一种,则不执行任何操作
default:
break;
}
}
void printOneFile(const std::string &Filepanth,const std::string &Filetxt )
{
//文件在系统中的路径
std::string _Filepanth=_path+Filepanth;
//可读可写方式创建log.txt
int fd = open(_Filepanth.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if(fd<0)
{
return ;
}
write(fd,Filetxt.c_str(),Filetxt.size());
}
void printClassFile(int level,const std::string Filetxt)
{
std::string _Filepanth =_path+ LevelTostring(level)+"/"+FILENAME;
// cout<<_Filepanth<<endl;
int fd = open(_Filepanth.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
//cout<<fd<<endl;
if(fd<0)
{
perror("Error opening file"); // 这将打印一个描述错误的消息
return ;
}
std:: string tmp("\n");
std::string Filetxttmp=Filetxt+tmp;
write(fd,Filetxttmp.c_str(),Filetxttmp.size());
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", LevelTostring(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
int len=strlen(rightbuffer);
rightbuffer[len]='\n';
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
~Log()
{
}
private:
std::string _path;//文件所在路径
int _printMethod;//打印方法
};
序列化和反序列化
序列化和反序列化 序列化和反序列化是保证数据的完整性的工作。
直接发送同样的结构体对象,是不可取的,虽然在某些情况下,它确实行,但是我们需要进行序列化和反序列化
定义:定义结构体来表示我们需要交互的信息 ; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 " 序列化 " 和 " 反序列化 "
序列化,反序列化的操作是在用户发送和网络发送中间加了一层软件层,
———————————————————————
序列化和反序列化的示意图: