• 【网络】网络编程套接字(二)


    简单的TCP网络程序

    TCP服务器创建套接字的做法与UDP服务器是基本一样的,但是TCP服务器会更加繁琐一些。

    1、服务端创建套接字并绑定

    TCP服务器在调用socket函数创建套接字时,参数设置如下:

    • 协议家族选择AF_INET,表示我们要进行的是网络通信。
    • 创建套接字时所需的服务类型应该是SOCK_STREAM,因为我们编写的是TCP服务器,SOCK_STREAM提供的就是一个有序的、可靠的、全双工的、基于连接的流式服务。
    • 协议类型默认设置为0即可。

    我们将TCP服务器封装成一个类,当我们定义出一个服务器对象后需要对其初始化,当析构服务器时,可以将服务器对应的文件描述符进行关闭。

    // tcp_server.hpp
    
    #pragma once
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    enum
    {
        USAGE_ERR = 1
        SOCKET_ERR,
        BIND_ERR
    };
    
    class TcpServer
    {
    public:
        TcpServer(uint16_t port)
            :_port(port)
        {}
    
        void Init()
        {
            // 1.创建监听套接字
            _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
            if (_listen_fd < 0)
            {
                std::cerr << "socket fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            
            // 2.进行绑定
            struct sockaddr_in local;
            socklen_t len = sizeof(local);
            memset(&local, 0, len);
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(_port);
            if (bind(_listen_fd, (struct sockaddr*)&local, len) < 0)
            {
                std::cerr << "bind fail : " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            // ...
        }
    
        ~TcpServer()
        {
            if (_listen_fd >= 0)
            {
                close(_listen_fd);
            }
        }
    
    private:
        int _listen_fd;         // 监听套接字
        uint16_t _port;         // 端口号
    };
    
    • 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

    2、服务端监听

    前面的步骤TCP与UDP的创建几乎是一模一样的,但是到了这一步就不一样了,因为TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

    因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态,这需要我们使用一个叫做listen的函数。

    int listen(int sockfd, int backlog);
    
    • 1

    参数说明:

    • sockfd:需要设置为监听状态的套接字对应的文件描述符。
    • backlog全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5~10即可。

    返回值说明:

    • 监听成功返回0,监听失败返回-1,同时错误码会被设置。

    TCP服务器在创建完套接字和绑定后,下一步就是要将套接字设置为监听状态,监听是否有新的连接到来。如果监听失败意味着TCP服务器无法接收客户端发来的连接请求,因此监听失败我们直接终止程序。

    class TcpServer
    {
    public:
     	// ...
        void Init()
        {
            // 1.创建监听套接字
            _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
            if (_listen_fd < 0)
            {
                std::cerr << "socket fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            
            // 2.进行绑定
            struct sockaddr_in local;
            socklen_t len = sizeof(local);
            memset(&local, 0, len);
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(_port);
            
            if (bind(_listen_fd, (struct sockaddr*)&local, len) < 0)
            {
                std::cerr << "bind fail : " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            
            // 3.开始监听
            if (listen(_listen_fd, 5) < 0)
            {
                std::cerr << "listen fail : " << strerror(errno) << std::endl;
                exit(LISTEN_ERR);
            }
        }
        
    	// ...
    };
    
    
    • 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

    注意:

    • 初始化TCP服务器时创建的套接字并不是用于网络数据传输的套接字,而应该叫做监听套接字。
    • 在初始化TCP服务器时,只有创建套接字成功、绑定成功、监听成功,此时TCP服务器的初始化才算完成。

    2、服务端获取连接

    TCP服务器初始化并设置为监听状态后就可以正常运行了,但是现在TCP服务器在与客户端还不能够进行网络通信,因为服务器还需要先获取到客户端的连接,我们可以使用accept函数来获取连接。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • 1

    参数说明:

    • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。
    • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
    • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。

    返回值说明:

    • 获取连接成功将返回一个套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

    accept函数获取连接是从监听套接字当中获取的,如果accept函数获取连接成功,此时会返回一个套接字对应的文件描述符。

    监听套接字与accept函数返回的套接字的作用:

    • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
    • accept函数返回的套接字:用于为本次accept获取到的连接提供网络数据传输的。监听套接字的任务只是不断获取新连接,而真正为这些连接提供数据传输的套接字是accept函数返回的套接字,而不是监听套接字。

    accept函数获取连接时可能会失败,但由于TCP服务器不会因为获取某个连接失败而退出,因此服务端获取连接失败后应该继续重新获取连接。

    // tcp_server.hpp
    class TcpServer
    {
    public:
        TcpServer(uint16_t port,int quit = true)
            :_port(port), _quit(quit)
        {}
    
        void Init()
        {
    		// ...
        }
    
    	void Start()
        {
            _quit = false;
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
    
            while (!_quit)
            {
                memset(&client, 0, len);
                // 1. 获取连接
                int sockfd = 0;
                sockfd = accept(_listen_fd, (struct sockaddr*)&client, &len);
                if (sockfd < 0)
                {
                    std::cerr << "连接失败,正在尝试重连..." << std::endl;
                    sleep(1);
                    continue;
                }
                else
                {
                    // 2.进行业务处理
                    std::string ip = inet_ntoa(client.sin_addr);
                    uint16_t port = ntohs(client.sin_port);
                    std::string name = ip + " - " + std::to_string(port);
                    std::cout << "获取连接成功! " << sockfd << " 来自监听套接字:" << _listen_fd
                        << " | " << name << std::endl;
                    Service(sockfd);
                }
            }
        }
    
    	
    	// 业务处理
        void Service(int sockfd)
        {
    		// ...
        }
    
        ~TcpServer()
        {
           // ...
        }
    
    private:
        int _listen_fd;         // 监听套接字
        uint16_t _port;         // 端口号
        bool _quit;             // 表示连接是否退出
    };
    
    • 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

    3、服务端处理请求

    现在TCP服务器已经能够获取连接请求了,下面当然就是要对获取到的请求进行处理了,具体怎么处理由你决定,我们这里就假设我们的处理是要进行回声处理,服务端将将客户端发来的数据重新发回给客户端。

    由于TCP服务器是面向字节流的,所以我们读取数据的函数可以使用流式接口read,该函数的函数原型如下:

    ssize_t read(int fd, void *buf, size_t count);
    
    • 1

    参数说明:

    • fd:特定的文件描述符,表示从该文件描述符中读取数据。
    • buf:数据的存储位置,表示将读取到的数据存储到该位置。
    • count:数据的个数,表示从该文件描述符中读取数据的字节数。

    返回值说明:

    • 如果返回值大于0,则表示本次实际读取到的字节个数。
    • 如果返回值等于0,则表示对端已经把连接关闭了。
    • 如果返回值小于0,则表示读取时遇到了错误。

    同理TCP服务器写入数据的函数可以使用流式接口write,该函数的函数原型如下:

    ssize_t write(int fd, const void *buf, size_t count);
    
    • 1

    参数说明:

    • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
    • buf:需要写入的数据。
    • count:需要写入数据的字节个数。

    返回值说明:

    • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

    服务端读取数据是从accept创建的套接字中读取的,而写入数据的时候也是写入accept创建的套接字的。也就是说这里为客户端提供服务的套接字,既可以读取数据也可以写入数据,这就是TCP全双工的通信的体现。

    在从服务套接字中读取客户端发来的数据时,如果调用read函数后得到的返回值为0,或者读取出错了,此时就应该直接将服务套接字对应的文件描述符关闭。因为文件描述符本质就是数组的下标,因此文件描述符的资源是有限的,如果我们一直占用,那么可用的文件描述符就会越来越少,因此服务完比以后后要及时关闭对应的文件描述符,否则会导致文件描述符泄漏。

    // tcp_server.hpp
    
    class TcpServer
    {
    	// 回调函数的类型,外部传入一个回调函数,让服务器执行此函数完成任务!
        using func_t = std::function<std::string(std::string)>;
    public:
        TcpServer(uint16_t port, func_t func, int quit = true)
            :_port(port), _func(func), _quit(quit)
        {}
    
        void Init()
        {
     		// ...
        }
    
        void Start()
        {
            // ...       
        }
    
        void Service(int sockfd)
        {
            char buf[1024];
            while (true)
            {
                int num = read(sockfd, buf, sizeof(buf) - 1);
                if (num > 0)
                {
                    buf[num] = '\0';
                    // 调用回调函数进行业务处理
                    std::string message =  _func(buf);
                    std::cout << "receive message : " << buf << std::endl;
                    write(sockfd, message.c_str(), message.size());
                }
                else if (num == 0)
                {
                    close(sockfd);
                    std::cout << "对方关闭了写端, 我也关闭了。" << std::endl;
                    break;
                }
                else
                {
                    close(sockfd);
                    std::cerr << "read fail: " << strerror(errno) << std::endl;
                    break;
                }
            }
        }
    
        ~TcpServer()
        {
           // ...
        }
    
    private:
        int _listen_fd;         // 监听套接字
        uint16_t _port;         // 端口号
        bool _quit;             // 表示连接是否退出
        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
    • 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

    服务端的主函数代码:

    // tcp_server.cpp
    
    #include 
    #include 
    #include "tcp_server.hpp"
    
    // 使用手册
    static void Usage(std::string proc)
    {
        std::cout << "usage\n\t" << proc << " 端口" << std::endl;
    }
    
    // 回声处理
    std::string echo(std::string message)
    {
        return message;
    }
    
    int main(int argc, char* argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
            exit(USAGE_ERR);
        }
        
        uint16_t server_port = atoi(argv[1]);
        std::unique_ptr<TcpServer> up(new TcpServer(server_port, echo));
        up->Init();
        up->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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    4、客户端进行连接

    对于TCP客户端的编写与UDP客户端类似,不同的是我们TCP的客户端想要进行网络通信首先要进行connect连接服务器。

    // tcp_client
    
    enum
    {
        USAGE_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR,
        ACCEPT_ERR,
        CONNECT_ERR
    };
    
    // 使用手册
    static void Usage(std::string proc)
    {
        std::cout << "usage\n\t" << proc << " IP 端口" << std::endl;
    }
    
    int main(int argc, char* argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(USAGE_ERR);
        }
        uint16_t port = atoi(argv[2]);
        
        // 1.填充结构体
        struct sockaddr_in server;
        socklen_t len = sizeof(server);
        memset(&server, 0, len);
        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        inet_pton(AF_INET, argv[1], &server.sin_addr.s_addr);
    
        // 2.创建套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "socket fail: " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        
        // 3. 进行连接
    	// ...
        
        // 4.开始通信
    	// ...
    
        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

    发起连接请求的函数叫做connect,该函数的函数原型如下:

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 1

    参数说明:

    • sockfd:特定的套接字,表示通过该套接字发起连接请求。
    • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
    • addrlen:传入的addr结构体的长度。

    返回值说明:

    • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

    客户端是不需要我们自己进行绑定操作,当客户端向服务端发起连接请求时,系统会给客户端随机指定一个端口号进行绑定,此外当我们连接失败时,不需要直接退出,我们可以尝试重新连接,如果实在是连接不上我们才退出。

    int main(int argc, char* argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(USAGE_ERR);
        }
        uint16_t port = atoi(argv[2]);
        
        // 1.填充结构体
       	// ...
    
        // 2.创建套接字
       	// ...
        
        // 3. 进行连接
        int count = 5;
        while (connect(sockfd, (struct sockaddr*)&server, len) != 0)
        {
            std::cout << "连接失败,正在尝试重连...,剩余重连次数 :" << count-- << std::endl;
            if (count < 0)
            {
                std::cerr << "connect fail: " << strerror(errno) << std::endl;
                exit(CONNECT_ERR);
            }
            // 避免此时网络拥堵,短时间内连接过快消耗了所有的连接次数。
            sleep(1);
        }
        
        // 4.开始通信
    
    
        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

    5、客户端发起通信

    由于我们实现的是一个简单的回声服务器,因此当客户端连接到服务端后,客户端就可以向服务端发送数据了,这里我们可以让客户端将用户输入的数据发送给服务端,发送时调用write函数向套接字当中写入数据即可。

    当客户端将数据发送给服务端后,由于服务端读取到数据后还会进行回显,因此客户端在发送数据后还需要调用read函数读取服务端的响应数据,然后将该响应数据进行打印,以确定双方通信无误。

    int main(int argc, char* argv[])
    {
        // ...
        
        // 4.开始通信
        std::string message;
        char buf[1024];
        while (true)
        {
            std::cout << "please enter >> ";
            std::getline(std::cin, message);
            
            // 写
            ssize_t num = write(sockfd, message.c_str(), message.size());
            if (num < 0)
            {
                std::cerr << "write fail: " << strerror(errno) << std::endl;
            }
            
            // 读
            num = read(sockfd, buf, sizeof(buf));
            if (num > 0)
            {
                buf[num] = '\0';
                std::cout << "server echo >> " << buf << std::endl;
            }
            else if(num == 0)
            {
                std::cout << " server quit !" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read fail: " << strerror(errno) << std::endl;
                break;
            }
        }
        close(sockfd);
        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

    6、通信测试

    我们先让服务端先运行,./tcp_server 端口号,然后让客户端再运行./tcp_client IP地址 端口号之后我们就可以进行网络通信了。

    在这里插入图片描述

    注意:我们现在所写的TCP服务器只能够服务一个客户端,因为当服务端的主执行流去执行Service了,就没有办法accept新的连接了,所以我们还要使用多进程或多线程来完善我们的服务端,这里我们就不咋完善了,有兴趣的话你可以将服务器改成多线程版本,给更多的客户端提供服务。

  • 相关阅读:
    R语言ggplot2可视化:使用ggplot2可视化散点图、aes函数中的colour参数指定不同分组的数据点使用不同的颜色显示
    基于Java的高校宿舍管理系统设计与实现(源码+lw+部署文档+讲解等)
    vuex和pinia
    从Purge机制说起,详解GaussDB(for MySQL)的优化策略
    Hive安装&sql去重的4种方式&Zeppelin安装
    【chatGPT API】Function Calling:将自然语言转换为API调用或数据库查询
    mysql基本操作1
    Linux之V4L2驱动框架
    使用 Databend Kafka Connect 构建实时数据同步
    ABAP 没有SM30权限,维护数据
  • 原文地址:https://blog.csdn.net/qq_65207641/article/details/132858235