当前位置: 首页 > news >正文

【Linux网络编程】Socket - TCP

目录

V1版本 - Echo Server

初始化服务器

启动服务器

客户端

 一些BUG与问题

解决服务器无法一次处理多个请求的问题

多进程版本

多线程版本

线程池版本

V2版本 - 多线程远程执行命令


V1版本 - Echo Server

初始化服务器

TCP大部分内容与UDP是相同的,我们直接写代码

static const uint16_t gport = 8080;class TcpServer
{
public:TcpServer(int port = gport):_port(port){}void InitServer(){// 1. 创建TCP套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(_sockfd < 0){LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket create success, sockfd is: " << _sockfd;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;// 2. 绑定int n = ::bind(_sockfd, CONV(&local), sizeof(local));if(n < 0){LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _sockfd;}void Start(){}~TcpServer(){}
private:int _sockfd;uint16_t _port;
};

到这,除了创建套接字时传入的参数有区别之外,其他都是与UDP一样的。之前说过,UDP是无连接的,客户端创建完套接字之后,直接就可以向服务端发送消息,没有连接;TCP是有连接的,客户端要向服务端发送消息时,需要先建立连接,当连接建立成功时,才可以发送,也就是说,服务端,随时随地等待被连接

举一个例子帮助理解。就像我们去餐厅吃饭,不是一进去就直接吃,而是要和老板交代要吃什么,付完钱后等上菜才可以吃,与老板沟通的过程叫做建立连接,通过握手来建立连接,是协商的过程。并且任何正常餐厅,老板或服务员一定是在店里等待着客人去吃饭。所以,TCP在套接字绑定完成之后,需要将套接字设置为监听状态,监听状态就是随时等待别人来连接我

#include <sys/socket.h>int listen(int sockfd, int backlog);

通过这个系统调用,将指定的套接字设置为监听状态。第二个参数先不管,不要传入0或太大的数即可。成功返回0,失败返回-1,并设置errno。假设餐厅人很多,吃饭需要排队,老板肯定不会要求排队的人太多,因为都在店里必然会占一些空间,这个backlog就是限制排队的人数的。具体是什么后面会说。

#define BACKLOG 8void InitServer()
{// 1. 创建TCP套接字_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket create success, sockfd is: " << _sockfd;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;// 2. 绑定int n = ::bind(_sockfd, CONV(&local), sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _sockfd;// 3. 将套戒指设置为监听状态n = ::listen(_sockfd, BACKLOG);if (n < 0){LOG(LogLevel::FATAL) << "listen errno";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _sockfd;
}

注意:这里不会阻塞在listen处,listen只是设置了套接字的状态。

启动服务器

void Start()
{_isrunning = true;while (_isrunning){}
}
void Stop()
{_isrunning = false;
}
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

当服务器的套接字处于监听状态时,是不能直接获取消息的,而是需要先获取新连接。此时使用系统调用accept,表示从指定的文件描述符中获取新连接,这个套接字就是服务端的套接字,第二、三个参数是获取谁来连接服务器的,是两个输出型参数。没人连接时,就会阻塞在accept处。返回值:若调用成功,返回一个文件描述符,调用失败,返回-1,并设置errno。

返回的这一个文件描述符是什么呢?我们举一个例子帮助理解。在一些景区饭店,为了增大客流量,可能会专门让一个人在街上拉客,拉客的人拉到客人后,并不会进入饭店,只是将客人带到饭店处,叫来一个服务员招呼客人,客人进店吃饭后,拉客的人又继续回去拉客了。当又拉到客人,带到饭店门口,又会叫来另一名服务员招呼客人。这个饭店就是一个服务器,我们今天只看一个服务器即可。饭店里一个一个的服务员就是一个一个的文件描述符,因为只有通过文件描述符才能提供服务,拉客的人也是文件描述符,只是他不提供服务,只负责拉客。拉客的人就是accept的第一个参数,店里服务员就是accept的返回值。也就是说,accept的第一个参数的文件描述符专门用来获取新链接,未来为客户端提供服务的由accept的返回值决定。修改一下成员变量_sockfd的名称

#define BACKLOG 8static const uint16_t gport = 8080;class TcpServer
{
public:TcpServer(int port = gport):_port(port), _isrunning(false){}void InitServer(){// 1. 创建TCP套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket create success, sockfd is: " << _listensockfd;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;// 2. 绑定int n = ::bind(_listensockfd, CONV(&local), sizeof(local));if(n < 0){LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;// 3. 将套戒指设置为监听状态n = ::listen(_listensockfd, BACKLOG);if(n < 0){LOG(LogLevel::FATAL) << "listen errno";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;}void Start(){_isrunning = true;while(_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if(sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;}}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd; // 监听套接字uint16_t _port;bool _isrunning;
};

我们可以来验证一下是否能与我们的服务器建立连接。 


当我们将服务器运行起来之后,此时就是在Linux系统上启动了一个TCP服务。netstat的-l选项是查看处于监听状态下的服务,我们的服务器此时就处于监听状态下。我们现在没有写客户端,要怎么连接服务端呢?现在,有非常多的应用底层使用的就是TCP。当我们打开浏览器,访问某个网站时,用的是HTTP或HTTPS协议,但是底层仍然使用的是TCP协议。云服务器也是在网络上公开的一个服务,因为是有公网IP的。而浏览器在访问某个网站时,底层协议就是TCP。所以,我们可以通过浏览器去访问我们的服务器。

可以看到,我们的服务端已经能够被连接了。

当服务端获取连接成功之后,就可以与客户端通信了。我们定义一个函数来处理请求。因为TCP是面向字节流的,所以可以使用文件接口来对网络进行读和写。另外,TCP是全双工的,所以读和写使用的是同一个文件描述符。

void HandlerRequest(int sockfd)
{LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while (true){ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';std::string echo_str = "server echo# ";echo_str += inbuffer;::write(sockfd, echo_str.c_str(), echo_str.size());}}
}
void Start()
{_isrunning = true;while (_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;// 处理请求HandlerRequest(sockfd);}
}

来验证一下能否向服务器发送请求,并得到回复的消息。我们现在并没有客户端,Linux下可以通过指令telnet去访问某一个服务器。


可以看到,此时服务器是可以正常工作的。连接成功,也能拿到回显。

客户端

// ./client_tcp server_ip server_port
int main(int argc, char* argv[])
{if(argc != 3){std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];int server_port = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cout << "create socket failed" << std::endl;return 2;}::close(sockfd);return 0;
}

客户端仍然是不需要显示绑定的。客户端创建完套接字之后,是不能直接向服务端发送消息的,需要先与服务端建立连接才可以向服务端发送消息。

#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个参数传入客户端的套接字,第二、三个参数的服务端的IP地址和端口号。成功返回0,失败返回-1,并设置errno。客户端在首次与服务器建立连接时,就会自动绑定。

// ./client_tcp server_ip server_port
int main(int argc, char* argv[])
{if(argc != 3){std::cout << "Usage:./client_tcp server_ip server_port" << std::endl;return 1;}std::string server_ip = argv[1];int server_port = std::stoi(argv[2]);int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cout << "create socket failed" << std::endl;return 2;}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port);server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str());// 与服务端建立连接int n = ::connect(sockfd, CONV(&server_addr), sizeof(server_addr));if(n < 0){std::cout << "connect failed" << std::endl;return 3;}// 与服务端通信std::string message;while(true){char inbuffer[1024];std::cout << "input message: ";std::getline(std::cin, message);n = ::write(sockfd, message.c_str(), message.size());if(n > 0){int m = ::read(sockfd, inbuffer, sizeof(inbuffer));if(m > 0){inbuffer[m] = '\0';std::cout << inbuffer << std::endl;}else break;}else break;}::close(sockfd);return 0;
}

 一些BUG与问题

