当前位置: 首页 > news >正文

计算机网络(四) —— 简单Tcp网络程序

目录

一,服务器初始化

1.0 部分文件代码

1.1 关于Tcp协议

1.2 创建和绑定套接字 

1.3 监听

二,服务器启动

2.1 获取连接

2.2 提供服务

2.3 客户端启动源文件 Main.cc 

二,客户端编写

2.1 关于Tcp客户端

2.2 客户端代码 

2.3 效果演示

2.4 优化

三,字段翻译的应用场景

3.1 翻译功能实现

3.2 效果演示

四,守护进程

4.1 理解“会话”,“前台”和“后台”

4.1 关于守护进程

4.4 将服务器实现成守护进程版本


一,服务器初始化

1.0 部分文件代码

代码文件:计算机网络/网络编程套接字/Tcp · 小堃学编程/Linux学习 - 码云 - 开源中国 (gitee.com)

此网络程序用到的头文件有这几个,可以先全部创建出来:

Log.hpp 日志文件:

#pragma once#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>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}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){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}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);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};Log log;

然后是makefile文件:

.PHONY:all
all:tcpserver tcpclient
tcpserver:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f tcpserver tcpclient 

 然后是线程池文件ThreadPool,这个文件其实就是我们之前写的线程池”:Linux系统编程——线程池_linux系统编程 线程池-CSDN博客

#pragma once#include <vector>
#include <queue>
#include <unistd.h>#include "thread.hpp"
#include "lockGuard.hpp"
#include "log.hpp"const int g_thread_num = 3; // 表示默认创建线程个数// 线程池本质是一个生产消费模型
template <class T>
class ThreadPool
{
public:pthread_mutex_t *getMutex() // 获取锁的地址{return &_lock;}bool isEmpty() // 判断队列是否为空{return task_queue_.empty();}void waitCond(){pthread_cond_wait(&_cond, &_lock); // 等待的时候释放锁,唤醒时再申请锁}T getTask(){T t = task_queue_.front();task_queue_.pop();return t;}public:void run() // 线程池启动{for (int i = 1; i <= _num; i++){_threads.push_back(new Thread(i, routine, this)); // 传this指针,让回调方法能够访问类}for (auto &iter : _threads){iter->start(); // 执行thread_create函数,创建线程,创建的数量由数组大小来定,而数组大小在构造函数定义好了,// std::cout << iter->GetName() << "启动成功" << std::endl;logMessage(NORMAL, "%s%s", iter->GetName().c_str(), "启动成功");}}// 取任务// 如果定义在类内,会有隐藏this指针从而影响使用,所以加上static// 如果一个类内部成员用static,那么它只能使用静态成员再调用静态方法,无法使用类内的成员属性和方法// 如果这个静态的routine是所谓的消费线程,那么要pop队列,但是编译时会报错,这就坑了// 所以为了能让routine拿到类内属性,我们再上面push_back的插入Thread对象时,可以把this指针传过来,通过函数来进行访问(与其让它拿到task_queue,不如让它拿到整体对象)static void *routine(void *args){ThreadData *td = static_cast<ThreadData *>(args);            // 该操作形象点说就是改文件后缀,这里的后缀是args指针ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(td->_args); // 然后这一步相当于解压操作,拿到指针指向对象的线程池指针// 消费逻辑// 先加锁,while(task_queue_.empty()) wait(); 如果任务队列为空就等待// 不为空就获取任务,然后处理,处理完就解锁while (true){T task;{lockGuard lockguard(tp->getMutex()); // 通过this指针调用getMutex获得锁的地址,实现加锁,保证该代码块是安全的代码块while (tp->isEmpty())tp->waitCond(); // 判断队列是否为空,为空就等待// 读取任务task = tp->getTask(); // 任务队列是共享的,这句话就是将任务从共享,拿到自己的私有空间}task(td->_name); // 执行任务,task是队列里的数据,也就是Task类,改类重载了operator(),所以可以直接使用圆括号执行任务// 测试能否传入this指针// tp->show();// sleep(1);}}// 往队列里塞任务void pushTask(const T &task){lockGuard lockguard(&_lock); // 只单纯加锁,加了任务后还应该要唤醒对应的消费线程来消费task_queue_.push(task);pthread_cond_signal(&_cond);}static ThreadPool<T> *GetInstance(){if (nullptr == _tp) // 首次使用时创建对象,并且在加锁前先判断一次,能减少加锁解锁的次数,提高效率{pthread_mutex_lock(&_mutex);if (nullptr == _tp){std::cout << "创建单例" << std::endl;_tp = new ThreadPool<T>();}pthread_mutex_unlock(&_mutex);}return _tp;}private:ThreadPool(int thread_num = g_thread_num): _num(thread_num){pthread_mutex_init(&_lock, nullptr); // 初始化锁pthread_cond_init(&_cond, nullptr);  // 初始化条件变量}~ThreadPool(){for (auto &iter : _threads){iter->join(); // 在释放前join下delete iter;}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=cprivate:std::vector<Thread *> _threads; // 这个数组存的是将来要创建的线程int _num;std::queue<T> task_queue_; // 别人发任务来放到队列里,然后派发给指定线程去执行,所以只要添加到队列里,就自动叫醒一个线程来处理pthread_mutex_t _lock;pthread_cond_t _cond;// 另一种方案:// 我们一开始定义两个队列queue1,queue2// 然后再定义两个制作std::queue<T> *p_queue,  *c_queue// 然后p_queue->queue1,  c_queue->queue2// 当生产一批任务后,我们放到queue1里,然后swap(p_queue, c)queue);// 然后消费者处理完毕后再swap(p_queue, c_queue);// 所以因为我们生产和消费用的是不同的队列,未来我们进行资源任务处理的时候,仅仅只需要交换制作,而且也只要把这个交换这一句加锁即可static ThreadPool<T> *_tp;static pthread_mutex_t _mutex;
};template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr; // 静态成员一般在类外面进行初始化template <class T>
pthread_mutex_t ThreadPool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER;

