构建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<TcpServer> tsvr = std::make_unique<TcpServer>(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 <sys/types.h>          
#include <sys/socket.h>
 
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 <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
 
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()执行服务,先从文件描述符中读数据,再写数据到文件描述符中!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值