在之前的篇章当中我们学习了socket编程的基本概念并且了解了Udp套接字进行通信的方式,实现了翻译字典和简单聊天室等的基于UDP套接字的客服端和服务器之间的通信。那么在了解了socket套接字UDP之后接下来就继续来实现socket套接字TCP,在此和之前学习UDP的类似也是先学习对应的接口再使用这些的接口来实现对应的实例。但是和之前不同的是在此已经有了UDP的基础之后学习TCP套接字的接口就会非常的类似。通过本篇的学习将能够使用TCP对应的接口来实现客户端和服务器之间的通信,在此我们还会了解到和之前学习的UDP的不同点,接下来就开始本篇的学习吧!!!

1. TCP套接字接口
实际上在TCP当中套接字对应的接口和之前我们学习的UDP是很类似的,只不过相比UDP,TCP是需要进行连接之后才能进行通过的,那么相比UDP就多出了建立连接的接口。
接口如下所示:
创建套接字:
cpp
// 创建套接字
int socket(int domain, int type, int protocol);
参数:
domain :协议族(常用 AF_INET 表示 IPv4)
type :套接字类型(TCP 使用 SOCK_STREAM)
protocol :协议编号,通常为 0(自动选择)
返回值:
成功返回套接字描述符(int),失败返回 -1。
作用:
创建一个通信端点,是网络通信的起点。
bind绑定:
cpp
// 绑定本地地址和端口(服务器端使用)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd :由 socket() 创建的套接字描述符
addr :指向包含本地 IP 和端口的 sockaddr_in 结构体
addrlen :addr 结构体的大小
返回值:
成功返回 0,失败返回 -1。
作用:
将套接字与指定的 IP 地址和端口号绑定。
(服务器端必须绑定,客户端通常不需要显式 bind)
listen监听:
cpp
// 将套接字设置为监听状态
int listen(int sockfd, int backlog);
参数:
sockfd :绑定好地址的套接字
backlog :允许的最大等待连接队列长度
返回值:
成功返回 0,失败返回 -1。
作用:
让当前套接字进入监听状态,准备接受客户端连接。
接收连接请求:
cpp
// 客户端:主动连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd :客户端套接字描述符
addr :服务器的地址结构体(IP + 端口)
addrlen :地址结构体长度
返回值:
成功返回 0,失败返回 -1。
作用:
发起与服务器的 TCP 连接请求。
发起连接请求:
cpp
// 服务器端:接受连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd :监听套接字(由 listen() 创建)
addr :用于保存客户端地址信息(可为 NULL)
addrlen :保存地址长度(可为 NULL)
返回值:
成功返回新的套接字描述符(用于通信)
失败返回 -1。
作用:
从连接队列中取出一个已完成连接,返回新的"已连接套接字"。
注意:服务器通过这个新套接字与客户端通信。
数据接收和发送:
cpp
// 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd :已连接的套接字描述符
buf :要发送的数据缓冲区
len :数据长度(字节)
flags :一般为 0(默认发送)
返回值:
成功返回实际发送的字节数,失败返回 -1。
作用:
通过 TCP 连接发送数据(可靠传输)。
// 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd :已连接的套接字描述符
buf :用于接收数据的缓冲区
len :缓冲区大小
flags :一般为 0
返回值:
成功返回接收到的字节数;
返回 0 表示对方关闭连接;
失败返回 -1。
作用:
从 TCP 连接中接收数据。
除了可以使用以上的接口来实现数据的传输,实际上在TCP当中一个进程进行数据传输的时候,那么实际上就可以类似之前打开系统当中的一个文件,因此使用read和write也能实现数据的传输。
cpp
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
通过以上的讲解就可以看出TCP套接字当中的接口很多和之前UDP当中的都是一样的,只不过相比之前多了accept和connect来进行连接和获取连接。
2. 基于TCP套接字实现简单通信
以上我们了解到了TCP套接字当中对应的通信接口,那么接下来就试着使用以上的接口来实现简单的客户端和服务器之间的通信。
2.1 普通echo_server版本
在此和之前的UDP当中实现简单的通信类似还是使用Server.hpp当中实现进行通信的类,在Server.cc当中实现通信当中的服务器,在Client.cc当中实现通信的客户端。实现的代码如下所示:
TcpServer.hpp
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "log.hpp"
#include <functional>
using namespace LogModule;
using func_t = std::function<std::string(std::string &)>;
//默认等待队列最大长度
const int default_ln = 5;
class Server
{
public:
Server(int port, func_t func)
: _port(port),
_isrunning(false),
_func(func)
{
}
//初始化
void Init()
{
//创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(ExitCode::SOCkET_ERR);
}
LOG(LogLevel::INFO) << "socket success!";
InetAddr addr(_port);
//进行bind绑定
int n = bind(_listensockfd, addr.GetAddr(), addr.SockLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success!";
//进行listen监听
n = listen(_listensockfd, default_ln);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success!";
}
//启动服务器
void Start()
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
sockaddr_in addr;
unsigned int len = sizeof(addr);
//使用accept发消息
int sockfd = accept(_listensockfd, COW(addr), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
LOG(LogLevel::INFO) << "accept success";
InetAddr peer(addr);
while (1)
{
//从sockfd当中获取对应的数据
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
LOG(LogLevel::ERROR) << "read error";
close(sockfd);
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clients quit";
close(sockfd);
break;
}
else
{
buffer[n] = 0;
std::string ret = "client say:";
ret += buffer;
LOG(LogLevel::INFO) << peer.StringAddr() << ",Client say:" << buffer;
//将数据写入到sockfd对应的文件
ssize_t n = write(sockfd, ret.c_str(), ret.size());
}
}
}
// close(_listensockfd);
}
private:
//套接字
int _listensockfd;
//端口号
uint16_t _port;
//是否运行标志位
bool _isrunning;
//回调函数
func_t _func;
};
cpp
#include <iostream>
#include <string>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include <memory>
// 回调函数
std::string StringEcho(std::string &str)
{
std::string ret = "client say:";
return ret + str;
}
// server port
int main(int argc, char *argv[])
{
// 判断命令行参数是否符合要求
if (argc != 2)
{
std::cout << "Server ip" << std::endl;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
// 创建对应的Server对象
std::shared_ptr<Server> server = std::make_shared<Server>(port, StringEcho);
server->Init();
server->Start();
return 0;
}
cpp
#include <iostream>
#include "log.hpp"
#include "InetAddr.hpp"
#include <string>
#include "Common.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{
// 判断命令行参数是否符合要求
if (argc != 3)
{
std::cout << "Client IP Port" << std::endl;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[2]);
std::string ip = argv[1];
// 创建客户端套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 客户端无需进行显示的绑定
InetAddr addr(ip, port);
// 建立于服务器的连接
int n = connect(sockfd, addr.GetAddr(), addr.SockLen());
if (n < 0)
{
std::cout << "connect error" << std::endl;
exit(ExitCode::CONNECT_ERR);
}
// 存储接收数据的缓冲区
char buffer[4096];
while (1)
{
std::cout << "please input:";
std::string input;
std::getline(std::cin, input);
if (input == "QUIT")
{
break;
}
// 发送数据到服务器
int n = write(sockfd, input.c_str(), input.size());
if (n < 0)
{
std::cout << "write error" << std::endl;
}
// 从服务器当中得到数据
n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
std::cout << "read error" << std::endl;
exit(ExitCode::READ_ERR);
}
else if (n > 0)
{
buffer[n] = 0;
std::cout << "Server say:" << buffer << std::endl;
}
else
{
std::cout << "Server quit" << std::endl;
exit(ExitCode::READ_ERR);
}
}
close(sockfd);
return 0;
}
将以上的代码编写之后,接下来就再将对应的makefile文件进行编写,以实现自动化编译
bash
.PHONY:all
all:Server Client
Server:TcpServer.cc
g++ -o $@ $^ -std=c++17
Client:TcpClient.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f Server Client
实现之后将以上的代码进行殡编译之后运行查看是否符合我们的要求,运行的结果如下所示:

通过的输出结果就可以看出客户端和服务器是能建立对应的连接的,并且在建立连接之后服务器是会阻塞的等待用户的输入,当用户输入之后会将对应的数据传输给服务器,服务器当中得到数据之后进行对应的处理后将数据再发送给客户端。
2.2 多进程echo_server版本
以上我们已经实现了基本的客户端和服务器之间的通信,但是实际上当前还是存在一定的问题的,就是当前是只能是同时进行一个进程的通信,这时就会造成当多个进程和服务器进行通信的时候会让第一个进程之后的进程再用户输入数据之后在发送的阶段会一直阻塞。

要解决以上的问题就需要让服务器当中进行数据接收和发送的不能是再由一个进程来实现,而是要让一个客户端和服务器之间的通信就由一个进程来实现。因此基于以上的分析就需要将原来我们实现的客户端修改为多进程的版本。
实现的代码如下所示:
TcpServer.hpp
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "log.hpp"
#include <unistd.h>
#include <functional>
#include <sys/wait.h>
using namespace LogModule;
using func_t = std::function<std::string(std::string &)>;
// 默认等待队列最大长度
const int default_ln = 5;
class Server
{
public:
Server(int port, func_t func)
: _port(port),
_isrunning(false),
_func(func)
{
}
// 初始化
void Init()
{
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(ExitCode::SOCkET_ERR);
}
LOG(LogLevel::INFO) << "socket success!";
InetAddr addr(_port);
// 进行bind绑定
int n = bind(_listensockfd, addr.GetAddr(), addr.SockLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success!";
// 进行listen监听
n = listen(_listensockfd, default_ln);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success!";
}
void Service(int sockfd, InetAddr &peer)
{
char buffer[4096];
while (1)
{
// 从sockfd当中获取对应的数据
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
LOG(LogLevel::ERROR) << "read error";
close(sockfd);
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clients quit";
close(sockfd);
break;
}
else
{
buffer[n] = 0;
std::string ret = "client say:";
ret += buffer;
LOG(LogLevel::INFO) << peer.StringAddr() << ",Client say:" << buffer;
// 将数据写入到sockfd对应的文件
ssize_t n = write(sockfd, ret.c_str(), ret.size());
}
}
}
// 启动服务器
void Start()
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
sockaddr_in addr;
unsigned int len = sizeof(addr);
// 使用accept发消息
int sockfd = accept(_listensockfd, COW(addr), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
LOG(LogLevel::INFO) << "accept success";
InetAddr peer(addr);
// 通过创建子进程的方式来实现数据的收发
pid_t fd = fork();
if (fd < 0)
{
LOG(LogLevel::ERROR) << "fork error";
exit(ExitCode::FORK_ERR);
}
else if (fd == 0)
{
// 在子进程当中关闭对应的listensockfd,并且再创建对应的子进程之后将当前的进程退出
// 这时就不再需要原来的父进程对该进程进行等待的操作
close(_listensockfd);
if (fork() > 0)
{
exit(ExitCode::OK);
}
// 进行数据的收发
Service(sockfd, peer);
exit(ExitCode::OK);
}
else
{
// 父进程当中关闭当前的sockfd
close(sockfd);
int ret = waitpid(fd, nullptr, 0);
(void)ret;
}
// Service(sockfd, peer);
}
// close(_listensockfd);
}
private:
// 套接字
int _listensockfd;
// 端口号
uint16_t _port;
// 是否运行标志位
bool _isrunning;
// 回调函数
func_t _func;
};
其余的代码和其他的实现还是一样的,那么这时候只需要将以上的TcpServer.hpp修改为以上的多进程形式,这时就可以实现多客户端同时对服务器的访问。
以上实现的的大致原理就是进行对应的accept操作之后接下来再使用fork创建出子进程之后,在子进程当中再创建出对应的孙子进程,在此实现成这样的原因是该原来再父进程当中是需要进行阻塞式等待,那么这时父进程还是会出现阻塞的情况,因此创建出孙子进程之后将子进程退出之后,这时孙子进程就会被一号进程领养,那么这时父进程就不再会被阻塞。这时候就能让不同的客户端给同时的访问服务器。
重新编译以上的代码,在不同的客户端下运行如下所示:
注:在此要在可执行程序传输到其他的客服端下,那么这时就需要将程序采取静态编译的方式。