其余的文件,在后面的讲解中会一一讲解的 

1.1 关于Tcp协议

首先,我们把服务器封装成一个类,这个类包含服务器的初始化函数和启动函数

Tcp协议服务器初始化的基本步骤和Udp是一样的,只是Tcp多了一点东西:

  • 在创建套接字是协议家族选择AF_INET,表示进行网络通信
  • 创建套接字时,服务类型选择SOCK_STREAM,表示有序的,可靠的,全双工的已经基于连接的流式服务,也就是Tcp协议

1.2 创建和绑定套接字 

 下面是

#pragma once
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <signal.h>
// 下面四个是套接字编程基本头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "ThreadPool.hpp"
#include "Log.hpp"
#include "Task.hpp"
#include "daemon.hpp"extern Log log;const int defaultfd = -1; // 套接字初始化为-1
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 这个是listen第二个参数,一般设置的时候不要设置太大,该参数和Tcp协议内部的一个等待队列有关,目前只要知道这个队列不能太长就可以了,以后详细解释Tcp协议时会讲解enum
{UsageError = 1,SOCKET_ERR,BIND_ERR,ListenError
};class TcpServer;class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *t): sockfd(fd), clientip(ip), clientport(port), tsvr(t){}public:int sockfd;std::string clientip;uint16_t clientport;TcpServer *tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t &port = 8888, const std::string &ip = defaultip): _listensockfd(defaultfd), _port(port), _ip(ip){}void InitServer(){// 1,创建Tcp套接字_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // SOCK_STREAM表示可靠的,双向的基于连接的字节流服务,就是Tcp协议if (_listensockfd < 0)                           // 创建失败{log(Fatal, "listensocket create error: %d, errorstring: %s", errno, strerror(errno));exit(SOCKET_ERR);}log(Info, "listensocket create success,_listensockfd: %d", _listensockfd); // 创建成功,输出日志int opt = 1;setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器挂掉后无法立即重启,(Tcp协议理论再详细了解)// 2,绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);// local.sin_addr.s_addr = inet_addr(_ip.c_str()); //这两个的效果一样,把字符串转四字节inet_aton(_ip.c_str(), &(local.sin_addr));// local.sin_addr.s_addr = INADDR_ANY;int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){log(Fatal, "bind errno: %d, errorstring: %s", errno, strerror(errno));exit(BIND_ERR);}log(Info, "bind success, errno: %d, errorstring: %s", errno, strerror(errno));// Tcp是面向连接的,所以服务器一般是比较“被动的”,必须随时应为来自客户端的请求,所以服务器要一直处于一种等待连接到来的状态// 3,监听后面实现}void Start(){// 后面实现  }~TcpServer() {}private:int _listensockfd; // 套接字uint16_t _port;    // 端口号std::string _ip;   // IP
};

1.3 监听

Udp服务器的初始化只有上面创建套接字绑定套接字两步,但是Tcp不一样:

服务器一般都是比较“被动的”,因为服务器必须随时应对来自客户端的请求,所以服务器要一直处于一种等到连接到来的状态,所以就需要将Tcp服务器从创建的套接字设置为监听状态。

问题:你这上面说了跟没说一样,我就想知道监听到底是啥,为什么Tcp需要监听,Udp不需要?

场景:有一家饭店甲开在景区,每到放假就有很多人来,但是店对面也开了一家饭店乙,所以为了提高竞争力,所以饭店甲派出了宣传员,在店门口招揽客人;当有客人来时,宣传员就把客人请进店里,然后招呼店里的服务员来提供服务;然后宣传员就又跑店门口宣传去了。

