【socket编程】TCP网络通信模型 {socket创建套接字文件;填充套接字结构+bind绑定;设置listen状态;accept获取链接;connect发起链接;telnet远程登录工具}

在这里插入图片描述
下面通过几个server/client通信程序了解一下TCP网络通信模型

一、单执行流TCP网络程序

首先回顾一下TCP的特点:

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

1.1 服务端

服务端要可以收到客户端发送给服务端的信息,并把消息回显给客户端,目前就先简单实现这样的功能

1.1.1 创建套接字文件

创建套接字的函数是socket,TCP/UDP 均可使用该函数进行创建套接字,该函数在上一篇编写UDP代码的时候已经详细介绍过了,这里就不再介绍,直接使用

int socket(int domain, int type, int protocol);
  1. 第一个参数:协议家族,如果要选择网络通信,则选择 AF_INET(IPv4)或者 AF_INET6(IPv6)
  2. 第二个参数:数据传输方式,如果是基于TCP的网络通信,我们采用的就是 SOCK_STREAM,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)
  3. 第三个参数:设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议

注意:

  • 创建套接字失败,没必要继续执行代码了,直接退出程序即可
  • TCP服务器创建套接字的做法与UDP服务器是一样的,只不过创建套接字时TCP需要的是流式服务,而UDP需要的是数据报服务
  • 析构服务器时,可以将服务器对应的 _sockfd 文件描述符进行关闭,也可以不用处理,因为套接字文件的生命周期随进程,进程退出,套接字文件就会被系统释放。

1.1.2 填充套接字结构+绑定端口

TCP 也是需要绑定端口号的,绑定端口号的函数是bind,TCP/UDP 均可使用进行该函数绑定端口

该函数在上一篇编写UDP代码的时候已经详细介绍过了,这里就不再介绍,直接使用

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. 第一个参数:套接字文件,用于绑定套接字创建成功返回的文件描述符
  2. 第二个参数:套接字结构,用于填充网络相关的属性信息,比如IP地址、端口号等。我们要做的工作就是:定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体地址传给第二个参数addr,需要强制类型转换
  3. 第三个参数:套接字结构的大小,传入的addr结构体的长度

注意:绑定失败,就直接退出程序了,不必要再执行

当定义好 sockaddr_in 结构体后,最好先用memset函数对该结构体进行清空,也可以用bzero函数进行清空。bzero函数也可以对特定的一块内存区域进行清空,bzero函数的函数原型如下:

#include <strings.h>

void bzero(void *s, size_t n);

bzero()函数将从s开始的区域的前n个字节设置为零。


1.1.3 设置监听状态

UDP服务器的初始化操作只有两步,第一步就是创建套接字,第二步就是绑定。而TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态

listen函数
listen for connections on a socket:监听套接字上的连接

函数:listen
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
         int listen(int sockfd, int backlog);
 
参数:
    第一个参数sockfd:需要设置为监听状态的套接字对应的文件描述符
    第二个参数backlog:全连接队列的最大长度
 
返回值:
    成功返回0,失败返回-1,同时错误码会被设置

第二个参数backlog:全连接队列的最大长度。。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不能设置太大,暂时直接设置为5

TCP服务器在创建完套接字和绑定后,需要再进一步将套接字设置为监听状态,监听是否有新的连接到来。如果监听失败也没必要进行后续操作了,因为监听失败也就意味着TCP服务器无法接收客户端发来的连接请求,因此我们直接终止程序即可

注意:在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成


1.1.4 获取新链接进行通信

上面的代码已经把服务器初始化完成了,客户端有新链接到来,服务端可以获取到新链接,这一步需要死循环获取客户端新链接

accept函数

ccept a connection on a socket:接受套接字上的连接

函数:accept
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
          int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
 
参数:
    第一个参数sockfd:从该监听套接字中获取连接
    第二个参数addr:对方一端网络相关的属性信息
    第三个参数addrlen:addr的长度
 
返回值:
    获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置
  • 第二个参数addr:对端的套接字结构,输出型参数,类型是struct sockaddr * (是谁发起的连接请求)
  • 第三个参数addrlen:对端套接字结构的大小,输入输出型参数

我们要做的是定义一个sockaddr_in的结构体,将该结构清空,把结构体地址强转后传给第二个参数addr;定义一个socklen_t类型的变量并初始化位套接字结构的大小,将其地址传给第三个参数addrlen。

注意:accept函数获取连接时可能会失败,但TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续获取新连接

