目录
[V1版本 - EchoServer](#V1版本 - EchoServer)
[V2 版本 - CommandServer](#V2 版本 - CommandServer)
[inet_ntoa 的线程安全问题](#inet_ntoa 的线程安全问题)
[inet_addr 的设计缺陷](#inet_addr 的设计缺陷)
V1版本 - EchoServer
Tcp Socket 和 Udp Socket 很多地方是类似的,我们重点介绍不同的地方!
第一步 调用 socket 创建套接字(第二个参数选择 SOCK_STREAM 流式服务),第二步 bind,依旧采用 绑定本主机任意 ip 地址的方式,由于 Tcp 是面向连接的,因此在正式通信前要先建立连接,这就要求 服务器先处于 listen(监听) 状态
cpp
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数: sockfd 为 socket 返回的套接字描述符,backlog 表示 套接字内核连接队列的最大长度,这个参数后续会详细介绍
返回值:成功返回0,失败返回-1,错误码被设置
设置完监听状态后,这就意味着如果有客户端发出了连接请求,服务器是能收到的,下来就是 调用 accept 接收请求了!
cpp
#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 类
cpp
#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;
};
#endif
HandlerIO 函数就用于读取客户端发送来的消息,并将消息发送回客户端,Tcp 读写消息也有自己专门的接口,我们此处先不做介绍,先使用 read / write 接口就可以完成字节流的读写!
cpp
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,下来就是客户端主动发起连接请求了!
cpp
#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 发送和读取消息了!
cpp
#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) { accept;HandlerIO},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
cpp
#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
cpp
#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_cast<ThreadData*>(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
cpp
#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::function<void()>;
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();
//接入线程池
ThreadPool<task_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 给客户端即可完成命令集解析!
cpp
#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::function<std::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_cast<ThreadData*>(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.hpp
Command.hpp
cpp
#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 + " 2>&1";
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::vector<std::string> _cmd_black_list; //指令黑名单
};
cpp
#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_ptr<CommandServer> tsvr = std::make_unique<CommandServer>(
[&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 的内部简化实现如下:
cpp
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 确实不是线程安全的!
cpp
#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 表示 IPV4
src:指向二进制地址的指针(网络地址),如 struct sockaddr_in 的地址
dst:指向输出字符串缓冲区的指针(由调用者提供)
size:缓冲区大小
**返回值:**成功返回 dst 指针,失败返回 nullptr,错误码被设置
cpp
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 表示 IPV4
src:指向待转换的点分十进制字符串
dst:指向存储二进制结果的指针
**返回值:**1表示转换成功,0表示格式错误(如src不是有效的地址字符串),-1表示系统错误(如 af 不支持),错误码被设置
cpp
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);
}
