网络和Linux网络_3(套接字编程)TCP网络通信代码(多个版本)

news2024/12/26 11:03:39

目录

1. TCP网络编程

1.1 前期代码

log.hpp

tcp_server.cc

1.2 accept和单进程版代码

1.3 多进程版strat代码

1.4 client.cc客户端

1.5 多进程版strat代码改进+多线程

1.6 线程池版本

Task.hpp

lockGuard.hpp

thread.hpp

threadPool.hpp

多个回调任务

tcp_client.cc

tcp_server.hpp

2. 笔试选择题

答案及解析

本篇完。


1. TCP网络编程

框架和前面udp通信一样,接口函数上一篇也讲了,这里直接放一部分代码:

1.1 前期代码

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)  // 可变参数
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) 
    {
        return;
    }
#endif
    char stdBuffer[1024]; // 标准日志部分
    time_t timestamp = time(nullptr); // 获取时间戳
    // struct tm *localtime = localtime(&timestamp); // 转化麻烦就不写了
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; // 自定义日志部分
    va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args); // 相当于ap=nullptr
    
    printf("%s%s\n", stdBuffer, logBuffer);

    // FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

client.cc

#include <iostream>

int main()
{
    return 0;
}

tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

class TcpServer
{
protected:
    const static int gbacklog = 20;  // listen的第二个参数,现在先不管
public:
    TcpServer(uint16_t port, std::string ip="")
        :_listensock(-1)
        , _port(port)
        , _ip(ip)
    {}
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
        if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }
    void start()
    {
        while(true)
        {
            sleep(7);
        }
    }
    ~TcpServer()
    {}
protected:
    uint16_t _port;
    std::string _ip;
    int _listensock;
};

tcp_server.cc

#include "tcp_server.hpp"
#include <memory>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}

// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}

编译运行:

此时程序就跑起来了。


1.2 accept和单进程版代码

再写start函数:

    void start()
    {
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            service(servicesock, client_ip, client_port);
        }
    }

第4步获取链接,写在start函数中,如上图所示,使用accept来接收客户端的连接请求,有点像udp中的recvfrom一样,只是accept是用来接收套接字的连接请求,而recvfrom是接收套接字中的数据的。man accept:

accept系统调用的参数和recvfrom中的一样,如上图所示,accept的作用就是接收来自套接字中的连接请求,也就是来自客户端的连接请求。

设置为listen状态的套接字不用了通信,只是用来接收客户端的网络请求,具体体现在accept的返回值上。

第一步中创建的套接字就像是一个门童,使用accept来接收客户端的连接请求,如果有连接请求并且接收成功,那么会返回一个文件描述符fd。 这里的文件描述符sock和前面的_listensock不是一个东西,_listensock是我们创建的,是专门用来接收连接请求的,而accept返回的sock是操作系统在接收成功连接请求后新创建的套接字的文件描述符。 sock指向的文件描述符是服务端专门用来和客户端通信的,所以每有一个客户端向服务器发起连接请求,客户端接收成功够都会创建一个套接字用来一对一的提供服务。

如果accept接收连接请求失败,则返回-1,并且设置错误码。这里的失败并不是致命的,就像门童拉客一样,拉客失败也没有什么,继续进行下一次拉客就行。 所以accept失败也没有什么,继续接收下一个连接请求即可,所以在代码中,如果接收失败,使用了continue继续接收连接请求。

accept是阻塞执行的,在没有网络连接请求的时候,会阻塞等待,直到客户端的网络连接请求到来。


至此,进行tcp网络通信的所有准备工作已经做完,接下来就是进行具体的服务了,也就是读取客户端发送来的数据并做相应的处理了。看一下start在最后调用的service函数:

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if(s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

如代码所示,就是服务器指向的具体服务函数。 客户端读取客户端发送来的数据时,是从accept返回的文件描述符sock指向的套接字中读取数据的,因为这个套接字是专门用来服务客户端的。

读取数据时,使用的是read系统调用,和读取普通文件一模一样。

数据读取成功后,做一些处理,先将读取的数据打印一下,加一个回显,再给客户端发送过去。

发送数据时,使用的是write系统调用,写入的也是sock指向的套接字,同样与向普通文件中写入数据一模一样。

在读取普通文件的时候,如果文件被读完了,read会返回0,表示文件的内容被读取完毕。 但是在使用read读取tcp套接字的时候,如果读取到0,表示客户端关闭了它的套接字,代表着客户端不再进行网络通信了,此时服务端就可以结束这次通信了,也就是将sock指向的套接字关闭。

这里再放下tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if(s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

class TcpServer
{
protected:
    const static int gbacklog = 20;  // listen的第二个参数,现在先不管
public:
    TcpServer(uint16_t port, std::string ip="")
        :_listensock(-1)
        , _port(port)
        , _ip(ip)
    {}
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
        if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(_servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            service(servicesock, client_ip, client_port);
        }
    }

    ~TcpServer()
    {}
protected:
    uint16_t _port;
    std::string _ip;
    int listensock;
};

编译运行:

telnet 是一个远程链接命令,这里切换到了root输入了这个下载指令:yum -y install telnet telnet-server xinetd,以后输入telnet 127.0.0.1 7070链接到启动了的程序,然后Ctrl+],再回车就能发消息了,最后Ctrl+]输入quit就退出了。