accept函数的返回值

  • 如果一直没有客户端发起链接请求的话,accept会阻塞等待,直到获取到新的链接accept才会返回。
  • accept获取连接成功后,为新链接创建一个套接字文件,并返回套接字文件的描述符。

区分两种套接字:

  • 监听套接字:之前使用socket创建的套接字文件是监听套接字,专门用于监听并接收客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • 通信套接字:accept函数返回的套接字才是真正用于通信的套接字,为本次accept获取到的连接提供通信服务。

获取到新链接后就可以通过套接字进行通信了,由于TCP使用的是流式套接字,所以可以使用文件接口read和write进行网络数据的读取和写入。也可以使用recv和send函数进行读写。

注意:这里使用 read、write 读写接口是有问题的, 但是暂时先这样用,后续有 TCP 的专用读写接口

netstat -atlpn 查看所有处于监听状态的tcp通信端口

img


1.1.5 服务端代码

tcpServer.hpp

#pragma once
 
#include <iostream>
#include <string>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
using namespace std;
 
static const int gbacklog = 5;
 
// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};
 
class tcpServer
{
public:
    tcpServer(const uint16_t &port)
        : _listensock(-1), _port(port)
    {}
 
    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字文件(监听套接字)
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock == -1)
        {
            cout << "create socket error" << endl;
            exit(SOCKET_ERR);
        }
        cout << "create socket success: " << _listensock << endl;
 
        // 2.绑定端口
        // 2.1 填充套接字结构 sockaddr_in 
        struct sockaddr_in local;
        bzero(&local, sizeof(local));       // 把 sockaddr_in结构体全部初始化为0
        local.sin_family = AF_INET;         // 未来通信采用的是网络通信
        local.sin_port = htons(_port);      // htons(_port)主机字节序转网络字节序
        local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY 就是  0x00000000
 
        // 2.2 绑定端口
        int n = bind(_listensock, (struct sockaddr *)&local, sizeof(local)); // 需要强转
        if (n == -1)
        {
            cout << "bind socket error" << endl;
            exit(BIND_ERR);
        }
        cout << "bind socket success" << endl;
 
        // 3. 把_listensock套接字设置为监听状态
        if (listen(_listensock, gbacklog) == -1)
        {
            cout << "listen socket error" << endl;
            exit(LISTEN_ERR);
        }
        cout << "listen socket success" << endl;
    }
 
    // 启动服务器
    void start()
    {
        for (;;)
        {
            // 4. 获取新链接,accept从_listensock套接字里面获取新链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 这里的sockfd才是真正为客户端请求服务
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
            {
                cout << "accept error, next!" << endl;
                continue;
            }
            cout << "accept a new line success, sockfd: " << sockfd << endl;
 
            // 5. 为sockfd提供服务,即为客户端提供服务
            serviceIo(sockfd);
            // 走到这里。服务已经提供完成,必须关闭 sockfd
            close(sockfd);
        }
    }
 
    // 提供服务
    void serviceIo(int sockfd)
    {
        char buffer[1024];
        while (true)
        {
            // 读取客户端发来的消息
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0) // 读取成功
            {
                buffer[n] = 0;
                cout << "recv a message: " << buffer << endl;
 
                // 回显消息给客户端
                string outbuffer = buffer;
                outbuffer += "server[echo]";
                write(sockfd, outbuffer.c_str(), outbuffer.size());
            }
            else if (n == 0) // 客户端退出
            {
                cout << "client qiut, me too!" << endl;
                break;
            }
        }
    }
 
    ~tcpServer()
    {}
 
private:
    int _listensock; // listen套接字,不是用来数据通信的,是用来监听链接到来
    uint16_t _port;  // 端口号
};

tcpServer.cc

#include "tcpServer.hpp"
#include <memory>
 
// 使用手册
// ./tcpServer port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
 
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Uage(argv[0]);
        exit(UAGE_ERR);
    }
 
    uint16_t port = atoi(argv[1]); // string to int
 
    unique_ptr<tcpServer> tsvr(new tcpServer(port));
    tsvr->initServer(); // 初始化服务器
    tsvr->start();      // 启动服务器
 
    return 0;
}

使用telnet远程登录工具进行简单测试

使用方法:

  1. 输入命令telnet ip port与服务器建立连接。
  2. 输入消息发送给远端服务器。同时telnet还会接收来自服务器的回传信息并将其打印在终端。
  3. 输入ctrl + ]脱离输入框进入命令模式,输入quit命令,断开与服务器的链接,并退出telnet工具。
  4. 输入ctrl + ]脱离输入框进入命令模式,此时输入回车会再次进入输入模式。

