前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Tcp协议Socket编程

Tcp协议Socket编程

作者头像
用户11029129
发布2024-11-22 13:00:06
发布2024-11-22 13:00:06
5400
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

🌎 Tcp协议Socket编程

  本次socket编程需要使用到 日志文件,此为具体日志编写过程。以及 线程池,线程池原理比较简单,看注释即可。


🚀TCP Socket API简介
代码语言:javascript
代码运行次数:0
复制
int socket(int domain, int type, int protocol);

  socket接口打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符。应用程序可以像读写文件一样用 read/write 在网络上收发数据。

  • domain参数:代表协议族,决定了套接字使用的协议类型。对于 IPv4, family 参数指定为 AF_INET
  • protocol参数protocol 参数指定为 0 即可
  • type参数对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议
  • 返回值如果 socket()调用出错则返回-1, 成功返回文件描述符

代码语言:javascript
代码运行次数:0
复制
int listen(int sockfd, int backlog);

  listen()声明sockfd正处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, backlog设置不会太大(一般是 5), 具体细节以后再说。

  • sockfd参数文件描述符
  • 返回值listen()成功返回 0,失败返回-1;

代码语言:javascript
代码运行次数:0
复制
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);

  三次握手完成后, 服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

  • sockfd参数文件描述符
  • addr参数: 是一个输出型参数,accept()返回时传出客户端的地址和端口号,如果给 addr 参数传 NULL,表示不关心客户端的地址
  • addrlen参数: 是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)

代码语言:javascript
代码运行次数:0
复制
int connect(int sockfd, struct sockaddr* addr, socklen_t addrlen);

客户端需要调用 connect()连接服务器

  • connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址
  • connect()成功返回 0,出错返回-1

🚀构建Tcp_echo_server

  TcpSocket编程并不是很需要我们非常理解tcp协议的原理以及实现方式方法。要实现TcpSocket编程与UdpSocket相同,都需要客户端与服务器端,服务器端实现具体的协议通信类型及方法,客户端实现对任务的调用等工作。

✈️TcpServer服务器端

  首先构建TcpServer服务器端,在TcpServer.hpp内写一个名为 TcpServer 的类:

代码语言:javascript
代码运行次数:0
复制
const static int defaultsockfd = -1;

class TcpServer
{
public:
    TcpServer(uint16_t port):_port(port), _listensock(defaultsockfd)
    {}

    void InitServer()// 初始化服务器
    {}

    void Loop()// 服务器是个死循环
    {}

    ~TcpServer()
    {}
private:
    uint16_t _port;// 端口号
    int _listensock;// 文件描述符
};

  我们知道,服务器没有被要求停下时是一直在运行的,所以在类内我们需要实现一个Loop函数,用来维持服务器的稳定。

  在初始化阶段与UdpServer类似,首先创建tcp的套接字,我们使用socket()接口创建,对返回值进行接收并判断是否合法,接着就是绑定端口和ip,绑定之前需要首先填充sockaddr_in 结构体信息字段,最后进行绑定:

代码语言:javascript
代码运行次数:0
复制
// 网络中错误类型枚举,类外实现
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
};

void InitServer()
{
    _listensock = ::socket(AF_INET, SOCK_STREAM, 0);
    if(_listensock < 0)
    {
        LOG(FATAL, "socket error");
        exit(SOCKET_ERROR);
    }
    LOG(DEBUG, "socket create success, sockfd is: %d", _listensock);

    //填充套接字信息
    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;

    int n = ::bind(_listensock, (struct sockaddr*)&local, sizeof(local));
    if(n < 0)
    {
        LOG(FATAL, "bind error");
        exit(BIND_ERROR);
    }
    LOG(DEBUG, "bind success, sockfd is: %d", _listensock);
}

  在UdpServer类内绑定完成后初始化工作就完成了,但是对于TcpServer来说,因为Tcp是面向连接的,所以在通信之前,必须得首先建立连接,而服务器将来则是被连接的,启动后未来则会一直等待客户端连接,所以我们使用listen()接口进行对客户端的监听,其中listen()接口的第二个参现在我们不需要关心,将其设置为全局变量即可:

代码语言:javascript
代码运行次数:0
复制
const static int gbacklog = 16;

