• Linux知识点 -- 网络编程套接字


    Linux知识点 – 网络编程套接字


    一、预备知识

    1.认识端口号

    端口号(port)是传输层协议的内容;

    • 端口号是一个2字节16位的整数;
    • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
    • IP地址+端口号能够标识网络上的某一台主机的某一个进程;
    • 一个端口号只能被一个进程占用;
    • 源端口号就是发送数据的端口,目的端口号就是接收数据的端口;

    注:一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定;

    在平常使用的APP上,客户端软件发出的请求,通过网络传输到服务端软件进行处理;
    真正的网络间通信,本质其实是进程间通信
    将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程;

    2.套接字

    IP地址 + 端口号就是套接字;

    3.TCP协议与UDP协议

    TCP协议:

    • 传输层协议
    • 有连接(可理解为打电话,对方必须响应)
    • 可靠传输(为保证可靠性,需要更多的策略)
    • 面向字节流

    UDP协议:

    • 传输层协议
    • 无连接(写信或发邮件)
    • 不可靠传输(使用成本更低;适合直播、视频网站)
    • 面向数据报

    4.网络字节序

    内存中的数据存储分大端和小端,因此网络通信中不同的主机也会有不同的大小端主机之间通信;
    网络规定:所有网络数据,都必须是大端

    网络字节序和主机字节序的转换库函数:
    在这里插入图片描述
    其中:

    • h表示host,n表示network,I表示32位长整数,s表示16位短整数;
    • 例如htonI表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
    • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
    • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回;

    二、socket编程接口

    1.socket常见API

    在这里插入图片描述

    2.sockaddr结构

    socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket;然而,各种网络协议的地址格式并不相同;

    常见的套接字类型:

    • 域间socket
    • 原始socket
    • 网络socket

    理论上是三种场景,对应三套接口;
    但实际上所有的地址接口都是统一的;
    所有接口传入的都是struct sockaddr这个类型的地址参数;
    网络和域间套接字的前两个字节是不同的,在函数内部会进行判断;
    在这里插入图片描述

    • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址;
    • IPv4、IPv6地址类型分别定义为常数AF_ INET、AF_INET6;这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
    • socket API可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in;这样的好处是程序的通用性可以接收IPv4, IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

    三、UDP套接字编程

    1.直接打印客户端信息

    udp_server.hpp

    • socket接口:创建套接字
      在这里插入图片描述
      创建套接字,建立一个通信的一端
      创建成功,返回文件描述符 ,但是UDP是非字节流的操作,不能使用之前的文件接口访问;失败返回-1,并设置errno;
      domain:域,套接字的类型;
      AF_INET是网络通信
      AF_LOCAL是本地通信
      在这里插入图片描述
      type:套接字通信种类,UDP是面向数据报,使用SOCK_DGRAM;
      SOCK_DGRAM:用户数据报
      在这里插入图片描述
      protocol:前两个参数确定,这个参数也确定了,一般写为0;

    • bind接口:绑定套接字
      在这里插入图片描述
      将用户设置的ip和port在内核中和我们当前的进程强关联;
      成功返回0,失败返回-1,设置errno;
      sockfd:套接字;
      sockaddr:通用addr结构,AF_INET网络通信使用sockaddr_in地址结构;
      要显示出来sockaddr_in类型,需包含头文件;

      在这里插入图片描述
      sockaddr_in结构体:
      在这里插入图片描述
      在这里插入图片描述
      其中,sin_port为端口号,sin_addr为ip地址,还需要设置一个sin_family成员,与套接字的类型一致;

    • IP地址格式转换
      “192.168.110.132” -> 点分十进制字符串风格的IP地址,每一个区域取值范围是[0-255]: 1字节 -> 4个区域;
      理论上,表示一个IP地址,其实4字节就够了;
      需要将点分十进制字符串风格的IP地址 <-> 4字节;
      Sin_addr其实是一个整数类型;
      在这里插入图片描述
      bzero接口:将指定长度的空间全部置0;
      先要将点分十进制字符串风格的IP地址 -> 4字节,再将4字节主机序列 -> 网络序列;
      有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据;
      在这里插入图片描述
      其中,inet_addr接口就是将字符串IP地址转换为网络序列IP地址;

    • recvfrom:从网络中读取数据
      在这里插入图片描述
      buf:数据存储缓冲区
      len:缓冲区大小
      flags:读取方式,默认0为阻塞方式
      src_addr:输出型参数,拿到数据发送方的ip和port
      addrlen:输入输出型参数;输入: src_addr 缓冲区大小;输出: 实际读到的src_addr大小

    • 网络IP转主机IP
      在这里插入图片描述

    • sendto:向目标主机发送数据
      在这里插入图片描述
      dest_addr:目的主机地址
      addrlen:dest_addr的大小

    • 本地环回:127.0.0.1
      client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中,用于本地服务器的测试;

    • 服务器IP
      云服务器无法bind公网IP,对于服务器来讲,也不建议绑定确定的IP;
      INADDR_ANY宏默认为0,是让服务器在工作过程中,可以获取任意IP的数据;
      在这里插入图片描述
      只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
      服务端只需要指定端口号;

    #ifndef _UDP_SERVER_HPP_
    #define _UDP_SERVER_HPP_
    
    #include"log.hpp"
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    #define SIZE 1024
    
    class UdpServer
    {
    public:
        UdpServer(uint16_t port, std::string ip = "")
            : _port(port)
            , _ip(ip)
            , _sock(-1) // 套接字先初始化为-1
        {}
    
        bool initServer()
        {
            // 1.创建套接字
            _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_NET与PF_NET是一样的
            if(_sock < 0)
            {
                logMessage(FATAL, "%d:%s", errno, strerror(errno));
                exit(2);
            }
    
            //2.bind:将用户设置的ip和port在内核中和我们当前的进程强关联
            struct sockaddr_in local; // 本地主机地址
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET; // 与套接字类型一致
            // 服务器的IP和端口号未来也是要发送给对方主机的,要先将数据发送到网络
            local.sin_port = htons(_port); // 考虑大小端转换
            // 将主机IP转换成网络IP
            local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
            if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)// 绑定,须强转成同一类型struct sockaddr*
            {
                logMessage(FATAL, "%d:%s", errno, strerror(errno));
                exit(3);
            }
            logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
            return true;
        }
    
        //直接显示客户端发的消息
        void start()
        {
            // 服务器是永不退出的
            char buffer[SIZE];
            for(;;)
            {
                struct sockaddr_in peer; // 远端地址,输出型参数
                bzero(&peer, sizeof(peer));
                socklen_t len = sizeof(peer);   // 输入输出型参数
                                                // 输入时,大小为src_addr的大小
                                                // 输出时,值为实际读到的dst_addr大小
                // 读取数据
                ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
                if(s > 0)
                {
                    buffer[s] = 0; //目前的数据当作字符串
                    uint16_t cli_port = ntohs(peer.sin_port); //输出型参数,从网络中来的
                    std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络4字节IP转为主机IP
                    printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
                }
                //写回数据
                sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            }
        }
    
    
        ~UdpServer()
        {
            if(_sock >= 0)
            {
                close(_sock); // 析构关闭套接字
            }
        }
    
    private:
        //一个服务器,一般需要ip地址和port(16位整数)
        uint16_t _port;
        std::string _ip;
        int _sock; // 套接字
    };
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    udp_client.cc

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    static void usage(std::string proc)
    {
        std::cout << "\nUsage: " << proc << "serverIP serverPort\n" << std::endl;
    }
    
    
    int main(int argc, char* argv[])
    {
        if(argc != 3)
        {
            usage(argv[0]);
            exit(1);
        }
        //创建套接字
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket error" << std::endl;
            exit(2);
        }
    
        // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
        // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
        // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
        // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
        std::string message;
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(atoi(argv[2]));
        server.sin_addr.s_addr = inet_addr(argv[1]);
        char buffer[1024];
        while(true)
        {
            std::cout << "请输入你的信息# ";
            std::getline(std::cin, message);
            if(message == "quit") break;
            //发送消息
            //当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和port
            sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            //接受回信
            ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
            if(s > 0)
            {
                buffer[s] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
        }   
        close(sock);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • client不需要bind特定的套接字
      client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢;
      client一般不需要显示的bind指定port,而是让OS自动随机选择;
      当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和port;

    udp_server.cc

    #include"udp_server.hpp"
    #include
    #include
    
    static void usage(std::string proc)
    {
        std::cout << "\nUsage: " << proc << "port\n" << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            usage(argv[0]);
            exit(1);
        }
    
        uint16_t port = atoi(argv[1]);
        std::unique_ptr<UdpServer> svr(new UdpServer(port)); // 使用智能指针管理对象
        svr->initServer();
        svr->start();
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 服务器不需要指定客户的IP地址获取数据;
      让服务器再工作过程中,可以获取任意IP的数据;
      只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
      服务端只需要指定端口号;

    Log.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // 日志级别
    #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);
        snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
    
        char logBuffer[1024]; // 自定义部分
        va_list args;
        va_start(args, format);
        vsnprintf(logBuffer, sizeof(logBuffer), format, args);
        va_end(args);
    
        // FILE *fp = fopen(LOGFILE, "a");
        // fprintf(fp, "%s %s\n", stdBuffer, logBuffer);
        // fclose(fp);
    
        printf("%s %s\n", stdBuffer, logBuffer);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    上面的代码实现了服务器直接打印客户端发送的信息,并将信息在发回客户端;

    • netstat -anup指令:查看当前网络中的UDP协议服务器状态
      在这里插入图片描述

    运行结果:
    在这里插入图片描述

    2.执行客户端发来的指令

    在这里插入图片描述
    popen接口可以执行传入的字符串command;
    执行command,创建pipe管道进行进程间通信,fork子进程执行(exec
    )command命令;
    FILE可以将执行成果通过FILE指针进行读取;
    *

    udp_server.hpp中的start成员函数

        // 执行客户端指令
        void start()
        {
            // 服务器是永不退出的
            char buffer[SIZE];
            for (;;)
            {
                struct sockaddr_in peer; // 远端地址,输出型参数
                bzero(&peer, sizeof(peer));
                socklen_t len = sizeof(peer); // 输入输出型参数
                                              // 输入时,大小为src_addr的大小
                                              // 输出时,值为实际读到的dst_addr大小
                char result[256];
                std::string cmd_echo;
                // 读取数据
                ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (s > 0)
                {
                    buffer[s] = 0; // 目前的数据当作字符串
                    if (strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir"))
                    {
                        std::string err_message = "bad !";
                        std::cout << err_message << buffer << std::endl;
                        sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                        continue;
                    }
                    FILE* fp = popen(buffer, "r");// 执行指令
                    if(nullptr == fp)
                    {
                        logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                        continue;
                    }
                    while(fgets(result, sizeof(result), fp) != nullptr) //通过fp读取执行结果
                    {
                        cmd_echo += result;
                    }
                    fclose(fp);
                }
                // 写回数据
                sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    运行结果:
    在这里插入图片描述

    3.多用户聊天

    服务器将返回的消息发送给所有用户,聊天室功能;
    udp_server.hpp中的start函数

    class UdpServer
    {
    public:
        void start()
        {
            // 服务器是永不退出的
            char buffer[SIZE];
            for (;;)
            {
                struct sockaddr_in peer; // 远端地址,输出型参数
                bzero(&peer, sizeof(peer));
                socklen_t len = sizeof(peer); // 输入输出型参数
                                              // 输入时,大小为src_addr的大小
                                              // 输出时,值为实际读到的dst_addr大小
                char key[64];                 // 保存客户端地址信息
                // 读取数据
                ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (s > 0)
                {
                    buffer[s] = 0; // 目前的数据当作字符串
                    uint16_t cli_port = ntohs(peer.sin_port); // 获取端口号
                    std::string cli_ip = inet_ntoa(peer.sin_addr); // 获取4字节ip地址
                    snprintf(key, sizeof key, "%s-%u", cli_ip.c_str(), cli_port); //格式化打印到key中
                    logMessage(NORMAL, "key: %s", key);
                    auto it = _users.find(key);
                    if(it == _users.end()) // 若用户不存在,则添加用户
                    {
                        logMessage(NORMAL, "add new user : %s", key);
                        _users.insert({key, peer});
                    }
                }
                for(auto &iter : _users) // 将消息推送给所有用户
                {
                    std::string sendMessage = key;
                    sendMessage += "# ";
                    sendMessage += buffer; // 发回消息的时候加上用户信息
                    logMessage(NORMAL, "push message to %s", iter.first.c_str());
                    sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
                }
            }
        }
        private:
            // 一个服务器,一般需要ip地址和port(16位整数)
            uint16_t _port;
            std::string _ip;
            int _sock; // 套接字
            std::unordered_map<std::string, struct sockaddr_in> _users; // 存储用户信息
        };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    客户端一边收消息,一边发消息,多线程;
    thread.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #include 
    
    typedef void *(*fun_t)(void *); // 定义函数指针类型,后面回调
    
    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);
            _name = nameBuffer;
    
            _tdata._args = args;
            _tdata._name = _name;
        }
    
        void start() // 创建线程
        {
            pthread_create(&_tid, nullptr, _func, (void *)&_tdata); // 直接将_tdata作为参数传给回调函数
        }
    
        void join() // 线程等待
        {
            pthread_join(_tid, nullptr);
        }
    
        std::string name()
        {
            return _name;
        }
    
        ~Thread()
        {
        }
    
    private:
        std::string _name;
        fun_t _func;
        ThreadData _tdata;
        pthread_t _tid;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    udp_client.cc

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "thread.hpp"
    
    uint16_t server_port = 0;
    std::string server_ip;
    
    static void usage(std::string proc)
    {
        std::cout << "\nUsage: " << proc << "serverIP serverPort\n"
                  << std::endl;
    }
    
    static void *udpSend(void *args)
    {
        int sock = *(int *)((ThreadData *)args)->_args;
        std::string name = ((ThreadData *)args)->_name;
        std::string message;
        struct sockaddr_in server; // 服务器地址
        memset(&server, 0, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());
        while (true)
        {
            std::cerr << "请输入你的信息# "; // 标准错误,fd == 2打印
            std::getline(std::cin, message);
            if (message == "quit")
            {
                break;
            }
            sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        }
        return nullptr;
    }
    
    static void *udpRecv(void *args)
    {
        int sock = *(int *)((ThreadData *)args)->_args;
        std::string name = ((ThreadData *)args)->_name;
        char buffer[1024];
        while(true)
        {
            memset(buffer, 0, sizeof buffer);
            struct sockaddr_in temp;
            socklen_t len = sizeof temp;
            ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
            if(s > 0)
            {
                buffer[s] = 0;
                std::cout << buffer << std::endl;
            }
        }
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            usage(argv[0]);
            exit(1);
        }
        // 创建套接字
        int sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock < 0)
        {
            std::cerr << "socket error" << std::endl;
            exit(2);
        }
    
        server_ip = argv[1];
        server_port = atoi(argv[2]);
    
        // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
        // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
        // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
        // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
        std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送消息线程
        std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收消息线程
        sender->start();
        recver->start();
        sender->join();
        recver->join();
    
        close(sock);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    运行结果:
    通过创建管道文件,让客户端发送信息和接收信息的会话分开:
    在这里插入图片描述

    4.在windows环境下运行客户端,与云服务器下的Linux服务端通信

    windows下的套接字编程与Linux下的大致接口相同,只需要添加一些windows下独有的语句就可以:
    udpClient.cpp

    #pragma warning(disable:4996) //禁用报错
    #include  //win套接字头文件
    #include 
    #include 
    using namespace std;
    #pragma comment(lib,"ws2_32.lib") //固定用法,引入win下的套接字库
    uint16_t serverport = 8080;
    std::string serverip = "120.78.126.148"; //云服务器公网ip
    int main()
    {
        // windows 独有的
        WSADATA WSAData;
        WORD sockVersion = MAKEWORD(2, 2); //选中库
        if (WSAStartup(sockVersion, &WSAData) != 0)
            return 0;
        SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
        if (INVALID_SOCKET == clientSocket)
        {
            cout << "socket error!";
            return 0;
        }
        sockaddr_in dstAddr;
        dstAddr.sin_family = AF_INET;
        dstAddr.sin_port = htons(serverport);
        dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
        char buffer[1024];
        while (true)
        {
            std::string message;
            std::cout << "请输入# ";
            std::getline(std::cin, message);
            sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));
            struct sockaddr_in temp;
            int len = sizeof(temp);
            int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
            if (s > 0)
            {
                buffer[s] = '\0';
                std::cout << "server echo# " << buffer << std::endl;
            }
        }
        // windows 独有
        closesocket(clientSocket);
        WSACleanup();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    运行结果:
    在这里插入图片描述

    四、TCP套接字

    1.打印客户端信息并发回

    (1)tcp_server.hpp

    • 套接字类型
      在这里插入图片描述
      由于TCP协议是面向字节流的协议,因此套接字的类型需选择SOCK_STREAM;
      在这里插入图片描述

    • 面向连接的协议
      因为TCP协议是面向连接的,当我们正式通信的时候,需要先建立连接;
      在这里插入图片描述
      将套接字状态设置为监听状态;
      backlog:全链接队列长度;
      成功返回0,失败返回-1,并设置errno;

    • netstat -antp指令:查看网络中的TCP协议服务器
      在这里插入图片描述

    • 获取连接
      在这里插入图片描述
      accept接口:获取与客户端的连接;
      addr:输出型参数,拿到客户端的ip和端口号;
      addlen:输入输出型参数,输入服务器addr大小,输出客户端addr大小;
      返回值:成功:返回套接字,这是真正进行IO服务的套接字;失败返回-1;
      传入的sockfd参数那个套接字只是获取底层的连接,是监听套接字

    • 读取消息
      TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取);

    单进程循环处理
    一次处理一个客户端,处理完了一个,才能处理下一个;

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "log.hpp"
    
    // 打印服务
    static void service(int sock, const std::string &cli_ip, const uint16_t &cli_port)
    {
        // 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)
    
        char buffer[1024];
        while (true)
        {
            ssize_t s = read(sock, buffer, sizeof(buffer - 1));
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << cli_ip << ":" << cli_port << "# " << buffer << std::endl;
            }
            else if (s == 0) // 对端关闭连接
            {
                logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);
                break;
            }
            else
            {
                logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));
                break;
            }
            write(sock, buffer, strlen(buffer));
        }
    }
    
    class TcpServer
    {
    private:
        const static int gbacklog = 20;
    
    public:
        TcpServer(uint16_t port, std::string ip = "")
            : _port(port), _ip(ip)
        {
        }
    
        void initServer()
        {
            // 1.创建套接字socket -- 进程和文件
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
                exit(2);
            }
            logMessage(NORMAL, "create socket success, listensock: %d", _listensock);
    
            // 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是面向连接的,当我们正式通信的时候,需要先建立连接
            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); // 得到真正进行IO服务的套接字
                if (servicesock < 0)
                {
                    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                    continue; // 获取连接失败后继续获取
                }
                // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
                uint16_t cli_port = ntohs(src.sin_port);
                std::string cli_ip = inet_ntoa(src.sin_addr);
                logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                           servicesock, cli_ip.c_str(), cli_port);
                // 开始进行通信服务
                // 单进程循环版 -- 一次处理一个客户端,处理完一个才能处理下一个
                service(servicesock, cli_ip, cli_port);
                close(servicesock);
            }
        }
    
        ~TcpServer()
        {
        }
    
    private:
        uint16_t _port;
        std::string _ip;
        int _listensock; // 监听套接字
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119

    (2)tcp_server.cc

    #include "tcp_server.hpp"
    #include 
    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 使用远程登陆工具telnet模拟客户端
      在这里插入图片描述
      连接服务端
      在这里插入图片描述
      按照提示按下ctrl + ]进入telnet
      在这里插入图片描述

    运行结果
    在这里插入图片描述
    如果创建两个客户端
    在这里插入图片描述
    服务端只会响应第一个客户端,只有当第一个客户端退出时,第二个客户端才会被响应;

    (3)多进程版服务器
    tcp_server.hpp中的start成员函数
    **父进程waitpid会阻塞进程,和单进程就没区别了;

    • 方案一:
      可以主动忽略SIGCHID信号让子进程自动释放僵尸状态;
      父进程关闭servicesock,因为每个进程可用的文件描述符都是有限的,这里关闭了,还有子进程的fd指向文件,因此不会有问题;
      如果不关闭,就会导致文件描述符泄露;
        void start()
        {
            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); // 得到真正进行IO服务的套接字
                if (servicesock < 0)
                {
                    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                    continue; // 获取连接失败后继续获取
                }
                // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
                uint16_t cli_port = ntohs(src.sin_port);
                std::string cli_ip = inet_ntoa(src.sin_addr);
                logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                           servicesock, cli_ip.c_str(), cli_port);
                // 开始进行通信服务
    
                //多进程版 -- 创建子进程
                //让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的
                pid_t id = fork();
                assert(id != -1);
                if(id == 0)
                {
                    //子进程,能够继承父进程的文件fd
                    //子进程是来提供服务的,不需要知道监听socket
                    close(_listensock);//关闭不需要的文件描述符
                    service(servicesock, cli_ip, cli_port);
                    exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态
                }
                close(servicesock);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    运行结果:
    可以进行多客户端通信了;
    在这里插入图片描述

    • 方案二(不忽略SIGCHLD信号)
      在子进程中再fork,创建孙子进程,让孙子进程执行service,子进程立马退出;
      孙子进程就变成孤儿进程,让OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程;
        void start()
        {
            while (true)
            {
                // 4.获取连接
                struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
                socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
                int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
                if (servicesock < 0)
                {
                    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                    continue; // 获取连接失败后继续获取
                }
                // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
                uint16_t cli_port = ntohs(src.sin_port);
                std::string cli_ip = inet_ntoa(src.sin_addr);
                logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                           servicesock, cli_ip.c_str(), cli_port);
    
                // 多进程版 -- 创建子进程
                // 让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的
                pid_t id = fork();
                assert(id != -1);
    
                // 多进程版 -- 创建孙子进程
                if (id == 0)
                {
                    // 子进程,能够继承父进程的文件fd
                    // 子进程是来提供服务的,不需要知道监听socket
                    close(_listensock); // 关闭不需要的文件描述符
                    if(fork() > 0)
                    {
                        exit(0);// 子进程调用fork,创建孙子进程,然后子进程退出
                    }
                    // 孙子进程在子进程退出后编程孤儿进程,OS领养,OS在孤儿进程退出后自动回收
                    service(servicesock, cli_ip, cli_port);
                    exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态
                }
                waitpid(id, nullptr, 0); // 子进程及时退出,不会阻塞等待
                close(servicesock);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    运行结果:
    在这里插入图片描述

    (4)客户端:
    TCP的服务端需要bind,一定需要一个确定的port;
    TCP的客户端不需要bind,一旦bind,就说明客户端绑定的是一个具体端口号,两个客户端可能是由不同公司写的,可能端口号会出现冲突;
    需要让操作系统自动进行port选择;
    客户端需要连接别人;

    • connect接口:连接特定的IP和port
      在这里插入图片描述
      后两个参数与sendto参数相同,目的主机的地址和地址的大小;
      成功返回0,失败返回-1;

    • send接口:tcp发送接口
      在这里插入图片描述
      flags默认为0

    • recv接口:tcp接收接口
      在这里插入图片描述

    注:send和recv也可以使用read和write代替;

    tcp_client.cc

    #include "tcp_server.hpp"
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    static void usage(std::string proc)
    {
        std::cout << "\nUsage: " << proc << "serverIP serverPort\n"
                  << std::endl;
    }
    
    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,但是一定需要port
                // 要让OS自动进行port选择
    
                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)
                {
                    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); // 向服务器发送消息
            if(s > 0)
            {
                // 发送成功
                char buffer[1024];
                ssize_t l = recv(sock, buffer, sizeof(buffer) - 1, 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    运行结果:
    在这里插入图片描述

    2.多线程版服务器

    (1)每次连接时都创建新的线程
    tcp_server.hpp

    class ThreadData
    {
    public:
        int _sock;
        std::string _ip;
        uint16_t _port;
    };
    
    class TcpServer
    {
    private:
        const static int gbacklog = 20;
    
        static void *threadRoutine(void *args)
        {
            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 = "")
            : _port(port), _ip(ip)
        {
        }
    
        void initServer()
        {
            // 1.创建套接字socket -- 进程和文件
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
                exit(2);
            }
            logMessage(NORMAL, "create socket success, listensock: %d", _listensock);
    
            // 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是面向连接的,当我们正式通信的时候,需要先建立连接
            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); // 得到真正进行IO服务的套接字
                if (servicesock < 0)
                {
                    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                    continue; // 获取连接失败后继续获取
                }
                // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
                uint16_t cli_port = ntohs(src.sin_port);
                std::string cli_ip = inet_ntoa(src.sin_addr);
                logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                           servicesock, cli_ip.c_str(), cli_port);
                // 多线程
                ThreadData* td = new ThreadData();
                td->_sock = servicesock;
                td->_ip = cli_ip;
                td->_port = cli_port;
                pthread_t tid;
                // 多线程不需要关闭文件描述符,因为多线程共享文件描述符
                pthread_create(&tid, nullptr, threadRoutine, td);
            }
        }
    
        ~TcpServer()
        {
        }
    
    private:
        uint16_t _port;
        std::string _ip;
        int _listensock; // 监听套接字
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    运行结果:
    在这里插入图片描述

    (2)使用线程池管理线程

    引入之前写的线程池,具体代码见Linux知识点 – Linux多线程(四)
    线程池代码中:
    Task.hpp
    更改了回调函数的类型;

    #pragma once
    
    #include 
    #include 
    #include 
    #include "log.hpp"
    
    //typedef std::function 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;
        func_t _func;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    tcp_server.hpp
    更改了service回调函数和类内成员函数start,每次将任务push进任务队列,等待线程池处理;

    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include "thread-pool/log.hpp"
    #include "thread-pool/threadPool.hpp"
    #include "thread-pool/Task.hpp"
    
    static void service(int sock, const std::string &cli_ip,
                        const uint16_t &cli_port, const std::string &thread_name)
    {
        // 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)
    
        char buffer[1024];
        while (true)
        {
            ssize_t s = read(sock, buffer, sizeof(buffer - 1));
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << thread_name << "|" <<  cli_ip << ":" << cli_port << "# " << buffer << std::endl;
            }
            else if (s == 0) // 对端关闭连接
            {
                logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);
                break;
            }
            else
            {
                logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));
                break;
            }
            write(sock, buffer, strlen(buffer));
        }
        close(sock); // 线程在回调函数中关闭不用的文件描述符
    }
    
    
    class TcpServer
    {
    private:
        const static int gbacklog = 20;
    
    public:
        TcpServer(uint16_t port, std::string ip = "0.0.0.0")
            : _listensock(-1)
            , _port(port)
            , _ip(ip)
            , _threadpool_ptr(ThreadPool<Task>::getThreadPool())
        {}
    
        void initServer()
        {
            // 1.创建套接字socket -- 进程和文件
            _listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
                exit(2);
            }
            logMessage(NORMAL, "create socket success, listensock: %d", _listensock);
    
            // 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是面向连接的,当我们正式通信的时候,需要先建立连接
            if (listen(_listensock, gbacklog) < 0)
            {
                logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
                exit(4);
            }
            logMessage(NORMAL, "init server success");
        }
    
    
        // 线程池
        void start()
        {
            _threadpool_ptr->run(); // 启动线程池
            while (true)
            {
                // 4.获取连接
                struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
                socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
                int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
                if (servicesock < 0)
                {
                    logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                    continue; // 获取连接失败后继续获取
                }
                // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
                uint16_t cli_port = ntohs(src.sin_port);
                std::string cli_ip = inet_ntoa(src.sin_addr);
                logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                           servicesock, cli_ip.c_str(), cli_port);
                // 线程池版本
                Task t(servicesock, cli_ip, cli_port, service);
                _threadpool_ptr->pushTask(t);
            }
        }
    
        ~TcpServer()
        {
        }
    
    private:
        uint16_t _port;
        std::string _ip;
        int _listensock; // 监听套接字
        std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 线程池指针
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134

    其他代码都与之前的一致;
    运行结果:
    在这里插入图片描述
    也可以传入其他的回调函数,完成不同的功能;

    五、关于地址转换函数

    1.字符串转in_addr的函数

    在这里插入图片描述
    inet_aton地址转换函数:
    在这里插入图片描述
    在这里插入图片描述
    inet_pton地址转换函数
    在这里插入图片描述

    2.in_addr转字符串的函数

    在这里插入图片描述

    • 关于inet_ntoa
      因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果,因此inet_ntoa是线程不安全的;
      在多线程环境下,推荐使用inet_ ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;

    六、TCP协议通讯流程

    在这里插入图片描述

    • 服务器初始化
      调用socket,创建文件描述符;
      调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败;
      调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备;
      调用accecpt,并阻塞,等待客户端连接过来;

    • 建立连接的过程(三次握手)
      调用socket,创建文件描述符;
      调用connect,向服务器发起连接请求;
      connect会发出SYN段并阻塞等待服务器应答(第一次);
      服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接"(第二次);
      客户端收到SYN-ACK后会从connect(返回,同时应答一个ACK段(第三次);

    • 数据传输的过程
      建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同-条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
      服务器从accept()返回后立刻调用read),读socket就像读管道一样,如果没有数据到达就阻塞等待;
      这时客户端调用write0发送请求给服务器,服务器收到后从read(返回,对客户端的请求进行处理,在此期
      间客户端调用read()阻塞等待服务器的应答;
      服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
      客户端收到后从read()返回,发送下一条请求如此循环下去;

    • 断开连接的过程(四次挥手)
      如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
      此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次);
      read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次);
      客户端收到FIN,再返回一个ACK给服务器(第四次);

  • 相关阅读:
    Django中序列化器or模型单独使用
    拥抱 Spring 全新 OAuth 解决方案
    【C语言进阶】文件操作(二)
    【云原生】MySql索引分析及查询优化
    PCIe系列专题之二:2.7 Flow Control的实现过程
    Hadoop 教程 - HDFS概述
    2023年全球市场新能源汽车车载充电器总体规模、主要生产商、主要地区、产品和应用细分研究报告
    [从零学习汇编语言] - 内中断
    ISAC通信感知一体化学习记录
    CSDN编程竞赛第六期总结
  • 原文地址:https://blog.csdn.net/kissland96166/article/details/132571206