Socket编程--TCP

一、TCP socket API

TCP编程与前面的UDP编程一样,都属于网络通信,所以也需要包含下面三个网络通信必备的头文件:

第一个接口socket()用于创建套接字:

cpp 复制代码
_sockfd = socket(AF_INET, SOCK_STREAM, 0);

与前面的UDP不同的地方在于,由于TCP协议是面向字节流的,所以类型type要采用SOCK_STREAM。

第二个接口bind()用于把socket文件描述符与本机地址进行绑定:

cpp 复制代码
int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));

由于TCP是面向连接的,所以TCP编程需要比UDP多一个环节,就是客户端需要向服务器端发起连接请求。也就是说服务器端需要处于一种监听的状态(叫做listen),便于收到客户端的连接请求。所以也就需要使用到第三个接口listen:

listen的返回值:

listen接口的作用就是设置套接字sockfd为监听状态。此接口中的backlog表示的是链接队列的长度,即链接队列最大容纳连接数。它是一个整型数值(如 10、128)。这里我们将链接数量设置为16:

cpp 复制代码
static const int gbacklog = 16;
n = listen(_listensockfd, gbacklog);

当完成这些工作后,就可以启动TCP服务了。查看我们所启动的TCP服务可以使用指令netstate -antp。-a表示查看所有的信息,-n表示1用数字显示,-t表示显示的是tcp的内容(-u表示的则是upd的内容),-p表示将进程信息带上。

在完成了上述的工作之后,我们接下来需要完成服务器的启动start();

与UDP不同的是,由于TCP是面向连接的,所以不能一上来就读取客户端的信息,先要做的是获取客户端的连接请求。获取连接我们需要使用的接口是accept():

上面这两个参数合起来可以帮助我们获取到客户端的socket ip+port信息。

cpp 复制代码
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);

这一步操作其实类似于UDP中recvfrom接口的作用,都是用于获取客户端的ip和端口号信息。

当获取失败,依旧是返回-1;但是当返回成功时,该接口将返回一个文件描述符。而在TCP中,与UDP不同的是,会存在很多的文件描述符,而我们之前所创建的文件描述符叫做listen_socket监听套接字。未来返回的套接字叫做IO_socket,即服务套接字。未来一个服务套件字代表一条连接1!

accept()用于接收连接,是服务器端对连接的操作;而用于客户端发起连接请求的接口是--connect():

连接成功返回0,失败返回-1。

在进行地址转换时会使用下面两个接口------inet_ntop,inet_pton:

inet_ntop

这个接口的作用是把二进制(网络字节序)IP 地址,转换成人可读的点分字符串格式

int af:地址族,AF_INET代表 IPv4,AF_INET6代表 IPv6

const void *src:输入二进制 IP(网络字节序),IPv4就传&(sockaddr_in)->sin_addr

char *dst:输出缓冲区,转换完成的 IP 字符串会写到这里,由调用者自己提供内存

socklen_t size:dst 缓冲区字节大小,防止缓冲区溢出

inet_pton

这个接口的作用是把点分字符串格式IP 地址,转换成二进制(网络字节序)IP地址。

int af:地址族,AF_INET(IPv4) / AF_INET6(IPv6)

const char *src:输入IP 字符串,如 "127.0.0.1"

void *dst:输出缓冲区,存放二进制网络序 IP。IPv4 传&addr.sin_addr

二、version0:单进程版本

version0仅用于做接口的说明和熟悉后续所要用的接口:

在成功获取到连接后,我们就可以处理新的socket套接字了

cpp 复制代码
InetAddr clientaddress(clientaddr);
serviceIO(sockfd, clientaddress);

TCP和UDP一样,在进行通信时都是全双工的

cpp 复制代码
void serviceIO(int socket,InetAddr &address)
{
    LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
    while(true)
    {
        char inbuffer[1024] = {0};
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
            if (n > 0)
             {
                 inbuffer[n] = 0;
                 LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

                 std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
                 echo_string += inbuffer;

                 write(sockfd, echo_string.c_str(), echo_string.size());//写入
             }
             else if (n == 0)//读到0说明连接断开了
             {
                 LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
                 break;
             }
             else
             {
                 LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
                 break;
             }
     }
}