1.3 多进程版strat代码

只用改strat函数:

    void start()
    {
        // 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            // service(servicesock, client_ip, client_port);

            // 2 version 2.0 -- 多进程版 --- 创建子进程
            // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
            pid_t id = fork();
            assert(id != -1);
            if(id == 0) // 子进程
            {
                // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
                // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
                close(_listensock); // 关闭自己不需要的套接字
                service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
                exit(0); // 僵尸状态
            }
            // 父进程
            close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
            // 如果父进程关闭servicesock,会不会影响子进程?

            // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
            // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        }
    }

编译运行:

此时就完整了多进程的第一个版本。


1.4 client.cc客户端

看看接口,man accept

如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,

accept()返回时传出客户端的地址和端口号。

addr是一个传输出型参数,如果给addr 参数传NULL,表示不关心客户端的地址。

addrlen参数是一个输入输出型参数(value-result argument),

输入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,

man connect

客户端需要调用connect()连接服务器;
connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址;
connect()成功返回0,出错返回-1。

client.cc

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = 0;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // client 要不要bind呢?不需要显示的bind,但是一定是需要port
    // 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
    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());

    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) // 比UDP多了这一步
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }
    std::cout << "connect success" << std::endl; // 到这链接成功了

    while (true)
    {
        std::string line;
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server 回显# " << buffer << std::endl;
        }
        else // 关闭链接或者读取失败
        {
            break;
        }
    }
    close(sock);
    return 0;
}

编译运行:


1.5 多进程版strat代码改进+多线程

直接放代码:

    void start()
    {
        // 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
            // // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            // service(servicesock, client_ip, client_port);


            // // 2 version 2.0 -- 多进程版 --- 创建子进程
            // // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0) // 子进程
            // {
            //     // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
            //     close(_listensock); // 关闭自己不需要的套接字
            //     service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
            //     exit(0); // 僵尸状态
            // }
            // // 父进程
            // close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
            // // 如果父进程关闭servicesock,会不会影响子进程?

            // // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
            // // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号


            // version2.1 -- 多进程版 -- version 2.0 改版
            pid_t id = fork();
            if(id == 0)
            {
                // 子进程
                close(_listensock);
                if(fork() > 0)  // 再创建子进程,子进程本身
                    exit(0); //子进程本身立即退出
                // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
                service(servicesock, client_ip, client_port);
                exit(0);
            }
            // 父进程
            waitpid(id, nullptr, 0); // 不会阻塞
            close(servicesock);
        }
    }

还是和1.4 一样的效果,创建进程的成本是很高的,所以再改成多线程版,直接放代码:

(给Makefile加上-lpthread)

这是改的部分:

tcp_server.hpp:

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if(s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

class ThreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

class TcpServer
{
protected:
    const static int gbacklog = 20;  // listen的第二个参数,现在先不管

    static void *threadRoutine(void *args) // 加上static就没this指针了
    {
        pthread_detach(pthread_self()); // 线程分离
        ThreadData *td = static_cast<ThreadData *>(args);
        service(td->_sock, td->_ip, td->_port);
        delete td;

        return nullptr;
    }
public:
    TcpServer(uint16_t port, std::string ip="")
        :_listensock(-1)
        , _port(port)
        , _ip(ip)
    {}
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
        if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        // 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务
            // // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            // service(servicesock, client_ip, client_port);


            // // 2 version 2.0 -- 多进程版 --- 创建子进程
            // // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0) // 子进程
            // {
            //     // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
            //     close(_listensock); // 关闭自己不需要的套接字
            //     service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
            //     exit(0); // 僵尸状态
            // }
            // // 父进程
            // close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
            // // 如果父进程关闭servicesock,会不会影响子进程?

            // // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
            // // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号


            // // version2.1 -- 多进程版 -- version 2.0 改版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0)  // 再创建子进程,子进程本身
            //         exit(0); //子进程本身立即退出
            //     // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); // 不会阻塞
            // close(servicesock);

            // version 3 --- 多线程版本
            // 创建进程的成本是很高的,所以再改成多线程版(不封装了)
            ThreadData *td = new ThreadData();
            td->_sock = servicesock;
            td->_ip = client_ip;
            td->_port = client_port;
            pthread_t tid;
            // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
            pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类
        }
    }

    ~TcpServer()
    {}
