网络编程套接字和传输层tcp,udp协议

认识端口号

我们知道在网络数据传输的时候,在IP数据包头部有两个IP地址,分别叫做源IP地址和目的IP地址。IP地址是帮助我们在网络中确定最终发送的主机,但是实际上数据应该发送到主机上指定的进程上的,所以我们不仅要确定主机,还要确定主机上的指定进程。而标识该进程的就是通过端口号。所以IP+端口号port就能标识互联网中唯一的一个进程。

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。

端口号和进程pid 

进程pid同样也是可以表示进程的唯一性,但是为什么网络通信还需要新引入一个端口号来标识进程呢?

  1. 并不是每一个进程都会进行网络通信,所以有端口号的则表明需要进行网络通信。
  2. 进程模块采用pid,网络通信模块采用端口号port,进行解耦。提高可维护性与扩展性。

一个进程可以绑定多个端口号(创建多个socket套接字); 但是一个端口号不能被多个进程绑定(端口号具有唯一性)。
 

认识传输层协议

传输层有两个最常见的协议就是传输控制协议(TCP)和用户数据报协议(UDP)。

TCP协议是一种面向连接的协议,它提供了可靠的、有序的数据传输,是Internet上最常见的传输层协议。面向字节流传输。

UDP协议则是一种无连接的协议,它不提供可靠的数据传输,但具有低延迟和高效率的特点,适用于需要实时性要求较高的应用场景,如实时音视频传输等。面相数据报传输。

 

网络字节序

不同的主机,大小端存储方式是不同的。内存和磁盘文件中的数据有大小端的区分,网络数据流同样有大端小端之分,而我们进行网络通信的时候就需要将大小端确定,这样接收到的消息才是正确的顺序。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址,TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

82b2197ccd0a4e01a44eb58e8a2f1ca2.png

一般 在网络通信时,会采用以上的库函数来进行网络字节序和主机字节序的转换。

cc7ff05dbae747359ea1cca3fdea333d.png

 

套接字的认识

 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);

sockaddr结构

sockaddr是一个通用的套接字地址结构体,在网络编程中用于表示套接字的地址信息。

struct sockaddr {  unsigned short sa_family; // 地址族  char sa_data[14]; // 地址信息  
};

该结构体的产生其实就是为了统一各种不同的网络协议的地址格式,是一个通用的地址类型。以便在不同函数接口中的参数能够统一 。在实际的网络通信中我们一般都是采用sockaddr_in结构体来存储套接字信息。

struct sockaddr_in {  short int sin_family; // 地址族(标识套接字所使用的网络协议类型)unsigned short int sin_port; // 端口号  struct in_addr sin_addr; // IP地址  unsigned char sin_zero[8]; // 保留的空字节,用于让sockaddr与sockaddr_in两个数据结构保持大小相同  
};

a5335599ed33465494334592a85f531e.gif

udp网络程序(多线程)

thread_pool.h

#pragma once#include <iostream>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>
#include <vector>
#include <unistd.h>
#include <condition_variable>using namespace std;
using namespace placeholders;#define numdefault 5template <class T>
class thread_pool
{thread_pool(const thread_pool&)=delete;thread_pool operator=(const thread_pool&)=delete;public:static thread_pool* get_instance()  // 单例{if(_instance==nullptr)_instance = new thread_pool();return _instance;}void task_execution(const string &args) // 多个线程开始任务执行{while (1){T t;//调用默认构造{//共享代码段unique_lock<mutex> ul(_mtx);while (_qt.empty()) // 无任务就等待{cond.wait(ul); // 等待期间会解锁,多线程会再等待队列中阻塞,等待成功会上锁}t = _qt.front();_qt.pop();}// 处理任务cout<<args<<": ";t();//执行bind好的函数sleep(1);}}void push(const T &t)//传任务{unique_lock<mutex> ul(_mtx);_qt.push(t);cond.notify_one();//有任务则条件满足}~thread_pool()//{for (int i = 0; i < _num; i++) // C++thread使用线程不join的话程序会崩溃{_vt[i].join();}}private:thread_pool(int num = numdefault)//构造函数私有: _num(num), _vt(num){for (int i = 0; i < _num; i++){string name = "thread_";name += to_string(i + 1);// 移动赋值,线程不支持左值拷贝_vt[i] = thread(bind(&thread_pool<T>::task_execution, this,_1), name);//bind其实与function功能一样,不过可以提前确定参数}}int _num;           // 线程数目queue<T> _qt;       // 任务管理vector<thread> _vt; // 管理线程mutex _mtx;              // 锁condition_variable cond; // 条件变量,任务为空等待static thread_pool* _instance;
};
template<class T>
thread_pool<T>* thread_pool<T>::_instance = nullptr;//单例

udpserver.h