通过以上的输出就可以看出当前确实能实现对应的多客户端同时的运行。
2.3 多线程echo_server版本
以上我们基于多进程的方式来实现对应的多客户端的方式,那么接下来试着来使用多线程的方式实现,实际上实现当中其他部分的代码都不需要进行修改只需要在Stat函数当中将对应的使用accept创建连接之后的通过子进程来实现服务的方式修改为通过线程来实现。
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "log.hpp"
#include <unistd.h>
#include <functional>
#include<sys/wait.h>
using namespace LogModule;
using func_t = std::function<std::string(std::string &)>;
// 默认等待队列最大长度
const int default_ln = 5;
class Server
{
public:
//............
void Service(int sockfd, InetAddr &peer)
{
char buffer[4096];
while (1)
{
// 从sockfd当中获取对应的数据
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
LOG(LogLevel::ERROR) << "read error";
close(sockfd);
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clients quit";
close(sockfd);
break;
}
else
{
buffer[n] = 0;
std::string ret = "client say:";
ret += buffer;
LOG(LogLevel::INFO) << peer.StringAddr() << ",Client say:" << buffer;
// 将数据写入到sockfd对应的文件
ssize_t n = write(sockfd, ret.c_str(), ret.size());
}
}
}
//线程执行函数,为实现参数的匹配需要设置为静态成员函数
static void* Routine(void* args)
{
pthread_detach(pthread_self());
ThreadData* td=static_cast<ThreadData*>(args);
td->_resv->Service(td->_sockfd,td->_addr);
delete td;
return nullptr;
}
// 启动服务器
void Start()
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
sockaddr_in addr;
unsigned int len = sizeof(addr);
// 使用accept发消息
int sockfd = accept(_listensockfd, COW(addr), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
LOG(LogLevel::INFO) << "accept success";
InetAddr peer(addr);
//V2多线程版本
//创建存储服务器信息的结构体
ThreadData* data=new ThreadData(sockfd,peer,this);
pthread_t p;
//创建线程
pthread_create(&p,nullptr,Routine,(void*)data);
//Service(sockfd, peer);
}
// close(_listensockfd);
}
private:
// 套接字
int _listensockfd;
// 端口号
uint16_t _port;
// 是否运行标志位
bool _isrunning;
// 回调函数
func_t _func;
};
以上就是实现的对应代码,在此我们是通过pthread库当中提供的接口来实现,而未使用我们自己创建的线程类来调用,这是为了能更好的向你展示出整体的调用过程是如何实现的。
首先创建应该名为ThreadData 的结构体在该结构体的内部成员变量有对应的套接字、InerAddr对象、以及Server指针。通过pthread_ctreate创建出线程之后接下来将创建出的ThreadData对象作为参数。通过在线程的执行函数Routine内调用对应的数据处理函数Service。
在此引入一个工具netstat来查看网络状态。在此使用该工具的时候有以下的选项
• n 拒绝显示别名, 能显示数字的全部转化成数字
• l 仅列出有在 Listen (监听) 的服務状态
• p 显示建立相关链接的程序名
• t (tcp)仅显示 tcp 相关选项
在此将Server和Client运行起来之后接下来使用netstat来将系统当中的有有端口号信息为8080的进程