protected:
    uint16_t _port;
    std::string _ip;
    int _listensock;
};

编译运行:还是和上面一样的效果


1.6 线程池版本

把以前写的线程池和有关的代码拷过来:(只有Task.hpp要改一改)

Task.hpp

#pragma once
#include <functional>

// typedef std::function<void (int , const std::string &, const uint16_t &, const std::string &)> func_t;
using func_t = std::function<void (int , const std::string &, const uint16_t &, const std::string &)>; // 和上一行一样的效果

class Task
{
public:
    Task()
    {}
    Task(int sock, const std::string ip, uint16_t port, func_t func)
        : _sock(sock)
        , _ip(ip)
        , _port(port)
        , _func(func)
    {}
    void operator ()(const std::string &name)
    {
        _func(_sock, _ip, _port, name);
    }
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
    // int type;
    func_t _func;
};

lockGuard.hpp

#pragma once
#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t* mtx) 
        :_pmtx(mtx)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmtx);
        // std::cout << "进行加锁成功" << std::endl;
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
        // std::cout << "进行解锁成功" << std::endl;
    }
    ~Mutex()
    {}
protected:
    pthread_mutex_t* _pmtx;
};

class lockGuard // RAII风格的加锁方式
{
public:
    lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
        :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~lockGuard()
    {
        _mtx.unlock();
    }
protected:
    Mutex _mtx;
};

thread.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_t

class ThreadData // 线程数据
{
public:
    void *_args; // 真实参数
    std::string _name; // 名字
};

class Thread // 封装的线程
{
public:
    Thread(int num, fun_t callback, void *args) 
        : _func(callback) // 回调函数
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer
        _name = nameBuffer;

        _tdata._args = args; // 线程构造时把参数和名字带给线程数据
        _tdata._name = _name;
    }
    void start() // 启动线程
    {
        pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据
    }
    void join() // join自己
    {
        pthread_join(_tid, nullptr);
    }
    std::string name() // 返回线程名
    {
        return _name;
    }
    ~Thread() // 析构什么也不做
    {}

protected:
    std::string _name; // 线程名字
    pthread_t _tid; // 线程tid
    fun_t _func; // 线程要执行的函数
    ThreadData _tdata; // 线程数据
};

threadPool.hpp

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"

const int g_thread_num = 7;
// 线程池->有一批线程,一批任务,有任务push有任务pop,本质是: 生产消费模型
template<class T>
class ThreadPool
{
private:
    ThreadPool(int thread_num = g_thread_num)
        :_num(thread_num)
    {
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&cond, nullptr);
        for(int i = 1; i <= _num; ++i)
        {
            _threads.push_back(new Thread(i, routine, this));
        }
    }
    ThreadPool(const ThreadPool<T> &other) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;

public:
    static ThreadPool<T> *getThreadPool(int num = g_thread_num) // 多线程使用单例的过程
    {
        // 可以有效减少未来必定要进行加锁检测的问题
        // 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为
        // 如果这里不加if,未来任何一个线程想获取单例,都必须调用getThreadPool接口
        // 一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的
        if (nullptr == thread_ptr) 
        {
            lockGuard lockguard(&mutex);
            // pthread_mutex_lock(&mutex);
            if (nullptr == thread_ptr)
            {
                thread_ptr = new ThreadPool<T>(num);
            }
            // pthread_mutex_unlock(&mutex);
        }
        return thread_ptr;
    }

    void run() // 1. 线程池的整体启动
    {
        for (auto &iter : _threads)
        {
            iter->start();
            std::cout << iter->name() << " 启动成功" << std::endl;
        }
    }

    pthread_mutex_t *getMutex()
    {
        return &lock;
    }
    bool isEmpty()
    {
        return _task_queue.empty();
    }
    void waitCond() // 特定的条件变量下等待
    {
        pthread_cond_wait(&cond, &lock);
    }
    T getTask()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
    static void *routine(void *args) // 每个线程启动后做的工作 
    {   // 类的成员函数有this指针 -> 两个参数 -> 类型不匹配 -> 所以加static
        // 消费过程 -> 访问_task_queue -> 静态访问不了 -> 构造函数传this指针
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->_args;
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex()); // 出花括号自动调用析构,花括号里的接口全是加锁的
                while (tp->isEmpty()) // 空就等待
                {
                    tp->waitCond();
                }
                // 任务队列不为空,读取任务
                task = tp->getTask(); // 是共享的-> 将任务从共享,拿到自己的私有空间
            }
            task(td->_name); // 告诉哪一个线程去处理这个任务就行了
        }
    }

    void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务
    {
        lockGuard lockguard(&lock); // 加锁,执行完这个函数自动解锁
        _task_queue.push(task); // 生产一个任务
        pthread_cond_signal(&cond); // 唤醒一个线程
    }

    // void joins()
    // {
    //     for (auto &iter : _threads)
    //     {
    //         iter->join();
    //     }
    // }
    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            // iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
    }