void InitServer()
{
    _listensock = ::socket(AF_INET, SOCK_STREAM, 0);
    if(_listensock < 0)
    {
        LOG(FATAL, "socket error");
        exit(SOCKET_ERROR);
    }
    LOG(DEBUG, "socket create success, sockfd is: %d", _listensock);

    //填充套接字信息
    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;// 接收任意地址

    int n = ::bind(_listensock, (struct sockaddr*)&local, sizeof(local));
    if(n < 0)
    {
        LOG(FATAL, "bind error");
        exit(BIND_ERROR);
    }
    LOG(DEBUG, "bind success, sockfd is: %d", _listensock);

    // tcp是面向连接的,所以通信之前,必须要先建立连接, 服务器是被连接的
    // tcpserver启动,未来首先是要等待客户端建立连接
    n = ::listen(_listensock, gbacklog);
    if(n < 0)
    {
        LOG(FATAL, "listen error");
        exit(LISTEN_ERROR);
    }
    LOG(DEBUG, "listen success, sockfd is: %d", _listensock);
}

  注意这里我填充网络字段用了 INADDR_ANY ,表示我使用任意IP进行通信,所以将来服务器端在启动时就可以通过./tcpserver + port 的形式运行服务器端了。


✈️服务器循环控制

  在Loop循环中,我们负责接收并处理数据,与udp不同,Tcp刚刚也说了是面向连接的,而我们收数据就需要使用到accept()这个接口了,至于这个接口最重要的不过是返回值,返回的也是文件描述符,不同的是,有几个客户端连接就会返回几个文件描述符

  用下面的例子来说明:

阿熊是一个餐馆的一个服务员,不同的是阿熊的嘴皮子还是比较突出的,所以阿熊就被委以重任,在店门口拉客,每过来一个客人,阿熊就会热情的去打招呼并介绍这里的饭店,如果有人正好饿了被阿熊的真挚所打动来到这个饭店,阿熊就会对里面喊:服务员来一位,这时候张三就出来负责这批人的饮食了

而阿熊则不会进入到餐馆里面,还是在餐馆外拉客,每次拉倒客人就会通知后厨来一个服务员,给新来的客人进行点餐服务

  这个例子实际上就揭示了accept()与listensockfd的关系:

  阿熊实际上就是listensockfd,用来负责网络中与本机发起连接的客户端,而这些所谓的服务员,实际上就是accept接口的返回值,服务器每链接一个客户端就会单独返回一个文件fd来进行服务。同时为了可以手动的控制服务器的运行,我们添加一个布尔位,用来判断是否可以支持运行:

代码语言:javascript
代码运行次数:0
复制
void Loop()
{
    _isrunning = true;
    // 不能直接收数据,必须先获取连链接
    while(_isrunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
        if(sockfd < 0)
        {
            LOG(WARNNING, "accept error");
            continue;
        }
    }
    _isrunning = false;
}

private:
    uint16_t _port;
    int _listensock;
    bool _isrunning;

✈️Service服务

    监听成功获取链接,接着就可以设计服务了,跟udpServer一样,服务方式有很多种,单进程,多线程等方式进行服务实现,首先我们来实现Version 0版本,单进程服务,直接实现一个服务接口,负责对数据进行处。

  当执行到Service这一步,表示当前已经链接成功,我们需要获取客户端的端口号和IP也就是说我们需要之前UdpServer实现的InetAddr类(用来记录IP和PORT),这样我们就有了客户端的信息,同时我们需要知道是哪个客户端通过哪个套接字传递的信息。综上所述Service接口的参数必须要有sockfd,与InetAddr类对象作为形参。

代码语言:javascript
代码运行次数:0
复制
void Service(int sockfd, InetAddr client) {}// Service服务

  首先我们可以通过客户端的sockfd来对消息进行接收,首先设置一个缓冲区,使用read()接口对客户端发来的信息放在缓冲区,当然read()的返回值表示读取的字节数,如果我们的缓冲区大小比较小是不能一次性读完的,所以我们要分批次读取数据,如果数据读取完毕,返回值为0,如果读取失败返回值为负数,所以需要将其放在循环内:

代码语言:javascript
代码运行次数:0
复制
void Service(int sockfd, InetAddr client)
{
    LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);
    std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";
    while(true)
    {
        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
        	// n > 0 进行处理动作
        }
        else if(n == 0)
        {
        	//client 退出 && 关闭链接
            LOG(INFO, "%s quit", clientaddr.c_str());
            break;
        }
        else
        {
        	LOG(ERROR, "read error", clientaddr.c_str());
            break;
        }
    }

    ::close(sockfd);// 不关闭会导致 文件描述符泄漏的问题(文件描述符不归还)
}

  当接收的返回值>0我们对数据进行处理,我们不做过多的处理,我们将数据在前面加上提示标语,然后再将数据原封不动的发回去,这就是我们的处理动作。所以完整的Service如下:

代码语言:javascript
代码运行次数:0
复制
void Service(int sockfd, InetAddr client)
{
    LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);
    std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";
    while(true)
    {
        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << clientaddr << inbuffer << std::endl;

            std::string echo_string = "[server echo]# ";
            echo_string += inbuffer;

            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if(n == 0)
        {
            //client 退出 && 关闭链接
            LOG(INFO, "%s quit", clientaddr.c_str());
            break;
        }
        else
        {
            LOG(ERROR, "read error", clientaddr.c_str());
            break;
        }
    }

    ::close(sockfd);// 不关闭会导致 文件描述符泄漏的问题(文件描述符不归还)
}

  服务完成就必须要关闭文件描述符,不然就是导致文件描述符泄漏,我们在系统部分学习过,文件描述符也是有限的,如果我们每一次对客户端的sockfd不归还势必会导致资源的浪费,也就是 文件描述符泄漏问题。实际上,只要是有限的资源我们不归还都有可能会导致泄漏问题。


✈️Tcp客户端

  在运行之前我们还需要对客户端代码进行编写,Tcp客户端与Udp客户端代码编写差别很小,首先进行参数控制,让其将来以 ./tcpclient + ip + port 的形式运行,接着调用socket()函数创建socket套接字。

  创建完套接字之后,我们是需要通过套接字进行网络通信的,那么就需要绑定客户端IP和PORT,但是在UdpServer中我们说过,客户端是不需要绑定ip和port的,这是因为通常一个服务器不止一个ip可以访问,如果手动将其绑定就限定死只能通过这个ip来访问这个端口的服务。所以客户端是不需要绑定ip和port的,OS会自动帮你随机绑定。

  虽然不用绑定端口号和IP,但是正常的网络通信我们需要进行,所以我们依旧需要填充sockaddr_in 字段,填充服务器端的信息以及IP类型:

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);// 表示进行tcp协议的通信
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    //tcp_client 要bind,但是不需要手动bind
    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_addr(serverip.c_str());
    
    return 0;
}

  套接字创建完毕,网络字段也填充完毕,这个时候我们就可以通过 connect() 接口,通过sockfd,向对端发起连接,如果客户端首次调用connect接口,OS则会自动的将sockfd与对端sockaddr_in 对象进行绑定。

  所以connect()内所传参数,需要服务器端的sockaddr_in 的对象信息,在上面,我们已将从命令行获取的端口号及IP,所以直接使用其IP和PORT对sockaddr_in 结构体进行填充即可。接下来我们就可以通过connect()将本地sockfd,与服务器端sockaddr_in 对象进行默认绑定了,并且朝着服务器端发起连接。

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    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 << "socket error" << std::endl;
        exit(2);
    }

    //tcp_client 要bind,但是不需要手动bind
    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_addr(serverip.c_str());
    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }
	
	return 0;
}

  连接成功,客户端就可以向服务器端发起请求了,今天我们不做过多的处理,仅仅作为一个聊天室存在,从命令行获取信息,并且发送到服务器端,而在Tcp服务里面,我们更喜欢使用的发送消息的接口是send,收消息使用recv

代码语言:javascript
代码运行次数:0
复制
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

  与sendto和recvfrom不同的是recv和send不需要sockaddr结构体字段,也就是不需要对端的网络ip和port,这是因为调用recv和send之前已经与对端建立了链接,所以直接使用recv和send来进行通信即可。

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <string>
#include <sys/types.h>
#include "Log.hpp"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}

// ./tcp_client serverip serverport
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    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 << "socket error" << std::endl;
        exit(2);
    }

    //tcp_client 要bind,但是不需要手动bind
    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_addr(serverip.c_str());
    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }

    while(true)
    {
        std::cout << "Please Enter# ";
        std::string outstring;
        std::getline(std::cin, outstring);

        ssize_t s = send(sockfd, outstring.c_str(), outstring.size(), 0);// write
        if(s > 0)
        {
            char inbuffer[1024];
            ssize_t m = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if(m > 0)
            {
                inbuffer[m] = 0;    
                std::cout << inbuffer << std::endl;
            }
            else  
            {
                break;
            }
        }
        else
        {
            break;
        }
    }

    return 0;
}

