认识端口号
我们知道在网络数据传输的时候,在IP数据包头部有两个IP地址,分别叫做源IP地址和目的IP地址。IP地址是帮助我们在网络中确定最终发送的主机,但是实际上数据应该发送到主机上指定的进程上的,所以我们不仅要确定主机,还要确定主机上的指定进程。而标识该进程的就是通过端口号。所以IP+端口号port就能标识互联网中唯一的一个进程。
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
端口号和进程pid
进程pid同样也是可以表示进程的唯一性,但是为什么网络通信还需要新引入一个端口号来标识进程呢?
- 并不是每一个进程都会进行网络通信,所以有端口号的则表明需要进行网络通信。
- 进程模块采用pid,网络通信模块采用端口号port,进行解耦。提高可维护性与扩展性。
一个进程可以绑定多个端口号(创建多个socket套接字); 但是一个端口号不能被多个进程绑定(端口号具有唯一性)。
认识传输层协议
传输层有两个最常见的协议就是传输控制协议(TCP)和用户数据报协议(UDP)。
TCP协议是一种面向连接的协议,它提供了可靠的、有序的数据传输,是Internet上最常见的传输层协议。面向字节流传输。
UDP协议则是一种无连接的协议,它不提供可靠的数据传输,但具有低延迟和高效率的特点,适用于需要实时性要求较高的应用场景,如实时音视频传输等。面相数据报传输。
网络字节序
不同的主机,大小端存储方式是不同的。内存和磁盘文件中的数据有大小端的区分,网络数据流同样有大端小端之分,而我们进行网络通信的时候就需要将大小端确定,这样接收到的消息才是正确的顺序。
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址,TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
一般 在网络通信时,会采用以上的库函数来进行网络字节序和主机字节序的转换。
套接字的认识
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两个数据结构保持大小相同
};
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;
}
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;
}
守护进程(精灵进程)
首先我们要知道,实际上我们的网络服务并不能在bash中以前台进程的方式运行,而是以守护进程的方式在后台一直运行着不退出。
守护进程的特点
- 在系统后台运行:守护进程在后台运行,不与控制台交互,也不会在终端上显示任何输出,所以不受任何终端控制
- 自己是一个独立的会话:守护进程不隶属于任何bash会话,自己自成进程组自成会话。
- 守护进程一般不会退出:就算系统退出,重新登录Linux系统,守护进程依旧不会退出,只有强制将守护进程kill -9掉,才能退出进程。
认识进程组,会话
当我们的其中一个中断执行sleep 120命令之后,在另一个中断查看sleep进程时,最上面的PGID就是进程组ID,SID就是会话ID,TTY就是指当前进程打开的终端设备。
可以发现我们的进程组ID等于当前进程ID,而进程的会话ID等于当前进程的父进程ID(bash)。
我们登录Linux时,操作系统都会提供一个bash和一个终端,给用户提供命令解析服务。其实这就是一个会话。而我们在命令行中启动的所有进程都是隶属于当前会话的,所以进程组也是属于会话的。而且会话ID其实就是bash进程的ID。因为bash提供的正是命令解析的服务
当我们查看我们的bash进程的时候会发现bash进程的PID,PGID,SID都是相等的,所以bash进程是自成进程组自成会话。所以具象化的认识就是如下:
其实可以通过创建一批进程来确定进程组ID:
(该方式创建的进程属于同一个进程组,进程组ID相同)
(该方式创建的进程属于三个不同进程组,进程组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);}
}