protected:
    std::vector<Thread *> _threads; // 保存一堆线程的容器
    int _num; // 线程的数量
    std::queue<T> _task_queue; // 任务队列
    pthread_mutex_t lock;
    pthread_cond_t cond;

    static ThreadPool<T> *thread_ptr; // 懒汉模式的单例对象指针
    static pthread_mutex_t mutex; // 单例对象的锁
};

template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr; // 定义初始化为空

template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER; // 定义锁

多个回调任务

改下tcp_server.hpp:

tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>
#include "log.hpp"
#include "threadPool.hpp"
#include "Task.hpp"

// static void service(int sock, const std::string &clientip, const uint16_t &clientport)
// {
//     //echo server
//     char buffer[1024];
//     while(true)
//     {
//         // read && write 可以直接被使用
//         ssize_t s = read(sock, buffer, sizeof(buffer)-1);
//         if(s > 0)
//         {
//             buffer[s] = 0; // 将发过来的数据当做字符串
//             std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
//         }
//         else if(s == 0) // 对端关闭连接
//         {
//             logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
//             break;
//         }
//         else
//         {
//             logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
//             break;
//         }
//         write(sock, buffer, strlen(buffer));
//     }
// }

static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name) // 带上线程名的service
{
    // echo server
    char buffer[1024];
    while (true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

// class ThreadData
// {
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;
// };

class TcpServer
{
protected:
    const static int gbacklog = 20;  // listen的第二个参数,现在先不管

    // static void *threadRoutine(void *args) // 加上static就没this指针了
    // {
    //     pthread_detach(pthread_self()); // 线程分离
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     service(td->_sock, td->_ip, td->_port);
    //     delete td;

    //     return nullptr;
    // }
public:
    TcpServer(uint16_t port, std::string ip="")
        :_listensock(-1)
        , _port(port)
        , _ip(ip)
        , _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
        if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        // 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        // signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        _threadpool_ptr->run();
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务

            // // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            // service(servicesock, client_ip, client_port);


            // // 2 version 2.0 -- 多进程版 --- 创建子进程
            // // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0) // 子进程
            // {
            //     // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
            //     close(_listensock); // 关闭自己不需要的套接字
            //     service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
            //     exit(0); // 僵尸状态
            // }
            // // 父进程
            // close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
            // // 如果父进程关闭servicesock,会不会影响子进程?

            // // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
            // // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号


            // // version2.1 -- 多进程版 -- version 2.0 改版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0)  // 再创建子进程,子进程本身
            //         exit(0); //子进程本身立即退出
            //     // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); // 不会阻塞
            // close(servicesock);

            // // version 3 --- 多线程版本
            // // 创建进程的成本是很高的,所以再改成多线程版(不封装了)
            // ThreadData *td = new ThreadData();
            // td->_sock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
            // pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类

            // verison4 --- 线程池版本
            Task t(servicesock, client_ip, client_port, service);
            _threadpool_ptr->pushTask(t);
        }
    }

    ~TcpServer()
    {}
protected:
    uint16_t _port;
    std::string _ip;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

编译运行

完成了多个线程实现回显任务的情景。下面写一个回调的函数,实现小写字母转大写字母:

static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    //  一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    char buffer[1024];
    // read && write 可以直接被使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        char *start = buffer;
        while(*start)
        {
            char c;
            if(islower(*start)) 
            {
                c = toupper(*start);
            }
            else
            {
                c = *start;
            }
            message.push_back(c);
            ++start;
        }
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }
    close(sock);
}

只需要改一下回调方法:

把client.cc改成循环的:

tcp_client.cc

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    bool alive = false;
    int sock = 0;
    std::string line;
    while (true) // 客户端不断的链接
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            // client 要不要bind呢?不需要显示的bind,但是一定是需要port
            // 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
            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());

            if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) // 比UDP多了这一步
            {
                std::cerr << "connect error" << std::endl;
                exit(3);
            }
            std::cout << "connect success" << std::endl; // 到这链接成功了
            alive = true;
        }
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        if (line == "quit")
            break;

        ssize_t s = send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
        if (s > 0) // ssize_t有符号的整数
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        else // 关闭链接或者读取失败
        {
            alive = false;
            close(sock);
        }
    }
    return 0;
}