1.2 客户端

客户端的功能是可以发送消息给服务端,并收到服务端回显的消息,目前就先简单实现这样的功能

1.2.1 关于客户端的绑定问题

客户端也是使用socket函数创建套接字,与TCP服务端一样

注意:

  • 客户端不需要显示的绑定端口号,在客户端发起链接的时候,操作系统自动会为该客户端进程分配空闲的端口号进行绑定

  • 客户端也不需要listen进行监听,也不需要accept获取新链接

  • 但是客户端需要发起链接,连接服务端


1.2.2 连接服务器进行通信

由于客户端不需要绑定,也不需要监听,因此当客户端创建完套接字后就可以向服务端发起连接请求。

connect函数

initiate a connection on a socket:在套接字上启动连接

函数:connect
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
        int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 
参数:
    第一个参数sockfd:表示通过该套接字发起连接请求
    第二个参数addr:对方一端网络相关的属性信息
    第三个参数addrlen:addr的长度
 
返回值:
    连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置
  • 第二个参数addr:对端的套接字结构地址,输入型参数,类型是 struct sockaddr *
  • 第三个参数addrlen:套接字结构的大小,输入型参数
  • 我们需要提前将服务端的IP+端口号填充到新创建的套接字结构当中,并将其地址传给addr;将其大小传给addrlen。

客户端调用connect函数向服务端发起链接请求时,OS会自动为传入的客户端套接字绑定IP地址和端口号。

客户端建立好链接后就可以通过套接字进行通信了,由于TCP使用的是流式套接字,所以可以使用文件接口read和write进行网络数据的读取和写入。也可以使用recv和send函数进行读写。

注意:这里使用 read、write 读写接口是有问题的, 但是暂时先这样用,后续有 TCP 的专用读写接口


1.2.3 客户端代码

tcpClient.hpp

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
using namespace std;
 
static const int gnum = 1024;
 
class tcpClient
{
public:
    tcpClient(const string &serverip, const uint16_t serverport)
        : _serverip(serverip), _serverport(serverport), _sockfd(-1)
    {}
 
    // 初始化客户端
    void initClient()
    {
        // 创建套接字文件
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd == -1)
        {
            cerr << "socket create error" << endl;
            exit(2);
        }
 
        // 2.绑定
        // 客户端必须要进行bind绑定,但是不需要我们自己bind,OS帮我们完成
        // 3.listen
        // 客户端不需要listen
        // 4. accept
        // 客户端不需要
    }
 
    // 启动客户端
    void start()
    {
        // 5. 客户端需要发起链接,链接服务端
        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()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
 
        if (connect(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0)
        {
            cerr << "socket connect error" << endl;
        }
        else // 连接成功
        {
            string message;
            while (true)
            {
                // 发送消息
                cout << "Enter# ";
                getline(cin, message);
                write(_sockfd, message.c_str(), message.size());
 
                // 接收服务端回显的消息
                char buffer[gnum];
                int n = read(_sockfd, buffer, sizeof(buffer) - 1);
                if (n > 0) // 读取成功
                {
                    buffer[n] = 0;
                    cout << "Server回显# " << buffer << endl;
                }
                else // 读取出错
                {
                    break; 
                }
            }
        }
    }
    ~tcpClient()
    {}
 
private:
    uint16_t _serverport; // 端口号
    string _serverip;     // ip地址
    int _sockfd;          // 文件描述符
};

tcpClient.cc

#include "tcpClient.hpp"
#include <memory>
 
// 使用手册
// ./tcpClient ip port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " server_ip server_port\n\n";
}
 
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Uage(argv[0]);
        exit(1);
    }
 
    // 客户端需要服务端的 IP 和 port
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]); // string to int
    std::unique_ptr<tcpClient> tcli(new tcpClient(serverip, serverport));
    tcli->initClient(); // 初始化服务器
    tcli->start();        // 启动服务器
 
    return 0;
}

1.3 服务端和客户端测试

编译链接好后,新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试

img

其中文件描述符会随着客户端的链接而递增

img

发送消息测试,测试正常

img

netstat -atlp 查看tcp网络端口

客户端和服务端在同一台机器上跑,就会查到三个:

img

  1. 第一个是服务器的tcp监听端口
  2. 第二个是服务器的tcp通信端口
  3. 第三个是客户端的tcp通信端口

客户端关闭,服务端相应的文件描述符也要随之关闭

img

该服务器的弊端

当我们仅用一个客户端连接服务端时,这一个客户端能够正常享受到服务端的服务

