【网络编程】之Udp网络通信步骤

【网络编程】之Udp网络通信步骤

  • TCP网络通信
    • TCP网络通信的步骤
      • 对于服务器端
      • 对于客户端
    • TCP实现echo功能
      • 代码实现
        • 服务器端
          • getsockname函数介绍
        • 客户端
        • 效果展示
  • 对比两组函数

TCP网络通信

TCP网络通信的步骤

对于服务器端

  1. 创建监听套接字。(调用socket函数)

    • 使用 socket 函数创建一个 TCP 套接字,为服务器提供网络通信的基础。该套接字将用于监听客户端的连接请求。例如:
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
  2. 显式bind服务器的IP地址和端口号。

    • 使用 bind 函数将服务器的 IP 地址和端口号绑定到创建的套接字上。确保服务器能够通过指定的地址和端口来接受客户端的连接请求。例如:
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用网络接口
    server_addr.sin_port = htons(8080);  // 指定端口号
    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    
  3. 设置监听套接字为监听状态,监听客户端请求。

    • 使用 listen 函数将套接字转换为监听状态,服务器开始等待客户端的连接请求。
    listen(server_fd, 5);  // 最大监听队列长度为 5
    
  4. 接受客户端的连接请求。(同时会创建一个专门与这个客户端通信的套接字)

    • 使用 accept 函数接受客户端的连接请求。在此期间,服务器会阻塞,直到有客户端发起连接请求。接受客户端请求后,函数会返回一个新的套接字,该套接字专门用于与客户端通信。
    int client_fd = accept(server_fd, NULL, NULL);
    

    最后两个参数是与正在请求连接的客户端地址相关的参数,如果你不需要发送数据,可以都传NULL

    • 返回的套接字(即与客户端通信的套接字)在大多数情况下会继承 监听套接字本地地址和端口。也就是说它并不需要重复bind
  5. 收发数据

    • 服务器通过 recv 接收客户端发送的数据,并通过 send 向客户端发送响应数据。此时,通信已经通过与客户端建立的专用套接字进行。

      recv(client_fd, buffer, sizeof(buffer), 0);  // 接收数据
      send(client_fd, response, strlen(response), 0);  // 发送数据
      

细节

  1. listen函数的作用是将监听套接字设置为监听状态,并不会阻塞,当监听套接字设置为监听状态后,服务器端才可以监听客户端请求,进而建立连接。
  2. 当客户端向服务器发起连接请求时,这些请求不会直接被服务器立即处理,而是由操作系统暂时存放在监听队列中。监听队列的长度backlog,由用户指定。但是它只影响监听队列的大小,而不限制已经成功建立的连接数量。
  3. 在默认情况下,accept 函数会阻塞,直到有客户端的连接请求到来并完成三次握手。

对于客户端

和服务器端的行为类似,不同的是:

  • 客户端是请求连接方,网络中不会有进程与它主动建立连接,所以它不需要监听套接字,进而也不需要调用listen函数。
  • 它需要主动调用connect函数与服务器端发起连接请求。

步骤

  1. 创建通信的套接字。
  2. bind(不用显式bind,当客户端发起连接时,OS会自动bind)。
  3. 向服务器发起连接请求(connect函数)。
  4. 收发数据。
  5. 关闭通信套接字。

TCP实现echo功能

客户端发送什么,服务器就返回什么。

代码实现

服务器端

服务器端需要不停的建立连接,可以使用多线程、线程池、或者多进程来实现。但是不能使用一个单线程的进程,因为可能需要连接的客户端有很多,建立连接成功后,每个连接都会进入死循环(不停收发数据),直到客户端退出。

如果使用单线程的进程,一个客户端建立连接成功,它就会阻塞到该客户端处理数据的函数中,无法继续处理请求了。

我们使用线程池版本来实现服务器端的代码:

#pragma once
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<cstdio>
#include<cstdlib>
#include"Log.hpp"
#include"InetAddr.hpp"
#include"ThreadPool.hpp"

// 错误码
enum
{
    SOCKETERROR = 1,
    BINDERROR,
    USAGEERROR
};

// 定义funccommunicate为一个函数类型,用于线程池的任务队列
using funccommunicate = function<void()>;

// TcpServer类实现TCP服务器的功能
class TcpServer
{   
private:
    int _listensock;  // 监听套接字
    uint16_t _port;   // 服务器端口
    bool _is_running; // 服务器是否在运行

public:
    // 构造函数,初始化套接字和端口号
    TcpServer(uint16_t port):_listensock(-1),_port(port),_is_running(false)
    {}

