認識端口號
我們知道在網絡數據傳輸的時候,在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);}
}
?
?