1. 无法重新获取连接

我们现在来试一试让客户端与服务端进行通信。


会发现,当我们将客户端退出,再重新启动客户端,此时向服务端发送消息服务端已经接收不到了。问题出在HandlerRequest,我们只做了read读取成功的判断。HandlerRequest永远不会退出,导致客户端会一直与服务端连接,服务端无法重新获取连接。

void HandlerRequest(int sockfd)
{LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while (true){ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';std::string echo_str = "server echo# ";echo_str += inbuffer;::write(sockfd, echo_str.c_str(), echo_str.size());}else if (n == 0){// 客户端已经退出了,应该退出处理逻辑,重新获取连接LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}
}



此时就可以正常通信了。

2. 文件描述符泄漏问题

会发现,每次获取新连接时,客户端的文件描述符都是会变化的。这里客户端的文件描述符就是指accept的返回值。

我们知道文件描述符是文件描述符表的下标,是有限的。所以,文件描述符是有用的、有限的,那么他就是一个资源。我们知道,文件的生命周期是随进程的,而服务器进程是永远不退出的,就会导致文件描述符泄漏问题。所以,当客户端退出时,要将这个文件描述符关闭。

void HandlerRequest(int sockfd)
{LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while (true){ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if (n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';std::string echo_str = "server echo# ";echo_str += inbuffer;::write(sockfd, echo_str.c_str(), echo_str.size());}else if (n == 0){// 客户端已经退出了,应该退出处理逻辑,重新获取连接LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd);
}

Linux下,进程的文件描述符表一般为64,难道一个服务器只能有几十个连接吗?并不是,文件描述符表是可以进行动态扩展的。云服务器的OS在上线之前已经被相应公司的工程师编译好了,一般是65535或10万。

3. UDP与TCP获取客户端信息的区别

在UDP中,接收客户端发来的消息可以使用recvfrom,recvfrom除了可以获取客户端发来的消息,还可以获取客户端的套接字信息。这里使用read要怎么获取客户端的套接字信息呢?

TCP获取客户端的套接字信息不在read,而是在accept获取。所以,获取客户端的信息:

  • 数据:客户端的文件描述符
  • 套接字信息:accept / recvfrom

获取客户端的套接字信息是有用的,因为服务端可能需要对客户端进行管理,像之前的聊天室。我们使用之前的InetAddr类对我们的代码进行修改:在获取到新连接后,将客户端的套接字信息打印

void Start()
{_isrunning = true;while (_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// 处理请求HandlerRequest(sockfd);}
}

4. Windows作为客户端访问Linux

#include <winsock2.h>
#include <iostream>
#include <string>#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = "47.113.120.114";  // 填写你的云服务器ip
uint16_t serverport = 8080; // 填写你的云服务开放的端口号int main()
{WSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0){std::cerr << "WSAStartup failed: " << result << std::endl;return 1;}SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket == INVALID_SOCKET){std::cerr << "socket failed" << std::endl;WSACleanup();return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址result = connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));if (result == SOCKET_ERROR){std::cerr << "connect failed" << std::endl;closesocket(clientSocket);WSACleanup();return 1;}while (true){std::string message;std::cout << "Please Enter@ ";std::getline(std::cin, message);if (message.empty()) continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] = { 0 };int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived > 0){buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾std::cout << "Received from server: " << buffer << std::endl;}else{std::cerr << "recv failed" << std::endl;}}closesocket(clientSocket);WSACleanup();return 0;
}