但在这个客户端正在享受服务端的服务时,我们让另一个客户端也连接服务器,此时虽然在客户端显示连接是成功的,但这个客户端发送给服务端的消息既没有在服务端进行打印,服务端也没有将该数据回显给该客户端

img

只有当第一个客户端退出后,服务端才会将第二个客户端发来是数据进行打印,并回显该第二个客户端。

img

通过实验现象可以看到,这服务端只有服务完一个客户端后才会服务另一个客户端。因为我们目前所写的是一个单执行流版的服务器,这个服务器一次只能为一个客户端提供服务。

当服务端调用accept函数获取到连接后就给该客户端提供服务,但在服务端提供服务期间可能会有其他客户端发起连接请求,但由于当前服务器是单执行流的,只能服务完当前客户端后才能继续服务下一个客户端

客户端为什么会显示连接成功?

当服务端在给第一个客户端提供服务期间,第二个客户端向服务端发起的连接请求时是成功的,只不过服务端没有调用accept函数将该连接获取上

实际在底层会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中,而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的,因此服务端虽然没有获取第二个客户端发来的连接请求,但是在第二个客户端那里显示是连接成功的。

如何解决这个问题?

单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此服务器一般是不会写成单执行流的。要解决这个问题就需要将服务器改为多执行流的,即多进程或多线程


二、多进程版TCP网络程序

把当前的单执行流服务器改为多进程版的服务器

  • 当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务,而是调用fork函数创建子进程,然后让子进程为新链接提供服务。

  • 而父进程就可以继续从监听套接字当中获取新连接,而不用关心获取上来的连接是否服务完毕。

  • 子进程创建后会继承父进程的文件描述符表,此时子进程就能够对特定的套接字文件进行读写操作,进而完成对对应客户端的服务

等待子进程的问题

  • 当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏

  • 如果服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完当前客户端,才能继续获取下一个连接请求,此时服务端仍然是以一种串行的方式为客户端提供服务。

  • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出。

  • 总之,服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不尽人意。此时我们可以考虑让服务端不等待子进程退出

方法一:捕捉SIGCHLD信号

SIGCHLD信号是在子进程退出时由内核发送给父进程的信号。默认情况下,父进程会等待子进程退出并进行处理。但是,可以通过捕捉SIGCHLD信号并将其处理动作设置为忽略,来实现父进程不等待子进程退出的效果,这样父进程就不必关心子进程了

服务端代码修改如下(只需要更改start函数):

// 启动服务器
    void start()
    {
        // 忽略SIGCHLD信号
        signal(SIGCHLD, SIG_IGN);
        for (;;)
        {
            // 4. 获取新链接,accept从_listensock套接字里面获取新链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 这里的sockfd才是真正为客户端请求服务
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
            {
                cout << "accept error, next!" << endl;
                continue;
            }
            cout << "accept a new line success, sockfd: " << sockfd << endl;
 
            // 多进程版(忽略信号)
            pid_t id = fork();
            if (id == 0) // 子进程
            {
                close(_listensock);
                serviceIo(sockfd);
                close(sockfd);
                exit(0);
            }
            // 父进程无需等待
            close(sockfd);
        }
    }

方法二:创建孙进程,退出子进程
让父进程创建子进程,子进程创建孙进程后立即退出,最后让孙子进程为客户端提供服务

由于子进程进程创建完孙子进程后就立刻退出了,因此实际为客户端提供服务的孙子进程就变成了孤儿进程,该进程就会被OS领养,当孙子进程为客户端提供完服务退出后OS会回收孙子进程,所以父进程是不需要等待孙子进程退出的

关闭不需要的文件描述符

父子进程需要关闭掉不需要的文件描述符,否则就会导致被占用的文件描述符越积越多,最终无法再打开文件。

  • 父进程创建子进程后,即可关闭新链接的套接字sockfd,因为父进程并不负责通信服务,而子进程已经将套接字继承了
  • 子进程(方法一)或孙进程(方法二)在提供完服务退出之前应该关闭通信套接字sockfd,以释放对应的套接字文件。

服务端代码修改如下(只需要更改start函数 ):

