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

目录

[1. TCP网络编程](#1. TCP网络编程)

[1.1 前期代码](#1.1 前期代码)

log.hpp

tcp_server.cc

[1.2 accept和单进程版代码](#1.2 accept和单进程版代码)

[1.3 多进程版strat代码](#1.3 多进程版strat代码)

[1.4 client.cc客户端](#1.4 client.cc客户端)

[1.5 多进程版strat代码改进+多线程](#1.5 多进程版strat代码改进+多线程)

[1.6 线程池版本](#1.6 线程池版本)

Task.hpp

lockGuard.hpp

thread.hpp

threadPool.hpp

多个回调任务

tcp_client.cc

tcp_server.hpp

[2. 笔试选择题](#2. 笔试选择题)

答案及解析

本篇完。


1. TCP网络编程

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

1.1 前期代码

log.hpp

cpp 复制代码
#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

cpp 复制代码
#include <iostream>

int main()
{
    return 0;
}

tcp_server.hpp

cpp 复制代码
#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

cpp 复制代码
#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函数:

cpp 复制代码
    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函数:

cpp 复制代码
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

cpp 复制代码
#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函数:

cpp 复制代码
    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

cpp 复制代码
#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代码改进+多线程

直接放代码:

cpp 复制代码
    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:

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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;
};

编译运行

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

cpp 复制代码
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

cpp 复制代码
#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;
}
*/

编译运行:

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

cpp 复制代码
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

cpp 复制代码
#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(应用层)序列化和反序列化(网络计算器)。

相关推荐
程序员-King.1 分钟前
2、桥接模式
c++·桥接模式
羑悻的小杀马特3 分钟前
环境变量简介
linux
chnming19875 分钟前
STL关联式容器之map
开发语言·c++
程序伍六七18 分钟前
day16
开发语言·c++
小陈phd36 分钟前
Vscode LinuxC++环境配置
linux·c++·vscode
是阿建吖!40 分钟前
【Linux】进程状态
linux·运维
火山口车神丶1 小时前
某车企ASW面试笔试题
c++·matlab
hzyyyyyyyu1 小时前
内网安全隧道搭建-ngrok-frp-nps-sapp
服务器·网络·安全
明明跟你说过1 小时前
Linux中的【tcpdump】:深入介绍与实战使用
linux·运维·测试工具·tcpdump