这里与UDP是十分类似的,就不过多介绍了。

我们现在让Linux上的客户端和Windows上的客户端都连接Linux上的服务端。



此时会发现,我们的服务端现在只能连接一个客户端。


当之前的客户端与服务端断开连接之后,服务端会立刻建立连接,并接收到之前没接收到的数据。我们当前的服务器一次只能处理一个请求,这显然是不行的。

注意:一定要先退出客户端,再退出服务端。如果先退出了服务端,这个服务端是不能立刻重启的,通常需要等待60到120秒。

解决服务器无法一次处理多个请求的问题

多进程版本
void Start()
{_isrunning = true;while (_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// 处理请求pid_t id = fork();if (id == 0){// child::close(_listensockfd);HandlerRequest(sockfd);exit(0);}// father::close(sockfd);int rid = ::waitpid(id, nullptr, 0);if (rid < 0){LOG(LogLevel::WARNING) << "waitpid error";}}
}

我们让创建的子进程来处理客户端的请求。子进程是会继承父进程的文件描述符表的,所以子进程是可以看到父进程打开的所有文件的文件描述符的,但是注意,是父子进程各有一张文件描述符表。子进程并不需要使用监听的文件描述符,父进程因为将处理客户端请求的任务已经交给了子进程,所以也不需要客户端的文件描述符。所以,父子进程均需要关闭掉不需要的文件描述符

此时会有一个问题,父进程是阻塞等待子进程的,而子进程处理接收任务是一个死循环,若是子进程一直不退出,父进程就会一直阻塞在这里,不还是只能处理一个请求吗?又必须要等待子进程,若不等待,子进程退出时就会有僵户问题。此时可以将子进程退出时给父进程发送的信号忽略掉,这样子进程退出,OS会自动回收资源,不用在wait了。这种做法是可以的,但是我们今天不使用,而是使用下面的做法:

void Start()
{_isrunning = true;while (_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// 处理请求pid_t id = fork();if (id == 0){// child::close(_listensockfd);// 子进程创建孙子进程if (fork() > 0) exit(0);// 子进程创建完成直接退出,孙子进程变成孤儿进程HandlerRequest(sockfd);exit(0);}// father::close(sockfd);// 不会阻塞int rid = ::waitpid(id, nullptr, 0);if (rid < 0){LOG(LogLevel::WARNING) << "waitpid error";}}
}

我们让子进程创建孙子进程,子进程退出后,父进程直接回收他。所以,爷爷进程不会阻塞在等待处了。爷爷进程不会管孙子进程,孙子进程就变成了孤儿进程,孤儿进程退出时,由1号进程进行回收。我们让Linux上的客户端和Windows上的客户端都连接Linux上的服务端。


可以看到,服务端就可以一次处理多个请求了。当有多个客户端时,是会有多个进程的。并且这些客户端的文件描述符都是4,因为客户端的父进程创建完子进程都,就将4这个文件描述符关闭了。

多进程版本是不太好的,因为创建进程、回收进程的工作量都是比较大的。并且这里不能使用之前实现的进程池,因为这里服务端要与客户端通信,子进程一定要能看到客户端的文件描述符,所以需要先有文件描述符,再创建子进程,而进程池是先创建出子进程,所以是看不到客户端的文件描述符的。当然有进程间传递文件描述符的技术,但是这并不是重点。

多线程版本

主线程和新线程是共享一强张文件描述符表的所以这里不能像上面一样关闭文件描述符。

线程要接收来自客户端的消息、向客户端发送消息,就一定要拿到客户端的文件描述符,所以要将客户端的文件描述符交给线程,怎么交给线程呢?肯定是将文件描述符传给线程执行的函数,但是要注意,一定不能将客户端的文件描述符的地址或值传过去,因为这个文件描述符是一个临时变量。将临时变量的地址传过去,可能将这个变量的地址交给线程后,就去调度其他线程了,等下次再调度到处理这个文件描述符的线程时,这个文件描述符对应的栈空间已经被其他临时变量覆盖了,此时就拿不到原先的文件描述符了。可以在堆上申请一块空间,将这块空间的值赋值为文件描述符,再将堆上的地址传过去。

// 处理请求
pthread_t tid;
int* sockfdp = new int(sockfd);
pthread_create(&tid, nullptr, HandlerRequest, sockfdp);

因为HandlerRequest不符合线程调用函数的条件,所以我们定义一个符合条件的函数,并让线程去调用它,在它内部再去调用HandlerRequest。这里一定要将这个函数设置成static的,否则成员函数会有一个默认的参数,导致不符合要求,但是定义成static就无法访问类内的成员函数了,所以,我们定义一个结构体来传参。

class TcpServer
{
private:struct ThreadData{int sockfd;TcpServer* self;};static void* ThreadEntry(void* args){// 将新线程设置为分离状态,主线程就不用等待它了pthread_detach(pthread_self());ThreadData* data = (ThreadData*)args;data->self->HandlerRequest(data->sockfd);return nullptr;}
public:TcpServer(int port = gport):_port(port), _isrunning(false){}void InitServer(){// 1. 创建TCP套接字_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){LOG(LogLevel::FATAL) << "socket error";Die(SOCKET_ERR);}LOG(LogLevel::INFO) << "socket create success, sockfd is: " << _listensockfd;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;// 2. 绑定int n = ::bind(_listensockfd, CONV(&local), sizeof(local));if(n < 0){LOG(LogLevel::FATAL) << "bind error";Die(BIND_ERR);}LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;// 3. 将套接字设置为监听状态n = ::listen(_listensockfd, BACKLOG);if(n < 0){LOG(LogLevel::FATAL) << "listen errno";Die(LISTEN_ERR);}LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;}void HandlerRequest(int sockfd){LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while(true){ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);if(n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';std::string echo_str = "server echo# ";echo_str += inbuffer;::write(sockfd, echo_str.c_str(), echo_str.size());}else if(n == 0){// 客户端已经退出了,应该退出处理逻辑,重新获取连接LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd);}void Start(){_isrunning = true;while(_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if(sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// 处理请求pthread_t tid;ThreadData* data = new ThreadData;data->sockfd = sockfd;data->self = this;pthread_create(&tid, nullptr, ThreadEntry, data);}}void Stop(){_isrunning = false;}~TcpServer(){}
private:int _listensockfd; // 监听套接字uint16_t _port;bool _isrunning;
};

可以看到,当有多个客户端连接时,这些客户端的文件描述符就不再都是4了。

每一次客户端向服务器发出请求时,也也就是客户端请求服务时才创建线程,效率较低。此时可以使用线程池。

线程池版本

使用到的线程池:

namespace ThreadPoolMoudle
{using namespace LockMoudle;using namespace LogMoudule;using namespace ThreadModule;using namespace CondMoudle;// 用来做测试的线程方法void DefaultTest(){while(true){LOG(LogLevel::DEBUG) << "我是一个测试方法";sleep(1);}}// 线程池中默认创建5个线程const static int defaultnum = 5;// thread_t就是一个指向Thread对象的智能指针using thread_t = std::shared_ptr<Thread>;// 这个模板表示的是消息队列中存放类型Ttemplate<typename T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 线程进入HandlerTask的逻辑";// LOG(LogLevel::INFO) << "线程进入HandlerTask的逻辑";while(true){// 1. 拿任务T t;{LockGuard lockguard(_lock);// 当任务队列为空,并且线程池正在运行时,才能让线程等待while(IsEmpty() && _isrunning) {// 队列为空,等待_wait_num ++;_cond.Wait(_lock);_wait_num --;}// 队列为空,并且线程池退出了,就让线程退出if(IsEmpty() && !_isrunning)break;t = _taskq.front();_taskq.pop();}// 2. 处理任务t(); // 规定:未来所有的任务处理,全部都必须提供()方法}LOG(LogLevel::INFO) << "线程: " << name << " 退出";}ThreadPool(const ThreadPool<T>&) = delete;ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;// 默认线程池是没有运行的ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false){// 创建出_num个线程对象for(int i = 0;i < _num;i ++){_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1)));LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象...成功";}}public:// 创建对象时就调用这个函数创建static ThreadPool<T>* getInstance(){if(instance == nullptr){LOG(LogLevel::INFO) << "单例首次被执行, 需加载对象...";instance = new ThreadPool<T>();instance->Start();}return instance;}// 向任务队列放入一个线程void Equeue(T& in){LockGuard lockguard(_lock);if(!_isrunning) return ;_taskq.push(std::move(in));// 若有处于等待状态的线程,唤醒if(_wait_num > 0)_cond.Notify();}// 创建线程池void Start(){// 让_num个线程对象都启动if(_isrunning) return ;_isrunning = true;for(auto& thread_ptr : _threads){thread_ptr->Start();LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << " ...成功";}}// 停止线程池void Stop(){LockGuard lockguard(_lock);if(_isrunning){// 不能再向任务队列中放入任务_isrunning = false;// 唤醒所有线程,并将任务队列中剩余的任务处理完成,此时已经无法向任务队列中放入任务了,所以任务是有限的if(_wait_num > 0)_cond.NotifyAll();}}// 等待线程void Wait(){// 等待线程池中的所有线程for(auto& thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << " ...成功";}}~ThreadPool() {}private:std::vector<thread_t> _threads; // 数组存放线程的指针int _num;                       // 线程池中线程个数int _wait_num;                  // 处于等待状态的线程数量bool _isrunning;                // 线程池是否正在运行std::queue<T> _taskq;           // 任务队列Mutex _lock;Cond _cond;static ThreadPool<T>* instance;};// 在类外对static的指针进行初始化template<typename T>ThreadPool<T>* ThreadPool<T>::instance = NULL;
}

首先要定义一个任务类型,这个任务类型是根据线程池来的,线程池中的是无参的,所以这里也要是无参的。

// 交给线程池的任务的类型
using task_t = std::function<void()>;

接下来就是根据客户端的文件描述符构建任务,并将任务交给线程池。

void Start()
{_isrunning = true;while (_isrunning){// 1. 获取新连接struct sockaddr_in peer;socklen_t peerlen = sizeof(peer);LOG(LogLevel::DEBUG) << "accept ing ...";int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);continue;}// 获取连接成功LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;InetAddr addr(peer);LOG(LogLevel::INFO) << "client info: " << addr.Addr();// 处理请求, 构建任务, 并将任务交给线程池task_t f = std::bind(&TcpServer::HandlerRequest, this, sockfd);ThreadPool<task_t>::getInstance()->Equeue(f);}
}

这里要说明一下,今天的HandlerRequest中,一旦连接上了,就会一直处理,直到客户端退出,这种任务叫做长任务。但是线程池中的线程个数是有上限的,所以是不推荐使用线程池来处理长任务的,线程池一般用于处理短任务,比如登录、注销、请求数据等。这里就不改了,因为这样方便测试。线程池除了适合处理短任务,还适合处理用户量较少的任务。

现在,我们已经完成了客户端发消息给服务器,发给服务器的消息统一当成了字符串处理。在这里服务端从客户端读取、向客户端写入是不完善的,可能会出现客户端发送给服务端一个hello world,而服务端只读到了一个hello的情况,因为TCP是面向字节流的。UDP就不存在这样的问题,UDP是面向数据报,只要接收成功了,读取到的一定是完整的报文。面向字节流读取到的数据的格式是需要我们自己处理的。

对于面向字节流的读取和发送,更加推荐使用recv和send。当然,即使使用了recv和send,仍然存在读取报文可能不全的情况。

#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t send(int sockfd, const void *buf, size_t len, int flags);

第4个参数是读取标志位,设置为0即可,0表示是阻塞读取。返回值与read、write相同。

void HandlerRequest(int sockfd)
{LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while (true){ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if (n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';std::string echo_str = "server echo# ";echo_str += inbuffer;::send(sockfd, echo_str.c_str(), echo_str.size(), 0);}else if (n == 0){// 客户端已经退出了,应该退出处理逻辑,重新获取连接LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd);
}

V2版本 - 多线程远程执行命令

在上面的代码中,服务端接收了客户端发来的消息后,并没有做任何的处理,直接就发送回去了。客户端将数据发送给客户端,肯定是要服务端对数据进行处理的,所以,我们给客户端引入一些服务。服务端将客户端发送过来的消息当成是Linux指令,服务端接收到来自客户端的消息后,对消息进行分析,若是合理的就进行执行,并将执行结果发送回客户端。

此时需要有一个处理上层任务的入口。

// 上层业务
using handler_t = std::function<std::string (std::string)>;

传入一个命令,也就是字符串,再将执行结果返回。在使用服务器时,要让服务器执行什么任务也需要传入。

class TcpServer
{
public:TcpServer(handler_t handler, int port = gport):_handler(handler), _port(port), _isrunning(false){}void HandlerRequest(int sockfd){LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;char inbuffer[4096];while(true){ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);if(n > 0){LOG(LogLevel::INFO) << inbuffer;inbuffer[n] = '\0';// 调用上层的处理方法处理任务std::string cmd_result = _handler(inbuffer);::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0);}else if(n == 0){// 客户端已经退出了,应该退出处理逻辑,重新获取连接LOG(LogLevel::INFO) << "client quit: " << sockfd;break;}else{// 读取失败break;}}::close(sockfd);}
private:int _listensockfd; // 监听套接字uint16_t _port;bool _isrunning;// 处理上层任务的入口handler_t _handler;
};

创建一个类来定义处理任务的函数

class Command
{
public:std::string Execute(std::string cmdstr){}
};

Execute总体思路是父进程拿到命令字符串后,创建一个子进程,让子进程来执行命令,再将命令执行结果交给父进程。但是,进程有独立性,所以子进程要将执行结果交给父进程就需要进程间通信,此处使用管道,对子进程做输出重定向,重定向到管道里,父进程从读端就能够拿到命令执行结果了。exec并不会影响dup2的结果。总结:

  • 父进程创建一个管道
  • 父进程创建一个子进程,子进程输出重定向到管道中,执行命令
  • 父进程从管道中读取命令执行的结果

此时可以直接使用popen函数。

#include <stdio.h>FILE *popen(const char*command,const char *type);
int pclose(FILE *stream);

popen用于创建管道并启动一个子进程来执行shel命令。第一个参数是shell命令,第二个参数:

  • " r " :从命令的输出读取(父进程读取子进程的输出)
  • " w ":向命令的输入写入(父进程向子进程提供输入)

成功时返回一个文件指针,可用于读取或写入(取决于mode),失败时返回NULL。

std::string Execute(std::string cmdstr)
{FILE* fp = ::popen(cmdstr.c_str(), "r");if (nullptr == fp){return std::string("Failed");}char buffer[1024];std::string result;while (true){char* ret = ::fgets(buffer, sizeof(buffer), fp);if (!ret) break;result += ret;}pclose(fp);return result.empty() ? std::string("Done") : result;
}

当前这个函数是没办法执行所有命令的。所以,我们要对执行的命令进行限制,设置一个白名单,规定那些命令可以执行。

class Command
{
private:bool SafeCheck(const std::string& cmdstr){auto iter = _white_list.find(cmdstr);return iter == _white_list.end() ? false : true;}
public:Command(){_white_list.insert("ls");_white_list.insert("pwd");_white_list.insert("ls -l");_white_list.insert("ll");_white_list.insert("touch");_white_list.insert("who");_white_list.insert("whoami");}std::string Execute(std::string cmdstr){if(!SafeCheck(cmdstr)){return std::string(cmdstr + " 不支持");}FILE* fp = ::popen(cmdstr.c_str(), "r");if(nullptr == fp){return std::string("Failed");}char buffer[1024];std::string result;while(true){char* ret = ::fgets(buffer, sizeof(buffer), fp);if(!ret) break;result += ret;}pclose(fp);return result.empty() ? std::string("Done") : result;}
private:std::set<std::string> _white_list;
};
int main()
{ENABLE_CONSOLE_LOG();Command cmd;std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>([&cmd](std::string cmdstr){return cmd.Execute(cmdstr);});tsvr->InitServer();tsvr->Start();return 0;
}
http://www.dtcms.com/a/271707.html

相关文章:

  • 【通识】NodeJS基础
  • LLaMA 学习笔记
  • Java 多态详解:从原理到实战,深入理解面向对象编程的核心特性
  • C#基础篇(09)结构体(struct)与类(class)的详细区别
  • Vue响应式原理三:响应式依赖收集-类
  • 大模型的下半场:从工具到智能体的产业变革与2025突围之路
  • AI大模型:(二)4.2 文生图训练实践-真人写实生成
  • 8.2 文档预处理模块(二)
  • 学习笔记(31):matplotlib绘制简单图表-直方图
  • UNet改进(19):基于残差注意力模块Residual Attention的高效分割网络设计
  • 编译安装的Mysql5.7报“Couldn‘t find MySQL server (mysqld_safe)“的原因 笔记250709
  • 主流大模型Agent框架 AutoGPT详解
  • 软件互联网产品发版检查清单
  • WIndows 编程辅助技能:格式工厂的使用
  • Dify教程更改文件上传数量限制和大小限制
  • JVM 调优
  • 双指针-15.三数之和-力扣(LeetCode)
  • AI技术如何重塑你的工作与行业?——实战案例解析与效率提升路径
  • gdb调试工具
  • Lingo软件学习(一)好学爱学
  • DPDK graph图节点处理框架:模块化数据流计算的设计与实现
  • dify配置邮箱,密码重置以及邮箱邀请加入
  • 【Java】【字节面试】字符串中 出现次数最多的字符和 对应次数
  • HTML应用指南:利用GET请求获取全国山姆门店位置信息
  • 跨服务sqlplus连接oracle数据库
  • 如何卸载本机的node.js
  • 源码角度解析 --- HashMap 的 get 和 put 流程
  • 前端使用fetch-event-source实现AI对话
  • AI Agent:我的第一个Agent项目
  • 爬虫-数据解析