    // 初始化服务器,创建监听套接字并绑定地址和端口
    void InitServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);  // 创建TCP套接字
        if (_listensock == -1)
        {
            LOG(FATAL, "socket error");
            exit(1);
        }
        LOG(INFO, "socket success");

        // 配置服务器地址
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(_port);  // 转换端口号为网络字节序
        addr.sin_addr.s_addr = INADDR_ANY;  // 绑定所有可用接口

        // 绑定套接字到指定端口和IP地址
        if (bind(_listensock, (struct sockaddr*)&addr, sizeof(addr)) == -1)
        {
            LOG(FATAL, "bind error");
            exit(1);
        }
        LOG(INFO, "bind success");

        // 开始监听客户端连接,最大连接数为5
        if (listen(_listensock, 5) == -1)
        {
            LOG(FATAL, "listen error");
            exit(1);
        }
    }

    // 处理每个连接的业务逻辑
    void Service(int sockfd, InetAddr addr)
    {
        LOG(INFO, "new connect: %s:%d", inet_ntoa(addr.addr().sin_addr), ntohs(addr.addr().sin_port));  // 输出连接信息

        // 处理接收和响应循环
        while (true)
        {
            char buffer[1024];  // 用于接收数据的缓冲区
            memset(buffer, 0, sizeof(buffer));  // 初始化缓冲区为0
            int n = recv(sockfd, buffer, sizeof(buffer), 0);  // 接收客户端数据

            // 构建客户端信息字符串
            string sender = "[" + addr.ip() + ":" + to_string(addr.port()) + "]#";

            if (n == -1)  // 如果接收失败
            {
                perror("recv");  // 输出错误信息
                break;
            }
            else if (n == 0)  // 客户端关闭了连接
            {
                LOG(INFO, "client close");
                break;
            }
            else  // 数据接收成功
            {
                buffer[n] = 0;  // 确保接收的数据是一个合法的C字符串
                LOG(INFO, "%s%s", sender.c_str(), buffer);  // 打印接收到的数据
                string echoserver = "[echo server]#" + string(buffer);  // 构建回显信息

                // 获取服务器端新套接字的本地地址和端口
                struct sockaddr_in local_addr;
                socklen_t len = sizeof(local_addr);
                if (getsockname(sockfd, (struct sockaddr*)&local_addr, &len) == -1)
                {
                    perror("Getsockname failed");
                    return;
                }

                // 输出本地地址和端口信息
                printf("New socket local address: %s:%d\n", inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));

                // 发送回显消息到客户端
                send(sockfd, echoserver.c_str(), echoserver.size(), 0);
            }
        }

        close(sockfd);  // 关闭套接字,结束与客户端的通信
    };

    // 服务器主循环,不断接收新的连接请求
    void Loop()
    {
        _is_running = true;
        while (_is_running)
        {
            struct sockaddr_in peer;  // 存储客户端的地址信息
            socklen_t len = sizeof(peer);

            // 等待并接受新的连接请求
            int sockfd = ::accept(_listensock, (struct sockaddr*)&peer, &len);
            cout << "建立新连接成功" << endl;

            if (sockfd == -1)  // 如果接收连接失败,输出错误信息
            {
                perror("accept");
                break;
            }

            InetAddr addr(peer);  // 将客户端地址封装到InetAddr对象中

            // 版本1:直接调用Service处理连接(不建议这种方式,因为它会阻塞并限制并发)
            // Service(sockfd, addr);  // 每次只能处理一个连接,无法同时处理多个连接

            // 版本2:使用线程池处理连接(推荐的方式,支持并发)
            bool ret = ThreadPoolModule::ThreadPool<funccommunicate>::GetInstance()->EnqueueTask(bind(&TcpServer::Service, this, sockfd, addr));
        }
        _is_running = false;
    }

    // 析构函数,关闭监听套接字
    ~TcpServer()
    {
        if (_listensock != -1)
        {
            close(_listensock);
        }
    }
};
getsockname函数介绍
  • int getsockname(int sockfd, struct sockaddr *restrict addr,socklen_t *restrict addrlen);
    • 函数功能:返回当前套接字bind的地址。
    • 参数
      • int sockfd:要查看bind地址的套接字描述符。
      • struct sockaddr *restrict addr:输出型参数,该函数会把地址写进这个变量指向的空间中。
      • socklen_t *restrict addrlen:指向保存结构体大小变量的指针,输入型参数。
    • 返回值:成功0被返回。否则-1被返回,errno被设置。
    • 头文件<sys/socket.h>