version0服务器端完整代码如下:

EchoTcpServer.hpp(version0)

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"

using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

static const int gbacklog = 16;
static const uint16_t gport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _port(port)
    {
    }
    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
        // 2. 填充本地socket信息
        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; // 任意地址bind

        // 3. bind
        int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";

        // 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
        n = listen(_listensockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }
    void serviceIO(int socket,InetAddr &address)
{
    LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
    while(true)
    {
        char inbuffer[1024] = {0};
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
            if (n > 0)
             {
                 inbuffer[n] = 0;
                 LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

                 std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
                 echo_string += inbuffer;

                 write(sockfd, echo_string.c_str(), echo_string.size());//写入
             }
             else if (n == 0)//读到0说明连接断开了
             {
                 LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
                 break;
             }
             else
             {
                 LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
                 break;
             }
     }
}
    void Start()
    {
        while (true)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 5. 获取连接
            int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
         //6. 处理新sockfd
         version 0
         InetAddr clientaddress(clientaddr);
         serviceIO(sockfd, clientaddress);
    }
~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    // 不需要显示包含ip
    int _listensockfd;
};

TCP的客户端与UDP的客户端的逻辑一模一样,要访问目标服务器,首先需要知道服务器端的ip

cpp 复制代码
// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

接下来就是创建客户端的tcp socket套接字:

cpp 复制代码
// 1. 创建tcpsocket套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);//AF_INET:网络通信服务,SOCK_STREAM:流式套接
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

然后是进行绑定操作bind。无论是UPD还是TCP,都需要绑定自己的ip号和端口号。但是都不需要自己手动地对客户端进行绑定,操作系统会自动为我们分配随机端口,防止端口号冲突!

但是TCP在这里比UDP多一步操作,由于TCP是面向连接的,所以在bind端口号和ip过后,需要发起建立连接的请求!所以我们在这里需要使用到接口connect()。

需要注意的是,connect()接口在建立连接时,会自动帮我们进行绑定,也就是说此处只需要只用到connect接口即可!

cpp 复制代码
// 2. bind端口号ip,发起建立连接的工作
InetAddr serveraddress(server_port, server_ip);//构建好对象后InetAddr中会自动转换为网络结构
    int n = connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len());
    if (n < 0)
    {
        std::cerr << "connect to " << serveraddress.ToString() << " failed!"<< std::endl;
        exit(3);
    }
    std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;

连接完成后就可以进行通信操作了:

cpp 复制代码
// 3. 通信
    while(true)
    {
        std::string line;
        std::cout << "please Enter# ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));//同一个套接字既可以读又可以写,证明TCP通信是全双工的。
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "read enf of file!" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error!" << std::endl;
            break;
        }
    }

客户端完整代码如下:

EchoTcpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "InetAddr.hpp"

static void Usage(const std::string &name)
{
    std::cerr << "Usage:\n\t";
    std::cerr << name << " server_ip server_port" << std::endl;
}

// ./client_tcp server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    // 1. 创建tcpsocket套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // 2. bind端口号ip,发起建立连接的工作
    // {client ip, client port} <-> {server ip, server port}
    InetAddr serveraddress(server_port, server_ip);
    int n = connect(sockfd, (struct sockaddr *)serveraddress.GetNetAddress(), serveraddress.Len());
    if (n < 0)
    {
        std::cerr << "connect to " << serveraddress.ToString() << " failed!"<< std::endl;
        exit(3);
    }
    std::cerr << "connect to " << serveraddress.ToString() << " success!" << std::endl;

    // 3. 通信
    while(true)
    {
        std::string line;
        std::cout << "please Enter# ";
        std::getline(std::cin, line);

        write(sockfd, line.c_str(), line.size());

        char inbuffer[1024];
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "read enf of file!" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error!" << std::endl;
            break;
        }
    }

    return 0;
}

