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

目录

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.mfbz.cn/a/161968.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux--网络概念

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

Mysql-复合查询

实际开发中往往数据来自不同的表&#xff0c;所以需要多表查询。 1.笛卡尔积 通俗来讲就是两个表的每一列都组合一遍&#xff0c;也就是穷举法。 穷举出来的数据表会有大量重复数据&#xff0c;而我们只需要加上一些限定条件就可以完成有效数据的筛选。 select EMP.ename, EM…

linux进程之进程的优先级➕环境变量

文章目录 1.优先级的认识1.1优先级的介绍1.2初识优先级1.3ps指令1.4查看/修改进程的优先级1.5对优先级的认识1.6对进程的深一步理解 2.环境变量2.0环境变量相关的命令2.1环境变量的概念2.2常见/查看环境变量2.3环境变量的作用2.4修改环境变量1.将zombie可执行程序放到PATH现有的…

牛客-- 求解立方根python

描述 计算一个浮点数的立方根&#xff0c;不使用库函数。 保留一位小数。 数据范围&#xff1a;∣val∣≤20 输入描述&#xff1a; 待求解参数&#xff0c;为double类型&#xff08;一个实数&#xff09; 输出描述&#xff1a; 输出参数的立方根。保留一位小数。 使用…

CCF CSP认证 历年题目自练Day47

题目 试题编号&#xff1a; 201712-3 试题名称&#xff1a; Crontab 时间限制&#xff1a; 10.0s 内存限制&#xff1a; 256.0MB 样例输入 3 201711170032 201711222352 0 7 * * 1,3-5 get_up 30 23 * * Sat,Sun go_to_bed 15 12,18 * * * have_dinner 样例输出 201711170…

shopee选品工具:Shopee选品工具—知虾精准选品与科学运营的利器

在如今竞争激烈的电商市场中&#xff0c;如何进行精准选品和科学运营成为了每个卖家都需要面对的问题。而Shopee选品工具——知虾&#xff0c;作为一款强大的大数据采集及分析平台&#xff0c;为卖家提供了全面的市场分析、产品分析和店铺分析功能&#xff0c;帮助卖家发现市场…

​软考-高级-系统架构设计师教程(清华第2版)【第19章 大数据架构设计理论与实践 (P691~716)-思维导图】​

软考-高级-系统架构设计师教程&#xff08;清华第2版&#xff09;【第19章 大数据架构设计理论与实践 &#xff08;P691~716&#xff09;-思维导图】 课本里章节里所有蓝色字体的思维导图

ARDUINO UNO 12颗LED超酷流水灯效果

效果代码&#xff1a; #define t 30 #define t1 20 #define t2 100 #define t3 50 void setup() { // set up pins 2 to 13 as outputs for (int i 2; i < 13; i) { pinMode(i, OUTPUT); } } /Effect 1 void loop() { effect_1(); effect_1(); effect_…

机器人制作开源方案 | 智能快递付件机器人

一、作品简介 作者&#xff1a;贺沅、聂开发、王兴文、石宇航、盛余庆 单位&#xff1a;黑龙江科技大学 指导老师&#xff1a;邵文冕、苑鹏涛 1. 项目背景 受新冠疫情的影响&#xff0c;大学校园内都采取封闭式管理来降低传染的风险&#xff0c;导致学生不能外出&#xff0c…

Microsoft SQL Server Management Studio(2022版本)启动无法连接到服务器

Microsoft SQL Server Management Studio&#xff08;2022版本&#xff09;启动无法连接到服务器 解决方法&#xff1a; 打开SQL Server 2022 配置管理器。 启动即可。

视频剪辑技巧:轻松搞定视频随机合并,一篇文章告知所有秘诀

在视频制作的过程中&#xff0c;视频随机合并是一种创新的剪辑手法&#xff0c;它打破了传统的线性剪辑模式&#xff0c;使得视频剪辑更加灵活和有趣。通过将不同的视频片段随机组合在一起&#xff0c;我们可以创造出独特的视觉效果和情感氛围。这种剪辑方式让观众在观看视频时…

Web之HTML笔记

Web之HTML、CSS、JS Web标准一、HTML&#xff08;超文本标记语言&#xff09;HTML 基本结构标签常用标签1.font标签2.p标签3.注释4.h系列标题5.img6.超链接a7.列表8.表格9.表单 Web之CSS笔记 Web标准 结构标准用于对网页元素进行整理和分类(HTML)表现标准用于设置网页元素的版…

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

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

SARAS-Net: Scale and Relation Aware Siamese Network for Change Detection

SARAS-Net&#xff1a;用于变化检测的尺度和关系感知的孪生网络 AAAI Chao-Peng Chen, Jun-Wei Hsieh, Ping-Yang Chen, Yi-Kuan Hsieh, Bor-Shiun Wang 2023 摘要&#xff1a;变化检测(CD)旨在找出不同时间两幅图像之间的差异&#xff0c;并输出变化图来表示该区域是否发生了…

中国净初级生产力年度合成产品NPP(MYD17A3H.006)

中国净初级生产力年度合成产品NPP&#xff08;MYD17A3H.006&#xff09;由航天宏图实验室提供&#xff0c;根据NASA MODIS数据&#xff08;MYD17A3H.006&#xff09;通过航天宏图 Smoother计算得到的平滑后NPP产品&#xff0c;解决了影像云雾覆盖、像元异常值等问题。对处理后的…

c语言-浅谈指针(3)

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

SpringSecurity6 | 自动配置(下)

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色专栏&#xf…

我叫:冒泡排序【JAVA】

1.什么是冒泡排序&#xff1f; 冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后&#xff08;从下标较小的元素开始)&#xff0c;依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。 2.来个实战应用 我们…

从键盘输入5个学生的信息(姓名、学号、成绩), 存入一个结构体数组中,计算平均分,并按成绩 高低排序并输出.

代码如下 #include<stdio.h> #include<string.h> #include<stdlib.h> /* 1.练习结构体数组排序   从键盘输入5个学生的信息&#xff08;姓名、学号、成绩&#xff09;,存入一个结构体数组中&#xff0c;计算平均分&#xff0c;并按成绩高低排序并输出. */…

python学习:break用法详解

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 在执行while循环或者for循环时&#xff0c;只要循环条件满足&#xff0c;程序会一直执行循环体。 但在某些场景&#xff0c;我们希望在循环结束前就强制结束循环。 Python中有两种强制结束循环的方法&#xff1a; continue语…
最新文章