✈️Version 0:单进程

  我们使用单进程服务,在Loop获取链接之后,我们直接调用Service服务:

代码语言:javascript
代码运行次数:0
复制
void Loop()
{
    _isrunning = true;
    // 不能直接收数据,必须先获取连链接
    while(_isrunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
        if(sockfd < 0)
        {
            LOG(WARNNING, "accept error");
            continue;
        }
        // version 0: 单进程
        Service(sockfd, InetAddr(peer));
    }
    _isrunning = false;
}

✈️Version 1: 多进程服务

  我们实现的是网络版聊天室,聊天室里总不能只有一个客户端进行通信,所以我们至少需要两个客户端进行通信,所以我们可以引入多进程。

  创建进程之后,由于子进程会继承父进程的文件描述符表,这并不是共享,我们在系统部分也学过进程,我们知道子进程会继承父进程的文件描述符表,但是是以拷贝的形式继承,所以子进程与父进程都会看到 listensockfd,而我们的目的是让子进程单独处理请求,父进程负责通过accept()来获取客户端发来的请求。

  也就是说,对于父进程来说accept()所返回的sockfd对自己不重要,所以父进程关闭自己文件描述符表的 sockfd,相反,accept获取请求是通过listensockfd,对于子进程来说也不重要,子进程可以关闭自己文件描述符表中的 listensockfd:

代码语言:javascript
代码运行次数:0
复制
void Loop()
    {
        _isrunning = true;
        // 不能直接收数据,必须先获取连链接
        while(_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
            if(sockfd < 0)
            {
                LOG(WARNNING, "accept error");
                continue;
            }
            // Version 1: 采用多进程
            pid_t id = fork();
            if(id == 0)
            {
                // child : 关心sockfd, 不关心listensock
                ::close(_listensock);
                exit(0);
            }
            // father : 关心listensock, 不关心sockfd
            ::close(sockfd);
        }
        _isrunning = false;
    }

  我们通过多进程的目的是父进程可以不断的获取新链接,子进程不断地给新链接提供服务,但是别忘记了,父进程是需要回收子进程的,所以父进程无论以哪种方式回收子进程,都需要等待子进程结束才会继续执行新的循环。所以这样一来程序执行的顺序依旧是串行执行,这跟单进程又有什么区别呢?

  没错,如果仅是如此,跟单进程的方式没有任何区别,但是我们可以在子进程中在进行fork(),创建出孙子进程,而执行fork()的子进程直接退出,这样父进程就可以接收到子进程退出信息,可以开始新的循环了。而孙子进程由于没有了父进程,则会被OS托孤,父进程变为了OS,这样我们让孙子进程执行任务,执行完直接就会被系统给回收了:

代码语言:javascript
代码运行次数:0
复制
void Loop()
    {
        _isrunning = true;
        // 不能直接收数据,必须先获取连链接
        while(_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
            if(sockfd < 0)
            {
                LOG(WARNNING, "accept error");
                continue;
            }
            // Version 1: 采用多进程
            pid_t id = fork();
            if(id == 0)
            {
                // child : 关心sockfd, 不关心listensock
                ::close(_listensock);
                if(fork() > 0) exit(0);// 当前进程退出,父进程wait成功,执行新的循环
                // 孙子进程
                Service(sockfd, InetAddr(peer));// 孙子进程 变成孤儿进程 由系统接管
                exit(0);
            }
            // father : 关心listensock, 不关心sockfd
            ::close(sockfd);
            waitpid(id, nullptr, 0);
        }
        _isrunning = false;
    }

  至于父子进程为什么获取的文件描述符都是4,很简单,我们知道给用户分配的文件描述符都是从下标3开始的,因为0,1,2已经被占据,而我们定义的listensocfd就是 文件描述符3,而通过accept获取的sockfd就是文件描述符下标4,这个文件描述符表会被拷贝到孙子进程,孙子进程执行结束直接被回收了。

  回到父进程时我们关闭了 accept返回的sockfd,这样以来下次父进程再次获取连接所分配的文件描述符还是4,复制到孙子进程还是四,不断循环,这样就能让文件描述符循环利用了。但是如果一次性连接多个进程就有可能分配多个文件描述符。


✈️Version 2: 多线程服务

  线程可以真正共享进程的文件描述符表,所以并不需要像多进程那样将文件描述符关闭,并且创建回收进程的成本是要比线程大的多的。创建的线程需要调用Service()接口去处理客户端请求,所以我们需要sockfd,以及客户端sockaddr 对象,于此我们将其封装为一个结构体去处理:

代码语言:javascript
代码运行次数:0
复制
class ThreadData
{
public:
    ThreadData(int fd, InetAddr addr)
        :sockfd(fd)
        ,clientaddr(addr)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
};

void Loop()
    {
        _isrunning = true;
        // 不能直接收数据,必须先获取连链接
        while(_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
            if(sockfd < 0)
            {
                LOG(WARNNING, "accept error");
                continue;
            }
            // versoin 2: 采用多线程
            pthread_t t;
            // to do...
        }
        _isrunning = false;
    }

  每个线程都会执行自己的回调函数,我们将回调函数命名为HandlerSock,通过HandlerSock来调用Service来处理客户端请求:

代码语言:javascript
代码运行次数:0
复制
// 线程回调函数  类内实现
void* HandlerSock(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
	// to do
    delete td;
    return nullptr;
}

  因为回调函数是在类内实现的,而线程回调函数严格要求参数只有一个并且为 void*类型,但是类内函数第一个参数都会隐藏this 指针,所以我们可以将函数设置为静态成员函数,并且在Struct ThreadData结构体中添加 this指针字段,以便于进行线程IO:

代码语言:javascript
代码运行次数:0
复制
class TcpServer;// 声明
 
class ThreadData
{
public:
    ThreadData(int fd, InetAddr addr, TcpServer* s)
        :sockfd(fd)
        ,clientaddr(addr)
        ,self(s)
    {}
public:
    int sockfd;
    InetAddr clientaddr;
    TcpServer *self;
};

static void* HandlerSock(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    td->self->Service(td->sockfd, td->clientaddr);
    delete td;
    return nullptr;
}

void Loop()
{
    _isrunning = true;
    // 不能直接收数据,必须先获取连链接
    while(_isrunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
        if(sockfd < 0)
        {
            LOG(WARNNING, "accept error");
            continue;
        }
        // versoin 2: 采用多线程
        pthread_t t;
        ThreadData *td = new ThreadData(sockfd, InetAddr(peer), this);
        pthread_create(&t, nullptr, HandlerSock, td);
    }
    _isrunning = false;
}

  但是这样以来,似乎依旧不能实现并发,因为我们知道,main线程需要等待其他线程回收成功之后才会向下执行,而我们main线程是在循环内部的,也就是说其他线程没有回收就不会接收新的客户端。所以会导致我们执行起来依旧是串行执行。不过请不要忘记了,线程可以detach(),也就是线程分离,如果线程进行了分离,main thread就不会等待所有线程回收在向下执行了。

代码语言:javascript
代码运行次数:0
复制
// 线程回调函数
static void* HandlerSock(void* args)
{
    pthread_detach(pthread_self());// 进行线程分离
    ThreadData* td = static_cast<ThreadData*>(args);
    td->self->Service(td->sockfd, td->clientaddr);
    delete td;
    return nullptr;
}

✈️Version 3: 线程池服务

  不论是多进程还是多线程,它们获取连接之后才会创建进程或者线程,创建的过程本身就在消耗时间,这个时间在客户端方面也是可以感受的到的,所以我们可以采用池化技术,预先创建一批线程,等到需要的时候直接拿来用即可,我们将线程池的代码引入进来。

  我们使用function来封装线程调用函数,将参数设置为空,返回值设置为void:

代码语言:javascript
代码运行次数:0
复制
using task_t = std::function<void()>;// using 表示重命名与 typedef 作用相同

  但是我们的Service服务的参数有两个,这个时候使用 task_t 类型 调用 Service接口肯定是会报错的,所以我们在调用前首先进行绑定,固定的将sockfd与sockaddr 对象传入,这样就可以调用了,最后线程池在获取单例执行任务即可:

代码语言:javascript
代码运行次数:0
复制
using task_t = std::function<void()>;

void Loop()
{
    _isrunning = true;
    // 不能直接收数据,必须先获取连链接
    while(_isrunning)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
        if(sockfd < 0)
        {
            LOG(WARNNING, "accept error");
            continue;
        }
        
        // Version 3: 采用线程池池化技术
        task_t t = std::bind(&TcpServer::Service, this, sockfd, InetAddr(peer));
        ThreadPool<task_t>::GetInstance()->Enqueue(t);
    }
    _isrunning = false;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 🌎 Tcp协议Socket编程
    • 🚀TCP Socket API简介
    • 🚀构建Tcp_echo_server
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档