目录
TCP socket API 详解
代码实现TCP通讯
服务端
客户端
Task
守护进程
守护进程
前台与后台
Linux进程间关系
编辑
设置为独立会话setsid
daemon接口
为什么需要设置umask
会话ID与组ID
TCP的相关机制
下图是基于TCP协议的客户端/服务器程序的一般流程:
数据传输的过程
断开连接的过程:
TCP 和 UDP 对比
TCP socket API 详解
代码实现TCP通讯
服务端
using namespace std;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
extern Log lg;
const int backlog = 10; //积压
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer
{
public:
TcpServer(const uint16_t port, const string ip = defaultip)
: _port(port)
, _ip(ip)
, _listensock(defaultfd)
{}
~TcpServer()
{
if (_listensock!= defaultfd)
{
close(_listensock);
}
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr*)&local, sizeof(local)) == -1)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
// Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
if (listen(_listensock, backlog) == -1) //第二个参数用来设置最大积压
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void Start()
{
Daemon();
ThreadPool<Task>::GetInstance()->Start();
lg(Info, "tcpServer is running....");
for(;;)
{
//1.获取accsocket
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int accsock = accept(_listensock, (struct sockaddr*)&peer, &len);
if (accsock == -1)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
continue; //只是接受一次失败,继续接受下一个连接
}
uint16_t peerport = ntohs(peer.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(peer.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", accsock, clientip, peerport);
//线程池版本
Task t(accsock, clientip, peerport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
注意
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
参数讲解
客户端
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(const 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(1);
}
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);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int cnt = 5; //重连次数
int isreconnect = false;
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
//设置重连机制
do
{
// tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
isreconnect = true; //设置重连标志
cnt--;
std::cerr << "connect error..., reconnect: " << cnt << std::endl;
sleep(2);
}
else
break;
}while (cnt && isreconnect); //重连次数为0,退出循环
if (cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
string msg;
std::cout << "Please Enter# ";
std::getline(std::cin, msg);
// TCP在接受发送数据的时候使用的接口有send、write、read、recv
int n = write(sockfd, msg.c_str(), msg.size());
if (n < 0)
{
std::cerr << "write error..." << std::endl;
// break;
}
char inbuf[1024] = {0};
n = read(sockfd, inbuf, sizeof(inbuf));
if (n > 0)
{
inbuf[n] = 0;
std::cout << inbuf << std::endl;
}
else{
// break; 不选择退出,而是断线重连
}
close(sockfd); //每次循环都会新建一个套接字描述符,所以需要关闭套接字描述符
}
return 0;
}
Task
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
Init init;
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()
{
// 测试代码
char buffer[4096];
// Tcp是面向字节流的,你怎么保证,你读取上来的数据,是"一个" "完整" 的报文呢?
ssize_t n = read(sockfd_, buffer, sizeof(buffer)); // BUG?
if (n > 0)
{
buffer[n] = 0;
std::cout << "client key# " << buffer << std::endl;
std::string echo_string = init.translation(buffer);
// sleep(5);
// // close(sockfd_);
// lg(Warning, "close sockfd %d done", sockfd_);
// sleep(2);
n = write(sockfd_, echo_string.c_str(), echo_string.size()); // 100 fd 不存在
if(n < 0)
{
lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
}
}
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_);
}
close(sockfd_);
}
void operator()()
{
run();
}
~Task()
{
}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};
守护进程
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
//1.忽略信号:SIGCHLD, SIGPIPE, SIGSTOP 2.独立会话 3.更改工作目录 4.重定向
void Daemon(const std::string &cwd = "")
{
// 1. 忽略其他异常信号
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2. 将自己变成独立的会话
if (fork() > 0)
exit(0); // 父进程退出,子进程变成后台进程继续执行
setsid();
// 3. 更改当前调用进程的工作目录
if (!cwd.empty())
chdir(cwd.c_str());
// 4. 标准输入,标准输出,标准错误重定向至/dev/null
int fd = open(nullfile.c_str(), O_RDWR);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
守护进程的几大关键:1.忽略信号(子进程会继承父进程的信号处理) 2.设置umask 3.创建子进程,独立会话 4.进行重定向到/dev/null
守护进程
守护进程(daemon)是一种在后台运行的进程,通常没有控制终端,它们执行系统任务,不需要用户交互。守护进程通常在系统启动时启动,并在系统关闭时终止。以下是一些为什么守护进程需要忽略某些信号的原因:
1. **防止意外终止**:守护进程通常执行关键任务,如网络服务、数据备份等。如果守护进程响应了某些信号(如SIGINT,通常由Ctrl+C产生),它们可能会被意外终止,这会影响系统的稳定性和服务的可用性。
2. **避免不必要的重启**:某些信号(如SIGHUP)默认情况下会导致进程重启。对于守护进程来说,这通常不是期望的行为,因为它们通常设计为长期运行,而不是需要定期重启。
3. **防止子进程成为僵尸进程**:守护进程可能会创建子进程来执行任务。如果子进程终止,而守护进程没有正确处理SIGCHLD信号,子进程可能会变成僵尸进程,占用系统资源。
4. **保持服务的连续性**:守护进程忽略某些信号可以确保它们不会因为信号而中断正在执行的任务,从而保持服务的连续性和可靠性。
前台与后台
一般来讲,谁拥有键盘文件(终端操作,如 Ctrl + C),谁就是前台进程。
前后台之间的指令操作
1.command + &
后台任务执行的时候,会给你一个后台任务号与pid
2.jobs查看任务
3.fg + 后台任务号 将后台任务提到前台
^C可以终止前台的2号任务
4. 暂停程序并放到后台 :Ctrl + Z
暂停某个任务,将这个任务放到后台,shell重新回到前台
5. bg(back ground) + 任务:把因为暂停放到后台的任务,在后台重新跑起来
Linux进程间关系
组ID是多个进程id的第一个(父进程)的pid
最开始这些进程的父进程都是bash,但是bash退出之后,这些进程就被托孤了,被OS领养
不受到会话的影响,即不受到bash的影响,也就是让进程的父进程变成OS,而不是bash,也就说,守护进程的本质还是孤儿进程
设置为独立会话setsid
谁调用这个函数,这个进程就会被设置为独立的回话(前提这个进程不能是进程组的组长)
怎么保证自己不是组长呢?---
父进程fork子进程之后,父进程直接退出,那么子进程就变成了了孤儿进程。孤儿进程默认是后台进程。
父进程关闭之后,其子进程的进程组ID(PGID)不会自动改变。进程组ID是在进程创建时确定的,并且会一直保持不变,除非显式地通过系统调用(如setpgid)来修改。
守护进程不受bash登录与退出的影响,因此bash退出时,关闭的0 1 2号文件需要关闭,为了不受他们的影响,应该重定向到一个垃圾桶文件---/dev/null
daemon接口
实际上系统实现了这个接口
允许将两个参数设置为0.
如果参数是0
第一个参数:改成根目录
第二个参数:把0 1 2重定向为垃圾桶
为什么需要设置umask
会话ID与组ID
在UNIX和类UNIX操作系统中,每个进程都属于一个进程组,并且可以属于一个会话。下面是组ID与会话ID的介绍:
### 组ID(Group ID,GID)
组ID是用于标识进程组的一个整数。进程组是一组相关进程的集合,通常是由一个进程通过 `fork()` 系统调用创建的。进程组的主要目的是允许信号被发送到一组进程,而不是单个进程。
- **进程组ID(PGID)**:每个进程组都有一个唯一的进程组ID,它是该组中任一进程的组ID。通常,进程组ID是创建该组的第一个进程的进程ID。
- **获取和设置PGID**:进程可以通过 `getpgrp()` 函数获取其进程组ID,通过 `setpgid()` 函数设置其进程组ID或另一个进程的进程组ID。
### 会话ID(Session ID)
会话ID是用于标识会话的一个整数。会话是一个或多个进程组的集合,它们是由进程通过 `setsid()` 系统调用创建的,用于实现进程的独立性。
- **会话**:会话通常是由控制终端(如用户登录的终端)启动的第一个进程创建的。会话中的所有进程组共享同一个会话ID。
- **会话领导进程**:创建会话的进程称为会话领导进程。会话领导进程的进程ID同时也是会话ID。
- **控制终端**:会话可以有一个控制终端。当会话领导进程打开一个终端时,该终端成为会话的控制终端。会话中的进程组可以接收来自控制终端的信号,如中断信号(SIGINT)。
### 关系和特性
- 一个会话可以包含多个进程组。
- 每个进程组只能属于一个会话。
- 会话领导进程不能成为另一个会话的成员。
- 如果会话领导进程终止,会话中的所有进程都不会受到影响,但会话可能失去控制终端。
- 如果会话没有进程了,会话会被销毁。
### 常用函数
- `getsid()`:获取调用进程的会话ID。
- `setsid()`:创建一个新的会话,并将调用进程设置为会话领导进程。
组ID与会话ID是UNIX系统中的基本概念,它们用于进程管理,特别是在信号处理和终端控制方面。了解这些概念对于编写多进程程序和守护进程至关重要。