EchoTcpServerMain.cc

cpp 复制代码
#include "EchoTcpServer.hpp"
#include <memory>

static void Usage(const std::string &process)
{
    std::cerr << "Usage:\n\t";
    std::cerr << process << " local_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY();
    uint16_t server_port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(server_port);
    tsvr->InitServer();
    tsvr->Start();

    return 0;
}

编译运行后启动服务器端和客户端,再输入命令netstate -antp查看所启动的TCP服务,会发现server_tc和client_tc已经建立起了对应的连接。一条是客户端向服务器端建立的连接,一条是服务器端向客户端建立的连接。由于是在一台机器上启动的服务器端和客户端,所以相当于一台机器上会看到两个不同视角的连接,一个是客户端的,一个是服务器端的。

但是,上述version0是一个单进程的TCP服务,当一个客户端与服务器端建立起连接后,如果客户端一直不退出,那么进程就会阻塞在读写那部分逻辑中,也就是其他的客户端无法与服务器建立起连接,服务器无法并发地处理多个客户端发出的请求。这显然与当代网络通信的要求不符。

三、version1:多进程版本

为了解决上述问题,需要在version0的基础上进行改造,进而引申出版本version1。

单进程的服务器对客户端请求的处理显然行不通。于是有了version1版本,引入了父子进程。由父进程负责对客户端的请求的接收,然后将这些接收到的请求交给子进程,让子进程进程处理IO。

如果创建子进程失败,返回-1。如果创建子进程失败则后面的操作都无法进行下去,所以直接让服务器进程终止。

cpp 复制代码
pid_t id = fork();
if(id < 0)
{
    LOG(LogLevel::FATAL) << "fork errr!";
    exit(FORK_ERR);
}

创建进程成功后,让子进程进行IO任务的处理

cpp 复制代码
else if(id == 0)
{
    InetAddr clientaddress(clientaddr);
    serviceIO(sockfd, clientaddress);
    close(sockfd);
    exit(0); // 终止子进程
}

当子进程完成任务之后,由于fork之后的代码是共享的,如果子进程不退出就有可能进执行父进程获取连接部分的代码,这样的情况显然不合理。所有当子进程完成任务之后,我们要直接终止子进程防止影响后续父进程的代码执行。

但是如果终止了子进程,那子进程就进入了僵尸状态。在前面的Linux进程中我们提到过,子进程终止需要父进程wait去回收子进程。但是如果按照这个逻辑来完成这部分的代码,代码的执行不就又变成串行了吗?父进程需要等待子进程退出,回收子进程后,才能执行后续任务。

或者采用在前面学到过的waitpid中的WNHANG信号,但是这种方法的调用时机、回收范围不对,依旧产生僵尸进程,从而产生bug。

所以,此处的最佳实践是加上signal(SIGCHLD, SIG_IGN),子进程一退出,会自动释放,无需父进程等待回收子进程。

在此处也有另外一种方式,那就是子进程再fork一次,创建孙子进程,然后子进程直接退出,让孙子进程来执行后续的IO任务。而由于子进程退出了,孙子进程就变成孤儿进程了。这是利用系统特性的写法。

父进程成功fork后创建出子进程,父进程的文件描述符表也要给子进程拷贝一份。但是在后续子进程的任务中并没有使用到监听套接字(listen_socket)。在网络通信中,有一个大的原则,那就是如果有一个资源不再使用了,那就需要尽早释放掉。文件描述符是有用且有限的资源,由于子进程在后续不再使用这部分资源,就需要及时释放掉。

同理,父进程只负责接收客户端的信号,所以不需要使用到服务套接字(IO_socket),所以也需要及时关闭这部分套接字:

cpp 复制代码
else if(id == 0)
{
    // 关闭掉自己不需要的sockfd
    close(_listensockfd);
    // 子进程
    if(fork() > 0) exit(0); // 子进程直接退出

    // 孙子进程 - 孤儿进程
    InetAddr clientaddress(clientaddr);
    serviceIO(sockfd, clientaddress);
    close(sockfd);
    exit(0); // 终止子进程
}
else
{
    // 父进程
    // wait(id); waitpid: WNOHANG
    // 关闭掉自己不需要的sockfd
    close(sockfd);
    pid_t rid = waitpid(id, nullptr, 0);
    (void)rid;
}

