Linux网络——套接字编程
目录
1. 网络通信基本脉络
2. 端口号
① 什么是套接字编程?
② 端口号 port && 进程 PID
3. 网络字节序
4. 套接字编程
① UDP版
② TCP版
5. 改进方案与拓展
①多进程版
②多线程版
③线程池版
④守护进程化
1. 简单的重联
2. session && 前台 && 后台
3. Linux 系统进程间关系
4. 进程的守护进程化
1. 网络通信基本脉络
基本脉络图如上,其中数据在不同层的叫法不一样,比如在传输层时称为数据段,而在网络层时称为数据报。我们可以在 Linux 中使用 ifconfig 查看网络的配置,如图
其中,inet 表示的是 IPv4,inet6 表示的是 IPv6,ehther(以太)表示的是 mac 地址。
2. 端口号
在进行网络通信时,是不是两台机器直接在进行通信呢?——当然不是
1. 网络协议中的下三层,主要解决的是数据能安全可靠的传输到另一台主机上
2. 这之后,用户使用应用软件完成数据的发送和接收
而一个应用软件会被操作系统解释成进程,也就是说网络通信的本质就是进程间通信!那么一个数据被 A 主机传输到 B 主机上后,怎么交给应用层呢?——端口号!端口号对于主机 A 和主机 B 都能唯一标识该主机上的一个网络应用程序的进程。
① 什么是套接字编程?
在公网上, ip 地址能标识唯一一台主机,端口号 port 能标识该主机上的唯一一个进程,因此
我们可以使用 ip:port 来表示全网唯一的一个进程
而我们将 client_ip:client_port 与 server_ip:server_port 间的通信称为套接字编程!
② 端口号 port && 进程 PID
既然 PID 已经能够标识一台主机上的唯一性了,那为什么我们还需要端口号这个概念呢?
1. 并非所有的进程都需要进行网络通信,但是所有的进程都有 PID
2. 使系统和网络的功能解耦
我们举个例子
假如现在你正在手机上使用抖音,想浏览一个视频,你的手机(客户端)就会将“想浏览一个视频”这个行为发送到服务端, 在发送的时候其会在自己的数据中附带上自己的端口号与服务端的端口号(每一个服务端的端口号必须是众所周知,精心设计,被用户端熟知的),而服务端在接收到这个消息后会按照 IP + port 的形式返回应答!
根据我们对端口号的了解
一个进程是可以绑定多个端口号的!但是一个端口号不能被多个进程绑定!
3. 网络字节序
我们知道在计算机中是存在大端与小端的,我们将低地址放在低位称为小端,而在 TCP/IP 协议中规定了采用大端字节序,我们可以使用 htonl 接口来转换(h: host 主机,n: net 网络,l: long 4字节),与其类似的还有 ntohl, htons(s: short), ntohs等。
4. 套接字编程
我们先来看看 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 API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及 UNIX Domain Socket。但是,各种网络协议的地址格式并不是相同的。套接字编程也分为几种
1. 域间套接字编程 -> 同一机器内
2. 原始套接字编程 -> 网络工具
3. 网络套接字编程 -> 用户间的网络通信
我们想将网络接口统一抽象化,那就表示着参数类型必须是统一的,比如对于这个 struct sockaddr* address 来说,其设计如下
我们在设计接口时,将其设计为基类 struct sockaddr* address ,在使用时我们根据需要传入其对应的子类,这实际上使用到了面向对象中的多态思想!
① UDP版
接下来我们就简单完成一个 UDP 版本的套接字,其模板如下
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
extern Log lg;
enum
{
SOCKET_ERR=1;
};
class UdpServer
{
public:
UdpServer()
{}
void Init()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d" , sockfd_);
exit(SOCKET_ERR);
}
}
void Run()
{}
~UdpServer()
{}
private:
int sockfd_; // 网路文件描述符
};
其调用逻辑如下
#include "Udpserver.hpp"
#include <memory>
int main()
{
std::unique_ptr<UdpServer> svr(new UdpServer());
svr->Init(/**/);
svr->Run();
return 0;
}
我们来看看 socket 这个接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
int 表示返回一个文件描述符,int domain 表示要在什么域进行传输(这里我们选择的是 AF_INET-> IPv4),int type 表示创建什么类型的套接字(这里我们选择的是 SOCK_DGRAM ,即 Supports datagrams 面向数据报,也就是 udp 使用的类型),而 int protocol 表示使用什么协议类型。
接下来我们来完成这个套接字
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>
#include "Log.hpp"
extern Log lg;
typedef std::function<std::string(const std::string&)> func_t;
// 枚举错误信息
enum
{
SOCKET_ERR=1,
BIND_ERR
};
// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
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)
:port_(port), ip_(ip), isrunning_(false)
{}
void Init()
{
// 1. 创建 udp socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
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
// 初始化 local
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清零 local
local.sin_family = AF_INET; // 设置为 IPv4
local.sin_port = htons(port_); // 需要保证这里的端口号是网络字节序列,因为该端口号是要给对方发送的
// 1. string -> uint32_t
// 2. uint32_t必须是网络序列的
local.sin_addr.s_addr = inet_addr(ip_.c_str());
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 socket success!");
}
void Run(func_t func)
{
// 设置服务器运行状态为 运行中
isrunning_ = true;
// 设置缓冲区
char inbuffer[size];
while(isrunning_)
{
// 输出型参数 client
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 从 inbuffer 中读取数据
// sizeof(inbuffer)-1 意思是将 inbufffer 视为字符串
// n 表示实际接收到了多少个字符
// 同时获取 client 信息,便于之后的发送
ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
if(n < 0)
{
lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
inbuffer[n]= 0;
//充当了一次数据的处理
std::string info = inbuffer;
std::string echo_string = func(info);
// server 向 client 发送应答信息
sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
}
}
~UdpServer()
{
if (sockfd_ > 0) close(sockfd_);
}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 服务器ip
uint16_t port_; // 服务器进程的端口号
bool isrunning_; // 服务器运行状态
};
接下来完成 udpserver 的编写
#include "UdpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
std::string Handler(const std::string &str)
{
std::string res = "Server get a message: ";
res += str;
std::cout << res << std::endl;
return res;
}
std::string ExcuteCommand(const std::string &cmd)
{
FILE *fp = popen(cmd.c_str(), "r");
if(nullptr == fp)
{
perror("popen");
return "error";
}
std::string result;
char buffer[4096];
while(true)
{
char *ok = fgets(buffer, sizeof(buffer), fp);
if(ok == nullptr) break;
result += buffer;
}
pclose(fp);
return result;
}
// ./udpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));
svr->Init();
svr->Run(ExcuteCommand);
return 0;
}
接下来我们编写一个 udpclient 来与其进行通信
#include "UdpServer.hpp"
#include <memory>
#include <iostream>
using namespace std;
extern Log lg;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
// ./udpclient serverip serverport
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]);
// 确认发送服务端
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
socklen_t len = sizeof(server);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
lg(Info, "socket create success, sockfd: %d", sockfd);
// client要bind吗?——要,只不过不需要用户显示的bind!一般由 OS 自由随机选择!
//一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
//其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
//系统什么时候bind呢?——首次发送数据的时候
string message;
char buffer[1024];
while(true)
{
// 1. 获取数据
cout << "Please Enter: ";
getline(cin, message);
// 2. 给服务端发送信息
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);
struct sockaddr_in tmp;
socklen_t t_len = sizeof(tmp);
ssize_t n = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &t_len);
if (n > 0)
{
buffer[n] = 0;
cout << buffer << endl;
}
}
close(sockfd);
return 0;
}
运行效果如下
② TCP版
我们先来看看 TCP 方案的模板
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>
#include "Log.hpp"
extern Log lg;
// 枚举错误信息
enum
{
SOCKET_ERR=1,
BIND_ERR
};
// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";
class TcpServer
{
public:
TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
:port_(port), ip_(ip)
{}
void InitServer()
{
// 创建套接字为 IPv4, 字节流(TCP)
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
lg(Info, "create socket success, sockfd: %d" , sockfd_);
// 初始化 local
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // 设置为 IPv4
local.sin_port = htons(port_);// 保证端口号是网络字节序列
// 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址
// 返回的字符串存储在静态区(多次调用只保存最后一次调用结果)
inet_aton(ip_.c_str(), &(local.sin_addr));
// 绑定套接字
if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind socket success!" , sockfd_);
}
void Start(){}
~TcpServer(){}
private:
int sockfd_; // 网路文件描述符
std::string ip_; // 服务器ip
uint16_t port_; // 服务器进程的端口号
bool isrunning_; // 服务器运行状态
};
我们完成它的代码,如下
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <functional>
#include "Log.hpp"
extern Log lg;
// 枚举错误信息
enum
{
SOCKET_ERR=1,
BIND_ERR,
LISTEN_ERR
};
// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";
// 监听接口 listen 的第二个参数
// listen 函数的第二个参数 backlog 表示等待队列的最大长度。这个等待队列是用于存放那些已经到达但还没有被 accept 函数接受的连接请求。
// 当一个新的连接请求到达时,如果服务器的等待队列还没有满,那么这个连接请求就会被添加到队列中,等待服务器的 accept 函数来处理。
// 如果等待队列已经满了,那么新的连接请求可能就会被拒绝,客户端可能会收到一个 ECONNREFUSED 错误。
const int backlog = 10;
class TcpServer
{
public:
TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
:port_(port), ip_(ip)
{}
void InitServer()
{
// 创建套接字为 IPv4, 字节流(TCP)
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if(listensock_ < 0)
{
lg(Fatal, "create listensock error, errno: %d, errstring: %s", errno, strerror(errno));
exit(SOCKET_ERR);
}
lg(Info, "create listensock success, listensock: %d" , listensock_);
// 初始化 local
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // 设置为 IPv4
local.sin_port = htons(port_);// 保证端口号是网络字节序列
// 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址
// 返回的字符串存储在静态区(多次调用只保存最后一次调用结果)
inet_aton(ip_.c_str(), &(local.sin_addr));
// 当 local.sin_addr.s_addr 被设置为 INADDR_ANY 时
// 意思是告诉操作系统,我们希望绑定的套接字监听所有可用的网络接口上的指定端口。
// 这样设置后,当有数据包到达端口时,无论它们来自哪个网络接口,套接字都能接收到。
// 在服务器编程中,这通常用于监听所有网络接口上的连接请求,而不是只监听某个特定的 IP 地址。
// 这样,服务器可以接受来自任何网络接口的连接,而不仅仅是一个特定的接口。
local.sin_addr.s_addr = INADDR_ANY;
// 绑定套接字
if(bind(listensock_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));
exit(BIND_ERR);
}
lg(Info, "bind socket success, sockfd: %d" , listensock_);
// tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
// 监听套接字
if (listen(listensock_, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s ", errno, strerror(errno));
exit(LISTEN_ERR);
}
lg(Info, "listen socket success, sockfd: %d" , listensock_);
}
// 启动服务器
void Start()
{
lg(Info, "TCP server is running...");
for (;;)
{
//1.获取新连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept 类似于 recvfrom
// 其返回一个文件描述符,后两个参数表示获取哪个用户的信息
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
// sockfd && listensock_
// 举个简单的例子,对于一个农家乐来说会存在两种人
// 一种是去拉客到农家乐内,另一种是在农家乐内进行服务的
// listensock_ -> 拉客的人; sockfd -> 进行服务的人
// listensock_ 只负责监听,如果监听失败会等待监听下一个主机
// sockfd 只负责通信,其可能会变得越来越多
if (sockfd < 0)
{
lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
// inet_ntop 将网络地址转换成文本表示形式
// 它是 inet_aton 的逆函数,它将一个网络地址(通常是 IP 地址)从二进制形式转换为人类可读的字符串形式。
// AF_INET 表示 IPv4 地址; &(client.sin_addr) 指向要转换的网络地址; clientip 是存储转换结果的字符串缓冲区; sizeof(clientip) 是 ipstr 缓冲区的大小
// 确保 inet_ntop 不会写入超出缓冲区范围的内存。
inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
// 2.根据新连接进行通信
lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
Service(sockfd, clientip, clientport);
}
}
void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
{
//测试代码
char buffer[4096];
while(true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say: " << buffer << std::endl;
std::string echo_string = "tcpserver echo: ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
}
}
~TcpServer()
{
if (listensock_ > 0) close(listensock_);
}
private:
int listensock_; // 监听套接字
std::string ip_; // 服务器ip
uint16_t port_; // 服务器进程的端口号
bool isrunning_; // 服务器运行状态
};
其调用逻辑如下
#include "TcpServer.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
// ./tcpserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> svr(new TcpServer(port));
svr->InitServer();
svr->Start();
return 0;
}
我们可以使用 telnet 来对其进行测试,如图
接下来我们编写一个 client 客户端进行测试,代码如下
#include "TcpServer.hpp"
#include <memory>
#include <iostream>
using namespace std;
extern Log lg;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
// ./tcpclient serverip serverport
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]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
lg(Info, "socket create success, sockfd: %d", sockfd);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
// inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
//tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind
//客户端发起connect的时候,进行自动随机bind
// connect 类似于sendto
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
if(n < 0)
{
std::cerr << "connect error. . ." << std::endl;
return 2;
}
std::string message;
while(true)
{
std::cout << "Please Enter: ";
std::getline(std::cin, message);
write(sockfd, message.c_str(), message.size());
char inbuffer[4096];
int n = read(sockfd, inbuffer, sizeof(inbuffer));
if(n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
}
close(sockfd);
return 0;
}
测试效果如下
5. 改进方案与拓展
①多进程版
我们修改 Start 函数,即
void Start()
{
lg(Info, "TCP server is running...");
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// 多进程服务
pid_t id = fork();
if(id == 0)
{
// child
close(listensock_);
if (fork() > 0) exit(0);
// 使用孙子进程服务,由 system 领养
// 从而使孙子进程与父进程并发执行
Service(sockfd, clientip, clientport);
close(sockfd);
exit(0);
}
// father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
}
}
此外,我们也可以在最开始设置
signal(SIGCHID, IGN);
来提升并发度,但是这种方案的成本太高了,所以我们一般不推荐这种做法。
②多线程版
我们稍作修改,有
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t)
:sockfd(fd), clientip(ip), clientport(p), tsvr(t)
{}
public:
int sockfd;
std::string clientip;
uint16_t clientport;
TcpServer *tsvr; // static routine 无法访问类内成员,因此需要一个 server 指针
};
void Start()
{
lg(Info, "TCP server is running...");
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
// 多线程版
ThreadData *td = new ThreadData(sockfd, clientip, clientport);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
}
}
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData*>(args);
td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
delete td;
return nullptr;
}
这种方案已经能大大提升运行效率,我们还可以对其进行优化——使用线程池!
③线程池版
修改方案如下
#include <iostream>
#include <string>
#include "Log.hpp"
extern Log lg;
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
: sockfd_(sockfd),clientip_(clientip),clientport_(clientport)
{}
Task(){}
void run()
{
//测试代码
while (true)
{
char buffer[4096];
ssize_t n = read(sockfd_, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
std::string buff = buffer;
if (buff == "Bye") break;
std::cout << "client say: " << buffer << std::endl;
std::string echo_string = "tcpserver echo: ";
echo_string += buffer;
write(sockfd_, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
}
}
lg(Info, "client sockfd is closed, sockfd: %d", sockfd_);
close(sockfd_);
}
void operator()()
{
run();
}
~Task()
{}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};
void Start()
{
lg(Info, "TCP server is running...");
ThreadPool<Task>::GetInstance()->Start();
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);
// 线程池版
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
运行效果如下
④守护进程化
1. 简单的重联
在实际的连接过程中我们可能会出现各种各样的问题,比如网络突然断了,或者服务器在一瞬间突然断开了和客户端的连接,此时我们需要有一种简单的重联方案,比如在游戏中断联就会出现当前正在重新连接,请稍等,接下来我们就简单实现一下这个功能
#include "TcpServer.hpp"
#include <memory>
#include <iostream>
using namespace std;
extern Log lg;
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}
// ./tcpclient serverip serverport
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]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
// inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
// 尝试进行5次重连
int cnt = 5;
int isreconnect = false;
int sockfd = 0;
do
{
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
//tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind
//客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
if(n < 0)
{
isreconnect = true;
cnt--;
std::cerr << "connect error, reconnecting... times: " << 5 - cnt << std::endl;
sleep(1);
}
else
{
break;
}
} while (cnt && isreconnect);
if (cnt == 0)
{
cout << "user offline..." << endl;
break;
}
std::string message;
std::cout << "Please Enter: ";
std::getline(std::cin, message);
int n = write(sockfd, message.c_str(), message.size());
if (n < 0)
{
cout << "write error" << endl;
continue;
}
char inbuffer[4096];
n = read(sockfd, inbuffer, sizeof(inbuffer));
if(n > 0)
{
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
close(sockfd);
}
return 0;
}
运行效果如下
2. session && 前台 && 后台
我们通过画图来理解,如图
如图,每当有一个用户登录时,OS 会为其分配一个会话(session),一个 session 只能有前台进程运行,且键盘信号只能发给前台进程。那我们如何分别前台与后台呢?——谁拥有键盘就叫前台!在命令行中,前台会一直存在;前台与后台都能向显示器打印数据,但是后台是不能从标准输入获取数据的。
我们可以在运行程序时在最后带上一个 & 使其在后台运行,举个例子
#include <iostream>
using namespace std;
int main()
{
while (true)
{
cout << "hello world" << endl;
}
return 0;
}
运行效果如下
对于 [1] 2744 ,[1] 表示后台任务号,我们可以使用一系列操作来操作它们
jobs -> 查看所有后台任务
fg -n -> 将 n 号任务提到前台
ctrl+z -> 将前台进程放到后台(暂停)
bg -n -> 将后台暂停的进程继续执行
3. Linux 系统进程间关系
我们在后台多运行几个 test 有
可以看到,多个任务(进程组)在同一个 session 内启动。那进程组和任务间有什么关系呢?
任务的完成往往需要多个进程协同工作,而这些进程可以被组织在一个或多个进程组中。例如,一个复杂的任务可能需要多个进程组来共同完成,每个进程组负责任务的不同部分。在这种情况下,进程组作为任务的一个执行单元,可以被看作是任务的一个子集或实现部分。
那当用户退出的时候后台进程会怎样呢?——会被 OS 领养,即成为孤儿进程,也就是说后台进程受到了用户登录和退出的影响!那我们将不想受到任何用户登录和注销影响的行为称为守护进程化!
4. 进程的守护进程化
那我们如何做到守护进程化呢?我们可以封装一个接口,即
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 1.忽略其他异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2.将自己变成独立的会话
if (fork() > 0)
exit(0);
// setsid 组长不能调用,只有组员可以调用
setsid();
// 3.更改当前调用进程的工作目录
if (!cwd.empty())
chdir(cwd.c_str());
// 4.标准输入,标准输出,标准错误重定向至 /dev/null
// 写到 /dev/null 的数据都会被丢弃
int fd = open(nullfile.c_str(), O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
接下来我们就可以让 TCP 服务器守护进程化,即
void Start()
{
Deamon();
lg(Info, "TCP server is running...");
ThreadPool<Task>::GetInstance()->Start();
for (;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
if (sockfd < 0)
{
lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);
// 线程池版
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
运行效果如下