#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <functional>
#include "thread_pool.h"
using namespace std;#define default_size 1024
using Func = function<string(string)>; // 该类型下创建的对象就当做参数string返回值string的函数使用
using task_t = function<void()>;       // 该类型下创建的对象就当做无参无返回值的函数使用,可以衔接bind修饰的函数,将参数确定化class Inet_addr
{
public:Inet_addr() {}Inet_addr(const struct sockaddr_in &clients): _si(clients), _ip(inet_ntoa(clients.sin_addr)), _port(ntohs(clients.sin_port)){}void print_client_info(const char *buffer){cout << "[port:" << _port << " "<< "ip:" << _ip << "]";cout << "client says:" << buffer << endl;}bool operator==(const Inet_addr &com){return _ip == com._ip && _port == com._port;}const struct sockaddr_in &addr(){return _si;}const string &ip(){return _ip;}const in_port_t &port(){return _port;}~Inet_addr(){}private:struct sockaddr_in _si;string _ip;in_port_t _port;
};class udp_server
{public:udp_server(uint16_t port, Func f): _port(port), _func(f){}void init(){// 1.创建套接字(本质就是创建文件细节)_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){exit(-1);}// 2.绑定套接字struct sockaddr_in local;bzero(&local, sizeof(local));       // 全部初始化为0local.sin_family = AF_INET;         // socket inet(ip) 协议家族,绑定网络通信的信息local.sin_port = htons(_port);      // 将主机端口号序列转成网络local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 转成网络序列的四字节ipint n = ::bind(_sockfd, (sockaddr *)&local, sizeof(local));if (n == -1){exit(-1);}// 单例的方式创建多线程thread_pool<task_t>::get_instance();}void myfunc(const char *tmp) // 子线程执行的函数任务,任务就是负责接收消息并发送出去{unique_lock<mutex> ul(_mtx);// 服务端接收消息后是将消息转发给所有的客户端for (auto ia : _vipport) // 遍历所有的客户端并依次发送{sendto(_sockfd, tmp, strlen(tmp), 0, (sockaddr *)&ia.addr(), sizeof(ia.addr()));}}void start()//{while (1){// 客户端的主线程可以一直的收消息,将服务端发送的消息交给创建的线程进行转发处理char buffer[default_size];struct sockaddr_in clients; // 是一个输入输出型参数,接收消息以后会存入发消息的主机信息socklen_t len = sizeof(clients);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&clients, &len); // 收消息// 将所有不同的客户端主机信息都插入进容器,以便服务端可以将信息发送给所有的客户端Inet_addr ia(clients);int i = 0;for (i = 0; i < _vipport.size(); i++) // 遍历查找是否是同一个用户发来的消息{if (_vipport[i] == ia)break;}if (i == _vipport.size())_vipport.push_back(ia);if (n > 0){// 只有主线程才会执行start函数里的内容,将服务器里的任务都压入线程池相关容器中buffer[n] = 0;ia.print_client_info(buffer); // 打印用户端发送方的相关ip端口信息// 将任务压入进程池的任务管理容器中,在等待队列中的线程会自动响应并处理task_t task = std::bind(&udp_server::myfunc, this, buffer); // 其实就是调用回调函数(bind可以固定参数)thread_pool<task_t>::get_instance()->push(task);//不采用线程池的方式,而是进行任务解析功能的代码// string messages = _func(buffer); // 对服务器发送的消息进行处理,然后再将处理结果发回去// sendto(_sockfd, messages.c_str(), messages.size(), 0, (sockaddr *)&clients, len);}}}~udp_server() {}private:uint16_t _port;int _sockfd;Func _func;                 // 回调(就相当于函数指针)mutex _mtx;                 // 锁vector<Inet_addr> _vipport; // 存放所有客户端的ip和端口
};

udpserver.cpp

#include "udpserv.h"string command(string message)//服务器对命令的解析
{FILE* fp = popen(message.c_str(),"r");//会将命令的结果回显到文件//popen的功能//1.创建管道(父进程可以通过该管道向子进程发送输入(指令),同时也可以从该管道接收子进程执行命令的输出结果(文件)。)//2.创建子进程(子进程程序替换执行参数一的命令)if(fp==nullptr){return "popen error!!!";}char buffer[default_size];string ret;while(1){char* s=fgets(buffer,sizeof(buffer)-1,fp);//采用重写缓冲区的形式从文件中读取每一行数据if(!s) break;else ret+=buffer;}return ret.empty()?"not find,please continue":ret;pclose(fp);
}int main(int argc, char *argv[])
{if (argc != 2){cout << "格式错误\n正确格式:" << argv[0] << " port" << endl;}uint16_t port = atoi(argv[1]);unique_ptr<udp_server> user(new udp_server(port,command)); // 自动析构user->init();user->start();return 0;
}

 udpclient.cpp

#include "udpserv.h"// 客户端不应该写成一发一收的形式,如果在服务器多转发数据的时候时,客户端只有发完消息以后才能收到消息
// 所以为客户端创建多线程形式,一个负责专门发消息,一个负责专门收消息void reciever(const u_int16_t &sockfd)//收数据的线程
{while (1){// 收消息char buffer[default_size];struct sockaddr_in other; // 是一个输入输出型参数,接收消息以后会存入发消息的主机信息socklen_t len = sizeof(other);ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&other, &len); // 收消息(来自于服务端)Inet_addr tmp(other);if (m > 0){buffer[m] = 0;tmp.print_client_info(buffer); // 打印发送方的相关ip端口信息}}
}
int main(int argc, char *argv[])
{if (argc != 3){cout << "格式错误\n正确格式:" << argv[0] << " ip"<< " port" << endl;}string ip = argv[1];uint16_t port = atoi(argv[2]);// 1.创建套接字(本质就是创建文件细节)u_int16_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){lg.Log_infom(Fatal, "创建套接字失败: sockfd=%d,%s", sockfd, strerror(errno));exit(-1);}lg.Log_infom(Fatal, "创建套接字成功: sockfd=%d", sockfd);// 不需要显式bind绑定,客户端发送消息的时候会自动绑定随机端口与当前ip// 服务端套接字信息配置struct sockaddr_in server;server.sin_family = AF_INET;                    // socket inet(ip) 协议家族,绑定网络通信的信息server.sin_port = htons(port);                  // 将主机端口号转成网络server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ip// 创建收消息的线程,执行reciev方法thread reciev(reciever, sockfd);while (1){string info;//cout << "please enter:";getline(cin, info);ssize_t n = sendto(sockfd, info.c_str(), info.size(), 0, (sockaddr *)&server, sizeof(server));// 发消息给server服务端(此时会绑定好相关套接字信息)if(n<=0) cout<<"发送消息失败"<<endl;}reciev.join();return 0;
}

 d718c6e60f8e49229c3612bfc4225509.png

 

tcp网络程序(多线程)

Log.h(打印日志信息)

#pragma once
#include <iostream>
#include <time.h>
#include <map>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;enum // 可以设置日志等级
{Debug,Info,Warning,Error,Fatal
};
enum // 打印方式
{Screen,onlyfile,classifyfile
};
const string logdir = "log"; // 目录文件
class Log
{
public:Log(){levermap[Debug] = "Debug";levermap[Info] = "Info";levermap[Warning] = "Warning";levermap[Error] = "Error";levermap[Fatal] = "Fatal";}void exchange(string &s, tm *&cur_time) // 时间戳转换成标准时间{s = to_string(cur_time->tm_year + 1900) + '/' + to_string(cur_time->tm_mon) + '/' + to_string(cur_time->tm_mday) + '-' + to_string(cur_time->tm_hour) + ':' + to_string(cur_time->tm_min) + ':' + to_string(cur_time->tm_sec);}void write_way(const string &filename, const string &loginfo) // 文件打印{mkdir(logdir.c_str(), 0777); // 创建目录,并在指定目录下打印int fd = open(filename.c_str(), O_WRONLY | O_APPEND | O_CREAT, 0666);if (fd == -1)cout << "文件打开失败" << endl;write(fd, loginfo.c_str(), loginfo.size());close(fd);}void write_log(int lever, const string &loginfo) // 日志写入位置{string tmp = logdir + '/' + "log.";switch (style){case 0: // 显示器打印cout << loginfo;break;case 1: // log.txt里打印write_way(tmp + "txt", loginfo);break;case 2: // 分类到各自对应的文件里打印write_way(tmp + levermap[lever], loginfo);break;default:break;}}void enable(int sty){style = sty;}void Log_infom(int lever, const char *format, ...) // 格式formats{char tmp[1024];va_list args;                              // 可变参数部分的起始地址va_start(args, format);                    // 初始化,通过format确定可变参数个数vsnprintf(tmp, sizeof(tmp), format, args); // 将数据写到tmp中va_end(args);                              //time_t t = time(nullptr);     // 得到当前的时间戳tm *cur_time = localtime(&t); // 传入时间戳string s;exchange(s, cur_time); // 转换成具体的时间string loginfo;loginfo = loginfo + tmp + ' ' + '[' + levermap[lever] + ']' + '[' + s + ']' + '\n';write_log(lever, loginfo);}~Log(){}private:map<int, string> levermap;int style = 0; // 默认往显示器中打印int lever = Debug;
};
Log lg;

tcp_server.h

#pragma once
#include "inet.hpp"
#include "Log.h"
#include "thread_pool.h"
#include <map>class thread_data;               // 提前声明
#define default_backlog 5        // 全连接队列
using task_t = function<void()>; // 包装器,无参无返回值
using func_t = function<void(thread_data)>;class thread_data // 线程对应的套接字描述符
{
public:int _sockfd;Inet_addr _inet;thread_data(int sockfd, Inet_addr tmp) : _sockfd(sockfd), _inet(tmp){}~thread_data(){// close(_sockfd);不能在这里关闭,因为线程的生命周期与thread_data对象不同步}
};class tcp_server
{
public:tcp_server(uint16_t port): _port(port){}void inite(){// 1.创建套接字_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd == -1){lg.Log_infom(Fatal, "创建套接字失败error:%d,strerrno:%s,_listen_sockfd = %d", errno, strerror(errno), _listen_sockfd);exit(-1);}lg.Log_infom(Debug, "创建套接字成功,sockfd = %d", _listen_sockfd);// 解决一些服务端绑定失败无法重启的问题int opt = 1;setsockopt(_listen_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 2.绑定网络信息struct sockaddr_in local;local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY; // 宏值就是0local.sin_port = htons(_port);      // 端口号没绑好就会出错int n = ::bind(_listen_sockfd, (sockaddr *)&local, sizeof(local));if (n != 0){lg.Log_infom(Fatal, "绑定网络信息失败error:%d,strerrno:%s,bind_ret = %d", errno, strerror(errno), n);exit(-1);}lg.Log_infom(Debug, "绑定网络信息成功,bind_ret = %d", n);// 3.客户端发起连接,服务器等待连接,将套接字设置为监听状态n = listen(_listen_sockfd, default_backlog);if (n == -1){lg.Log_infom(Fatal, "监听套接字失败error:%d,strerrno:%s,listensocket = %d", errno, strerror(errno), n);exit(-1);}lg.Log_infom(Debug, "监听套接字成功,bind_ret = %d", n);// 创建线程池thread_pool<task_t>::get_instance();}// void Service(int sockfd) // (用于v1和v2)// {//     while (1)//     {//         char buffer[1024] = {0};//         int n = read(sockfd, buffer, sizeof(buffer) - 1);//         if (n > 0)//         {//             buffer[n] = 0;//             lg.Log_infom(Debug, "server recieve info:%s", buffer);//         }//         else if (n == 0) // 读到文件末尾//         {//             lg.Log_infom(Info, "数据已经全部读取完毕...");//             break;//         }//         else//         {//             lg.Log_infom(Error, "数据读取失败");//             break;//         }//         write(sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024//         cout << "send info: " << buffer << endl;//     }// }// void Service(thread_data tmp) // 重载(v3多线程使用)// {//     while (1)//     {//         char buffer[1024] = {0};//         int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);//         if (n > 0)//         {//             buffer[n] = 0;//             tmp._inet.print_client_info(buffer);//         }//         else if (n == 0) // 读到文件末尾//         {//             lg.Log_infom(Info, "数据已经全部读取完毕...");//             break;//         }//         else//         {//             lg.Log_infom(Error, "数据读取失败");//             break;//         }//         write(tmp._sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024//         cout << "send info: " << buffer << endl;//     }// }// void handler(thread_data tmp)// {//     Service(tmp); // 内部是while循环,在v4线程池是,将线程与用户一一分配了,当客户端>线程个数就无法输入//     close(tmp._sockfd);// }void start(){while (1){// 4.服务端获取客户端连接(提供断线重连的方式)struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(_listen_sockfd, (sockaddr *)&client, &len); // 每连接一次都会返回一个新的sockfd负责接下来的通信if (sockfd < 0){lg.Log_infom(Warning, "服务端获取连接失败error:%d,strerrno:%s,newsockedt = %d", errno, strerror(errno), sockfd);continue; // 获取连接失败继续获取}lg.Log_infom(Debug, "服务端获取连接成功,newsocket = %d", sockfd);***v1:一般模式// 5.提供服务进行通信// Service(sockfd);// 关闭文件// close(sockfd);***v2:创建子进程,父进程进行获取不同客户端的连接,子进程进行通信(此时就可以多客户端通信)// 此时有3、4号文件描述符(父子共享)// signal(SIGCHLD,SIG_IGN);//在linux环境中,对该信号进行忽略则表明在子进程退出的时候,就会自动释放资源// pid_t id = fork(); // 创建子进程// if (id == -1)// {//     lg.Log_infom(Error, "创建子进程失败,fork_ret=%d", id);//     close(sockfd);//     continue;// }// a.创建孙子进程的方式//  else if (id == 0) // 子进程//  {//      close(_listen_sockfd);//      if (fork() > 0) // 执行完毕后退出当前进程(子进程)//          exit(0);//      // 接下来就是孙子进程所执行的代码//      Service(sockfd); // 孙子进程的父进程已经退出了,所以被OS领养回收资源//      exit(0);//  }//  else if (id > 0)//  {//      close(sockfd);//      // 等待子进程退出//      pid_t ret = waitpid(id, nullptr, 0);//      if (ret == id)//          ;//  }// b.采用父进程不等待的方式,而是信号的方式//  else if (id == 0) // 子进程//  {//      close(_listen_sockfd);//      Service(sockfd); // 孙子进程的父进程已经退出了,所以被OS领养回收资源//      exit(0);//  }//  else if (id > 0)//  {//      close(sockfd);//  }***v3创建多线程(父进程不断地循环等待连接,每个线程(取决于客户端申请连接)执行自己的任务)// thread_data tmp(sockfd,Inet_addr(client));//第二个参数存放发送者的套接字信息// thread t(std::bind(&tcp_server::handler,this,placeholders::_1),tmp);// //此时父进程执行后续代码,可能会再进行一次accept获取连接,那么sockfd的值可能会改变// t.detach();//线程分离,父进程不用等待回收***v4线程池(提前将线程创建好,主线程进行任务的接收并存入线程池的任务栏,子线程进行任务处理)// thread_data tmp(sockfd, Inet_addr(client)); // 第一个参数是每个线程对应的sockfd,第二个参数存放发送者的套接字信息// task_t t = std::bind(&tcp_server::handler, this, tmp);// thread_pool<task_t>::get_instance()->push(t); // 将任务压入进程池的任务栏***v4.2线程池执行任务thread_data tmp(sockfd, Inet_addr(client)); // 第一个参数是每个线程对应的sockfd,第二个参数存放发送者的套接字信息task_t t = std::bind(&tcp_server::routine, this, tmp);thread_pool<task_t>::get_instance()->push(t); // 将任务压入进程池的任务栏}}void registr(string s, func_t f) // 将任务提前登记{_mapfunc[s] = f;}void routine(thread_data tmp) // 线程接收客户端发送的任务种类,并进行处理,代替handler下的service功能{//读取任务种类char buffer[1024] = {0};int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);string s;if (n > 0){buffer[n] = 0;s=buffer;}else if (n == 0) // 读到文件末尾{lg.Log_infom(Info, "数据已经全部读取完毕...");}else{lg.Log_infom(Error, "数据读取失败");}//子进程判断任务并执行if (s=="ping")_mapfunc[s](tmp);else if(s=="translate")_mapfunc[s](tmp);else if(s=="transform")_mapfunc[s](tmp);else_mapfunc["default_func"](tmp);close(tmp._sockfd);//线程执行完毕就关闭}~tcp_server(){}private:uint16_t _port;int _listen_sockfd;map<string, func_t> _mapfunc;
};

tcpserver.cpp

#include <fstream>
#include <algorithm>
#include <ctype.h>
#include "tcp_server.hpp"void Ping(thread_data tmp)
{tmp._inet.print_client_info("ping");char buffer[1024] = {0};int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;lg.Log_infom(Debug, "server recieve info:%s", buffer);}else if (n == 0) // 读到文件末尾{lg.Log_infom(Info, "数据已经全部读取完毕...");}else{lg.Log_infom(Error, "数据读取失败");}write(tmp._sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024cout << "send info: " << buffer << endl;
}
class dict
{
public:map<string, string> _dicts;dict(){// 直接将txt文本文件中的单词全部录入到dicts中std::ifstream file("./test.txt"); // 打开文件vector<string> lines;string line;if (file.is_open())// 检查文件是否成功打开{while (std::getline(file, line)) // 按行读取文件内容{lines.push_back(line);}file.close(); // 关闭文件}else{std::cerr << "无法打开文件" << std::endl;}// 将lines中的数据按照key-val的形式填入for (auto &s : lines){string tmp = s;int i = s.find(' ');_dicts[tmp.substr(0, i)] = tmp.substr(i + 1);}}const string operator[](const string &tmp){if (_dicts.find(tmp) == _dicts.end())return "暂时还未录入该数据到词典中";return _dicts[tmp];}~dict(){}
};dict dictionary;//放到外面就不用每次都重新初始化
void Translate(thread_data tmp)
{    tmp._inet.print_client_info("translate");// 读取任务char buffer[1024] = {0};int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);string s;if (n > 0){buffer[n] = 0;s = buffer;}string chines = dictionary[s];// 返回任务结果write(tmp._sockfd, chines.c_str(), chines.size()); // sizeof此时大小还是1024cout << "send info: " << chines << endl;
}void Transform(thread_data tmp)
{tmp._inet.print_client_info("transform");// 读取任务char buffer[1024] = {0};int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);string s;if (n > 0){buffer[n] = 0;s = buffer;}std::transform(s.begin(), s.end(), s.begin(), [](char c) -> char{ return toupper(c); });// 返回任务结果write(tmp._sockfd, s.c_str(), s.size()); // sizeof此时大小还是1024cout << "send info: " << s << endl;
}
void default_func(thread_data tmp)
{tmp._inet.print_client_info("default");// 读取任务不处理char buffer[1024] = {0};read(tmp._sockfd, buffer, sizeof(buffer) - 1);// 返回任务结果string s = "目前没有该类型任务,请重新输入正确的任务类型,例如1.ping 2.translate 3.transform";write(tmp._sockfd, s.c_str(), s.size());cout << "send info: " << s << endl;
}int main(int argc, char *argv[])
{if (argc != 2){cout << "格式错误,正确格式:" << argv[0] << " port" << endl;}uint16_t port = atoi(argv[1]);unique_ptr<tcp_server> user(new tcp_server(port)); // 自动析构// 登记消息对应的方法user->registr("ping", Ping);user->registr("translate", Translate);user->registr("transform", Transform);user->registr("default_func", default_func);user->inite();user->start();return 0;
}

tcpclient.cpp(设置断线重连)

#include "tcp_server.hpp"#define default_count 5int main(int argc, char *argv[])
{if (argc != 3){cout << "格式错误\n正确格式:" << argv[0] << " ip"<< " port" << endl;}string ip = argv[1];uint16_t port = atoi(argv[2]);int count = 1;while (count <= default_count){// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1){lg.Log_infom(Fatal, "创建套接字失败error:%d,strerrno:%s,sockfd = %d", errno, strerror(errno), sockfd);exit(-1);}lg.Log_infom(Debug, "创建套接字成功,sockfd = %d", sockfd);// 需要绑定网络信息,但是不用显式绑定,一般在通信的时候就自动绑定上了// tcp在发起连接的时候就被os绑定好了// 2.建立连接struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;   // socket inet(ip) 协议家族,绑定网络通信的信息server.sin_port = htons(port); // 将主机端口号转成网络// server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ipinet_pton(AF_INET, ip.c_str(), &server.sin_addr); // 转成网络序列的四字节ipint n = connect(sockfd, (sockaddr *)&server, sizeof(server)); // 自动bindstring tmp; // 先读取任务类型if (n == -1){lg.Log_infom(Fatal, "客户端连接失败...连接次数: %d", count++);sleep(1);goto END; // 该段代码段之间不能创建对象}lg.Log_infom(Error, "客户端建立连接成功,connect_ret: %d", n);count = 1;// 3.数据传输cout << "please enter style: ";getline(cin, tmp);write(sockfd, tmp.c_str(), tmp.size()); // 对端已经关闭,写端继续写的话就会触发异常while (1){string s;cout << "please enter: ";getline(cin, s);int m = write(sockfd, s.c_str(), s.size()); // 对端已经关闭,写端继续写的话就会触发异常if (m > 0)                                  // 发送成功{char buffer[1024] = {0};int n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;lg.Log_infom(Debug, "client recieve info:%s", buffer);break;}else if (n == 0) // 读到文件末尾{lg.Log_infom(Info, "数据已经全部读取完毕,即服务端关闭了文件描述符sockfd...");break;}else{lg.Log_infom(Error, "数据读取失败");break;}}else{cout << "写数据失败" << endl;break;}}END:close(sockfd);}return 0;
}

 

90965045bd004fa2a5268d27536723cf.jpeg

守护进程(精灵进程)

首先我们要知道,实际上我们的网络服务并不能在bash中以前台进程的方式运行,而是以守护进程的方式在后台一直运行着不退出。

守护进程的特点

  1. 在系统后台运行:守护进程在后台运行,不与控制台交互,也不会在终端上显示任何输出,所以不受任何终端控制
  2. 自己是一个独立的会话:守护进程不隶属于任何bash会话,自己自成进程组自成会话。
  3. 守护进程一般不会退出:就算系统退出,重新登录Linux系统,守护进程依旧不会退出,只有强制将守护进程kill -9掉,才能退出进程。

 

 认识进程组,会话

31a98a216e054fb18ce800cdc63a538b.png当我们的其中一个中断执行sleep 120命令之后,在另一个中断查看sleep进程时,最上面的PGID就是进程组ID,SID就是会话IDTTY就是指当前进程打开的终端设备。

可以发现我们的进程组ID等于当前进程ID,而进程的会话ID等于当前进程的父进程ID(bash)。

我们登录Linux时,操作系统都会提供一个bash和一个终端,给用户提供命令解析服务。其实这就是一个会话。而我们在命令行中启动的所有进程都是隶属于当前会话的,所以进程组也是属于会话的。而且会话ID其实就是bash进程的ID。因为bash提供的正是命令解析的服务3ca6d2f9b8ae4feab9c1a95526c00649.png

当我们查看我们的bash进程的时候会发现bash进程的PID,PGID,SID都是相等的,所以bash进程是自成进程组自成会话。所以具象化的认识就是如下:1c186ab4ea4f49878abf62c239ef100e.png

其实可以通过创建一批进程来确定进程组ID:

e5ce64d00b8d4ed1b925b23d9a7d534a.png

(该方式创建的进程属于同一个进程组,进程组ID相同)

93faf5828a8f4ed29f24cd4a202ca415.png

(该方式创建的进程属于三个不同进程组,进程组ID不同)

我们可以知道同一个会话中不管运行多少个进程组,会话ID都是bash 。而进程组ID取决于进程的运行,如果是兄弟进程同时运行的方式,则进程组ID就是最先运行的那个进程PID,但如果采用后台进程的方式创建多个进程的话,那么自己的进程组ID就等于自己进程的PID。还有一点就是任何时刻一个会话内部可以存在多个进程组,但是只有一个进程组在前台。

 

 守护进程实现

想要实现守护进程,首先就要创建一个会话

pid_t setsid(void);//创建一个新会话,并让自己成为会话的话首进程

但是调用setsid创建新会话是有条件的:代用setsid的进程不能是一个进程组组长,而进程组组长是会话中创建进程组的第一个进程(所以一个会话中可以有多个进程组组长)。

所以我们的解决方式是创建子进程并让父进程退出,子进程执行后续代码。此时我们父进程虽然退出了,但进程组ID依旧是父进程的PID(因为进程组ID是与会话 相关联的,而不是与单个进程相关联的。只有当会话中的最后一个进程退出时,会话和与之相关联的进程组才会结束)。而且可以知道守护进程本就是孤儿进程。

void daemon(int is_change)
{// 一个会话内部可以有多个进程组,但默认任何时刻只有一个进程组在前台// 1.守护进程自己是一个独立的会话,不隶属于任何一个bash会话。pid_t fi = fork(); // 当父进程退出时,进程组的组长不会改变,仍然是原来的组长进程// 2.让自己不要成为组长,关闭父进程,守护进程也就是孤儿进程,其父进程是系统(pid=1)if (fi > 0)exit(0);// 3. // 返回新的会话,即pid=pgid=sid(条件是,调用进程不能是进程组的组长)pid_t si = setsid();if (si == -1){cout << "调用该函数失败失败,不能是组长调用该进程" << endl;exit(-1);}// 4.是否将当前工作目录更改为根目录if (is_change)chdir("/");// 5.守护进程不需要进行输入输出,将输入输出到/dev/null下(自动丢弃)int fd = open("/dev/null", O_RDWR);if (fd > 0){// 重定向dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/324354.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

虚拟化技术 分离虚拟机数据流量与ESXi的流量管理

一、实验内容 为ESXi主机添加网卡通过vClient查看已添加的网卡信息为ESXi添加网络&#xff0c;创建标准交换机修改网络配置&#xff0c;实现虚拟机数据流量与ESXi的管理流量分离 二、实验主要仪器设备及材料 安装有64位Windows操作系统的台式电脑或笔记本电脑&#xff0c;建…

泰达克仿钻点水晶饰品包装印刷防滑UV胶特性及应用场景

仿钻点UV滴胶是一种特殊的胶水 常用于模拟钻石的效果 它是一种透明的胶水 具有高光泽度和折射率 可以在物体表面形成类似钻石的亮闪效果 仿钻点UV滴胶通常由紫外线固化胶组成 需要通过紫外线照射来固化和硬化 它具有以下特点&#xff1a; 1. 透明度&#xff1a;仿钻点UV滴胶具有…

使用 OpenCV 创建视频(74)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV 库来捕获和处理视频输入和相似度测量(73) 下一篇&#xff1a;OpenCV使用 Kinect 和其他兼容 OpenNI 的深度传感器(75) 目标 每当您使用视频源时&#xff0c;您最终可能希望将图像处理结果保…

Zabbix6.0容器化部署(Docker-Composed)

Zabbix 为每个 Zabbix 组件提供 Docker image 作为可移植和自给自足的容器&#xff0c;以加快部署和更新过程。 Zabbix 组件在 Ubuntu、Alpine Linux 和 CentOS 基础 image 上提供:Zabbix 组件支持 MySQL 和 PostgreSQL 数据库、Apache2 和 Nginx Web 服务器。 1. Zabbix 组件…

ASP.NET网上车辆档案管理系统

摘 要 本文采用基于Web的Asp.net技术&#xff0c;并与sql server 2000数据库相结合&#xff0c;研发了一套车辆档案管理系统。该系统扩展性好&#xff0c;易于维护。简化了车辆档案设计流程&#xff0c;去除了冗余信息。汽车销售企业可以通过本系统完成整个销售及售后所有档案…

C语言指针的初级练习

前言 从0开始记录我的学习历程&#xff0c;我会尽我所能&#xff0c;写出最最大白话的文章&#xff0c;希望能够帮到你&#xff0c;谢谢。 提示&#xff1a;文章作者为初学者&#xff0c;有问题请评论指正&#xff0c;感谢。 哥们嗷 不知道你和我是否一样在学习C语言指针的时…

【ZZULI数据结构实验】压缩与解码的钥匙:赫夫曼编码应用

&#x1f4c3;博客主页&#xff1a; 小镇敲码人 &#x1f49a;代码仓库&#xff0c;欢迎访问 &#x1f680; 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f442;&#x1f3fd;留言 &#x1f60d;收藏 &#x1f30f; 任尔江湖满血骨&#xff0c;我自踏雪寻梅香。 万千浮云遮碧…

使用 Docker 部署 TaleBook 私人书籍管理系统

1&#xff09;项目介绍 GitHub&#xff1a;https://github.com/talebook/talebook Talebook 是一个简洁但强大的私人书籍管理系统。它基于 Calibre 项目构建&#xff0c;具备书籍管理、在线阅读与推送、用户管理、SSO 登录、从百度/豆瓣拉取书籍信息等功能。 友情提醒&#x…

工业交换机外壳材质大比拼,看看哪种外壳适合你

在工业领域里&#xff0c;交换机就像我们的网络心脏&#xff0c;时刻跳动着确保信息畅通无阻。而它的外壳&#xff0c;就是保护这颗“心脏”的铠甲。今天&#xff0c;咱们就来聊聊这些铠甲——工业交换机外壳的材质和防护等级&#xff0c;看看它们如何守护我们的网络世界。 首…

网络编程基础回顾

计算机网络&#xff08;5&#xff09;&#xff1a;运输层 OSI 模型与 TCP/IP 协议 OSI七层协议模型 (open system interconnection) 应用层&#xff1a;为应用数据提供服务表示层&#xff1a;数据格式转化&#xff0c;数据加密会话层&#xff1a;建立、维护和管理会话传输层&…

算法设计与分析 动态规划/回溯

1.最大子段和 int a[N]; int maxn(int n) {int tempa[0];int ans0;ansmax(temp,ans);for(int i1;i<n;i){if(temp>0){tempa[i];}else tempa[i];ansmax(temp,ans);}return ans; } int main() {int n,ans0;cin>>n;for(int i0;i<n;i) cin>>a[i];ansmaxn(n);co…

【吃透Java手写】4-Tomcat-简易版

【吃透Java手写】Tomcat-简易版-源码解析 1 准备工作1.1 引入依赖1.2 创建一个Tomcat的启动类 2 线程池技术回顾2.1 线程池的使用流程2.2 线程池的参数2.2.1 任务队列&#xff08;workQueue&#xff09;2.2.2 线程工厂&#xff08;threadFactory&#xff09;2.2.3 拒绝策略&…

财务管理|基于SprinBoot+vue的财务管理系统(源码+数据库+文档)

财务管理系统 目录 基于SprinBootvue的财务管理系统 一、前言 二、系统设计 三、系统功能设计 系统功能实现 1管理员功能模块 2员工功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1…

视频批量剪辑指南:一键合并视频并添加背景音乐,高效便捷

在数字化时代&#xff0c;视频剪辑已经成为了一项常见且重要的技能。无论是制作家庭影片、工作展示还是社交媒体内容&#xff0c;掌握高效的视频剪辑技巧都能极大地提升我们的工作效率和创作质量。本文将为您介绍云炫AI智剪中高效的视频批量剪辑方法&#xff0c;让您能够一键合…

在线教程|图灵奖得主Yann LeCun盛赞!小红书开源InstantID,一张原图即可定制多种风格写真

不久前&#xff0c;一群来自小红书的 95 后工程师联合北大团队发布了开源项目「InstantID」&#xff0c;只需上传一张照片&#xff0c;这款 AI 写真神器就能轻松定制多种风格的 AI 写真&#xff0c;告别繁琐修图。 InstantID 一经发布就引起了广泛关注&#xff0c;GitHub 收藏量…

Java实现的网上书店系统(附带完整源码)

作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者 实现技术:JSP技术;javaBean;servlet;MySql数据库。 系统功能结构图 该系统为MVC结构,它的运行环境分客户端、应用服务器端和数据库服务器端三部分 书店系统需求分析: 通过…

通用人工智能AGI,究竟是一个哲学问题还是技术问题?

引言 在探索人工智能的未来方向中&#xff0c;人工通用智能&#xff08;AGI&#xff09;的概念逐渐成为科技领域和哲学探讨的焦点。AGI旨在创建可以执行任何智能任务的机器&#xff0c;甚至在某些方面超越人类的能力。然而&#xff0c;关于AGI的研究不仅仅是技术问题&#xff…

天龙怀旧游戏python脚本

设置图&#xff1a; 游戏窗口最大化。 海贼洞这里定位你要回点的定位。 运行bat就行&#xff0c;脚本出错了还是会重新运行脚本&#xff0c;运行自动启动&#xff0c;end暂停脚本&#xff0c;home重新启动脚本 1. 我常用的是内挂回点脚本&#xff0c; 下面都是前台脚本&…

数据结构与算法学习笔记六-二叉树的顺序存储表示法和实现(C语言)

目录 前言 1.数组和结构体相关的一些知识 1.数组 2.结构体数组 3.递归遍历数组 2.二叉树的顺序存储表示法和实现 1.定义 2.初始化 3.先序遍历二叉树 4.中序遍历二叉树 5.后序遍历二叉树 6.完整代码 前言 二叉树的非递归的表示和实现。 1.数组和结构体相关的一些知…

【React】React-redux多组件间的状态传递

效果&#xff08;部分完整代码在最底部&#xff09;&#xff1a; 编写 Person 组件 上面的 Count 组件&#xff0c;已经在前面几篇写过了&#xff0c;也可以直接翻到最底部看 首先我们需要在 containers 文件夹下编写 Person 组件的容器组件 首先我们需要编写 index.jsx 文件…