解答:在上面的场景中,我们把宣传员在店门口“拉客”这种行为,就叫做“监听”,对应到程序中,“监听”的作用就是:将连到我这个服务器的套接字从底层“拉”上来,方便服务器提供服务;因为Tcp是面向连接的,对于连接的各方面的细节把控都要比Udp严格。

监听的sock API和它的英文翻译一样,就是listen

 第一个参数就是需要设置成监听的套接字,而对于第二个参数,我们在后面讲Tcp协议报头的时候再讲,现在只需要知道这个东西和一个队列有关,而这个队列不能太长,目前我们设置为10即可

// Tcp是面向连接的,所以服务器一般是比较“被动的”,必须随时应为来自客户端的请求,所以服务器要一直处于一种等待连接到来的状态
// 3,listen监听(man 2 listen):表示将套接字设置为监听状态,成功返回0,错误返回-1,错误码被设置
if (listen(_listensockfd, backlog) < 0)
{log(Fatal, "listen error: %d, errorstring: %s", errno, strerror(errno));exit(ListenError);
}
log(Info, "listen success, errno: %d, errorstring: %s", errno, strerror(errno));

二,服务器启动

2.1 获取连接

服务器启动之后,要做的事情主要也就是两个:获取连接,然后进行处理

Tcp是面向连接的,所以Tcp在真正进行数据通信前,都要先与客户端建立连接才能通信,用到的sock API也和连接的英文翻译一样:accept函数:

  • sockfd:这个就是我们前面listen监听函数使用的那个,因为_listrnsockfd核心工作是在底层获取新连接,真正提供数据通信服务的,是accept返回的sockfd
  • addr:我们的老朋友sockaddr的结构体指针,作为输出型参数,保存客户端的各种信息,方便后面返回信息给客户端
  • addrlen:表示sockaddr的大小
void Start()
{log(Info, "tcpserver is running...");while (true){// 1,获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);// 问题:Udp只要一个套接字,为啥Tcp有上面的sockfd和类的_listensockfd两个甚至以后会有多个呢?//_listensockfd核心工作就是在底层获取新的连接,真正提供数据通信服务的,是accept返回的sockfd。所以我们会有两个套接字,获取新链接的套接字叫做“监听套接字”if (sockfd < 0){log(Warning, "accept errno: %d, errorstring: %s", errno, strerror(errno));continue; // 一次获取失败就重新获取}uint16_t clientport = ntohs(client.sin_port); // 获取客户端port、char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip)); // 获取客户端的ip地址// 2,根据新连接来进行通信log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);Service(sockfd, clientip, clientport); // 给连接过来的ip进行服务//  问题:1,客户端退了服务器咋办  2,客户端断线了咋办close(sockfd);}
}

2.2 提供服务

确认连接成功之后,就是提供服务了,提供服务也分为三步:

  1. 读取客户端发来的数据
  2. 处理好数据
  3. 将结果返回给客户端

后面两点和Udp一样,也很好理解,但是第一点读取数据,Tcp的处理方式和Udp有很大差别: 

  • Udp服务器是客户端直接发给服务器的,所以Udp服务器需要用recvfrom函数去阻塞式地接受信息
  • 但是Tcp协议在底层做了很多工作,就比如Tcp是直接维护了网卡文件,将客户端发来的数据直接保存在了网卡文件里
  • 所以我们Tcp服务器要想获取链接,可以用recvfrom阻塞式读取,也可以直接用系统的read接口,以sockfd为文件描述符,直接像读文件那样读取客户端发来的消息即可

下面是处理函数Service的代码:

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)); // 可以用文件的接口从sockfd里面读数据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()); // 也可以用文件的接口往sockfd写回数据}else if (n == 0) // 客户端退出会关闭套接字,那么read会读出错,返回值n会赋值为0{log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else // 读取出错{log(Warning, "read error, sockfd: %d, client port: %d", sockfd, clientip.c_str(), clientport);break;}}
}

2.3 客户端启动源文件 Main.cc 

作用主要是读取命令行输入的IP和Port,创建服务器对象,初始化服务器,运行服务器 

#include "TcpServer.hpp"
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << "port[1024+]\n"<< std::endl;
}// ./tcpserver 8080
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);exit(UsageError);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));tcp_svr->InitServer();tcp_svr->Start();return 0;
}

二,客户端编写

2.1 关于Tcp客户端

客户端大部分内容和Udp客户端差不多,作用很简单,就是发送数据给服务器,服务器处理好数据后发回来,最后客户端打印数据,但是也有下面几个需要注意的地方:

  • 由于是Tcp协议服务器和客户端,所以客户端也要和服务器一样建立连接才能进行通信,所以客户端确认连接需要用到connect函数
  • 客户端必须要有“断线重连机制”,因为Tcp服务是“保证通信可靠的服务”而为了保证“可靠”,所以需要做更多的工作,花费更多的成本,但是也能理解,毕竟“世上没有免费的午餐”

