Linux网络之----TCP网络编程
1.相关函数
1)socket
这个跟UDP是一样的,这里就介绍一下三个参数传什么参数吧
第三个依旧传0
2)listen
将套接字转换为被动套接字,使其能够接受客户端的连接请求,类似于将电话设置为接听模式。服务器端套接字准备好接受客户端的连接请求。
参数说明:
sockfd:就是socket创建套接字那个返回值
backlog:指定内核为该套接字排队的最大连接数。详细来说就是全连接队列的长度会受到 listen 第二个参数的影响。全连接队列满了的时候,就无法继续让当前连接的状态进入 established 状态了。这个队列的长度是 listen 的第二个参数 + 1,说白了就是假设你想链接5个,那你就传6
返回值:

3)accept
接受客户端的连接请求,类似于接听电话。当客户端发起连接请求时,服务器端使用 accept()
函数接受连接,并返回一个新的套接字描述符,用于与该客户端进行通信。
参数说明:
第一个不说了,套接字描述符
第二个指向 struct sockaddr
类型的指针,用于存储客户端的地址信息,跟前面差不多
第三个地址结构的长度,也和前面一样的
返回值
成功时,返回一个新的套接字描述符,用于与客户端通信。失败-1
可以和UDP里面的recvfrom函数对比一下
4)connect
客户端使用 connect()
函数发起连接请求,类似于拨打电话。客户端通过指定服务器的地址和端口,请求与服务器建立连接。
三个参数跟上一个一样的,不多说了!
返回值
所以说TCP会产生更多的fd~
这个可以对比UDP那里的sendto函数
5)popen
用于启动一个子进程来执行一个命令,并且可以与该子进程的输入/输出流进行交互。它通常用于执行外部程序或命令,并获取其输出。
参数说明:
command
:要执行的命令字符串。
type
:指定打开文件的方式,通常是 "r"
或 "w"
。
"r"
:表示从子进程的输出中读取数据。"w"
:表示向子进程的输入中写入数据。
返回值:
成功时,返回一个指向 FILE
类型的指针,表示与子进程的输入/输出流关联的文件流。失败时,返回 nullptr
。
2.TCP网络编程
2.1 TcpEchoServer的编写
这个练习实现的功能和前面那个UDP是一样的,这里是为了练习TCP相关函数,具体讲解可以参考UDP篇,这里仅介绍新的内容
Mutex.hpp和Logger,hpp,InetAddr.hpp,thread.hpp,ThreadPool.hpp,Cond.hpp跟上一个这个项目是一模一样的,这里就不在给出了
好,下面我们来编写主要部分,首先由于创建TCP的交流会有很多错误类型,这一点与UDP不同,所以我单独写了一个文件用于存放其错误原因
Comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>enum
{OK,SOCKET_CREATE_ERR,SOCKET_BIND_ERR,SOCKET_LISTEN_ERR,SOCKET_CONNECT_ERR,FORK_ERR
};#endif
TcpEchoServer.hpp
基本步骤参考TcpCommand项目对应部分
#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__#include "Comm.hpp"
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>using namespace std;static const int gdefaultfd = -1;
static const int gbacklog = 8;
static const int gport = 8080;
class TcpEchoServer
{
private:void HandlerIO(int sockfd, InetAddr client){char buffer[1024];while (true){buffer[0] = 0;// 约定:你给我发过来的是命令字符串!ls -a -l touch XXssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;string echo_string = "server echo# "; // 回显字符串echo_string += buffer;LOG(LogLevel::DEBUG) << client.ToString() << "say: " << buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::INFO) << "client "<< client.ToString() << " quit, me too, close fd: " << sockfd;break;}else{LOG(LogLevel::WARNING) << "read client "<< client.ToString() << " error, sockfd : " << sockfd;break;}}close(sockfd); // 一定要关闭}public:TcpEchoServer(uint16_t port = gport): _listensockfd(gdefaultfd), _port(port){}void Init(){// 1. create socket fd_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(LogLevel::FATAL) << "create tcp socket error";exit(SOCKET_CREATE_ERR);}LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd;// 2. bind socket fdInetAddr local(_port); // 设置本地端口号if (bind(_listensockfd, local.Addr(), local.Length() )!= 0){LOG(LogLevel::FATAL) << "bind socket error:" << strerror(errno);exit(SOCKET_BIND_ERR);}LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;// 3. set socket listen// 一个tcp server,listen,启动之后,服务器已经算是运行了if (listen(_listensockfd, gbacklog) != 0){LOG(LogLevel::FATAL) << "listen socket error";exit(SOCKET_LISTEN_ERR);}LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;}class ThreadData{public:ThreadData(int sockfd,TcpEchoServer *self,const InetAddr& addr):_sockfd(sockfd),_self(self),_addr(addr){}public:int _sockfd;TcpEchoServer *_self;InetAddr _addr;};void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept client error";continue; // 可能出现一次连接失败的情况,这里可以设置成多次链接}InetAddr clientaddr(peer);LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : " << sockfd<< " client addr: " << clientaddr.ToString();// 多进程,多线程// 1. 效率问题,创建进程线程// 2. 执行流个数没有上限// 进程池4: 你这个task_t哪来的?// ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, clientaddr](){//用到线程池的地方不整模板参数就行了auto cb = [this, sockfd, clientaddr](){this->HandlerIO(sockfd, clientaddr);};ThreadPool<task_t>::GetInstance()->Enqueue(cb);}}~TcpEchoServer(){}private:int _listensockfd; // 监听socketuint16_t _port;
};
#endif
TcpServer.cc
#include"TcpEchoServer.hpp"
#include <memory>void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " localport" << std::endl;
}int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}uint16_t serverport=std::stoi(argv[1]);EnableConsoleLogStrategy();std::unique_ptr<TcpEchoServer> tsvr=std::make_unique<TcpEchoServer>(serverport);tsvr->Init();tsvr->Start();return 0;
}
TcpClient.cc
也是参考TcpCommand对应部分
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Comm.hpp"
#include "InetAddr.hpp"using namespace std;void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "create client sockfd error" << endl;exit(SOCKET_CREATE_ERR);}// tcp客户端,要不要显示的bind?0 ?要不要"bind"?1// 客户端自己的socket地址,让本地os自主随机选择,尤其是端口号// 向目标服务器发起连接请求 !InetAddr server(serverport, serverip);if (connect(sockfd, server.Addr(), server.Length()) != 0){cerr << "connect server error" << endl;exit(SOCKET_CONNECT_ERR);}cout << "connect " << server.ToString() << " success" << endl;while (true){cout << "Please Enter@ ";string line;getline(cin, line);ssize_t n = write(sockfd, line.c_str(), line.size());if (n >= 0){char buffer[1024];ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}}return 0;
}
2.2 TcpCommand的编写
在本练习中,我们要制作一个Linux指令白名单,我们先把之前写过的文件先拽过来
makefile
.PHONY:all
all:main commandclient
main:Main.cc g++ -o $@ $^ -std=c++17
commandclient:CommandClient.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f main commandclient
Logger.hpp
#pragma once#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"using namespace std;
// 规定出场景的日志等级
enum class LogLevel
{DEBUG,INFO,WARNING,ERROR,FATAL
};string Level2String(LogLevel level)
{switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "Info";case LogLevel::WARNING:return "Warning";case LogLevel::ERROR:return "Error";case LogLevel::FATAL:return "Fatal";default:return "Unknown";}
}string GetCurrentTime()
{// 获取时间戳time_t currtime = time(nullptr);// 把时间戳转换成为20XX-08-04 12:27:03struct tm currtm;localtime_r(&currtime, &currtm);// 转换成为字符串char timebuffer[64];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",currtm.tm_year + 1900,currtm.tm_mon + 1,currtm.tm_mday,currtm.tm_hour,currtm.tm_min,currtm.tm_sec);return timebuffer;
}
// 基类方法
class LogStrategy
{
public:virtual ~LogStrategy() = default;virtual void SyncLog(const string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:~ConsoleLogStrategy(){}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);cout << logmessage << endl;}private:Mutex _lock;
};const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:FileLogStrategy(const string &dir = logdefaultdir,const string &filename = logfilename): _dir_path_name(dir), _filename(filename){LockGuard lockguard(&_lock);if (filesystem::exists(_dir_path_name)){return;}try{filesystem::create_directories(_dir_path_name);}catch (const filesystem::filesystem_error &e){std::cerr << e.what() << "\r\n";}}void SyncLog(const string &logmessage) override{LockGuard lockguard(&_lock);string target = _dir_path_name;target += "/";target += _filename;ofstream out(target.c_str(), ios::app);if (!out.is_open()){return;}out << logmessage << "\n"; // out.writeout.close();}~FileLogStrategy(){}private:string _dir_path_name;string _filename;Mutex _lock;
};// 网络刷新
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:Logger(){}void EnableConsoleLogStrategy(){_strategy = make_unique<ConsoleLogStrategy>();}void EnableFileLogStrategy(){_strategy = make_unique<FileLogStrategy>();}// 形成一条完整日志的方式class LogMessage{public:LogMessage(LogLevel level, string &filename, int line, Logger &logger): _curr_time(GetCurrentTime()),_level(level),_pid(getpid()),_filename(filename),_line(line),_logger(logger){std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "]"<< " - ";_loginfo = ss.str();}template<class T>LogMessage& operator <<(const T&info){stringstream ss;ss<<info;_loginfo+=ss.str();return *this;}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 日志时间LogLevel _level; // 日志等级pid_t _pid;std::string _filename;int _line;string _loginfo; // 一条合并完成的,完整的日志信息Logger &_logger; // 提供刷新策略的具体做法};LogMessage operator()(LogLevel level, string filename, int line){return LogMessage(level, filename, line, *this);}~Logger(){}private:unique_ptr<LogStrategy> _strategy;
};Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()
Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>class Mutex
{
public:Mutex(){pthread_mutex_init(&_lock, nullptr);}void Lock(){pthread_mutex_lock(&_lock);}void Unlock(){pthread_mutex_unlock(&_lock);}pthread_mutex_t *Get(){return &_lock;}~Mutex(){pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard
{
public:LockGuard(Mutex*_mutex):_mutexp(_mutex){_mutexp->Lock();}~LockGuard(){_mutexp->Unlock();}
private:Mutex *_mutexp;
};
Comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>enum
{OK,SOCKET_CREATE_ERR,SOCKET_BIND_ERR,SOCKET_LISTEN_ERR,SOCKET_CONNECT_ERR,FORK_ERR
};#endif
InetAddr.hpp
#pragma once// 这个类,描述client socket信息的类
// 方便我们后续用它来管理客户端#include<iostream>
#include<string>
#include<cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)
using namespace std;
class InetAddr
{
public:InetAddr(const struct sockaddr_in &addr):_addr(addr){Net2Host();}InetAddr(uint16_t port,const string&ip="0.0.0.0"):_port(port),_ip(ip){Host2Net();}string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr* Addr(){return Conv(_addr);}socklen_t Length(){return sizeof(_addr);}string ToString(){return _ip+"-"+to_string(_port);}bool operator==(const InetAddr &addr){return (_ip==addr._ip&&_port==addr._port);}~InetAddr(){}
private:struct sockaddr_in _addr; // 网络风格地址string _ip; // 主机风格地址uint16_t _port; //端口号void Net2Host(){_port=ntohs(_addr.sin_port);_ip=inet_ntoa(_addr.sin_addr); //将 IPv4 地址转换成点分十进制字符串格式}void Host2Net(){std::cout << "bindaddr:" << _ip << ":" << _port << std::endl;memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;_addr.sin_port=htons(_port);_addr.sin_addr.s_addr=inet_addr(_ip.c_str());}};
下面我们来正式写一下:
首先就是创建指令白名单,这里可以使用vector数组,之后在类中使用popen函数,读取我们用户输入的指令,其返回一个FILE*类型,之后我们可以用fgets函数,从fp中读取,并存到buffer数组里面
Command.hpp
#pragma once#include <iostream>
#include <vector>
#include <string>using namespace std;
class Command
{
private:bool IsSafe(const string& cmd){for(auto& c:_command_white_list){if(cmd==c){return true;}}return false;}
public:Command(){_command_white_list.push_back("ls -a -l");_command_white_list.push_back("ll");_command_white_list.push_back("cat test.txt");_command_white_list.push_back("touch touch.txt");_command_white_list.push_back("tree");_command_white_list.push_back("whoami");_command_white_list.push_back("who");_command_white_list.push_back("pwd");}string Exec(const string &cmd){//我们要先检测输入的指令是否安全,是不是在白名单内(以防有坏人想给我们系统捣乱)if(!IsSafe(cmd)){//指令不安全return "危险的操作,已禁止!!!";}string result;FILE* fp=popen(cmd.c_str(),"r"); //读取指令if(fp==nullptr){result=cmd+"exec error";}else{char buffer[1204];while(fgets(buffer,sizeof(buffer),fp)!=nullptr) //从fp中读取流,存到buffer里面{result+=buffer;}pclose(fp);}return result;}~Command(){}
private:vector<string> _command_white_list; // 指令白名单
};
之后是server端的编写:
其主要内容包括解决初始化,读写问题,以及如何运行
首先是初始化,步骤还是1.创建套接字2.绑定套接字3.设置监听套接字
之后是读写问题,这里我将其设为私有成员函数,原因下文说,在这里我们可以设置一个buffer数组,并用read函数读取,如果读取成功就将其写到sockfd中,最后别忘了关闭文件就好
之后我们可以将其和线程结合,定义一个内部类ThreadData,这样当我们想执行handlerIO时,就可以直接访问了,
CommandServer.hpp
#ifndef __TCP_ECHO_SERVER_HPP__
#define __TCP_ECHO_SERVER_HPP__#include <iostream>
#include <string>
#include <functional>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>#include "Logger.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"using namespace std;
using callback_t = function<string(const string &)>;
static const int gport = 8080;
static const int gdefaultfd = -1;
static const int gbacklog = 8;
class CommandServer
{
private:void HandlerIO(int sockfd, InetAddr client){char buffer[1024];while (true){buffer[0] = 0;// 约定:你给我发过来的是命令字符串!ls -a -l touch XX// 面向字节流的!// "ls -a -l" -> "ls -" -> "a -l" -> 为什么会这样?udp不存在呢?// 如何解决呢?ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;LOG(LogLevel::DEBUG) << client.ToString() << "say:" << buffer;string result = _cb(buffer);write(sockfd, result.c_str(), result.size());}else if (n == 0){LOG(LogLevel::INFO) << "client "<< client.ToString() << " quit, me too, close fd: " << sockfd;break;}else{LOG(LogLevel::WARNING) << "read client "<< client.ToString() << " error, sockfd : " << sockfd;break;}}close(sockfd); // 一定要关闭}public:CommandServer(callback_t cb, uint16_t port = gport): _listensockfd(gdefaultfd), _port(port), _cb(cb){}void Init(){// 1. create socket fd_listensockfd = socket(AF_INET, SOCK_STREAM, 0); if (_listensockfd < 0){LOG(LogLevel::FATAL) << "create tcp socket error";exit(SOCKET_CREATE_ERR);}LOG(LogLevel::INFO) << "create tcp socket success: " << _listensockfd; // 3// 2. bind socket fdInetAddr local(_port);if (bind(_listensockfd, local.Addr(), local.Length()) != 0){LOG(LogLevel::FATAL) << "bind socket error";exit(SOCKET_BIND_ERR);}LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;// 3. set socket listen// 一个tcp server,listen,启动之后,服务器已经算是运行了if (listen(_listensockfd, gbacklog) == -1){LOG(LogLevel::FATAL) << "listen socket error";exit(SOCKET_LISTEN_ERR);}LOG(LogLevel::INFO) << "Listen socket success: " << _listensockfd;}class ThreadData // 将 ThreadData 定义为内部类,可以方便地在 Routine 中访问这些数据,而不需要额外的类型转换或复杂的逻辑。{public:ThreadData(int sockfd, CommandServer *self, const InetAddr &addr): _sockfd(sockfd), _self(self), _addr(addr){}public:int _sockfd;CommandServer *_self;InetAddr _addr;};static void *Routine(void *args){ThreadData *td = static_cast<ThreadData *>(args);pthread_detach(pthread_self());td->_self->HandlerIO(td->_sockfd, td->_addr);delete td;return nullptr;}void Start(){// signal(SIGCHLD, SIG_IGN); // 最佳实践while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept client error";continue;}InetAddr clientaddr(peer);LOG(LogLevel::INFO) << "获取新连接成功, sockfd is : " << sockfd<< " client addr: " << clientaddr.ToString();pthread_t tid;ThreadData *td=new ThreadData(sockfd,this,clientaddr);pthread_create(&tid,nullptr,Routine,(void*)td);}}~CommandServer(){}private:int _listensockfd; // 监听socketuint16_t _port;callback_t _cb;
};#endif
之后我们再将server的main函数写一下吧,实现思路和前面的.cc都是一样的,这里还是可以用lambda表达式来完成
Main.cc
#include"Command.hpp"
#include"CommandServer.hpp"
#include<memory>void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " localport" << std::endl;
}// "ls -a -l"
// std::string CommandExec(const std::string &commandstr)
// {
// // 线程内部可以创建进程吗?可以!
// // 1. 管道
// // 2. fork
// // 3. 命令分析,exec,1->pipe[1]
// }
int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(0);}uint16_t serverport=stoi(argv[1]);EnableConsoleLogStrategy();Command cmdobj;unique_ptr<CommandServer> tsvr=make_unique<CommandServer>([&cmdobj](const string& cmd)->string{ //->用于显示指定返回类型return cmdobj.Exec(cmd);},serverport);tsvr->Init();tsvr->Start();return 0;
}
最后就是客户端,还是先检测命令行参数是否符合要求,之后拿到server的ip和port,并创建套接字,之后将创建的套接字和server的ip和port进行connect链接,最后就可以进行指令的输入了,用write函数向sockfd中进行写入,之后再从sockfd里面进行读取
CommandClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Comm.hpp"
#include "InetAddr.hpp"using namespace std;void Usage(std::string proc)
{std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(0);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "create client sockfd error" << std::endl;exit(SOCKET_CREATE_ERR);}InetAddr server(serverport, serverip);if (connect(sockfd, server.Addr(), server.Length()) != 0){std::cerr << "connect server error" << std::endl;exit(SOCKET_CONNECT_ERR);}cout << "connect " << server.ToString() << " success" << endl;while(true){cout << "Please Enter@ ";string line;getline(cin,line);ssize_t n=write(sockfd,line.c_str(),line.size());if(n>=0){char buffer[1024];ssize_t m=read(sockfd,buffer,sizeof(buffer)-1);if(m>0){buffer[m]=0;cout<<buffer<<endl;}}}return 0;
}
好了,最后我们运行一下