/*
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = 0;
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // client 要不要bind呢?不需要显示的bind,但是一定是需要port
    // 需要让os自动进行port选择,客户端需要连接别人的能力 -> connect
    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());

    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0) // 比UDP多了这一步
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }
    std::cout << "connect success" << std::endl; // 到这链接成功了

    while (true)
    {
        std::string line;
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        send(sock, line.c_str(), line.size(), 0); // 比write就多了后面的0,效果一样
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 比read就多了后面的0,效果一样
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server 回显# " << buffer << std::endl;
        }
        else // 关闭链接或者读取失败
        {
            break;
        }
    }
    close(sock);
    return 0;
}
*/

编译运行:

此时链接的时候没有把回显信息打印出来,不过先不改了。下面再弄一个类似英汉互译的:

static void dictOnline(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    //  一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
        {"apple", "苹果"},
        {"watermelon", "西瓜"},
        {"banana", "香蕉"},
        {"hard", "好难"}
    };
    // read && write 可以直接被使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        auto iter = dict.find(buffer);
        if(iter == dict.end()) 
            message = "此字典找不到...";
        else 
            message = iter->second;
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}

编译运行:

完成运行,为了方便这里再放下tcp_server.hpp。

tcp_server.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <memory>
#include "log.hpp"
#include "threadPool.hpp"
#include "Task.hpp"
#include <unordered_map>

// static void service(int sock, const std::string &clientip, const uint16_t &clientport)
// {
//     //echo server
//     char buffer[1024];
//     while(true)
//     {
//         // read && write 可以直接被使用
//         ssize_t s = read(sock, buffer, sizeof(buffer)-1);
//         if(s > 0)
//         {
//             buffer[s] = 0; // 将发过来的数据当做字符串
//             std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
//         }
//         else if(s == 0) // 对端关闭连接
//         {
//             logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
//             break;
//         }
//         else
//         {
//             logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
//             break;
//         }
//         write(sock, buffer, strlen(buffer));
//     }
// }

static void service(int sock, const std::string &clientip,
                    const uint16_t &clientport, const std::string &thread_name) // 带上线程名的service
{
    // echo server
    char buffer[1024];
    while (true)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
            break;
        }
        else
        {
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

static void change(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    //  一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    char buffer[1024];
    // read && write 可以直接被使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        char *start = buffer;
        while(*start)
        {
            char c;
            if(islower(*start)) 
            {
                c = toupper(*start);
            }
            else
            {
                c = *start;
            }
            message.push_back(c);
            ++start;
        }
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }
    close(sock);
}

static void dictOnline(int sock, const std::string &clientip,
                   const uint16_t &clientport, const std::string &thread_name)
{
    //  一般服务器进程业务处理,如果是从连上,到断开,要一直保持这个链接, 长连接
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
        {"apple", "苹果"},
        {"watermelon", "西瓜"},
        {"banana", "香蕉"},
        {"hard", "好难"}
    };
    // read && write 可以直接被使用
    ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
    if (s > 0)
    {
        buffer[s] = 0; // 将发过来的数据当做字符串
        std::cout << thread_name << "|" << clientip << ":" << clientport << "# " << buffer << std::endl;
        std::string message;
        auto iter = dict.find(buffer);
        if(iter == dict.end()) 
            message = "此字典找不到...";
        else 
            message = iter->second;
        write(sock, message.c_str(), message.size());
    }
    else if (s == 0) // 对端关闭连接
    {
        logMessage(NORMAL, "%s:%d shutdown, me too", clientip.c_str(), clientport);
    }
    else
    {
        logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
    }

    close(sock);
}

// class ThreadData
// {
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;
// };

class TcpServer
{
protected:
    const static int gbacklog = 20;  // listen的第二个参数,现在先不管

    // static void *threadRoutine(void *args) // 加上static就没this指针了
    // {
    //     pthread_detach(pthread_self()); // 线程分离
    //     ThreadData *td = static_cast<ThreadData *>(args);
    //     service(td->_sock, td->_ip, td->_port);
    //     delete td;