// 启动服务器
    void start()
    {
        for (;;)
        {
            // 4. 获取新链接,accept从_listensock套接字里面获取新链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 这里的sockfd才是真正为客户端请求服务
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
            {
                cout << "accept error, next!" << endl;
                continue;
            }
            cout << "accept a new line success, sockfd: " << sockfd << endl;
 
            // 多进程版(孙子进程)
            pid_t id = fork();
            if (id == 0) // 子进程
            {
                close(_listensock);
                // 创建孙子进程,让子进程退出
                if (fork() > 0)
                    exit(0);
                // 孙子进程执行后序代码
                serviceIo(sockfd);
                close(sockfd);
                exit(0);
            }
            // 父进程回收子进程
            pid_t ret = waitpid(id, nullptr, 0);
            if (ret > 0)
            {
                cout << "wait success" << endl;
            }
            close(sockfd); // 必须关掉
        }
    }

三、多线程版TCP网络程序

频繁的创建进程会给OS带来巨大的负担,而创建线程的成本比创建进程低得多。因此在实现多执行流的服务器时最好采用多线程进行实现。这块在多线程已经谈过,不再赘述

当服务进程调用accept函数获取到一个新连接后,就可以直接创建一个线程,让该线程为对应客户端提供服务。

主线程创建出新线程后,也是需要等待新线程退出的,否则也会造成类似于僵尸进程这样的问题。但对于线程来说,如果不想让主线程等待新线程退出,直接线程分离即可,当这个线程退出时系统会自动回收该线程所对应的资源。

文件描述符关闭的问题

  • 各个线程共享是同一张文件描述符表,也就是说服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是能够直接访问这个文件描述符的。

  • 对于主线程accept上来的文件描述符,主线程不能对其进行关闭操作,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭

服务端代码修改如下(只需要更改start函数,外加一个线程的入口函数即可):

	class ThreadDate
	{
	public:
    	ThreadDate(tcpServer *self, int sockfd)
        : _self(self), _sockfd(sockfd)
    	{}
 
	public:
    	tcpServer *_self;
    	int _sockfd;
	};

	// 启动服务器
    void start()
    {
        for (;;)
        {
            // 4. 获取新链接,accept从_listensock套接字里面获取新链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 这里的sockfd才是真正为客户端请求服务
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
            {
                cout << "accept error, next!" << endl;
                continue;
            }
            cout << "accept a new line success, sockfd: " << sockfd << endl;
 
            // 多线程版
            pthread_t tid;
            ThreadDate *td = new ThreadDate(this, sockfd);
            pthread_create(&tid, nullptr, threadRoutine, td);
        }
    }
 
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离
        ThreadDate *td = static_cast<ThreadDate *>(args);
        td->_self->serviceIo(td->_sockfd);
        close(td->_sockfd); // 通信套接字必须由新线程关闭
        delete td;
        return nullptr;
    }

四、线程池版TCP网络程序

多线程存在的问题

  • 每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样做不仅麻烦,而且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程。
  • 如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。
  • 计算机当中的线程越多,CPU的压力就越大

解决方法:线程池

  • 可以在服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程(频繁创建线程的开销)

  • 当某个线程为客户端提供完服务后,不要让该线程退出,而是让该线程继续为下一个客户端提供服务,如果当前没有客户端连接请求,则可以让该线程先进入休眠状态,当有客户端连接到来时再将该线程唤醒。

  • 服务端创建的这一批线程的数量不能太多,此时CPU的压力也就不会太大

  • 需要在服务端引入线程池,因为线程池的存在就是为了避免处理短时间任务时创建与销毁线程的代价,此外,线程池还能够保证内核充分利用,防止过分调度

服务端代码修改如下:

  1. 引入之前编写的线程池
  2. 将通信服务改为短时间服务,线程池不适合进行循环式的长期服务
  3. 编写线程池的任务类Task
	// 启动服务器
    void start()
    {
        // 初始化线程池
        unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
        tp->run();
 
        for (;;)
        {
            // 4. 获取新链接,accept从_listensock套接字里面获取新链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 这里的sockfd才是真正为客户端请求服务
            int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0) // 获取新链接失败,但不会影响服务端运行
            {
                cout << "accept error, next!" << endl;
                continue;
            }
            cout << "accept a new line success, sockfd: " << sockfd << endl;
 
            // 5. 为sockfd提供服务,即为客户端提供服务
            // 构建任务
            tp->push(Task(sockfd, serviceIo));
        }
    }

ps -aL 查看一下线程信息

img

运行服务端后,就算没有客户端发来连接请求,此时在服务端就已经有了6个线程,其中有一个是接收新连接的服务线程,而其余的5个是线程池当中为客户端提供服务的线程

与之前不同的是,无论现在有多少客户端发来请求,在服务端都只会有线程池当中的5个线程为之提供服务,线程池当中的线程个数不会随着客户端连接的增多而增多,这些线程也不会因为客户端的退出而退出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值