客户端

TcpClient.cc:

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

using namespace std;

// Usage函数:如果程序参数不正确,打印如何使用该程序的提示信息
void Usage(char* s)
{
    cout << "Usage:\n\t" << s << " serverip serverport" << endl;
    exit(1);
}

int main(int argc, char* argv[])
{
    // 检查传入的参数数量,若参数不正确,则调用Usage函数
    if(argc != 3)
    {
        Usage(argv[0]);  // 打印使用帮助信息并退出
        return 1;
    }

    // 从命令行参数获取服务器IP地址和端口号
    string ip = argv[1];  // 服务器IP地址
    uint16_t port = stoi(argv[2]);  // 服务器端口号,将字符串转换为整数

    // 创建套接字,使用IPv4地址族和TCP协议(SOCK_STREAM表示流式套接字)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 创建TCP套接字
    if(sockfd == -1)  // 如果创建套接字失败,打印错误并返回
    {
        perror("socket create error");  // 输出错误信息
        return 1;
    }

    // 客户端不需要bind,bind通常用于服务器端
    // 客户端也不需要listen,监听请求是服务器端的工作

    // 设置服务器的地址信息
    struct sockaddr_in addr;  // sockaddr_in结构体用于存储服务器的网络地址
    addr.sin_family = AF_INET;  // 使用IPv4地址族
    addr.sin_port = htons(port);  // 设置服务器的端口号(htons将端口号转换为网络字节序)
    inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);  // 将IP地址字符串转换为网络字节序的二进制格式

    // 连接到服务器
    if(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1)  // 调用connect连接服务器
    {
        perror("connect error");  // 如果连接失败,输出错误信息
        return 1;
    }

    // 客户端和服务器之间进行通信
    while(true)
    {
        string message;
        cout << "please input message:";  // 提示用户输入消息
        getline(cin, message);  // 从标准输入获取一行字符串作为消息

        // 将输入的消息发送到服务器
        send(sockfd, message.c_str(), message.size() + 1, 0);  // 发送消息到服务器,+1用于包括消息结尾的'\0'

        // 接收服务器返回的消息
        char buffer[1024];  // 定义接收缓冲区,大小为1024字节
        memset(buffer, 0, sizeof(buffer));  // 将缓冲区初始化为0

        int n = recv(sockfd, buffer, sizeof(buffer), 0);  // 从服务器接收数据
        if(n == -1)  // 如果接收数据失败
        {
            perror("recv error");  // 输出错误信息
            break;  // 跳出循环,关闭连接
        }
        else if(n == 0)  // 如果服务器关闭了连接
        {
            cout << "server close" << endl;  // 打印提示信息
            break;  // 跳出循环,结束通信
        }
        else  // 数据接收成功
        {
            buffer[n] = 0;  // 确保接收到的数据是一个合法的C字符串(添加终止符'\0')
            cout << buffer << endl;  // 输出服务器返回的消息
        }
    }

    // 关闭套接字,结束与服务器的通信
    close(sockfd);  // 关闭套接字
    return 0;  // 程序正常结束
}
效果展示

image-20250115191358332


  • 打印服务器端与客户端通信的socket套接字描述符的地址,发现端口一样(8080),但是和虚拟机客户端和本地的客户端通信的服务器端的套接字bindIP地址不同,这是因为服务器bind的IP地址是0.0.0.0,表示监听主机内所以网络接口的流量,虚拟机客户端访问和本地访问的流量进入主机内,流量会经过不同的网络接口,所以与他们通信的套接字的IP地址会不同。

对比两组函数

  1. recvrecvformread

    • recvread

      • 相似之处:都是从文件描述符🀄️读取数据。

      • 不同之处recv是专门用于网络套接字中读取数据,而read更加通用,可以读取任何类型的文件描述符。允许指定标志(flags)来控制接收操作的行为。例如,标志可以指定如何处理数据或是否采用非阻塞模式等。

    • recvfromrecv

      • recvfromrecvfrom() 是设计用来接收数据包并且能够获取发送方的地址信息的。常常在UDP中使用,在TCP中也可以接收数据,但它不会返回对端的地址信息。

        image-20250115195824850

      • recvrecv是专门用于TCP接收数据,它是从一个已经建立的连接中获取数据,因此不需要提供发送方的地址信息。recv也能在UDP中接收数据,但它无法获取发送方的地址信息。

      image-20250115195840621

  2. sendsendtowrite

    • 相同点:这一组函数都是用于发送数据的。

    • 不同点sendsendto用于网络通信,从套接字描述符中读取数据。而write更加的通用。send用于TCP通信,面向连接,不需要指定客户端地址。而sendto需要指定客户端的地址。

      image-20250116161803708

      • 尽管从用户角度看它们的功能略有重叠,网络相关的功能通常会选择 send 和 sendto,因为它们支持更多与网络协议相关的选项。

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

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