    //     return nullptr;
    // }
public:
    TcpServer(uint16_t port, std::string ip="")
        :_listensock(-1)
        , _port(port)
        , _ip(ip)
        , _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}
    void initServer()
    {
        // 1. 创建socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0); // 域 + 类型 + 0 // UDP第二个参数是SOCK_DGRAM
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", _listensock); // 3

        // 2. bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当正式通信的时候,需要先建立连接,UDP没这一步
        if(listen(_listensock, gbacklog) < 0) // 第一个参数是套接字,第二个参数后面再说
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        // 下面父进程的阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号
        // signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        _threadpool_ptr->run();
        while(true)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            int servicesock = accept(_listensock, (struct sockaddr*)&src, &len); // accept获取新链接
            // servicesock(服务套接字,相当于此小区域专门给你负责的) 
            // 对比 类内的_sock(类似于小区域外面拉客的,拉来小区域就交给servicesock管了)
            if(servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            } 
            // 获取连接成功了
            uint16_t client_port = ntohs(src.sin_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
                 /*换行符*/servicesock, client_ip.c_str(), client_port);
            // 开始进行通信服务

            // // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // // 很显然,是不能够直接被使用的 -- 为什么? -> 要从单进程改成多线程
            // service(servicesock, client_ip, client_port);


            // // 2 version 2.0 -- 多进程版 --- 创建子进程
            // // 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢? -> 能
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0) // 子进程
            // {
            //     // 子进程会不会继承父进程打开的文件与文件fd呢? -> 会
            //     // 子进程是来进行提供服务的,需不需要知道监听socket呢? -> 不需要
            //     close(_listensock); // 关闭自己不需要的套接字
            //     service(servicesock, client_ip, client_port); // 子进程对新链接提供服务
            //     exit(0); // 僵尸状态
            // }
            // // 父进程
            // close(servicesock); // 父进程关闭自己不需要的套接字,不关的话文件描述符会越少
            // // 如果父进程关闭servicesock,会不会影响子进程?

            // // 前面有了signal(SIGCHLD, SIG_IGN); -> 父进程不用等了
            // // waitpid(); // 阻塞式等待 -> 用信号代替 -> 子进程退出会像父进程发送SIGCHLD信号


            // // version2.1 -- 多进程版 -- version 2.0 改版
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0)  // 再创建子进程,子进程本身
            //         exit(0); //子进程本身立即退出
            //     // 到这是(孙子进程),是孤儿进程,被OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程
            //     service(servicesock, client_ip, client_port);
            //     exit(0);
            // }
            // // 父进程
            // waitpid(id, nullptr, 0); // 不会阻塞
            // close(servicesock);

            // // version 3 --- 多线程版本
            // // 创建进程的成本是很高的,所以再改成多线程版(不封装了)
            // ThreadData *td = new ThreadData();
            // td->_sock = servicesock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // // 在多线程这里不用(不能)进程关闭特定的文件描述符,因为是共享的
            // pthread_create(&tid, nullptr, threadRoutine, td); // 到这里写threadRoutine再写ThreadData类

            // verison4 --- 线程池版本
            // Task t(servicesock, client_ip, client_port, service);
            // Task t(servicesock, client_ip, client_port, change);
            Task t(servicesock, client_ip, client_port, dictOnline);
            _threadpool_ptr->pushTask(t);
        }
    }

    ~TcpServer()
    {}
protected:
    uint16_t _port;
    std::string _ip;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

2. 笔试选择题

1. 在网络字节序中,所谓”小端”(little endian)说法正确的是( )

A.高字节数据存放在低地址处,低字节数据存放在高地址处

B.低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处

C.和编译器相关

D.上述答案都不正确

2. 当一个UDP报文道达目的主机时,操作系统使用(  )选择正确的socket.

A.源IP地址

B.源端口号

C.目的端口号

D.目的IP地址

3. 以下有关于端口号的说法错误的是()

A.tcp的最大端口号是65535

B.端口号是一个2字节16位的整数

C.IP地址 + 端口号能够标识网络上的某一台主机的某一个进程

D.一个进程至多只能绑定一个端口

4. 【多选题】socket编程中经常需要进行字节序列的转换,下列哪几个函数是将网络字节序列转换为主机字节序列?()

A.htons

B.ntohs

C.htonl

D.ntohl

5. 【多选题】Socket,即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口。socket的使用类型主要有()

A.基于TCP协议,采用流方式,提供可靠的字节流服务

B.基于IP协议,采用流数据提供数据网络发送服务

C.基于HTTP协议,采用数据包方式提供可靠的数据包装服务

D.基于UDP协议,采用数据报方式提供数据打包发送服务

6. 下列有关Socket的说法,错误的是()

A.Socket用于描述IP地址和端口,是一个通信链的句柄

B.Socket通信必须建立连接

C.Socket客户端的端口是不固定的

D.Socket服务端的端口是固定的

答案及解析

1. B

小端:低字节位数据存放在内存低地址处, 高字节位数据存放在内存高地址处

大端:高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址

网络字节序为大端字节序

2. C

IP地址用于标识主机,端口号用于标识主机上的对应网络通信socket

因此正确选项为C选项,通过目的端口号来区分是哪个socket

3. D