通过以上的输出结果就可以看出在当前的主机当中分别存在端口号一个发送端和一个接收方为8080的进程,这是因为当中我们是将客户端和服务器是在同一个主机当中运行的。并且通过以上也能验证在TCP连接中是全双工的,能实现同时的接和发。
2.4 线程池echo_server版本
以上在实现了多进程和多线程的版本的echo_server,那么接下来再来实现线程池版本的。实现的代码如下所示:
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "log.hpp"
#include <unistd.h>
#include <functional>
#include <sys/wait.h>
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;
using namespace LogModule;
using func_t = std::function<void()>;
// 默认等待队列最大长度
const int default_ln = 5;
class Server
{
public:
Server(int port)
: _port(port),
_isrunning(false)
{
}
// 初始化
void Init()
{
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(ExitCode::SOCkET_ERR);
}
LOG(LogLevel::INFO) << "socket success!";
InetAddr addr(_port);
// 进行bind绑定
int n = bind(_listensockfd, addr.GetAddr(), addr.SockLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success!";
// 进行listen监听
n = listen(_listensockfd, default_ln);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success!";
}
void Service(int sockfd, InetAddr &peer)
{
char buffer[4096];
while (1)
{
// 从sockfd当中获取对应的数据
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
LOG(LogLevel::ERROR) << "read error";
close(sockfd);
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clients quit";
close(sockfd);
break;
}
else
{
buffer[n] = 0;
std::string ret = "client say:";
std::string str = buffer;
//std::string ret = _func(str, peer);
ret += buffer;
LOG(LogLevel::INFO) << peer.StringAddr() << ",Client say:" << buffer;
// 将数据写入到sockfd对应的文件
ssize_t n = write(sockfd, ret.c_str(), ret.size());
}
}
}
class ThreadData
{
public:
ThreadData(int sockfd, InetAddr &addr, Server *resv)
: _sockfd(sockfd),
_addr(addr),
_resv(resv)
{
}
int _sockfd;
InetAddr _addr;
Server *_resv;
};
// 线程执行函数,为实现参数的匹配需要设置为静态成员函数
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_resv->Service(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
// 启动服务器
void Start()
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
sockaddr_in addr;
unsigned int len = sizeof(addr);
// 使用accept发消息
int sockfd = accept(_listensockfd, COW(addr), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
LOG(LogLevel::INFO) << "accept success";
InetAddr peer(addr);
// V3线程池版本
ThreadPool<func_t>::GwetInstance()->Enqueue([this,sockfd,&peer](){
this->Service(sockfd,peer);
});
}
// close(_listensockfd);
}
private:
// 套接字
int _listensockfd;
// 端口号
uint16_t _port;
// 是否运行标志位
bool _isrunning;
// 回调函数
func_t _func;
};
以上的代码就通过引入之前我们实现的线程池之后将每个客户端和服务器建立连接之后,接下来将对应的任务插入到线程池当中的任务队列当中,那么这时线程池就会自动的在内部进行任务的调度,在此就实现服务器和客户端之间连接和数据处理的解耦,不同的功能就在不同对的模块当中实现。
编译以上的代码,运行的结果如下所示:




2.4 实时翻译字典
以上我在实现了简单的echo_server的代码,那么接下来就继续来试着实现基于TCP套接字的实时翻译的字典,在之前学习UDP套接字当中我们已经实现对应翻译模块的代码,那么在此就只需要将Server和Client端的代码进行修改即可,并且将之前实现的Dict.hpp代码拷贝到当前的目录当中。
在此我们需要将Server.hpp当中的代码修改为以下的形式:
cpp
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include "log.hpp"
#include <unistd.h>
#include <functional>
#include <sys/wait.h>
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;
using namespace LogModule;
//具体翻译功能的回调函数
using task_t = std::function<std::string( std::string &words, InetAddr &addr)>;
//线程池任务队列当中插入的任务
using func_t = std::function<void()>;
// 默认等待队列最大长度
const int default_ln = 5;
class Server
{
public:
Server(int port, task_t task)
: _port(port),
_isrunning(false),
_task(task)
{
}
// 初始化
void Init()
{
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(ExitCode::SOCkET_ERR);
}
LOG(LogLevel::INFO) << "socket success!";
InetAddr addr(_port);
// 进行bind绑定
int n = bind(_listensockfd, addr.GetAddr(), addr.SockLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
exit(ExitCode::BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success!";
// 进行listen监听
n = listen(_listensockfd, default_ln);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(ExitCode::LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success!";
}
void Service(int sockfd, InetAddr &peer)
{
char buffer[4096];
while (1)
{
// 从sockfd当中获取对应的数据
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if (n < 0)
{
LOG(LogLevel::ERROR) << "read error";
close(sockfd);
break;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clients quit";
close(sockfd);
break;
}
else
{
buffer[n] = 0;
std::string str = buffer;
std::string ret = _task(str, peer);
if(ret!="None")ret += buffer;
LOG(LogLevel::INFO) << peer.StringAddr() << ",Client say:" << buffer;
// 将数据写入到sockfd对应的文件
ssize_t n = write(sockfd, ret.c_str(), ret.size());
}
}
}
class ThreadData
{
public:
ThreadData(int sockfd, InetAddr &addr, Server *resv)
: _sockfd(sockfd),
_addr(addr),
_resv(resv)
{
}
int _sockfd;
InetAddr _addr;
Server *_resv;
};
// 线程执行函数,为实现参数的匹配需要设置为静态成员函数
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->_resv->Service(td->_sockfd, td->_addr);
delete td;
return nullptr;
}
// 启动服务器
void Start()
{
_isrunning = true;
char buffer[1024];
while (_isrunning)
{
sockaddr_in addr;
unsigned int len = sizeof(addr);
// 使用accept发消息
int sockfd = accept(_listensockfd, COW(addr), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
LOG(LogLevel::INFO) << "accept success";
InetAddr peer(addr);
// V3线程池版本
ThreadPool<func_t>::GwetInstance()->Enqueue([this,sockfd,&peer](){
this->Service(sockfd,peer);
});
}
// close(_listensockfd);
}
private:
// 套接字
int _listensockfd;
// 端口号
uint16_t _port;
// 是否运行标志位
bool _isrunning;
// 回调函数
task_t _task;
};
将Server.cc修改以下的形式:
cpp
#include <iostream>
#include <string>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include <memory>
#include "Dict.hpp"
// 回调函数
std::string StringEcho(std::string &str)
{
std::string ret = "client say:";
return ret + str;
}
// server port
int main(int argc, char *argv[])
{
// 判断命令行参数是否符合要求
if (argc != 2)
{
std::cout << "Server ip" << std::endl;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
// 创建对应的Dict对象
Dict dict;
// 将dictionary.txt字典文件当中的内容加载
dict.LoadDict();
// 创建对应的Server对象
std::shared_ptr<Server> server = std::make_shared<Server>(port, [&dict](std::string &words, InetAddr &peer)
{ return dict.Translate(words, peer); });
// std::shared_ptr<Server> server=std::make_shared<Server>(port);
server->Init();
server->Start();
return 0;
}
以上代码实现的大体逻辑就是在Server.cc当中创建出对应得到Dict对象之后接下来将对象当中的成员函数Translate函数通过回调的方式作为参数传给创建出的Server对象,接下来在该Server对象当中调用对应Init函数喝Start函数,那么在Stat函数就会创建出一个线程池,这时将Service作为线程池要执行的任务,在该任务当中集体又进行客户端喝服务器之间的数据传输,并且在得到客户端的数据之后将该数据通过回调函数_task进行翻译的功能。
编译代码运行的结构如下所示:


2.5 远程命令执行
以上我们实现了不同版本的echo_server和实时翻译字典,那么接下来再来基于对应的TCP套接字实现一个远程命令执行的功能。在此要实现的是用户再远程的客户端当中输入对应的命令之后,服务器会通过网络得到对应的指令之后在服务器当前的路径下执行对应的命令,并且将执行的结果通过网络返回给对服务器。
在此就在对应的Command.hpp当中实现对应的命令操作,在该文件当中实现对应的Command类在该类当中再实现Excte函数,在服务器当中通过得到用户输入的命令之后再服务器当中将该命令在服务器当前的路径下执行,并且将执行的结果在该Excute当中将数据发送给指定的客户端。实现的代码如下所示:
cpp
#pragma once
#include <iostream>
#include "log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
class command
{
public:
command()
{
}
~command()
{
}
//命令执行结果发送功能函数
std::string Excute(const std::string &cmd, InetAddr &addr)
{
std::string who = addr.StringAddr();
//使用open得到对应命令的执行结果储存在fd文件描述符指向的管道当中
FILE *fp = popen(cmd.c_str(), "r");
if (fp == nullptr)
{
return std::string("你要执行的命令不存在") + cmd;
}
std::string res;
char line[1024];
//读取管道当中的数据,通过数据创建出返回的字符串res
while (fgets(line, sizeof(line), fp))
{
res += line;
}
std::string result = who + "excutedone,result is:\n" + res;
LOG(LogLevel::DEBUG) << result;
return result;
}
};
以上使用到了一个C库当中提供的函数popen,其实现的功能是将用户传入的参数当中的指令进行执行,并且将执行的结果通过对应的管道返回。
cpp
#include <stdio.h>
FILE *popen(const char *command, const char *type);
参数:
command:要执行的 shell 命令字符串,比如 "ls -l" 或 "grep something file.txt"。
type:打开管道的方向:
"r":从命令的标准输出读取(子进程 → 父进程)
"w":向命令的标准输入写入(父进程 → 子进程)
返回值:
是一个类似文件的指针(FILE*),你可以用 fgets、fputs 等函数来读写
使用以上的函数就不再需要我们使用之前传统的方式创建子进程只会再将子进程执行的结果写入到对应的文件当中。实际上在以上实现远程命令还可以创建对应的白名单,因为实际的远程执行的用户可能不是你自己,那么这时正在执行对的用户可能会执行一些非法的操作,这时就会造成了数据的丢失等问题。
那么为了解决以上的问题就可以在在Command类当中创建一个unordered_set的成员变量,那么在构造函数当中将远程可以执行的指令写入到插入到对应的unordered_set当中,实现的代码如下所示:
cpp
#pragma once
#include <iostream>
#include "log.hpp"
#include "InetAddr.hpp"
#include <unordered_set>
using namespace LogModule;
class command
{
public:
command()
{
st.insert("pwd");
st.insert("ls");
st.insert("whoaimi");
st.insert("ls -l");
st.insert("touch test.txt");
st.insert("cd ..");
st.insert("cd -");
}
~command()
{
}
bool IsSafe(const std::string str)
{
auto it = st.find(str);
return it == nullptr ? false : true;
}
// 命令执行结果发送功能函数
std::string Excute(const std::string &cmd, InetAddr &addr)
{
if (!IsSafe(cmd))
{
return "Nono";
}
std::string who = addr.StringAddr();
// 使用open得到对应命令的执行结果储存在fd文件描述符指向的管道当中
FILE *fp = popen(cmd.c_str(), "r");
if (fp == nullptr)
{
return std::string("你要执行的命令不存在") + cmd;
}
std::string res;
char line[1024];
// 读取管道当中的数据,通过数据创建出返回的字符串res
while (fgets(line, sizeof(line), fp))
{
res += line;
}
std::string result = who + "excutedone,result is:\n" + res;
LOG(LogLevel::DEBUG) << result;
return result;
}
private:
std::unordered_set<std::string> st;
};
以上将Command.hpp的代码实现之后,那么接下来就可以就可以在Server.cc文件当中创建出Comman对象之后再将创建出的Server对象的第二次参数传对应的lambda表达式。
cpp
#include <iostream>
#include <string>
#include "log.hpp"
#include "TcpServer.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include <memory>
#include"Dict.hpp"
#include"Command.hpp"
// 回调函数
std::string StringEcho(std::string &str)
{
std::string ret = "client say:";
return ret + str;
}
// server port
int main(int argc, char *argv[])
{
// 判断命令行参数是否符合要求
if (argc != 2)
{
std::cout << "Server ip" << std::endl;
exit(ExitCode::USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]);
//创建对应的Dict对象
// Dict dict;
// //将dictionary.txt字典文件当中的内容加载
// dict.LoadDict();
//创建Command对象
command cm;
std::shared_ptr<Server> server = std::make_shared<Server>(port, [&cm](std::string& words,InetAddr& peer){
return cm.Excute(words,peer);
});
//std::shared_ptr<Server> server=std::make_shared<Server>(port);
server->Init();
server->Start();
return 0;
}
编译以上的程序执行查看是否能满足我们的要求:

通过以上的输出结果就可以看出当前实现的远程命令行是符合我们的要求的。
以上就是本篇的全部内容了,接下来我们将继续来学习序列化和反序列化以及自定义协议相关的概念,通过下一篇的学习我们将对TCP有更深刻的理解欸,未完待续......