【Linux网络编程】第六弹---构建TCP服务器:从基础到线程池版本的实现与测试详解_【linux编程】tcpserver 类的设计与实现:构建高性能的 tcp 服务器(二)
✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】
目录
1、TcpServerMain.cc
2、TcpServer.hpp
2.1、TcpServer类基本结构
2.2、构造析构函数
2.3、InitServer()
2.4、Loop()
2.4.1、Server 0(不靠谱版本)
2.4.2、Server 1(多进程版本)
2.4.3、Server 2(多线程版本)
2.4.4、Server 3(线程池版本)
3、TcpClientMain.cc
4、测试结果
4.1、不靠谱版本
4.2、多进程版本
4.3、多线程版本
4.4、线程池版本
5、完整代码
5.1、Makefile
5.2、TcpClientMain.cc
5.3、TcpServer.hpp
5.4、TcpServerMain.cc
前面几弹使用UDP协议实现了相关功能,此弹使用TCP协议实现客户端与服务端的通信,相比与UDP协议,TCP协议更加可靠,也更加复杂!与UDP类似,我们先写主函数,然后实现相关函数!
1、TcpServerMain.cc
服务端主函数使用智能指针构造Server对象,然后调用初始化与执行函数,调用主函数使用该可执行程序 + 端口号!
// ./tcpserver 8888int main(int argc,char* argv[]){ if(argc != 2) { std::cerr << \"Usage: \" << argv[0] << \" local-post\" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); std::unique_ptr tsvr = std::make_unique(port); tsvr->InitServer(); tsvr->Loop(); return 0;}
2、TcpServer.hpp
TcpServer.hpp封装TcpServer类!
枚举常量:
enum { SOCKET_ERROR, BIND_ERROR, LISTEN_ERROR};
全局静态变量:
const static uint16_t gport = 8888;const static int gsockfd = -1;const static int gblcklog = 8;
2.1、TcpServer类基本结构
TcpServer类的基本成员有端口号,文件描述符,与运行状态!
// 面向字节流class TcpServer{public: TcpServer(uint16_t port = gport); void InitServer(); void Loop(); ~TcpServer();private: uint16_t _port; int _sockfd; // TODO bool _isrunning;};
2.2、构造析构函数
构造函数初始化成员变量,析构函数无需处理!
注意:此处需要用到两个全局静态变量!
TcpServer(uint16_t port = gport) :_port(port),_sockfd(gsockfd),_isrunning(false){}~TcpServer(){}
2.3、InitServer()
InitServer() 初始化服务端!
初始化函数主要分为三步:
- 1、创建socket(类型与UDP不同)
类型需要使用 SOCK_STREAM
- 2、bind sockfd 和 socket addr
- 3、获取连接(与UDP不同)
获取连接需要使用listen函数(将套接字设置为监听模式,以便能够接受进入的连接请求)
listen()
#include #include int listen(int sockfd, int backlog);
参数
sockfd
:这是一个已创建的套接字文件描述符,它应该是一个绑定到某个地址和端口的套接字。backlog
:这个参数定义了内核应该为相应套接字排队的最大连接数(此处暂时使用8)。如果队列已满,新的连接请求可能会被拒绝。需要注意的是,这个值只是内核用于优化性能的一个提示,实际实现可能会有所不同。
返回值
- 成功时,
listen
函数返回 0。 - 失败时,返回 -1,并设置
errno
以指示错误类型。
注意:此处需要用到全局静态变量和枚举常量!
// _sockfd 版本void InitServer(){ // 1.创建socket _sockfd = ::socket(AF_INET,SOCK_STREAM,0); if(_sockfd < 0) { LOG(FATAL,\"socket create eror\\n\"); exit(SOCKET_ERROR); } LOG(INFO,\"socket create success,sockfd: %d\\n\",_sockfd); // 3 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.bind sockfd 和 socket addr if(::bind(_sockfd,(struct sockaddr*)&local,sizeof(local)) < 0) { LOG(FATAL,\"bind eror\\n\"); exit(BIND_ERROR); } LOG(INFO,\"bind success\\n\"); // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接 // 老板模式,随时等待被连接 if(::listen(_sockfd,gblcklog) < 0) { LOG(FATAL,\"listen eror\\n\"); exit(LISTEN_ERROR); } LOG(INFO,\"listen success\\n\");}
为了测试该函数,先将Loop函数设计成死循环!
Loop()
// 测试void Loop(){ _isrunning = true; while(_isrunning) { sleep(1); } _isrunning = false;}
2.4、Loop()
Loop() 函数一直执行服务!
执行服务函数主要分为两步:
- 1、获取新连接(accept函数[从已完成连接队列的头部返回下一个已完成连接,如果队列为空,则阻塞调用进程])
accept()
#include /* See NOTES */#include int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
sockfd
:这是一个监听套接字的文件描述符,它应该是一个已经通过socket
函数创建,并通过bind
函数绑定到特定地址和端口,以及通过listen
函数设置为监听模式的套接字。addr
:这是一个指向sockaddr
结构的指针,该结构用于存储接受连接的客户端的地址信息。如果不需要这个信息,可以传递NULL
。addrlen
:这是一个指向socklen_t
类型的变量的指针,用于存储addr
结构的大小。在调用accept
之前,应该将该变量的值设置为addr
结构的大小。在调用返回后,该变量将包含实际返回的地址信息的长度。如果addr
是NULL
,则这个参数也可以是NULL
。
返回值
- 成功时,
accept
函数返回一个新的套接字文件描述符,用于与接受的连接进行通信。这个新的套接字是原始监听套接字的子套接字,它继承了许多属性(如套接字选项),但与原始套接字是独立的。 - 失败时,返回 -1,并设置
errno
以指示错误类型。
因此TcpServer类的_sockfd应该改为_listensockfd!!!
TcpServer类
// 面向字节流class TcpServer{public: TcpServer(uint16_t port = gport):_port(port),_listensockfd(gsockfd),_isrunning(false) {} void InitServer() { // 1.创建socket _listensockfd = ::socket(AF_INET,SOCK_STREAM,0); if(_listensockfd < 0) { LOG(FATAL,\"socket create eror\\n\"); exit(SOCKET_ERROR); } LOG(INFO,\"socket create success,sockfd: %d\\n\",_listensockfd); // 3 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.bind sockfd 和 socket addr if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0) { LOG(FATAL,\"bind eror\\n\"); exit(BIND_ERROR); } LOG(INFO,\"bind success\\n\"); // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接 // 老板模式,随时等待被连接 if(::listen(_listensockfd,gblcklog) < 0) { LOG(FATAL,\"listen eror\\n\"); exit(LISTEN_ERROR); } LOG(INFO,\"listen success\\n\"); } ~TcpServer() {}private: uint16_t _port; int _listensockfd; bool _isrunning;};
- 2、执行服务(前提是获取到新连接)
执行服务总共有四个版本!
2.4.1、Server 0(不靠谱版本)
Server 0版本直接执行长服务!
Loop()
Loop()函数先获取新连接,获取成功则执行服务函数!
void Loop(){ _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd < 0) { LOG(WARNING,\"sccept reeor\\n\"); continue; } InetAddr addr(client); LOG(INFO,\"get a new link,client info: %s,sockfd:%d\\n\",addr.AddrStr().c_str(),sockfd); // 4 // 获取成功 // version 0 -- 不靠谱版本 Server(sockfd,addr); } _isrunning = false;}
Server()
注意:tcp协议可以直接使用read,write函数读写文件描述符的内容(因为tcp是面向字节流的)!
Server()执行服务,先从文件描述符中读数据,再写数据到文件描述符中!
void Server(int sockfd,InetAddr addr){ // 长服务 while(true) { char inbuffer[1024]; // 当做字符串 // 1.读文件 ssize_t n = ::read(sockfd,inbuffer,sizeof(inbuffer) - 1); if(n > 0) { inbuffer[n] = 0; LOG(INFO,\"get message from client [%s],message: %s\\n\",addr.AddrStr().c_str(),inbuffer); std::string echo_string = \"[server echo]# \"; echo_string += inbuffer; // 2.写文件 write(sockfd,echo_string.c_str(),echo_string.size()); } // 读到文件结尾 else if(n == 0) { LOG(INFO,\"client %s quit\\n\",addr.AddrStr().c_str()); break; } else { LOG(ERROR,\"read error\\n\",addr.AddrStr().c_str()); break; } } ::close(sockfd);}
2.4.2、Server 1(多进程版本)
多进程版本即创建子进程,让子进程执行服务函数,父进程回收子进程,但是如果以阻塞等待回收子进程会有一个问题,如果子进程一直没有退出,那么父进程会一直阻塞!为了解决这个问题,我们可以让子进程再创建一个孙子进程,让孙子进程去执行服务函数,子进程直接退出,父进程回收子进程,孙子进程此时会成为孤儿进程,孤儿进程退出OS会自动回收!
void Loop(){ _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd 0) exit(0); // 让孙子进程执行服务,保证能不阻塞 Server(sockfd,addr); exit(0); } // father ::close(sockfd); // 防止文件描述符泄漏(打开的不关闭) int n = waitpid(id,nullptr,0); // 0阻塞等待 if(n > 0) { LOG(INFO,\"wait child success\\n\"); } } _isrunning = false;}
2.4.3、Server 2(多线程版本)
多线程版本即让新线程去执行服务函数,但是主线程需要回收新线程,为了做到主线程无需回收新线程,可以让新线程分离,此时无需回收新线程!还有一个问题,类内的成员函数有this指针,而新线程的函数只能有一个参数,此时需要使用静态成员函数,但是使用静态之后还有一个问题,不能看到类内的成员,此处可以使用地址传参,将一个包含sockfd,TcpServer类的指针和InetAddr类的 成员变量的地址传入!
内部类
// 内部类class ThreadData{public: int _sockfd; TcpServer* _self; InetAddr _addr;public: ThreadData(int sockfd,TcpServer* self,const InetAddr &addr) :_sockfd(sockfd),_self(self),_addr(addr) {}};
Loop()
void Loop(){ _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd < 0) { LOG(WARNING,\"sccept reeor\\n\"); continue; } InetAddr addr(client); LOG(INFO,\"get a new link,client info: %s,sockfd:%d\\n\",addr.AddrStr().c_str(),sockfd); // 4 // 获取成功 // version 2 -- 多线程版 -- 不能关闭fd了,也不需要 pthread_t tid; ThreadData *td = new ThreadData(sockfd, this,addr); pthread_create(&tid,nullptr,Execute,td); // 新线程分离 } _isrunning = false;}
新线程执行函数
// 无法调用类内成员 无法看到sockfdstatic void *Execute(void *args){ ThreadData *td = static_cast(args); pthread_detach(pthread_self()); // 分离新线程,无需主线程回收 td->_self->Server(td->_sockfd,td->_addr); delete td; return nullptr;}
2.4.4、Server 3(线程池版本)
线程池版本即 将执行服务的函数入线程池队列,该函数需要是参数为空和返回值为void的函数,因此需要bind绑定函数!
声明函数类型
using task_t = std::function;
Loop()
void Loop(){ _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd < 0) { LOG(WARNING,\"sccept reeor\\n\"); continue; } InetAddr addr(client); LOG(INFO,\"get a new link,client info: %s,sockfd:%d\\n\",addr.AddrStr().c_str(),sockfd); // 4 // 获取成功 // version 3 -- 线程池版本 task_t t = std::bind(&TcpServer::Server,this,sockfd,addr); ThreadPool::GetInstance()->Equeue(t); } _isrunning = false;}
3、TcpClientMain.cc
客户端主函数主要实现向服务端发送消息的功能,调用主函数使用该可执行程序 + IP + 端口号!
主函数主要分为四步:
- 1、创建socket(与服务端一样)
- 2、与服务端建立连接(使用connect[客户端与服务器建立TCP连接])
connect()
#include /* See NOTES */#include int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
sockfd
:这是一个由socket
函数返回的套接字描述符。addr
:这是一个指向sockaddr
结构的指针,它包含了目标服务器的地址和端口信息。对于IPv4地址,通常使用sockaddr_in
结构;对于IPv6地址,使用sockaddr_in6
结构。addrlen
:这是addr
参数的长度,以字节为单位。对于sockaddr_in
,它通常是sizeof(struct sockaddr_in)
;对于sockaddr_in6
,它通常是sizeof(struct sockaddr_in6)
。
返回值
- 成功时,
connect
返回0。 - 失败时,返回-1,并设置
errno
以指示错误类型。
- 3、发送消息
- 4、关闭socket
// ./tcpclient server-ip server-ipint 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]); // 1.创建socket int sockfd = ::socket(AF_INET,SOCK_STREAM,0); if(sockfd < 0) { std::cerr << \"create socket error\" << std::endl; exit(1); } // 不需要显示的bind,但是一定要有自己的IP和port,需要隐式的绑定(OS用自己的IP和随机端口号) // 什么时候进行bind? If the connection or binding succeeds struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); // server.sin_addr.s_addr = ::inet_pton(AF_INET,serverip.c_str(),&server.sin_addr); // 2.与服务端建立连接 int n = ::connect(sockfd,(struct sockaddr*)&server,sizeof(server)); // 也可以重连 if(n < 0) { std::cerr << \"connect socket error\" << std::endl; exit(2); } // 3.发送消息 while(true) { std::string message; std::cout < 0) { echo_buffer[n] = 0; std::cout << echo_buffer << std::endl; } else { break; } } // 4.关闭socket ::close(sockfd); return 0;}
4、测试结果
4.1、不靠谱版本
该版本是一个只能执行一个客户端的版本,因此称为不靠谱版本!
4.2、多进程版本
该版本是一个能执行多客户端的版本,但是创建进程的开销比较大,也不是很完美!
4.3、多线程版本
该版本是一个能执行多客户端的版本,相比与多进程版本效果会更好,因为创建线程的开销比进程更少!
4.4、线程池版本
该版本是一个能执行多客户端的版本,与线程池版本差不太多,此处只是使用以前实现的线程池!
5、完整代码
前面一弹就有且没有修改的代码此处就没有再放上来了!
5.1、Makefile
.PHONY:allall:tcpserver tcpclienttcpserver:TcpServerMain.ccg++ -o $@ $^ -std=c++14tcpclient:TcpClientMain.ccg++ -o $@ $^ -std=c++14.PHONY:clean clean:rm -rf tcpserver tcpclient
5.2、TcpClientMain.cc
#include #include #include #include #include #include #include // ./tcpclient server-ip server-ipint 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]); // 1.创建socket int sockfd = ::socket(AF_INET,SOCK_STREAM,0); if(sockfd < 0) { std::cerr << \"create socket error\" << std::endl; exit(1); } // 不需要显示的bind,但是一定要有自己的IP和port,需要隐式的绑定(OS用自己的IP和随机端口号) // 什么时候进行bind? If the connection or binding succeeds struct sockaddr_in server; memset(&server,0,sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); // server.sin_addr.s_addr = ::inet_pton(AF_INET,serverip.c_str(),&server.sin_addr); // 2.与服务端建立连接 int n = ::connect(sockfd,(struct sockaddr*)&server,sizeof(server)); // 也可以重连 if(n < 0) { std::cerr << \"connect socket error\" << std::endl; exit(2); } // 3.发送消息 while(true) { std::string message; std::cout < 0) { echo_buffer[n] = 0; std::cout << echo_buffer << std::endl; } else { break; } } // 4.关闭socket ::close(sockfd); return 0;}
5.3、TcpServer.hpp
#pragma once#include #include #include #include #include #include #include #include #include #include #include \"Log.hpp\"#include \"InetAddr.hpp\"#include \"ThreadPool.hpp\"using namespace log_ns;enum { SOCKET_ERROR, BIND_ERROR, LISTEN_ERROR};const static uint16_t gport = 8888;const static int gsockfd = -1;const static int gblcklog = 8;using task_t = std::function;// 面向字节流class TcpServer{public: // _sockfd 版本 // TcpServer(uint16_t port = gport):_port(port),_sockfd(gsockfd),_isrunning(false) // {} TcpServer(uint16_t port = gport):_port(port),_listensockfd(gsockfd),_isrunning(false) {} // _sockfd 版本 // void InitServer() // { // // 1.创建socket // _sockfd = ::socket(AF_INET,SOCK_STREAM,0); // if(_sockfd < 0) // { // LOG(FATAL,\"socket create eror\\n\"); // exit(SOCKET_ERROR); // } // LOG(INFO,\"socket create success,sockfd: %d\\n\",_sockfd); // 3 // 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.bind sockfd 和 socket addr // if(::bind(_sockfd,(struct sockaddr*)&local,sizeof(local)) < 0) // { // LOG(FATAL,\"bind eror\\n\"); // exit(BIND_ERROR); // } // LOG(INFO,\"bind success\\n\"); // // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接 // // 老板模式,随时等待被连接 // if(::listen(_sockfd,gblcklog) < 0) // { // LOG(FATAL,\"listen eror\\n\"); // exit(LISTEN_ERROR); // } // LOG(INFO,\"listen success\\n\"); // } void InitServer() { // 1.创建socket _listensockfd = ::socket(AF_INET,SOCK_STREAM,0); if(_listensockfd < 0) { LOG(FATAL,\"socket create eror\\n\"); exit(SOCKET_ERROR); } LOG(INFO,\"socket create success,sockfd: %d\\n\",_listensockfd); // 3 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.bind sockfd 和 socket addr if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local)) < 0) { LOG(FATAL,\"bind eror\\n\"); exit(BIND_ERROR); } LOG(INFO,\"bind success\\n\"); // 3.因为tcp是面向连接的,tcp需要未来不短地获取连接 // 老板模式,随时等待被连接 if(::listen(_listensockfd,gblcklog) < 0) { LOG(FATAL,\"listen eror\\n\"); exit(LISTEN_ERROR); } LOG(INFO,\"listen success\\n\"); } // 内部类 class ThreadData { public: int _sockfd; TcpServer* _self; InetAddr _addr; public: ThreadData(int sockfd,TcpServer* self,const InetAddr &addr) :_sockfd(sockfd),_self(self),_addr(addr) {} }; // 测试 // void Loop() // { // _isrunning = true; // while(_isrunning) // { // sleep(1); // } // _isrunning = false; // } void Loop() { _isrunning = true; while(_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 1.获取新连接 int sockfd = ::accept(_listensockfd,(struct sockaddr*)&client,&len); // 获取失败继续获取 if(sockfd 0) exit(0); // 让孙子进程执行服务,保证能不阻塞 // Server(sockfd,addr); // exit(0); // } // // father // ::close(sockfd); // 防止文件描述符泄漏(打开的不关闭) // int n = waitpid(id,nullptr,0); // 0阻塞等待 // if(n > 0) // { // LOG(INFO,\"wait child success\\n\"); // } // version 2 -- 多线程版 -- 不能关闭fd了,也不需要 // pthread_t tid; // ThreadData *td = new ThreadData(sockfd, this,addr); // pthread_create(&tid,nullptr,Execute,td); // 新线程分离 // version 3 -- 线程池版本 task_t t = std::bind(&TcpServer::Server,this,sockfd,addr); ThreadPool::GetInstance()->Equeue(t); } _isrunning = false; } // 无法调用类内成员 无法看到sockfd static void *Execute(void *args) { ThreadData *td = static_cast(args); pthread_detach(pthread_self()); // 分离新线程,无需主线程回收 td->_self->Server(td->_sockfd,td->_addr); delete td; return nullptr; } void Server(int sockfd,InetAddr addr) { // 长服务 while(true) { char inbuffer[1024]; // 当做字符串 // 1.读文件 ssize_t n = ::read(sockfd,inbuffer,sizeof(inbuffer) - 1); if(n > 0) { inbuffer[n] = 0; LOG(INFO,\"get message from client [%s],message: %s\\n\",addr.AddrStr().c_str(),inbuffer); std::string echo_string = \"[server echo]# \"; echo_string += inbuffer; // 2.写文件 write(sockfd,echo_string.c_str(),echo_string.size()); } // 读到文件结尾 else if(n == 0) { LOG(INFO,\"client %s quit\\n\",addr.AddrStr().c_str()); break; } else { LOG(ERROR,\"read error\\n\",addr.AddrStr().c_str()); break; } } ::close(sockfd); } ~TcpServer() {}private: uint16_t _port; // int _sockfd; // TODO int _listensockfd; bool _isrunning;};
5.4、TcpServerMain.cc
#include \"TcpServer.hpp\"#include // ./tcpserver 8888int main(int argc,char* argv[]){ if(argc != 2) { std::cerr << \"Usage: \" << argv[0] << \" local-post\" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); std::unique_ptr tsvr = std::make_unique(port); tsvr->InitServer(); tsvr->Loop(); return 0;}
注意:线程池只需将全局变量gdefaultnum改为10即可!
static const int gdefaultnum = 10; // 默认创建10个线程