端口号是一个无符号16位的整数,用于表示主机上的网络通信socket,因为是16位,因此最大端口号是65535(从0开始)

IP地址可以在网络当中标识一台主机,端口号可以在主机当中标识一个进程(socket会关联到对应进程)

一个进程当中对于端口的绑定是和socket强相关,理论上该进程如果创建多个socket,可以给每一个socket都进行绑定一个端口

基于以上理解,错误选项为:D (一个进程可能会创建多个socket,因此有可能会绑定多个端口)

4. BD

函数名称解析:

  • n 对应是 network
  • h 对应是 host
  • s 对应是 short
  • l 对应是 long

因此网络字节序到主机字节序的转换是 ntoh

5. AD

TCP协议,采用流方式,SOCK_STREAM, 可靠

UDP协议,采用数据报方式,SOCK_DGRAM, 不可靠

HTTP协议是应用层协议,在传输层基于TCP协议实现, (可靠是TCP提供的,TCP提供字节流传输)

IP协议是网络层协议,TCP和UDP协议在网络层都是基于IP协议的。(实现数据报传输)

6. B

A正确:概念性理解,socket就是一条通信的句柄

B错误:socket 可以基于TCP 面向连接 也可以基于UDP无连接

C正确:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定

D正确:服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了


本篇完。

下一篇:网络和Linux网络_4(应用层)序列化和反序列化(网络计算器)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1224279.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【电路笔记】-最大功率传输

最大功率传输 文章目录 最大功率传输1、概述2、最大功率传输定理 (MPTT)3、示例4、阻抗匹配5、总结 当工程师设计电子电路时&#xff0c;他们会跟踪许多不同的参数&#xff0c;但最重要的参数之一是功率。 在现代电路中&#xff0c;功率在多个阶段中不断变化&#xff0c;有时由…

(论文阅读46-50)图像描述2

46.文献阅读笔记 简介 题目 Learning a Recurrent Visual Representation for Image Caption Generation 作者 Xinlei Chen, C. Lawrence Zitnick, arXiv:1411.5654. 原文链接 http://www.cs.cmu.edu/~xinleic/papers/cvpr15_rnn.pdf 关键词 2014年rnn图像特征和文本特…

算法-二叉树-简单-二叉树的遍历

记录一下算法题的学习6 首先我们要回忆一下怎么样遍历一个树&#xff1a; 三种遍历概念 先序遍历&#xff1a;先访问根节点&#xff0c;再访问左子树&#xff0c;最后访问右子树。 后序遍历&#xff1a;先左子树&#xff0c;再右子树&#xff0c;最后根节点。 中序遍历&…

常见的近似算法

前言 最近有个项目要用到近似算法&#xff0c;就到处摸了下&#xff0c;整理了一个小结。 近似算法统计 在Java中&#xff0c;你可以使用各种近似算法来解决不精确但接近于最优解的问题。以下是几种常见的近似算法的实现方法&#xff1a; 贪心算法&#xff08;Greedy Algori…

常见的反爬+文字加解密

一、常见的反爬介绍 基于身份识别的反爬&#xff1a;1.User-agent 2.Referer 3.Captcha 验证码 4.必备参数 基于爬虫行为的反爬&#xff1a;1.单位时间内请求数量超过一定阈值 2.相邻两次请求之间间隔小于一定阈值3.蜜罐陷阱 通过对数据加密进行反爬&#xff1a;1.对文字加密…

记录联系ThinkPad T490扬声器无声音但插耳机有声音的解决办法

型号&#xff1a;联想ThinkPad T490&#xff0c;系统Win10 64位。 现象&#xff1a;扬声器无声音&#xff0c;插耳机有声音。且右下角小喇叭正常&#xff0c;设备管理器中驱动显示一切也都正常&#xff08;无黄色小叹号&#xff09;。 解决办法&#xff1a; 尝试了各种方法&a…

【机器学习Python实战】logistic回归

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;机器学习python实战 欢迎订阅&#xff01;后面的内容会越来越有意思~ ⭐内容说明&#xff1a;本专栏主要针对机器学习专栏的基础内容进行python的实现&#xff0c;部分…

带你快速掌握Linux最常用的命令(图文详解)- 最新版(面试笔试常考)

最常用的Linux指令&#xff08;图文详解&#xff09;- 最新版 ls&#xff1a;列出目录中的文件和子目录。&#xff08;重点&#xff09;cd&#xff1a;改变当前工作目录。绝对路径&#xff1a;相对路径 pwd&#xff1a;显示当前工作目录的路径。mkdir&#xff1a;创建一个新的目…

盘点60个Python各行各业管理系统源码Python爱好者不容错过