相关文章

【教程】MySQL数据库学习笔记(七)——多表操作(持续更新)

写在前面&#xff1a; 如果文章对你有帮助&#xff0c;记得点赞关注加收藏一波&#xff0c;利于以后需要的时候复习&#xff0c;多谢支持&#xff01; 【MySQL数据库学习】系列文章 第一章 《认识与环境搭建》 第二章 《数据类型》 第三章 《数据定义语言DDL》 第四章 《数据操…

国自然地区基金|影像组学联合病理组学预测进展期胃癌术后预后的研究|基金申请·25-02-13

小罗碎碎念 今天和大家分享一个国自然地区科学项目&#xff0c;执行年限为2020.01&#xff5e;2023.12&#xff0c;直接费用为34万元。 胃癌在我国发病形势严峻&#xff0c;现有TNM分期预后评估存在局限&#xff0c;难以满足精准医疗需求。本项目运用“医工结合&#xff0c;学科…

nvm下载安装教程(node.js 下载安装教程)

前言 nvm 官网地址&#xff1a;https://nvm.uihtm.com nvm 是一个 node.js 的版本管理工具&#xff0c;相比于仅安装 node.js&#xff0c;我们可以使用 nvm 直接下载或卸载 node.js&#xff0c;可以同时安装多个 node.js 版本&#xff0c;并动态的切换本地环境中的 node.js 环…

项目BUG

项目BUG 前言 我创作这篇博客的目的是记录学习技术过程中的笔记。希望通过分享自己的学习经历&#xff0c;能够帮助到那些对相关领域感兴趣或者正在学习的人们。 项目BUG 1.低频率信号(100k或 200K以下)可以直接用一根导线焊接出几根导线来分几路&#xff0c;高频率信号只能…

Apollo 9.0 速度动态规划决策算法 – path time heuristic optimizer

文章目录 1. 动态规划2. 采样3. 代价函数3.1 障碍物代价3.2 距离终点代价3.3 速度代价3.4 加速度代价3.5 jerk代价 4. 回溯 这一章将来讲解速度决策算法&#xff0c;也就是SPEED_HEURISTIC_OPTIMIZER task里面的内容。Apollo 9.0使用动态规划算法进行速度决策&#xff0c;从类名…

吴恩达深度学习——词嵌入

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 词表特征词嵌入的类比推理嵌入矩阵词嵌入Word2Vec跳字模型模型细节负采样 GloVe词向量&#xff08;了解&#xff09; 情绪分类 词表特征 使用 one-hot 对词汇进行编码时&#x…

数据结构——Makefile、算法、排序(2025.2.13)

目录 一、Makefile 1.功能 2.基本语法和相关操作 &#xff08;1&#xff09;创建Makefile文件 &#xff08;2&#xff09;编译规则 &#xff08;3&#xff09;编译 &#xff08;4&#xff09;变量 ①系统变量 ②自定义变量 二、 算法 1.定义 2.算法的设计 &#xff…

达梦:TPCC 压测

目录 造数1. 脚本启动2. 检查数据库信息3. 删除旧用户和表空间4. 创建新的表空间5. 创建用户和表6. 数据加载7. 创建索引8. 创建存储过程和序列9. 检查数据空间使用情况10. 启用表的快速访问池11. 数据加载完成总结 压测1. 脚本启动2. 检查数据表空间3. 设置表的快速池标志4. 检…

2024 StoryDiffusion 文字/文字+图像----->视频

基于扩散模型的生成模型在生成长序列图像和视频时面临内容一致性的重大挑战&#xff0c;尤其是涉及复杂主题和细节的场景中&#xff0c;角色身份、服饰风格等元素难以保持连贯。传统方法通常依赖潜在空间的运动预测&#xff0c;但长视频生成时易出现不稳定性。针对这些问题&…

在带有Intel Arc GPU的Windows上安装IPEX-LLM

在带有Intel Arc GPU的Windows上安装IPEX-LLM 在带有Intel Arc GPU的Windows上安装IPEX-LLM先决条件安装 GPU 驱动安装 Visual Studio 2022 社区版安装 Intel oneAPI Base Toolkit安装 IPEX-LLM创建虚拟环境环境验证 可能遇到的问题 在带有Intel Arc GPU的Windows上安装IPEX-LL…