version1服务器端完整代码:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"

using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    FORK_ERR
};

static const int gbacklog = 16;
static const uint16_t gport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _port(port)
    {
    }
    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
        // 2. 填充本地socket信息
        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; // 任意地址bind

        // 3. bind
        int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";

        // 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
        n = listen(_listensockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }
    void serviceIO(int socket,InetAddr &address)
{
    LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
    while(true)
    {
        char inbuffer[1024] = {0};
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
            if (n > 0)
             {
                 inbuffer[n] = 0;
                 LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

                 std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
                 echo_string += inbuffer;

                 write(sockfd, echo_string.c_str(), echo_string.size());//写入
             }
             else if (n == 0)//读到0说明连接断开了
             {
                 LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
                 break;
             }
             else
             {
                 LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
                 break;
             }
     }
}
    void Start()
    {
        while (true)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 5. 获取连接
            int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
            //version1:多进程版本
            pid_t id = fork();
            if(id < 0)
            {
                LOG(LogLevel::FATAL) << "fork errr!";
                exit(FORK_ERR);
            }
            else if(id == 0)
            {
                // 关闭掉自己不需要的sockfd
                close(_listensockfd);
                // 子进程
                if(fork() > 0) exit(0); // 子进程直接退出

                // 孙子进程 - 孤儿进程
                InetAddr clientaddress(clientaddr);
                serviceIO(sockfd, clientaddress);
                close(sockfd);
                exit(0); // 重要
            }
            else{
                // 父进程
                // wait(id); waitpid: WNOHANG
                // 关闭掉自己不需要的sockfd
                close(sockfd);
                pid_t rid = waitpid(id, nullptr, 0);
                (void)rid;
            }
    }
~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    // 不需要显示包含ip
    int _listensockfd;
};         

    

这里介绍一个工具,用于网络编程测试服务器------telnet:

命令格式

bash 复制代码
telnet 服务IP 端口号

连接成功后,按下Ctrl + ]调出命令行,键盘输入文字回车,会直接发给服务端。服务端返回的数据实时打印在终端。再按下Ctrl + ]调出命令行,输入quit回车将会断开连接。

四、version2:多线程版本

在前面的version1多进程服务器中,每有一个客户端就需要创建一个进程,这样的话未免开销太大。所以也就延伸出了version2多线程版本。在这里,我们直接使用C++库封装的线程。

需要包含头文件

cpp 复制代码
#include<pthread.h>
cpp 复制代码
//version 2:多线程版本
pthread_t tid;
InetAddr clientaddress(clientaddr);
ThreadData *td = new ThreadData(this, sockfd, clientaddress);
pthread_create(&tid, nullptr, thread_routine, (void *)td);

同理,由于主线程也需要等待回收线程join(),一旦join就会阻塞等待,这样不就出现和父子进程一样的问题了吗?所以我们要把线程设置为分离状态。

这里的thread_routine任务函数使用static静态方法,是为了避免this指针,保证参数传递的正确性。但是将线程函数中无法调用ServiceIO函数,因为pthread线程函数只能传一个 void* 参数,而使用ServiceIO函数需要传入多个参数,所以需要一个类来封装多组数据。

此处不需要像前面的父子进程一样关闭不需要的套接字,因为父子线程是共享文件描述符表的。

cpp 复制代码
class ThreadData
    {
    public:
        ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
            : _this(ts), _sockfd(sockfd), _addr(addr)
        {
        }
        ~ThreadData()
        {
            close(_sockfd);
        }

    public:
        TcpServer *_this;
        int _sockfd;
        InetAddr _addr;
    };

    static void *thread_routine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());
        td->_this->serviceIO(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }

由于上述代码中服务器端提供的服务是长服务,长连接。而多进程、多线程的模式不太适用于处理这种长服务,长连接的服务,大多用于小型应用的服务。一旦有大量的客户端的请求涌入服务器,进程和线程数量大量增多,就会增加CPU的调度成本,进程和线程切换的成本增加,使得服务器卡顿。所以如果要使用多进程或多线程版本的服务器,我们就需要将长服务改为短服务。由此需要将代码中的长服务改为短服务(就是去掉while循环的逻辑)

所以version2服务器端完整代码为:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"

using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    FORK_ERR
};

static const int gbacklog = 16;
static const uint16_t gport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _port(port)
    {
    }
    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
        // 2. 填充本地socket信息
        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; // 任意地址bind

        // 3. bind
        int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";

        // 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
        n = listen(_listensockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }

    void serviceIO(int socket,InetAddr &address)
{
    LOG(LogLevel::DEBUG) << "client info is: " << address.ToString();
    //长服务改为短服务
    char inbuffer[1024] = {0};
    ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);//先读后写入
        if (n > 0)
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer; 
            std::string echo_string = "server echo# ";//新建一个新字符串用于接收客户端的信息
            echo_string += inbuffer;

            write(sockfd, echo_string.c_str(), echo_string.size());//写入
        }
            else if (n == 0)//读到0说明连接断开了
         {
             LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
         }
            else
          {
             LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
          }
}

    void Start()
    {
        while (true)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 5. 获取连接
            int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
            //version 2:多线程版本
            pthread_t tid;
            InetAddr clientaddress(clientaddr);
            ThreadData *td = new ThreadData(this, sockfd, clientaddress);
            pthread_create(&tid, nullptr, thread_routine, (void *)td);
        }   
    }
~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    // 不需要显示包含ip
    int _listensockfd;
};         

五、version3:接入线程池

前面多线程的版本仍然有缺陷。原因是由于提供的服务是短服务,而每来一个短服务都需要创建一个线程,有可能创建线程的开销比短服务的开销还要大。所以我们可以直接接入线程池!这样就省去了来了一个客户端才创建一个线程,减少了大批量创建线程的开销。由此引出version3------线程池版本服务器。

version3服务器端完整代码为:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
// #include <signal.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "ThreadPool.hpp"

using namespace NS_LOG_MODULE;
using namespace NS_THREAD_POOL_MODULE;

using task_t = std::function<void()>;

enum
{
    SUCCESS = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    FORK_ERR
};

static const int gbacklog = 16;
static const uint16_t gport = 8888;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport) : _port(port)
    {
    }
    void InitServer()
    {
        // 1. 创建socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "create socket success: " << _listensockfd;
        // 2. 填充本地socket信息
        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; // 任意地址bind

        // 3. bind
        int n = bind(_listensockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";

        // 4. tcp是面向连接的,所以,TCP服务器要处于一种叫做listen,监听状态。
        n = listen(_listensockfd, gbacklog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }

    void serviceIO(int sockfd, InetAddr address)
    {
        // 短连接,短服务
        char inbuffer[1024] = {0};
        ssize_t n = read(sockfd, inbuffer, sizeof(inbuffer) - 1);
        if (n > 0)
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << address.ToString() << " say# " << inbuffer;

            // 写
            std::string echo_string = "server echo# ";
            echo_string += inbuffer;

            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "client quit, address: " << address.ToString();
        }
        else
        {
            LOG(LogLevel::ERROR) << "client read error, address: " << address.ToString();
        }

        close(sockfd);

    }

    class ThreadData
    {
    public:
        ThreadData(TcpServer *ts, int sockfd, const InetAddr &addr)
            : _this(ts), _sockfd(sockfd), _addr(addr)
        {
        }
        ~ThreadData()
        {
            close(_sockfd);
        }

    public:
        TcpServer *_this;
        int _sockfd;
        InetAddr _addr;
    };

    static void *thread_routine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());
        td->_this->serviceIO(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }
    void Start()
    {
        while (true)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            // 5. 获取连接
            int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept errr!";
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success, sockfd: " << sockfd;
            // version 3: 接入线程池
            InetAddr clientaddress(clientaddr);
            ThreadPool<task_t>::Instance()->Enqueue([this, sockfd, clientaddress]()->void{
                this->serviceIO(sockfd, clientaddress);
            });
        }
    }
    ~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    // 不需要显示包含ip
    int _listensockfd;
};