盘点60个Python各行各业管理系统源码Python爱好者不容错过 学习知识费力气&#xff0c;收集整理更不易。 知识付费甚欢喜&#xff0c;为咱码农谋福利。 源码下载链接&#xff1a;https://pan.baidu.com/s/1VdAFp4P0mtWmsA158oC-aA?pwd8888 提取码&#xff1a;8888 项目名…

c语言-浅谈指针(3)

文章目录 1.字符指针变量常见的字符指针初始化另一种字符指针初始化例&#xff1a; 2.数组指针变量什么是数组指针变量数组指针变量创建数组指针变量初始化例&#xff08;二维数组传参的本质&#xff09; 3.函数指针变量什么是函数指针变量呢&#xff1f;函数指针变量创建函数指…

C语言基本算法----冒泡排序

原理 冒泡排序就是对一个存放N个数据的数组进行N次扫描&#xff0c;每次把最小或者最大的那个元素放到数组的最后&#xff0c;达到排序的目的。 原理图解 冒泡排序过程分析 冒泡排序的执行过程 冒泡排序总结 在此感谢 冒泡排序法_哔哩哔哩_bilibili 这篇blog是对这位up此视…

二维码智慧门牌管理系统升级解决方案:门牌聚合,让管理更便捷!

文章目录 前言一、传统门牌管理系统的瓶颈二、地图门牌聚合展示的优势三、地图门牌聚合展示的实现方法四、智慧门牌管理系统的未来发展 前言 随着城市的发展和建设&#xff0c;对于地址信息的管理变得越来越重要。而智慧门牌管理系统作为管理地址信息的重要工具&#xff0c;其…

Linux--网络概念

1.什么是网络 1.1 如何看待计算机 我们知道&#xff0c;对于计算机来说&#xff0c;计算机是遵循冯诺依曼体系结构的&#xff08;即把数据从外设移动到内存&#xff0c;再从内存到CPU进行计算&#xff0c;然后返回内存&#xff0c;重新读写到外设中&#xff09;。这是一台计算机…

机器人走迷宫问题

题目 1.房间有XY的方格组成&#xff0c;例如下图为64的大小。每一个方格以坐标(x,y) 描述。 2.机器人固定从方格(0, 0)出发&#xff0c;只能向东或者向北前进&#xff0c;出口固定为房间的最东北角&#xff0c;如下图的 方格(5,3)。用例保证机器人可以从入口走到出口。 3.房间…

英伟达AI布局的新动向:H200 GPU开启生成式AI的新纪元

英伟达Nvidia是全球领先的AI计算平台和GPU制造商&#xff0c;近年来一直在不断推出创新的AI产品和解决方案&#xff0c;为各行各业的AI应用提供强大的支持。 最近&#xff0c;英伟达在GTC 2023大会上发布了一款专为训练和部署生成式AI模型的图形处理单元&#xff08;GPU&#…

如何实现用户未登录不可访问系统

在开发web系统时&#xff0c;如果用户不登录&#xff0c;发现用户也可以直接正常访问系统&#xff0c;这种设计本身并不合理&#xff0c;那么我们希望看到的效果是&#xff0c;只有用户登录成功之后才可以正常访问系统&#xff0c;如果没有登录则拒绝访问。那么我们可以使用过滤…

回溯算法(3)--n皇后问题及回溯法相关习题

一、n皇后问题 1、概述 n皇后要求在一个nn的棋盘上放置n个皇后&#xff0c;使得他们彼此不受攻击&#xff0c;皇后可以攻击同一行、同一列、同一斜线上的敌人&#xff0c;所以n皇后问题要求寻找在棋盘上放置这n个皇后的方案&#xff0c;使得任意两个皇后都不在同一行、同一列或…

口袋参谋:一键下载任意买家秀图片、视频,是怎么做到的!

​对于淘宝商家来说&#xff0c;淘宝买家秀是非常的重要的。买家秀特别好看的话&#xff0c;对于提升商品的销量来说&#xff0c;会有一定的帮助&#xff0c;如何下载别人的买家秀图片&#xff0c;然后用到自己的店铺中呢&#xff1f; 这里我可以教叫你们一个办法&#xff01;那…

ROS基础—关于参数服务器的操作

1、rosparam list 获取参数服务器的所有参数。 2、rosparam get /run_id 获取参数的值

【uniapp】使用扫码插件,解决uni.scanCode扫码效率低的问题

1. 背景 uniapp 中自带的二维码扫描的 API 是 uni.scanCode&#xff0c;但有如下问题&#xff1a; 二维码扫描的效率不高&#xff0c;有些需要扫2秒左右 较小或模糊的一些二维码无法识别出来&#xff0c;多次扫同样的一个码可能出现扫码失败的情况 受环境影响大&#xff0c…