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

news2024/11/25 6:30:22

网络编程套接字 二

  • 简单的TCP网络程序
    • 1、服务端创建套接字并绑定
    • 2、服务端监听
    • 2、服务端获取连接
    • 3、服务端处理请求
    • 4、客户端进行连接
    • 5、客户端发起通信
    • 6、通信测试

简单的TCP网络程序

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

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

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

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

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

// tcp_server.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

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;         // 端口号
};

2、服务端监听

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

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

int listen(int sockfd, int backlog);

参数说明:

  • 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);
        }
    }
    
	// ...
};

注意:

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

2、服务端获取连接

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

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

参数说明:

  • 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;             // 表示连接是否退出
};

3、服务端处理请求

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

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

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

参数说明:

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

返回值说明:

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

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

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

参数说明:

  • 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;           // 业务的处理函数
};

服务端的主函数代码:

// tcp_server.cpp

#include <iostream>
#include <memory>
#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;
}

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;
}

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

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

参数说明:

  • 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;
}

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;
}

6、通信测试

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

在这里插入图片描述

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

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

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

相关文章

独立产品灵感周刊 DecoHack #053 - 有意思的地图网站

本周刊记录有趣好玩的独立产品设计开发相关内容&#xff0c;每周发布&#xff0c;往期内容同样精彩&#xff0c;感兴趣的伙伴可以 点击订阅我的周刊。为保证每期都能收到&#xff0c;建议邮件订阅。欢迎通过 Twitter 私信推荐或投稿。 周刊继续发布 ❤️ &#x1f4bb; 产品推…

电脑屏幕模糊?这5个方法教你恢复清晰屏幕!

“我的电脑最近看着看着莫名就觉得好模糊&#xff0c;这到底是为什么呢&#xff1f;有什么方法可以解决电脑屏幕模糊的问题吗&#xff1f;” 使用电脑时&#xff0c;电脑屏幕是否清晰会很影响我们的使用体验感。如果电脑屏幕模糊&#xff0c;可能会给我们带来一种视觉上的不好体…

C#中的日期时间比较和格式化的方法

摘要&#xff1a;在C#中&#xff0c;日期和时间的比较以及格式化是常见的操作。了解如何正确比较和格式化日期和时间可以帮助我们更好地处理这些数据。本文将介绍C#中常用的日期时间比较方法&#xff08;CompareTo、Equals和比较运算符&#xff09;以及日期时间格式化方法&…

vue重修之路由【下】

文章目录 版权声明路由重定向、404&#xff0c;路由模式重定向404路由模式 声明式导航vue-routerrouter-link-active 和 router-link-exact-active定制router-link-active 和 router-link-exact-active跳转传参两种跳转传参总结 编程式导航两种语法路由传参path路径跳转传参nam…

Kafka3.x安装以及使用

一、Kafka下载 下载地址&#xff1a;https://kafka.apache.org/downloads 二、Kafka安装 因为选择下载的是 .zip 文件&#xff0c;直接跳过安装&#xff0c;一步到位。 选择在任一磁盘创建空文件夹&#xff08;不要使用中文路径&#xff09;&#xff0c;解压之后把文件夹内容剪…

10个最流行的开源机器视觉标注工具

推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 我们知道寻找良好的图像标记和注释工具对于创建准确且有用的数据集的重要性。 随着图像注释空间的增长&#xff0c;我们看到开源工具的可用性激增&#xff0c;这些工具使任何人都可以免费标记他们的图像并从强大的功能中受…

这5种炫酷的动态图,都是用Python实现的!

数据可以帮助我们描述这个世界、阐释自己的想法和展示自己的成果&#xff0c;但如果只有单调乏味的文本和数字&#xff0c;我们却往往能难抓住观众的眼球。而很多时候&#xff0c;一张漂亮的可视化图表就足以胜过千言万语。本文将介绍 5 种基于 Plotly 的可视化方法&#xff0c…

IP地址SSL证书 IP证书

在许多企业用例中&#xff0c;公司需要SSL证书作为IP地址。公司使用IP地址通过Internet访问各种类型的应用程序。 公网IP地址的SSL证书&#xff1a; 内部IP&#xff08;也称为私有IP&#xff09;是IANA设置为保存的IPv4或IPv6地址&#xff0c;例如&#xff1a; RFC 1918范围内…

编译原理如何写出不带回溯的递归子程序?