接入线程池的好处在于线程池会预先创建固定上限的工作线程,不会随客户端并发请求无限制新建线程。当大量客户端同时接入、空闲线程耗尽时,新任务会存入任务队列排队等待,避免瞬间大批量创建线程耗尽系统资源,导致服务器卡顿、崩溃。

六、其他代码

InetAddr.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
    {
        _ip = inet_ntoa(_address.sin_addr);
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);                  // h->n
        _address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        _len = sizeof(_address);
    }
    bool operator == (const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    InetAddr()
    {}
    struct sockaddr_in *GetNetAddress()
    {
        return &_address;
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {}
private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};

在上面的地址转换中,inet_ntoa内部使用静态全局缓冲区,在多线程环境下存在线程安全问题,多线程同时调用会覆盖数据。所以需要做适当修改:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


// 对客户端进行先描述
class InetAddr
{
public:
    InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
    {
        // _ip = inet_ntoa(_address.sin_addr);
        char ipstr[32];
        inet_ntop(AF_INET, &(_address.sin_addr), ipstr, sizeof(ipstr));
        _ip = ipstr;
        _port = ntohs(_address.sin_port);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
    {
        bzero(&_address, sizeof(_address));
        _address.sin_family = AF_INET;
        _address.sin_port = htons(_port);                  // h->n
        //_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
        inet_pton(AF_INET, ip.c_str(), &(_address.sin_addr));
        _len = sizeof(_address);
    }
    bool operator == (const InetAddr &addr)
    {
        return (this->_ip == addr._ip) && (this->_port == addr._port);
    }
    std::string ToString()
    {
        return "[" + _ip + ":" + std::to_string(_port) + "]";
    }
    InetAddr()
    {}
    struct sockaddr_in *GetNetAddress()
    {
        return &_address;
    }
    socklen_t Len()
    {
        return _len;
    }
    ~InetAddr()
    {}
private:
    // net address
    struct sockaddr_in _address;
    socklen_t _len;
    // host address
    std::string _ip;
    uint16_t _port;
};

Makefile

bash 复制代码
.PHONY:all
all:client_tcp server_tcp

server_tcp:EchoTcpServerMain.cc
	g++ -o $@ $^ -std=c++17 -lpthread
client_tcp:EchoTcpClient.cc
	g++ -o $@ $^ -std=c++17 -static

.PHONY:clean
clean:
	rm -f client_tcp server_tcp

以下略(往期博客中有):

Cond.hpp

Mutex.hpp

Thread.hpp

ThreadPool.hpp

Logger.hpp

相关推荐
caimouse2 小时前
ReactOS 窗口系统架构分析
服务器·c语言
王志来137944730083 小时前
聚焦五大核心场景:工控服务器机箱平台的垂直深耕之路
运维·服务器·python
zhonyu鱼3 小时前
RustDesk:开源免费的远程桌面软件,可自建服务器掌控全部数据
运维·服务器·开源
便利店10243 小时前
边界与远程:墙、洞与加密通道
运维·服务器·防火墙·acl·nat
LHX sir3 小时前
HubPort 和 SCADA / MES 是什么关系?是替代还是增强
网络·工业互联网·scada·mes
Zhang~Ling3 小时前
Linux多线程互斥锁:从现象到原理解析
linux·运维·网络
zmzmzmalo6 小时前
Linux ELF文件加载与内存管理揭秘
linux·网络·数据库
三言老师12 小时前
K8s集群运行时自动化运维全覆盖落地实操(下)
linux·运维·服务器·网络
Super 含13 小时前
Android 启动优化(五):线程、GC 与 IO 为什么会拖慢启动?
java·服务器·数据库