目录V1版本 - EchoServerV2 版本 - CommandServerinet_ntoa 的线程安全问题inet_addr 的设计缺陷V1版本 - EchoServerTcp Socket 和 Udp Socket 很多地方是类似的我们重点介绍不同的地方第一步 调用 socket 创建套接字第二个参数选择 SOCK_STREAM 流式服务第二步 bind依旧采用 绑定本主机任意 ip 地址的方式由于 Tcp 是面向连接的因此在正式通信前要先建立连接这就要求 服务器先处于 listen(监听) 状态#include sys/socket.h int listen(int sockfd, int backlog);参数:sockfd 为 socket 返回的套接字描述符backlog 表示 套接字内核连接队列的最大长度这个参数后续会详细介绍返回值成功返回0失败返回-1错误码被设置设置完监听状态后这就意味着如果有客户端发出了连接请求服务器是能收到的下来就是 调用 accept 接收请求了#include sys/socket.h int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数:sockfd 为 socket 返回的套接字描述符addr 是输出型参数addrlen 是输入输出型参数这两个参数用于获取连接服务器的当前客户端的地址信息类比 Udp 中的 recvfrom返回值成功返回一个新的套接字文件描述符专门用于和客户端进行通信失败返回 -1如果 accept 失败了说明接受当前的一个客户端连接失败了直接继续循环去 accept 下一个客户端连接即可注意现在出现了两个套接字文件描述符第一个套接字文件描述符是 socket 的返回值、bind 和 listen 的参数这个套接字文件描述符应该命名为 _listensockfd专门用来通信前建立连接的工作而第二个套接字文件描述符是 accept 的返回值用于后续服务器和客户端之间的通信举个例子饭店门口有个招揽客人的服务员(张三)张三只负责揽客在店门外将客人拉拢进饭店里然后叫专门给客人提供吃饭服务的服务员(李四)接着张三就继续去拉拢客人了张三就是监听套接字而李四是后续用来服务器和客户端通信的套接字accept 接收请求之后就可以开始处理请求了我们这一版服务器实现的是简单的消息回显功能TcpEchoServer.hpp我们直接使用上一篇 Udp Socket 封装好的 InetAddr 类#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { //服务器是一个死循环 while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); HandlerIO(sockfd, client); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endifHandlerIO 函数就用于读取客户端发送来的消息并将消息发送回客户端Tcp 读写消息也有自己专门的接口我们此处先不做介绍先使用 read / write 接口就可以完成字节流的读写void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! }注意HandlerIO 结束时必须手动关闭文件描述符因为 Tcp Socket 每次处理一个客户端请求就要调用 一次 accept得到一个新的文件描述符如果不关闭历史打开的文件描述符就会有严重的文件描述符泄漏问题客户端比较多的话后续就没有文件描述符可以分配了而 Udp Socket 中全程使用的都是同一个 文件描述符不会存在不够用的情况但最佳实践还是用完关掉必修手动Tcp客户端依旧是先创建套接字依旧不需要显式bind下来就是客户端主动发起连接请求了#include sys/socket.h int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数:sockfd 为 socket 返回的套接字描述符addr 和 addrlen 输入型参数这两个参数用于指定连接服务器的地址信息类比 Udp 中的 sendto 接口返回值成功返回0失败返回-1错误码被设置接下来就是调用 write 和 read 发送和读取消息了TcpClient.cc#include iostream #include unistd.h #include cstring #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include InetAddr.hpp void Usage(std::string proc) { std::cerr Usgae: proc serverip serverport std::endl; } int main(int argc, char* argv[]) { if(argc ! 3) { Usage(argv[0]); exit(-1); } std::string serverip argv[1]; uint16_t serverport std::stoi(argv[2]); // 1.create socket int sockfd socket(AF_INET, SOCK_STREAM, 0); if(sockfd 0) { std::cerr create socket error std::endl; exit(SOCKET_CREATE_ERROR); } std::cout create socket success: sockfd std::endl; //2.connect socket InetAddr peer(serverport, serverip); int n connect(sockfd, peer.Addr(), peer.Length()); if(n 0) { std::cerr connect socket error std::endl; exit(SOCKET_CONNECT_ERROR); } std::cout connect socket success: sockfd std::endl; //3. send messgae while(true) { std::cout Please Enter ; std::string line; std::getline(std::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; }上图展示了服务器和客户端的通信功能确实完成了 echo_server 功能但是当我们再启动一个客户端服务器没有打印出任何接受新连接成功的消息第二个客户端发消息也没有任何反应但是当我们终止掉第一个客户端后服务器立马刷出了 accept 连接成功的消息并且第二个客户端发送的消息也收到了第二个客户端也立马收到了服务器回显的消息因为目前我们的服务器是单进程/单线程的处理请求的逻辑是这样的 while(true) { acceptHandlerIO}HandlerIO 是在一个循环中的客户端1 connect服务器 accept 了该请求然后去和客户端1进行收发消息了只要客户端1没有退服务器就在 HandlerIO 中无法再 accept其他请求了只有客户端1退出了HandlerIO 中 read 读到0才会跳出循环然后继续去 accept 客户端2的连接才能和客户端2收发消息那为啥 Udp Socket 中 也是 单进程 / 单线程 却没有问题呢因为 Udp 通信不需要建立连接上来直接 recvfrom 读取消息然后 sento 发送消息有消息就读取没有消息就阻塞在 recvfrom 处有客户端发来消息就立马可以读到而 Tcp 需要先 accept 连接accept 成功之后处理当前客户端的请求了就无法再 accept 其他客户端了其他客户端只能等了为了解决这个问题我们需要引入多进程/多线程多进程版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pid_t pid fork(); if(pid 0) { LOG(LogLevel::ERROR) resource not enough, create child process error; exit(FORK_ERROR); } else if(pid 0) { close(_listensockfd); //子进程不再需要_listensockfd, 就关掉 if(fork() 0) exit(OK); //子进程再创建孙子进程, 创建成功, 子进程直接退出, waitpid直接返回, 孙子进程被托管! HandlerIO(sockfd, client); //孙子进程 exit(OK); } else { //father close(sockfd); //父进程不再需要sockfd, 就关掉, 并且防止fd泄漏 pid_t rid waitpid(pid, nullptr, 0); (void)rid; } } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif多线程版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } struct ThreadData { ThreadData(int sockfd, TcpEchoServer* self, InetAddr addr) :_sockfd(sockfd), _self(self), _addr(addr) {} int _sockfd; TcpEchoServer* _self; InetAddr _addr; }; static void* Routine(void* args) { ThreadData* td static_castThreadData*(args); pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接 td-_self-HandlerIO(td-_sockfd, td-_addr); return nullptr; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pthread_t tid; ThreadData* td new ThreadData(sockfd, this, client); pthread_create(tid, nullptr, Routine, td); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif上述多进程/多线程的代码作为日常 demo 当然没有问题但创建进程和线程都是有成本的当客户端都发来消息了再去创建多进程/多线程有点慢并且当前代码中进程/线程个数是没有上限限制的来一个客户端创建一个进程/线程客户端太多最后服务器可能就撑不住了因此我们可以引入线程池预先创建一批线程并且线程个数是有上限的当线程个数到达上限时客户端发来的消息作为任务就在任务队列中等待线程可以暂时不处理线程池版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp #include ThreadPool.hpp static const int gdefaultfd -1; static const int gbacklog 8; using task_t std::functionvoid(); class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); //接入线程池 ThreadPooltask_t::GetInstance()-Push([sockfd, this, client](){ this-HandlerIO(sockfd, client); }); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif上述的 HanderIO 本质是长任务因为内部是 while 死循环来一个客户端请求线程池中就会有一个线程去处理但只要客户端不退出这个线程就要一直处理这个客户端请求这就叫做长任务长任务意味着要维持长连接这种任务其实是不适合用线程池来处理的后续会有更合适的做法此处仅仅是为了 demo 演示 !V2 版本 - CommandServer这一版的服务器我们实现的功能是用户发来一个指令服务器将执行完指令的结果返回给客户端CommandServer.hpp 中我们在类中加入回调函数对象成员参数为 string 表示客户端发来的命令返回值为 string 表示命令执行后的结果在 read 之后调用回调函数将返回值 write 给客户端即可完成命令集解析#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include functional #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; using callback_t std::functionstd::string (const std::string); class CommandServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; LOG(LogLevel::INFO) client.ToString() say: buffer; std::string result _cb(buffer); //_cb回调负责将命令执行的结果返回 write(sockfd, result.c_str(), result.size()); } else if(n 0) { LOG(LogLevel::INFO) client client.ToString() quit, me too!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: CommandServer(callback_t cb, uint16_t port 8080) :_cb(cb), _listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } struct ThreadData { ThreadData(int sockfd, CommandServer* self, InetAddr addr) :_sockfd(sockfd), _self(self), _addr(addr) {} int _sockfd; CommandServer* _self; InetAddr _addr; }; static void* Routine(void* args) { ThreadData* td static_castThreadData*(args); pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接 td-_self-HandlerIO(td-_sockfd, td-_addr); return nullptr; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pthread_t tid; ThreadData* td new ThreadData(sockfd, this, client); pthread_create(tid, nullptr, Routine, td); } } ~CommandServer(){} private: int _listensockfd; uint16_t _port; callback_t _cb; }; #endif我们将命令解析封装成一个类Command.hppCommand.hpp#include iostream #include string #include vector #include cstdio #include cstring #include logger.hpp class Command { private: bool isSafe(const std::string cmd) { for(auto t : _cmd_black_list) { if(strstr(cmd.c_str(), t.c_str())) //判断t是否是cmd的子串 { return false; } } return true; } public: Command(){ _cmd_black_list.push_back(rm); _cmd_black_list.push_back(shutdown); _cmd_black_list.push_back(kill); _cmd_black_list.push_back(userdel); } std::string Exec(const std::string cmd) { if(!isSafe(cmd)) return 坏人; std::string safe_cmd cmd 21; FILE* fp popen(safe_cmd.c_str(), r); std::string result; if(fp NULL) { result cmd exec error; } else { char buffer[1024]; while(fgets(buffer, sizeof(buffer), fp) ! nullptr) { result buffer; } pclose(fp); } return result; } ~Command(){} private: std::vectorstd::string _cmd_black_list; //指令黑名单 };ServerMain.cc#include CommandServer.hpp #include memory #include Command.hpp void Usage(std::string proc) { std::cerr Usgae: proc localport std::endl; } int main(int argc, char* argv[]) { if(argc ! 2) { Usage(argv[0]); return 1; } uint16_t port std::stoi(argv[1]); Command cmdObj; std::unique_ptrCommandServer tsvr std::make_uniqueCommandServer( [cmdObj](const std::string cmdstr) { return cmdObj.Exec(cmdstr); } ,port ); tsvr-Init(); tsvr-Start(); return 0; }inet_ntoa 的线程安全问题我们在学习 Udp Socket 和 Tcp Socket 时需要将 4字节 ip 转为字符串 ip 时使用的函数都是 inet_ntoa但其实 inet_ntoa 并不是线程安全的inet_ntoa 的内部简化实现如下char *inet_ntoa(struct in_addr in) { static char buf[16]; // 这是一个静态缓冲区所有线程共享 unsigned char *bytes (unsigned char *)in.s_addr; snprintf(buf, sizeof(buf), %d.%d.%d.%d, bytes[0], bytes[1], bytes[2], bytes[3]); return buf; // 返回指向共享缓冲区的指针 }核心问题使用一个静态内部缓冲区来存储转换后的IP地址字符串并返回指向该缓冲区的指针。后果该缓冲区被进程中所有线程共享。当多个线程几乎同时调用此函数时后调用的线程会覆盖前一个线程存放在缓冲区中的数据导致所有线程最终都可能得到错误的结果。下面这段代码可以验证 inet_ntoa 确实不是线程安全的#include stdio.h #include netinet/in.h #include arpa/inet.h int main() { struct sockaddr_in addr1; struct sockaddr_in addr2; addr1.sin_addr.s_addr 0; addr2.sin_addr.s_addr 0xffffffff; char* ptr1 inet_ntoa(addr1.sin_addr); char* ptr2 inet_ntoa(addr2.sin_addr); printf(ptr1: %s, ptr2: %s\n, ptr1, ptr2); return 0; }因此以后需要将网络序列转主机序列时不要再用 inet_ntoa 函数了直接使用 inet_ntop 函数该函数要求用户提供存放转换后的字符串ip的缓冲区因此该函数是线程安全的参数af协议族使用 AF_INET 表示 IPV4src指向二进制地址的指针(网络地址)如 struct sockaddr_in 的地址dst指向输出字符串缓冲区的指针由调用者提供size缓冲区大小返回值成功返回 dst 指针失败返回 nullptr错误码被设置void NetToHost() { _port ntohs(_addr.sin_port); // _ip inet_ntoa(_addr.sin_addr); //不是线程安全的! //最佳实践, 是线程安全的! char buffer[1024]; inet_ntop(AF_INET, _addr.sin_addr, buffer, sizeof(buffer)); _ip buffer; }inet_addr 的设计缺陷inet_addr 的作用是把点分十进制的字符串变成网络整数 IP它是线程安全的因为它只是单纯地解析你传进去的字符串然后直接把算出来的 32 位整数传值返回给你不需要依赖任何全局静态变量。因为它在设计上有一个无法填补的逻辑漏洞如果转换失败比如你传了个 hello它会返回 INADDR_NONE也就是 -1 的补码形式 0xFFFFFFFF。但是有一个完全合法且非常有用的 IP 地址叫做 255.255.255.255局域网全网广播地址。当你试图转换这个合法地址时它转换出来的整数刚好也是 0xFFFFFFFF这就导致你根本无法分辨它返回 -1 到底是因为报错还是因为你传的就是 255.255.255.255。参数af协议族使用 AF_INET 表示 IPV4src指向待转换的点分十进制字符串dst指向存储二进制结果的指针返回值1表示转换成功0表示格式错误(如src不是有效的地址字符串)-1表示系统错误(如 af 不支持)错误码被设置void HostToNet() { 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()); //有设计上的缺陷 //最佳实践, inet_pton inet_pton(AF_INET, _ip.c_str(), _addr.sin_addr); }
Tcp Socket
目录V1版本 - EchoServerV2 版本 - CommandServerinet_ntoa 的线程安全问题inet_addr 的设计缺陷V1版本 - EchoServerTcp Socket 和 Udp Socket 很多地方是类似的我们重点介绍不同的地方第一步 调用 socket 创建套接字第二个参数选择 SOCK_STREAM 流式服务第二步 bind依旧采用 绑定本主机任意 ip 地址的方式由于 Tcp 是面向连接的因此在正式通信前要先建立连接这就要求 服务器先处于 listen(监听) 状态#include sys/socket.h int listen(int sockfd, int backlog);参数:sockfd 为 socket 返回的套接字描述符backlog 表示 套接字内核连接队列的最大长度这个参数后续会详细介绍返回值成功返回0失败返回-1错误码被设置设置完监听状态后这就意味着如果有客户端发出了连接请求服务器是能收到的下来就是 调用 accept 接收请求了#include sys/socket.h int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数:sockfd 为 socket 返回的套接字描述符addr 是输出型参数addrlen 是输入输出型参数这两个参数用于获取连接服务器的当前客户端的地址信息类比 Udp 中的 recvfrom返回值成功返回一个新的套接字文件描述符专门用于和客户端进行通信失败返回 -1如果 accept 失败了说明接受当前的一个客户端连接失败了直接继续循环去 accept 下一个客户端连接即可注意现在出现了两个套接字文件描述符第一个套接字文件描述符是 socket 的返回值、bind 和 listen 的参数这个套接字文件描述符应该命名为 _listensockfd专门用来通信前建立连接的工作而第二个套接字文件描述符是 accept 的返回值用于后续服务器和客户端之间的通信举个例子饭店门口有个招揽客人的服务员(张三)张三只负责揽客在店门外将客人拉拢进饭店里然后叫专门给客人提供吃饭服务的服务员(李四)接着张三就继续去拉拢客人了张三就是监听套接字而李四是后续用来服务器和客户端通信的套接字accept 接收请求之后就可以开始处理请求了我们这一版服务器实现的是简单的消息回显功能TcpEchoServer.hpp我们直接使用上一篇 Udp Socket 封装好的 InetAddr 类#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { //服务器是一个死循环 while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); HandlerIO(sockfd, client); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endifHandlerIO 函数就用于读取客户端发送来的消息并将消息发送回客户端Tcp 读写消息也有自己专门的接口我们此处先不做介绍先使用 read / write 接口就可以完成字节流的读写void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! }注意HandlerIO 结束时必须手动关闭文件描述符因为 Tcp Socket 每次处理一个客户端请求就要调用 一次 accept得到一个新的文件描述符如果不关闭历史打开的文件描述符就会有严重的文件描述符泄漏问题客户端比较多的话后续就没有文件描述符可以分配了而 Udp Socket 中全程使用的都是同一个 文件描述符不会存在不够用的情况但最佳实践还是用完关掉必修手动Tcp客户端依旧是先创建套接字依旧不需要显式bind下来就是客户端主动发起连接请求了#include sys/socket.h int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数:sockfd 为 socket 返回的套接字描述符addr 和 addrlen 输入型参数这两个参数用于指定连接服务器的地址信息类比 Udp 中的 sendto 接口返回值成功返回0失败返回-1错误码被设置接下来就是调用 write 和 read 发送和读取消息了TcpClient.cc#include iostream #include unistd.h #include cstring #include sys/socket.h #include sys/types.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include InetAddr.hpp void Usage(std::string proc) { std::cerr Usgae: proc serverip serverport std::endl; } int main(int argc, char* argv[]) { if(argc ! 3) { Usage(argv[0]); exit(-1); } std::string serverip argv[1]; uint16_t serverport std::stoi(argv[2]); // 1.create socket int sockfd socket(AF_INET, SOCK_STREAM, 0); if(sockfd 0) { std::cerr create socket error std::endl; exit(SOCKET_CREATE_ERROR); } std::cout create socket success: sockfd std::endl; //2.connect socket InetAddr peer(serverport, serverip); int n connect(sockfd, peer.Addr(), peer.Length()); if(n 0) { std::cerr connect socket error std::endl; exit(SOCKET_CONNECT_ERROR); } std::cout connect socket success: sockfd std::endl; //3. send messgae while(true) { std::cout Please Enter ; std::string line; std::getline(std::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; }上图展示了服务器和客户端的通信功能确实完成了 echo_server 功能但是当我们再启动一个客户端服务器没有打印出任何接受新连接成功的消息第二个客户端发消息也没有任何反应但是当我们终止掉第一个客户端后服务器立马刷出了 accept 连接成功的消息并且第二个客户端发送的消息也收到了第二个客户端也立马收到了服务器回显的消息因为目前我们的服务器是单进程/单线程的处理请求的逻辑是这样的 while(true) { acceptHandlerIO}HandlerIO 是在一个循环中的客户端1 connect服务器 accept 了该请求然后去和客户端1进行收发消息了只要客户端1没有退服务器就在 HandlerIO 中无法再 accept其他请求了只有客户端1退出了HandlerIO 中 read 读到0才会跳出循环然后继续去 accept 客户端2的连接才能和客户端2收发消息那为啥 Udp Socket 中 也是 单进程 / 单线程 却没有问题呢因为 Udp 通信不需要建立连接上来直接 recvfrom 读取消息然后 sento 发送消息有消息就读取没有消息就阻塞在 recvfrom 处有客户端发来消息就立马可以读到而 Tcp 需要先 accept 连接accept 成功之后处理当前客户端的请求了就无法再 accept 其他客户端了其他客户端只能等了为了解决这个问题我们需要引入多进程/多线程多进程版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pid_t pid fork(); if(pid 0) { LOG(LogLevel::ERROR) resource not enough, create child process error; exit(FORK_ERROR); } else if(pid 0) { close(_listensockfd); //子进程不再需要_listensockfd, 就关掉 if(fork() 0) exit(OK); //子进程再创建孙子进程, 创建成功, 子进程直接退出, waitpid直接返回, 孙子进程被托管! HandlerIO(sockfd, client); //孙子进程 exit(OK); } else { //father close(sockfd); //父进程不再需要sockfd, 就关掉, 并且防止fd泄漏 pid_t rid waitpid(pid, nullptr, 0); (void)rid; } } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif多线程版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } struct ThreadData { ThreadData(int sockfd, TcpEchoServer* self, InetAddr addr) :_sockfd(sockfd), _self(self), _addr(addr) {} int _sockfd; TcpEchoServer* _self; InetAddr _addr; }; static void* Routine(void* args) { ThreadData* td static_castThreadData*(args); pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接 td-_self-HandlerIO(td-_sockfd, td-_addr); return nullptr; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pthread_t tid; ThreadData* td new ThreadData(sockfd, this, client); pthread_create(tid, nullptr, Routine, td); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif上述多进程/多线程的代码作为日常 demo 当然没有问题但创建进程和线程都是有成本的当客户端都发来消息了再去创建多进程/多线程有点慢并且当前代码中进程/线程个数是没有上限限制的来一个客户端创建一个进程/线程客户端太多最后服务器可能就撑不住了因此我们可以引入线程池预先创建一批线程并且线程个数是有上限的当线程个数到达上限时客户端发来的消息作为任务就在任务队列中等待线程可以暂时不处理线程池版本TcpEchoServer.hpp#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp #include ThreadPool.hpp static const int gdefaultfd -1; static const int gbacklog 8; using task_t std::functionvoid(); class TcpEchoServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; std::string echo_string server echo# ; echo_string buffer; LOG(LogLevel::INFO) 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!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: TcpEchoServer(uint16_t port 8080) :_listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); //接入线程池 ThreadPooltask_t::GetInstance()-Push([sockfd, this, client](){ this-HandlerIO(sockfd, client); }); } } ~TcpEchoServer(){} private: int _listensockfd; uint16_t _port; }; #endif上述的 HanderIO 本质是长任务因为内部是 while 死循环来一个客户端请求线程池中就会有一个线程去处理但只要客户端不退出这个线程就要一直处理这个客户端请求这就叫做长任务长任务意味着要维持长连接这种任务其实是不适合用线程池来处理的后续会有更合适的做法此处仅仅是为了 demo 演示 !V2 版本 - CommandServer这一版的服务器我们实现的功能是用户发来一个指令服务器将执行完指令的结果返回给客户端CommandServer.hpp 中我们在类中加入回调函数对象成员参数为 string 表示客户端发来的命令返回值为 string 表示命令执行后的结果在 read 之后调用回调函数将返回值 write 给客户端即可完成命令集解析#ifndef __TCP_ECHO_SERVER_HPP__ #define __TCP_ECHO_SERVER_HPP__ #include iostream #include cstdlib #include cstring #include functional #include sys/wait.h #include sys/types.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include Comm.hpp #include logger.hpp #include InetAddr.hpp static const int gdefaultfd -1; static const int gbacklog 8; using callback_t std::functionstd::string (const std::string); class CommandServer { private: void HandlerIO(int sockfd, InetAddr client) { char buffer[1024]; while(true) { buffer[0] 0; ssize_t n read(sockfd, buffer, sizeof(buffer) - 1); if(n 0) { buffer[n] 0; LOG(LogLevel::INFO) client.ToString() say: buffer; std::string result _cb(buffer); //_cb回调负责将命令执行的结果返回 write(sockfd, result.c_str(), result.size()); } else if(n 0) { LOG(LogLevel::INFO) client client.ToString() quit, me too!; break; } else { LOG(LogLevel::WARNING) read client client.ToString() error; break; } } close(sockfd); //必须手动关闭文件描述符! } public: CommandServer(callback_t cb, uint16_t port 8080) :_cb(cb), _listensockfd(gdefaultfd), _port(port) {} void Init() { //1.create socket _listensockfd socket(AF_INET, SOCK_STREAM, 0); if(_listensockfd 0) { LOG(LogLevel::FATAL) create socket error; exit(SOCKET_CREATE_ERROR); } LOG(LogLevel::INFO) create socket success: _listensockfd; //2.bind socket InetAddr local(_port); if(bind(_listensockfd, local.Addr(), local.Length()) ! 0) { LOG(LogLevel::FATAL) bind socket error; exit(SOCKET_BIND_ERROR); } LOG(LogLevel::INFO) bind socket success: _listensockfd; //3.set socket listen if(listen(_listensockfd, gbacklog) ! 0) { LOG(LogLevel::FATAL) listen socket error; exit(SOCKET_LISTEN_ERROR); } LOG(LogLevel::INFO) listen socket success: _listensockfd; } struct ThreadData { ThreadData(int sockfd, CommandServer* self, InetAddr addr) :_sockfd(sockfd), _self(self), _addr(addr) {} int _sockfd; CommandServer* _self; InetAddr _addr; }; static void* Routine(void* args) { ThreadData* td static_castThreadData*(args); pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接 td-_self-HandlerIO(td-_sockfd, td-_addr); return nullptr; } void Start() { while(true) { //1. accept socket struct sockaddr_in peer; socklen_t len sizeof(peer); int sockfd accept(_listensockfd, (struct sockaddr*)peer, len); if(sockfd 0) { LOG(LogLevel::WARNING) accept socket error; continue; //让服务器继续去accept其他链接 } //2.echo server InetAddr client(peer); LOG(LogLevel::INFO) accept socket success: sockfd , client addr: client.ToString(); pthread_t tid; ThreadData* td new ThreadData(sockfd, this, client); pthread_create(tid, nullptr, Routine, td); } } ~CommandServer(){} private: int _listensockfd; uint16_t _port; callback_t _cb; }; #endif我们将命令解析封装成一个类Command.hppCommand.hpp#include iostream #include string #include vector #include cstdio #include cstring #include logger.hpp class Command { private: bool isSafe(const std::string cmd) { for(auto t : _cmd_black_list) { if(strstr(cmd.c_str(), t.c_str())) //判断t是否是cmd的子串 { return false; } } return true; } public: Command(){ _cmd_black_list.push_back(rm); _cmd_black_list.push_back(shutdown); _cmd_black_list.push_back(kill); _cmd_black_list.push_back(userdel); } std::string Exec(const std::string cmd) { if(!isSafe(cmd)) return 坏人; std::string safe_cmd cmd 21; FILE* fp popen(safe_cmd.c_str(), r); std::string result; if(fp NULL) { result cmd exec error; } else { char buffer[1024]; while(fgets(buffer, sizeof(buffer), fp) ! nullptr) { result buffer; } pclose(fp); } return result; } ~Command(){} private: std::vectorstd::string _cmd_black_list; //指令黑名单 };ServerMain.cc#include CommandServer.hpp #include memory #include Command.hpp void Usage(std::string proc) { std::cerr Usgae: proc localport std::endl; } int main(int argc, char* argv[]) { if(argc ! 2) { Usage(argv[0]); return 1; } uint16_t port std::stoi(argv[1]); Command cmdObj; std::unique_ptrCommandServer tsvr std::make_uniqueCommandServer( [cmdObj](const std::string cmdstr) { return cmdObj.Exec(cmdstr); } ,port ); tsvr-Init(); tsvr-Start(); return 0; }inet_ntoa 的线程安全问题我们在学习 Udp Socket 和 Tcp Socket 时需要将 4字节 ip 转为字符串 ip 时使用的函数都是 inet_ntoa但其实 inet_ntoa 并不是线程安全的inet_ntoa 的内部简化实现如下char *inet_ntoa(struct in_addr in) { static char buf[16]; // 这是一个静态缓冲区所有线程共享 unsigned char *bytes (unsigned char *)in.s_addr; snprintf(buf, sizeof(buf), %d.%d.%d.%d, bytes[0], bytes[1], bytes[2], bytes[3]); return buf; // 返回指向共享缓冲区的指针 }核心问题使用一个静态内部缓冲区来存储转换后的IP地址字符串并返回指向该缓冲区的指针。后果该缓冲区被进程中所有线程共享。当多个线程几乎同时调用此函数时后调用的线程会覆盖前一个线程存放在缓冲区中的数据导致所有线程最终都可能得到错误的结果。下面这段代码可以验证 inet_ntoa 确实不是线程安全的#include stdio.h #include netinet/in.h #include arpa/inet.h int main() { struct sockaddr_in addr1; struct sockaddr_in addr2; addr1.sin_addr.s_addr 0; addr2.sin_addr.s_addr 0xffffffff; char* ptr1 inet_ntoa(addr1.sin_addr); char* ptr2 inet_ntoa(addr2.sin_addr); printf(ptr1: %s, ptr2: %s\n, ptr1, ptr2); return 0; }因此以后需要将网络序列转主机序列时不要再用 inet_ntoa 函数了直接使用 inet_ntop 函数该函数要求用户提供存放转换后的字符串ip的缓冲区因此该函数是线程安全的参数af协议族使用 AF_INET 表示 IPV4src指向二进制地址的指针(网络地址)如 struct sockaddr_in 的地址dst指向输出字符串缓冲区的指针由调用者提供size缓冲区大小返回值成功返回 dst 指针失败返回 nullptr错误码被设置void NetToHost() { _port ntohs(_addr.sin_port); // _ip inet_ntoa(_addr.sin_addr); //不是线程安全的! //最佳实践, 是线程安全的! char buffer[1024]; inet_ntop(AF_INET, _addr.sin_addr, buffer, sizeof(buffer)); _ip buffer; }inet_addr 的设计缺陷inet_addr 的作用是把点分十进制的字符串变成网络整数 IP它是线程安全的因为它只是单纯地解析你传进去的字符串然后直接把算出来的 32 位整数传值返回给你不需要依赖任何全局静态变量。因为它在设计上有一个无法填补的逻辑漏洞如果转换失败比如你传了个 hello它会返回 INADDR_NONE也就是 -1 的补码形式 0xFFFFFFFF。但是有一个完全合法且非常有用的 IP 地址叫做 255.255.255.255局域网全网广播地址。当你试图转换这个合法地址时它转换出来的整数刚好也是 0xFFFFFFFF这就导致你根本无法分辨它返回 -1 到底是因为报错还是因为你传的就是 255.255.255.255。参数af协议族使用 AF_INET 表示 IPV4src指向待转换的点分十进制字符串dst指向存储二进制结果的指针返回值1表示转换成功0表示格式错误(如src不是有效的地址字符串)-1表示系统错误(如 af 不支持)错误码被设置void HostToNet() { 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()); //有设计上的缺陷 //最佳实践, inet_pton inet_pton(AF_INET, _ip.c_str(), _addr.sin_addr); }