构建TCP服务器:从基础到线程池版本的实现与测试详解
目录
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 8888
int 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->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
");
exit(SOCKET_ERROR);
}
LOG(INFO,"socket create success,sockfd: %d
",_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
");
exit(BIND_ERROR);
}
LOG(INFO,"bind success
");
// 3.因为tcp是面向连接的,tcp需要未来不短地获取连接
// 老板模式,随时等待被连接
if(::listen(_sockfd,gblcklog) < 0)
{
LOG(FATAL,"listen eror
");
exit(LISTEN_ERROR);
}
LOG(INFO,"listen success
");
}
为了测试该函数,先将Loop函数设计成死循环!
Loop()
// 测试
void Loop()
{
_isrunning = true;
while(_isrunning)
{
sleep(1);
}
_isrunning = false;
}
2.4、Loop()
Loop() 函数一直执行服务!
执行服务函数主要分为两步:
1、获取新连接(accept函数[从已完成连接队列的头部返回下一个已完成连接,如果队列为空,则阻塞调用进程])
accept()
#include
#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
");
exit(SOCKET_ERROR);
}
LOG(INFO,"socket create success,sockfd: %d
",_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
");
exit(BIND_ERROR);
}
LOG(INFO,"bind success
");
// 3.因为tcp是面向连接的,tcp需要未来不短地获取连接
// 老板模式,随时等待被连接
if(::listen(_listensockfd,gblcklog) < 0)
{
LOG(FATAL,"listen eror
");
exit(LISTEN_ERROR);
}
LOG(INFO,"listen success
");
}
~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
");
continue;
}
InetAddr addr(client);
LOG(INFO,"get a new link,client info: %s,sockfd:%d
",addr.AddrStr().c_str(),sockfd); // 4
// 获取成功
// version 0 -- 不靠谱版本
Server(sockfd,addr);
}
_isrunning = false;
}
Server()
注意:tcp协议可以直接使用read,write函数读写文件描述符的内容(因为tcp是面向字节流的)!
Server()执行服务,先从文件描述符中读数据,再写数据到文件描述符中!