断线重连主要涉及两个地方,一个是连接之前,一个是连接之后;客户端和服务器对这两个时段的断线处理机制都不一样

连接之前:

  • 服务器:就是一直阻塞着等待连接到来,
  • 客户端:客户端在建立链接时,如果第一次没连上,一般不会立即break退出,而是等待一秒,再连一次,这样依次进行下去,当超过重连次数后,客户端才会提示说服务器断线,或者网络连接断开;

连接之后: 

  • 连接成功之后,客户端和服务器就都是对网络文件进行读写,所以会直接在读取网络文件时顺便处理断线问题,因为只要有一方退出了,网络文件就都没了,那么服务器就会读取失败,

2.2 客户端代码 

下面是客户端的代码:包括断线重连机制:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>void Usage(const std::string &proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[])
{int x = 0;if (argc != 3){Usage(argv[0]);exit(1);}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);// server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 字符串转四字节,效果和inet_pton是一样的inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true) // 每次进行翻译后都要重新创建套接字和建立连接,因为目前服务器只会提供一次服务{// 创捷套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}int cnt = 5; // 断线重连次数int isreconnect = false;do{// tcp客户端要bind,但是不需要显示bind,客户端发起连接的时候,系统会自动进行bind,随机端口,这点和Udp是一样的int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0) // 连接错误{std::cerr << "connect error..., reconnect: " << cnt << std::endl;isreconnect = true; // 重连失败,继续重连cnt--;x = 1;sleep(2);}else{isreconnect = false; // 重连几次后如果成功就不再重连if (x == 1){std::cout << "reconnect success!" << std::endl;x = 0;}}// 连接成功} while (cnt && isreconnect);if (cnt == 0) // 超过断线重连次数就直接break退出{std::cerr << "user offline... " << std::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){std::cerr << "write error..." << std::endl;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer)); // 收消息if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}else{std::cerr << "read error" << std::endl;}close(sockfd);}return 0;
}

2.3 效果演示

我们先演示正常的通信,再演示断线机制,下面是正常通信:

接下来我们演示断线重连的场景:

2.4 优化

就拿我们这个处理方式作为例子,其实细想一下,可以发现单进程处理任务是有问题的:

单线程处理任务直接导致我们服务器的执行代码是线性的,如果处理时间过长,会直接阻塞住,而这个时候其他客户端再来连接的话就直接访问失败了,因为服务器阻塞着,所以服务器不要这样搞

对于这种情况,下面有几种处理方法:

①多进程处理 

// ②:多进程------------------------------------------
pid_t id = fork();
if (id == 0) // 子进程
{close(_listensockfd); // sockfd是前面打开的描述符,所以正常情况下_listensockfd子进程用不到,所以可以关闭if (fork() > 0)       // 创建孙子进程{// 父进程//  exit(0);// 这样一写,后面的wait等待就不会被阻塞了,因为在子进程里面又fork了一次,这个父进程退了相当于子进程退了,// 但是这个小的父进程的子进程没有退,所以到下面的代码时,其实是孙子进程最后提供的服务}// 孙子进程,而孙子继承的父进程直接挂掉了,最后就会被系统“领养”,最后被系统自动回收Service(sockfd, clientip, clientport); // 给连接过来的ip进行服务close(sockfd);exit(0);
}
else // 父进程
{// 前面父进程获取到的sockfd已经给子进程继承下去给子进程用了,所以父进程就不再关心sockfd了,//和子进程不关心_listensockfd一样,如果不关就会导致系统里面有很多打开的文件没有关,所以要关闭不必要的文件描述符close(sockfd);// 这个步骤和管道重定向有相似之处,可以重复关,因为会有引用计数,关掉了只是把计数-1pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待,但是阻塞不满足要求,所以有了孙子进程(void)rid;                           // 可以直接用signal忽略,取消等待
}

 ​​​

②多线程处理 

// ③:多线程
// 创建进程是需要代价的,所以多进程版了解一下即可,实际开发中不会用多进程模式去搞,一般都是用线程去搞
ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
// 多线程这里不能和多进程那样关闭文件描述符,因为所有的线程都公用的一个当前进程的文件描述符表

 

③最实用的,就是利用线程池去搞

首先我们会构建任务,然后创建初始化和启动线程池,然后把任务放进线程池里,这样线程池就会自动帮我们处理任务了:

任务头文件Task.hpp的代码如下,对于线程池优化我们会结合后面的翻译场景一起搞,所以所以Init.hpp会在后面实现:

#pragma once
#include <iostream>
#include <string>
#include <string.h>
#include "Log.hpp"
#include "Init.hpp"extern Log log;
Init init;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); // 执行翻译任务,该操作由线程池执行// echo_string += buffer; //对话打开,翻译去掉int n = write(_sockfd, echo_string.c_str(), echo_string.size()); // 写的时候万一对应的客户端断开连接了,那么写会崩溃if (n < 0){log(Warning, "write error, errno: %d, errstring: %s", errno, strerror);}}else if (n == 0){log(Info, "%s:%d quit, server close sockfd: %d", _clientip.c_str(), _clientport, _sockfd);}else{log(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;
};
// ④:线程池 ---------------------
Task t(sockfd, clientip, clientport);
ThreadPool<Task>::GetInstance()->Push(t);

 

三,字段翻译的应用场景

3.1 翻译功能实现

客户端什么都不变,比较我们这个客户端的作用很简单,就是发消息接受消息,所以实现翻译功能只要在服务器处理客户端发来的数据的方式上做点事情就好了,上面优化的步骤已经将线程池和任务对象搞好了,然后我们只需要直线翻译的具体实现即可:

翻译需要两个文件:

  • 一个是字典文件,负责存放部分英文单词和对应的中文翻译的键值对(dict.txt
  • 一个是查询程序,负责读取字典文件里的数据,并负责查询并返回(Init.hpp

dict.txt字典文件,负责存放部分英文单词和中文翻译: 

apple:苹果...
banana:香蕉...
red:红色...
yellow:黄色...
the: 这
be: 是
to: 朝向/给/对
and: 和
I: 我
in: 在...里
that: 那个
have: 有
will: 将
for: 为了
but: 但是
as: 像...一样
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他们
we: 我们
their: 他们的
his: 它的
with: 和...一起
she: 她
he: 他(宾格)
it: 它

 Init.hpp查询程序头文件,使用unoedered_map:

#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"const std::string dictname = "./dict.txt";
const std::string sep = ":";// yellow:黄色...
static bool Split(std::string &s, std::string *part1, std::string *part2)
{auto pos = s.find(sep);if (pos == std::string::npos)return false;*part1 = s.substr(0, pos);*part2 = s.substr(pos + 1);return true;
}class Init
{
public:Init(){std::ifstream in(dictname);if (!in.is_open()){log(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}std::string line;while (std::getline(in, line)) // 从文件流中读一行{std::string part1, part2;Split(line, &part1, &part2); // 以冒号为分隔符,把字典里的英文和中文隔开然后分别加载到part1和part2里dict.insert({part1, part2}); // 然后再把两个东东放到unordered_map和键值对里去}in.close();}std::string translation(const std::string &key){auto iter = dict.find(key);if (iter == dict.end()) // 找到迭代器结尾,表示没找到return "Unknow";elsereturn iter->second;}private:std::unordered_map<std::string, std::string> dict;
};

3.2 效果演示

四,守护进程

4.1 理解“会话”,“前台”和“后台”

一个用户尝试在Linux登录时,Linux会形成一个“会话(session)”,每一个会话都会启动一个bash进程,这个bash和我们的键盘和显示器相关。

我们执行程序时,可以在后面加上“ & ”,表示将此进程放到后台运行,jobs命令可以查看后台任务,fg 后套进程编号可以将后台进程重新放回前台

问题:如何理解前台和后台?

解答:哪个进程和标准输入(键盘文件)关联,哪个进程就是前台

  • 我们自己创建的单进程,pid和pgid是一样的,叫做“自成一组” 
  • 而上面我们创建的三个sleep进程,pid和pgid是一样的,叫做“三个自成一组”,这三个sleep合起来就叫做进程组

问题:SID是什么?

解答:用户登录时会创建session会话,当登录的用户多了的时候,session就多了起来,所以Linux需要把这些session也管理起来,“先描述,再组织”,所以系统就会维护一些session结构体,同时为了区分各个session结构体,就会给它们编号,最后就是我们的session id,也就是上面的SID

问题:上面是创建session会话,那么退出会话的时候是什么样的呢?

解答:当终端直接关掉,再重新开个终端重新查的时候,之前的后台pppid全变成1了,TTY变成问号了TPGID变成-1了。这是因为退了后,这几个进程的父进程是bash,bash退了,变孤儿进程了,被1号进程领养,所以这种进程是受到了用户登录和退出的影响的

引出守护进程:如果我们想让一个进程不受用户登录和注销的影响,就要让一个进程守护进程化 

4.1 关于守护进程

这里应该是第一次接触到守护进程的这样一个概念,要想学一个一个概念,还是离不开那三个问题:

  1. 是什么
  2. 有什么用
  3. 咋做到的

守护进程是运行在后台的一种特殊进程,独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件

Linux大多数服务器都是守护进程化的,Internet服务器的inetd,Web服务器的httpd等。Linux启动时会启动很多系统服务进程,这些进程都是在Linux启动时创建,因此不受用户登录注销的影响,ps ajx可以查看系统中的进程 

问题:如何做到守护进程?

解答 :有个接口作用是创建一个会话,setsid函数:

  •  调用这个函数的进程不能是一个进程组的组长,但是如果是自成进程组长的进程,就难搞了
  • 需要保证自己不是组长,第一个进程是组长,那么让我不是第一个进程就好了,这里就要用到fork,父进程直接exit,子进程调用setsid();

所以守护进程的本质也是孤儿进程,但是这个孤儿拒绝被领养,自强成为一个会话

4.4 将服务器实现成守护进程版本

下面是Deamon.hpp头文件的实现,该文件作用就是将调用这个函数的进程实现为守护进程:

#pragma once#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "") // 不传参数的话就是默认把守护进程工作目录放到根目录去
{// 1,守护进程需要忽略其它信信号signal(SIGCLD, SIG_IGN);  // 直接忽略17号信号,为了防止万一出现一些读端关掉了,写端还在写的情况,守护进程signal(SIGPIPE, SIG_IGN); // 直接忽略13号信号signal(SIGSTOP, SIG_IGN); // 忽略19号暂停信号// 2,将自己变成独立的会话if (fork() > 0)exit(0); // 直接干掉父进程setsid();    // 子进程自成会话// 3,更改当前调用进程的工作目录if (!cwd.empty())chdir(cwd.c_str());// 4,不能直接关闭三个输入流,打印时会出错,Linux中有一个/dev/null 字符文件,类似垃圾桶,所有往这个文件写的数据会被直接丢弃,读也读不到// 所以可以把标准输入输出错误全部重定向到这个文件中// 如果需要就往文件里写,反正不能再打印到屏幕上了int fd = open(nullfile.c_str(), O_RDWR); // 以读写方式打开if (fd > 0)                              // 打开成功{// 把三个默认流全部重定向到垃圾桶的null的套接字里去dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

然后直接在服务器启动函数开头,调用Daemon函数,即可将服务器实现为守护进程:

 

相关文章:

计算机网络(四) —— 简单Tcp网络程序

目录 一&#xff0c;服务器初始化 1.0 部分文件代码 1.1 关于Tcp协议 1.2 创建和绑定套接字 1.3 监听 二&#xff0c;服务器启动 2.1 获取连接 2.2 提供服务 2.3 客户端启动源文件 Main.cc 二&#xff0c;客户端编写 2.1 关于Tcp客户端 2.2 客户端代码 2.3 效果…...

简单的Linux Ftp服务搭建

简单的Linux FTP服务搭建 1.需求 公司有一个esb文件传输代理&#xff0c;其中我们程序有文件传输功能&#xff0c;需要将本地文件传输到esb文件代理服务器上&#xff0c;传输成功之后发送http请求&#xff0c;告知esb将固定文件进行传输到对应外围其他服务的文件目录中&#…...

SQL的高级查询练习知识点(day24)

目录 1 学习目标 2 基础查询 2.1 语法 2.2 例子 3 条件查询 3.1 含义 3.2 语法 3.3 条件表达式 3.3.1 条件运算符 3.3.2 例子 3.4 逻辑表达式 3.4.1 逻辑运算符 3.4.2 例子 3.5 模糊查询 3.5.1 概述 3.5.2 例子 4 DISTINCT关键字 4.1 含义 4.2 例子 5 总结…...

Python条件表达式优化的10个实例

Python 中的条件表达式&#xff08;也称为三元运算符&#xff09;是一种简洁的语法&#xff0c;用于在单个表达式中执行 if-else 逻辑。虽然它们本身并不直接“优化”代码的执行速度&#xff0c;但它们可以使代码更加简洁、易读&#xff0c;并且有助于避免不必要的嵌套或复杂的…...

oatpp apiclient 客户端get,post请求python fastapi demo

最新用fastapi搞了个服务端,python功能太强了,就是环境不好弄,弄好后,不要轻易换python版本,不要装多个python版本 前面搞了个oatpp webapi服务端,现在要用客户端,为什么用opatpp客户端,因为他不再带其他库了 demo: 我的请求比较简单,就是向python 的 fastapi服务端…...

RK3568平台(内存篇)EMMC介绍

一.eMMC是什么 eMMC (Embedded Multi Media Card)是MMC协会订立、主要针对手机或平板电脑等产品的内嵌式存储器标准规格。由一个嵌入式存储解决方案组成,带有MMC(多媒体卡)接口、快闪存储器设备及主控制器。所有都在一个小型的BGA 封装。接口速度高达每秒52MBytes,eMMC具…...

Python批量读取身份证信息录入系统和重命名

前言 大家好&#xff0c; 如果你对自动化处理身份证图片感兴趣&#xff0c;可以尝试以下操作&#xff1a;从身份证图片中快速提取信息&#xff0c;填入表格并提交到网页系统。如果你无法完成这个任务&#xff0c;我们将在“Python自动化办公2.0”课程中详细讲解实现整个过程。…...

IBM Storwize V7000存储控制器故障节点报错574

背景&#xff1a;由于客户机房搬迁&#xff0c;需要下电迁移设备。该存储自2016年投入生产使用后&#xff0c;从未关过机&#xff0c;已正常运行七八年时间&#xff0c;期间只更换过硬盘&#xff0c;无其他硬件故障。 在GUI界面点击关闭系统后&#xff0c;大概等了40分钟&…...

通信工程学习:什么是SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制

SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制 SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制是三种不同的调制方式&#xff0c;它们在通信系统中各有其独特的应用和特点。以下是对这三种调制方式的详细解释&#xff1a; 一、SSB单边带调制 1、SSB单边带…...

MapSet之二叉搜索树

系列文章&#xff1a; 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 目录 前言 1.二叉搜索树 1.1 定义 1.2 操作-查找 1.3 操作-新增 1.4 操作-删除(难点) 1.5 总体实现代码 1.6 性能分析 前言 TreeMap 和 TreeSet 是 Java 中基于搜索树实现的 M…...

OpenCV图像分割教程

OpenCV 图像分割教程 OpenCV 是一个非常强大的计算机视觉库&#xff0c;支持各种图像处理任务。图像分割是 OpenCV 支持的一个重要功能&#xff0c;它用于将图像划分为不同的区域&#xff0c;识别感兴趣的部分。我们将通过介绍 OpenCV 中的图像分割方法&#xff0c;包括基础功…...

python科学计算:NumPy 线性代数与矩阵操作

1 NumPy 中的矩阵与数组 在 NumPy 中&#xff0c;矩阵实际上是一种特殊的二维数组&#xff0c;因此几乎所有数组的操作都可以应用到矩阵上。不过&#xff0c;矩阵运算与一般的数组运算存在一定的区别&#xff0c;尤其是在点积、乘法等操作中。 1.1 创建矩阵 矩阵可以通过 Nu…...

Unity面向对象补全计划 之 List<T>与class(非基础)

C# & Unity 面向对象补全计划 泛型-CSDN博客 关于List&#xff0c;其本质就是C#封装好的一个数组&#xff0c;是一个很好用的轮子&#xff0c;所以并不需要什么特别说明 问题描述 假设我们有一个表示学生的类 Student&#xff0c;每个学生有姓名和年龄两个属性。我们需要创…...

ant design vue+vue3+ts+xlsx实现表格导出问excel文件(带自定义表头)~

1、首先默认你已安装ant design vue、xlsx 库、及file-saver。 2、导入&#xff1a; import * as XLSX from xlsx; import { saveAs } from file-saver; 注&#xff1a;这里的xlsx导入不能这么写&#xff0c;否则会报错&#xff0c;原因是版本不一致&#xff0c;语法向上兼容…...

基于Python爬虫的淘宝服装数据分析项目

文章目录 一.项目介绍二.爬虫代码代码分析 三. 数据处理四. 数据可视化 一.项目介绍 该项目是基于Python爬虫的淘宝服装数据分析项目&#xff0c;以致于帮助商家了解当前服装市场的需求&#xff0c;制定更加精确的营销策略。首先&#xff0c;需要爬取淘宝中关于服装的大量数据…...

Tomcat控制台乱码问题已解决(2024/9/7

步骤很详细&#xff0c;直接上教程 问题复现&#xff1a; 情景一 情景二 原因简述 这是由于编码不一致引起的&#xff0c;Tomcat启动后默认编码UTF-8&#xff0c;而Windows的默认编码是GBK。因此你想让其不乱码&#xff0c;只需配置conf\logging.properties的编码格式即可 解决…...

vue通过html2canvas+jspdf生成PDF问题全解(水印,分页,截断,多页,黑屏,空白,附源码)

前端导出PDF的方法不多&#xff0c;常见的就是利用canvas画布渲染&#xff0c;再结合jspdf导出PDF文件&#xff0c;代码也不复杂&#xff0c;网上的代码基本都可以拿来即用。 如果不是特别追求完美的情况下&#xff0c;或者导出PDF内容单页的话&#xff0c;那么基本上也就满足业…...

服务器数据恢复—Raid磁盘阵列故障类型和常见故障原因

出于尽可能避免数据灾难的设计初衷&#xff0c;RAID解决了3个问题&#xff1a;容量问题、IO性能问题、存储安全(冗余)问题。从数据恢复的角度讨论RAID的存储安全问题。 常见的起到存储安全作用的RAID方案有RAID1、RAID5及其变形。基本设计思路是相似的&#xff1a;当部分数据异…...

C++字符串中的string类操作

愿我如星君如月&#xff0c;夜夜流光相皎洁。 ——《车逍遥篇》【宋】范成大 目录 正文&#xff1a; 主要特点&#xff1a; 基本操作&#xff1a; 代码演示&#xff1a; 总结&#xff1a; 今天我们接着上次的章节继续&#xff0c;这次我们来说一个为解决上个方法的缺陷而诞…...

axios设置responseType: ‘blob‘,获取接口返回的错误信息

在axios的请求中当后端接口返回的是文件流的情况下&#xff0c;我们需要在请求参数里面设置responseType: blob&#xff0c;如果接口报错&#xff0c;默认前端无法获取后端返回的错误信息。 解决方法&#xff1a;通过FileReader获取错误信息 async handleFetch() {const res aw…...

【C++】:模板初阶—函数模板|类模板

✨ Blog’s 主页: 白乐天_ξ( ✿&#xff1e;◡❛) &#x1f308; 个人Motto&#xff1a;他强任他强&#xff0c;清风拂山岗&#xff01; &#x1f4ab; 欢迎来到我的学习笔记&#xff01; 本文参考博客&#xff1a;一同感受C模版的所带来的魅力 一、泛型编程思想 首先…...

Java 远程执行服务器上的命令

在Java中使用JSch库执行远程服务器上的命令是一种常见的做法&#xff0c;特别是在需要自动化运维任务或者进行远程文件操作时。以下是基于Codekru网站提供的示例&#xff0c;展示如何使用JSch库在远程服务器上执行单个或多个命令。 准备工作 首先&#xff0c;确保您的项目中已…...

3DMax基础- 创建基础模型

目录 零.软件简介 一. 标准基本型 长方体 圆锥体 球体 圆柱体 管状体 圆环 四棱锥 茶壶 平面​编辑 加强型文本 二. 扩展基本体 三.复合对象 变形 散布 一致 连接 图形合并 布尔 并集 合并 交集 差集 四.门和窗 门 窗 植物,栏杆,墙 零.软件简介 3…...

JavaScript 知识点(从基础到进阶)

&#x1f30f;个人博客主页&#xff1a;心.c ​ 前言&#xff1a;JavaScript已经学完了&#xff0c;和大家分享一下我的笔记&#xff0c;希望大家可以有所收获&#xff0c;花不多说&#xff0c;开干&#xff01;&#xff01;&#xff01; &#x1f525;&#x1f525;&#x1f5…...

计算机网络知识点复习——TCP协议的三次握手与四次挥手(连接与释放)

TCP协议的三次握手与四次挥手&#xff08;连接与释放&#xff09; 一、前言二、简单的知识准备1. TCP协议的主要特点2. TCP报文段 三、TCP连接的建立&#xff08;三次握手&#xff09;四、TCP连接的释放&#xff08;四次挥手&#xff09;五、TCP连接与释放的总结六、结束语 一、…...

SpringDataJPA系列(7)Jackson注解在实体中应用

SpringDataJPA系列(7)Jackson注解在实体中应用 常用的Jackson注解 Springboot中默认集成的是Jackson&#xff0c;我们可以在jackson依赖包下看到Jackson有多个注解 一般常用的有下面这些&#xff1a; 一个实体的示例 测试方法如下&#xff1a; 按照上述图片中的序号做个简…...

【Spring Boot 3】【Web】统一封装 HTTP 响应体

【Spring Boot 3】【Web】统一封装 HTTP 响应体 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总…...

Linux如何做ssh反向代理

SSH反向代理是一种通过SSH协议实现的安全远程访问方式&#xff0c;它允许客户端通过SSH连接到一台具有公网IP的代理服务器&#xff0c;然后这台代理服务器再将请求转发给内部网络中的目标主机。以下是实现SSH反向代理的步骤&#xff1a; 一、准备工作 确保服务器配置&#xff…...

Verilog语法+:和-:有什么用?

Verilog语法:和-:主要用于位选择&#xff0c;可以让代码更简洁。 一、位选择基础 在Verilog中&#xff0c;位选择可以通过直接索引来实现&#xff0c;例如&#xff1a; reg [7:0] data; wire select_a; wire [2:0] select_b; assign select_a data[3]; assign select_b …...

stm32F103 串口2 中断 无法接收指定字符串 [已解决]

stm32F103 串口2中断接收指定字符串 USART 初始化和中断配置示例中断处理函数示例关键点总结 确保在串口配置中正确使能空闲中断 ( USART_IT_IDLE) 是关键。这个中断可以帮助你在串口接收一帧数据完成后&#xff0c;进行相应的处理和分析。 为了确保你在串口配置时能避免类似问…...