递归子程序 使用不带回溯的递归子程序解析文法是预测性语法分析的基础&#xff0c;这通常需要该文法是LL(1)文法。每个非终结符对应一个递归子程序&#xff0c;并使用当前的输入符号和FIRST集合来决定调用哪个产生式。 让我们以一个简单的文法为例&#xff1a; 对于此文法&am…

大模型开发06:LangChain 概述

大模型开发06:LangChain 概述 LangChain 是一个基于语言模型开发应用程序的框架。它可以实现以下功能: 上下文感知: 将语言模型与上下文源相连接(提示词、示例、用于支撑响应的内容等)推理能力: 依赖语言模型进行推理(如何根据提供的上下文来回答问题或采取哪些行动等)La…

抖音手把手带,开放到月底!

这个月一直在做的两件事&#xff0c;一个是带内部社员&#xff0c;去把抖音项目跑通。一个是招募合伙人。简单说下这两个事&#xff0c;之前一直没在公众号说。 带学员这件事&#xff0c;默认收徒只到月底。感兴趣的直接报名&#xff0c;价格4980。这块无需多言&#xff0c;做一…

如何解散微信群?这两个方法收藏好!

微信群&#xff0c;简单来说就是多人社交&#xff0c;能够让用户与多个人进行交流与互动。群主可以邀请有共同爱好的朋友在一个群里聊天、分享信息等等&#xff0c;以此来增强社交互动。 如果是一些临时活动群或者群成员已经不活跃的情况下&#xff0c;那么群主可能会选择将群…

mysql图片存取初探

mysql数据库中使用blob存储使用base64加密图片数据 前言 这个方法并不好&#xff0c;因为传输的数据量还是蛮大的&#xff0c;可以存一些诸如头像的小图片&#xff0c;但是如果要存较大的图片会很慢。 不过只是课程作业中简单的功能&#xff0c;这样子简单又快捷&#xff0c;…

各类深度学习框架详解+深度学习训练环境搭建-GPU版本

目录 前言 一、深度学习框架 TensorFlow PyTorch Keras Caffe PaddlePaddle 二、深度学习框架环境搭建 1.CUDA部署 CUDA特性 CUDA下载 2.cuDNN cuDNN 的主要特性 cuDNN 下载 3.安装TensorFlow框架 TensorFlow 2 旧版 TensorFlow 1 4.安装PyTorch框架 5.安装Ca…

MySQL字段加密方案 安当加密

要通过安当KSP密钥管理系统实现MySQL数据库字段的加密&#xff0c;您可以按照以下步骤进行操作&#xff1a; 安装和配置安当KSP密钥管理系统&#xff1a;首先&#xff0c;您需要安装安当KSP密钥管理系统&#xff0c;并按照说明进行配置。确保您已经正确地设置了密钥管理系统的用…

数据结构与算法之图: Leetcode 417. 太平洋大西洋水流问题 (Typescript版)

太平洋大西洋水流问题 https://leetcode.cn/problems/pacific-atlantic-water-flow/description/ 描述 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界和下边界。 这个岛被…

Elsevier上传LaTeX修改稿常见问题解决方法

在撰写科研论文时&#xff0c;一般会使用latex或者word两种工具。在论文的返修阶段&#xff0c;很多期刊要求我们上传可编辑格式的稿件。word在上传到爱思唯尔系统中时候很方便&#xff0c;但latex是较为麻烦的&#xff0c;下面和大家分享一下我在上传latex手稿时遇到的一些问题…

【人工智能】LLM 大型语言模型和 Transformer 架构简介

目录 大型语言模型 (LLM) 一、LLM的起源 二、LLM的发展阶段 三、LLM的应用领域

torch.nn.Parameter()

一文通俗理解torch.nn.Parameter() 一、起源 首先&#xff0c;我写这篇文章的起源是因为&#xff0c;我突然看到了一段有关torch.nn.Parameter()的代码。 因此就去了解了一下这个函数&#xff0c;把自己的一些理解记录下来&#xff0c;希望可以帮到你。 二、官方文档 网址如下…

单目3D目标检测[基于几何约束篇]

基于语义和几何约束的方法 1. Deep3DBox 3D Bounding Box Estimation Using Deep Learning and Geometry [CVPR2017]https://arxiv.org/pdf/1612.00496.pdfhttps://zhuanlan.zhihu.com/p/414275118 核心思想&#xff1a;通过利用2D bounding box与3D bounding box之间的几何约…