- [V1- Echo Server](#V1- Echo Server)
- [V2- Echo Server 多进程版本](#V2- Echo Server 多进程版本)
- V3-EchoServer多线程版本
- V4-EchoServer线程池版本
- V5-多线程远程命令执行
- 完整代码
V1- Echo Server
Inite
和UDP一样,TCP我们也学着使用他的接口来实现一些简单的网络通信功能。
首先是实现echo功能,实际上我们后面实现的也是echo功能,只不过对他改进而已。
首先看到我们main函数的主逻辑:

那么接下来就开始实现TcpServer的封装。
首先我们要创建套接字,tcp这里创建的套接字我们称为监听套接字 ,具体原因后面阐述。
然后我们需要选择socket的第二个参数为:

这是有连接、面向字节流的数据传输方式,正对应我们的tcp协议。

随后自然是绑定:

然后和UDP不同的是,TCP是面向连接的通信,因此我们要先建立连接.我们在服务端获取连接用的函数是listen:

第一个参数传入我们的监听套接字,第二个参数目前不做讨论,传入8.

完整初始化逻辑:
cpp
void InitServer()
{
int listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (listensockfd < 0)
{
LOG(FATAL, "socket create error\n");
exit(SOCKET_ERROR);
}
LOG(INFO, "socket create success\n");
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (::bind(listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
LOG(FATAL, "bind failed\n");
exit(BIND_ERROR);
}
if (listen(listensockfd, gblcklog) < 0)
{
LOG(FATAL, "listen erroe\n");
exit(LISTEN_ERR);
}
LOG(INFO, "listen success\n");
}
Loop
获取连接之后,我们还要建立连接,这里用的函数就是accept

这里要注意我们的返回值:

没错,返回了一个文件描述符。这里才是我们通信的地方。
一开始创建的套接字只是源源不断监听有没有客户端向我们建立连接的,因此叫做监听套接字。

建立完连接就要开始实现我们的功能:

Service
由于我们的TCP协议是面向字节流的,我们可以直接用write和read函数向文件缓冲区写和读:

因为这样的逻辑已经反复写过很多遍就不做赘述了。
Clinet
首先还是老一套的逻辑,我们必须知道服务器的ip地址和端口号:

然后我们要向服务器建立连接,这里客户端建立连接的函数用connect:


最后向sockfd写入即可:
cpp
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect error" << std::endl;
exit(2);
}
while (true)
{
std::string message;
std::cout << "Enter #";
std::getline(std::cin, message);
write(sockfd, message.c_str(), message.size());
char echo_buffer[1024];
n = read(sockfd, echo_buffer, sizeof(echo_buffer) - 1);
if (n > 0)
{
echo_buffer[n] = 0;
std::cout << echo_buffer << std::endl;
}
}
::close(sockfd);
return 0;
}
我们来试行一下:

没有毛病。
但这个实现有个问题,就是服务器是单进程的,意味着同时只能响应一个客户端,这自然是不好的。
V2- Echo Server 多进程版本
为解决上述问题,我们自然可以采用多进程的做法:

注意到我们其实并不想让父进程等待子进程退出信息,我们可以手动忽略子进程的退出信号。
然后子进程最好将监听套接字关闭,防止误操作。
父进程则是将sockfd关闭,防止文件描述符溢出。
V3-EchoServer多线程版本
进程创建的开销还是要比线程大的,我们可以考虑使用多线程来解决上述问题:
因为线程是共享文件描述符的,因此我们不需要也不能关闭文件了。
此外,我们必然不想要线程等待,因此要让线程分离。
但是我们Service的参数这么多该如何传进去呢?
可以考虑封装一个内部类:

这样我们还把this指针传进去,就能调用内部成员函数了:

尝试调用:

V4-EchoServer线程池版本
有了多线程版本自然可以实现一个线程池版本,这个其实非常简单,只需要将任务入队列即可:


调用一下:

可以看到大体没有问题,还需要调整一些细节。这里就不调整了,毕竟这个并不适合用线程池。
V5-多线程远程命令执行
一直都是执行无聊的echo命令也没有意思,我们这里实现一个远程传来命令,服务器执行并返回结果。
为了更好地实现IO和业务的解耦,我们传入相应的回调函数即可:



Command
那么接下来我们实现指令功能吧。
首先自然是简单的初始化工作:

然后就可以调整ServerMain的逻辑:

然后我们就要接收远端传来的指令,这里可以用TCP的读写接口:


和write、read相比就是多了个flags标记位罢了,这里依旧置0处理。

那么问题来了,我们该如何调用指令呢?
用我们自主实现的shell吗?
当然不用这么麻烦,这里直接调用一个库函数即可:

这里就会将指令运行的结果放到文件里。这个文件也是一个类似管道的内核文件。第二个参数就是该文件的打开方式,我们当然是以只读形式打开:

讲道理我们现在已经可以运行了,但是远端执行的指令不可控,我们还可以设置一个简单的白名单。只允许执行白名单上的指令:


然后就要做白名单检查了:


不过老实说这个白名单心理安慰作用比较大。
毕竟我们可以;或|或&&等形式执行多条命令。

运行结果尚可。