流程控制(if—elif—else,while , for ... in ...)

1. 流程控制 流程&#xff1a;计算机执行代码的顺序 流程控制&#xff1a;对计算机执行代码的顺序的管理 2. 流程控制分类 流程控制分类&#xff1a; 顺序流程&#xff1a;自上而下的执行结构&#xff0c;即 Python 默认流程 选择/分支流程&#xff1a;根据某一步的判断&am…

SpringBoot实战:高效获取视频资源

文章目录 前言技术实现SpringBoot项目构建产品选取配置数据采集 号外号外 前言 在短视频行业高速发展的背景下&#xff0c;海量内容数据日益增长&#xff0c;每天都有新的视频、评论、点赞、分享等数据涌现。如何高效、精准地获取并处理这些庞大的数据&#xff0c;已成为各大平…

SSL域名证书怎么申请?

在数字化时代&#xff0c;网络安全已成为企业和个人不可忽视的重要议题。SSL&#xff08;Secure Sockets Layer&#xff0c;安全套接层&#xff09;域名证书&#xff0c;作为保障网站数据传输安全的关键工具&#xff0c;其重要性日益凸显。 一、SSL域名证书&#xff1a;网络安…

用大模型学大模型04-模型与网络

目前已经学完深度学习的数学基础&#xff0c;开始学习各种 模型和网络阶段&#xff0c;给出一个从简单到入门的&#xff0c;层层递进的学习路线。并给出学习每种模型需要的前置知识。增加注意力机制&#xff0c;bert, 大模型&#xff0c;gpt, transformer&#xff0c; MOE等流行…

DeepSeek4j 已开源,支持思维链,自定义参数,Spring Boot Starter 轻松集成,快速入门!建议收藏

DeepSeek4j Spring Boot Starter 快速入门 简介 DeepSeek4j 是一个专为 Spring Boot 设计的 AI 能力集成启动器&#xff0c;可快速接入 DeepSeek 大模型服务。通过简洁的配置和易用的 API&#xff0c;开发者可轻松实现对话交互功能。 环境要求 JDK 8Spring Boot 2.7Maven/Gr…

graphRAG的原理及代码实战(2)基本原理介绍(中)

graphRAG-结果解读 1、简介 前文中&#xff0c;graphRAG项目index索引建立完成后&#xff0c;会生成7个parquet文件。 为什么用 Parquet 格式保存知识图谱&#xff1f; 高效存储&#xff1a; 知识图谱中的数据通常是结构化的&#xff0c;包含大量的实体、关系、嵌入等。Parq…

TLQ-CN10.0.2.0 (TongLINK/Q-CN 集群)部署指引 (by lqw)

文章目录 安装准备虚拟机部署部署zk集群安装zk集群启动zk集群初始化元数据&#xff08;zk&#xff09;关闭zk集群 部署BookKeeper集群安装BookKeeper集群初始化元数据&#xff08;bk&#xff09;启动BookKeeper停止 BookKeeper 部署Brokers集群安装Brokers集群启动 broker停止 …

深入剖析推理模型:从DeepSeek R1看LLM推理能力构建与优化

著名 AI 研究者和博主 Sebastian Raschka 又更新博客了。原文地址&#xff1a;https://sebastianraschka.com/blog/2025/understanding-reasoning-llms.html。这一次&#xff0c;他将立足于 DeepSeek 技术报告&#xff0c;介绍用于构建推理模型的四种主要方法&#xff0c;也就是…

【Sceneform-EQR】实现3D场景背景颜色的定制化(背景融合的方式、Filament材质定制)

写在前面的话 Sceneform-EQR是基于&#xff08;filament&#xff09;扩展的一个用于安卓端的渲染引擎。故本文内容对Sceneform-EQR与Filament都适用。 需求场景 在使用Filament加载三维场景的过程中&#xff0c;一个3D场景对应加载一个背景纹理。而这样的话&#xff0c;即便…

Visual Studio 2022在配置远程调试服务器时无法连接到OpenEuler24.03

表现为在VS中为OpenEuler24.03创建远程服务器时&#xff0c;界面上直接报主机密钥算法失败&#xff0c;或直接提示无法连接到服务器&#xff0c;导致无法创建远程服务器。 经查询日志发现一些蛛丝马迹 09:25:15.2035105 [Info, Thread 53] liblinux.Local.Services.WslEnumer…