【网络编程】网络套接字udp通用服务器和客户端

news2024/11/24 13:44:45

1.预备知识

认识端口号

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

  • 端口号是一个2字节16位的整数(uint16)
  • 端口号用来标识主机上的一个进程
  • IP地址+port能够标识网络上的某一台主机和某一个进程
  • 一个端口号只能被一个进程占用

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议) 有一个直观的认识,后面再详细讨论TCP的一些细节问题。

  • 传输层协议
  • 有连接
  • 可靠性传输
  • 面向字节流

认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识,后面再详细讨论。

  • 传输层协议
  • 无连接
  • 不可靠性传输
  • 面向数据报

网络字节序

内存中的多字节数据相比于内存地址有大端和小端之分,磁盘文件中的多字节数据相比于文件中的偏移地址也有大端小端之分,网络数据流同样有大端和小端之分,那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按照内存地址从低到高顺序发出
  • 接受主机把从网络上接到的字节序依次保存在接收缓冲区,也是按照内存地址从低到高的顺序保存
  • 因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/UDP协议规定,网络数据流应该采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机,都会按照这个TCP/UDP规定的网络字节序来发送/接收数据
  • 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可

存储在内存中的数据有大端和小端之分,低位存储在低地址的是小端,低位存储在高地址的是大端。

为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常的运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(unit32_t hostlong);
uint16_t htons(unit16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
  • 例如htonl 表示将32位长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
  • 如果主机是小端字节序,这些函数将参数做相应的大小端进行转换,然后返回
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回

2.socket编程接口

socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr结构

sockaddr是通用的网络接口,网络通信中经常使用到的是struct sockaddr_in。

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

sockaddr结构

sockaddr_in结构

虽然socket api的接口是sockaddr,但是真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型,端口号,IP地址

in_addr结构

in_addr用来表示一个IPv4的IP地址,其实就是一个32位的整数;

3.简单的UDP网络程序

实现一个简单的英译汉功能

封装UdpSocket

udp_socket.hpp

#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>


class UdpSocket
{
public:
    UdpSocket() : fd_(-1)
    {}
    bool Socket()
    {
        fd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (fd_ < 0)
        {
            perror("socket");
            return false; //创建套接字失败
        }
        return true;
    }
    bool Close()
    {
        close(fd_); //关闭套接字
        return true;
    }
    bool Bind(const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;              //用于网络传输
        addr.sin_family = AF_INET;     //地址类型-网络
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));
        if (ret < 0)
        {
            perror("bind");
            return false; //绑定失败
        }
        return true;
    }
    bool RecvFrom(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL)
    {
        char tmp[1024 * 10] = {0};
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t read_size = recvfrom(fd_, tmp,
                                     sizeof(tmp) - 1, 0, (sockaddr *)&peer, &len);
        if (read_size < 0)
        {
            perror("recvfrom");
            return false;
        }
        // 将读到的缓冲区内容放到输出参数中
        buf->assign(tmp, read_size);
        if (ip != NULL)
        {
            *ip = inet_ntoa(peer.sin_addr);
        }
        if (port != NULL)
        {
            *port = ntohs(peer.sin_port);
        }
        return true;
    }
    bool SendTo(const std::string &buf, const std::string &ip, uint16_t port)
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        addr.sin_port = htons(port);
        ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 
        0, (sockaddr *)&addr, sizeof(addr));
        if (write_size < 0)
        {
            perror("sendto");
            return false;
        }
        return true;
    }

private:
//socket函数是实现网络通信的重要工具,用于创建、绑定、监听、连接和关闭套接字,以及发送和接收数据。
    int fd_;  
};

udp通用服务器

#pragma once
#include "udp_socket.hpp"

// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lamda
#include <functional>
typedef std::function<void(const std::string &, std::string *resp)> Handler;
class UdpServer
{
public:
    UdpServer()
    {
        assert(sock_.Socket());
    }
    ~UdpServer()
    {
        sock_.Close();
    }
    bool Start(const std::string &ip, uint16_t port, Handler handler)
    {
        // 1. 创建 socket
        // 2. 绑定端口号
        bool ret = sock_.Bind(ip, port);
        if (!ret)
        {
            return false;
        }
        // 3. 进入事件循环
        for (;;)
        {
            // 4. 尝试读取请求
            std::string req;
            std::string remote_ip;
            uint16_t remote_port = 0;
            bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
            if (!ret)
            {
                continue;
            }
            std::string resp;
            // 5. 根据请求计算响应
            handler(req, &resp);
            // 6. 返回响应给客户端
            sock_.SendTo(resp, remote_ip, remote_port);
            printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,
                   req.c_str(), resp.c_str());
        }
        sock_.Close();
        return true;
    }

private:
    UdpSocket sock_;
};

实现英译汉服务器

以上代码是对udp服务器进行通用接口的封装,基于以上封装,实现一个查字典的服务器就很容易了;

dict_server.cc

#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>

std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string &req, std::string *resp)
{
    auto it = g_dict.find(req);
    if (it == g_dict.end())
    {
        *resp = "未查到!";
        return;
    }
    *resp = it->second;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage ./dict_server [ip] [port]\n");
        return 1;
    }
    // 1. 数据初始化
    g_dict.insert(std::make_pair("hello", "你好"));
    g_dict.insert(std::make_pair("world", "世界"));
    g_dict.insert(std::make_pair("c++", "最好的编程语言"));
    // 2. 启动服务器
    UdpServer server;
    server.Start(argv[1], atoi(argv[2]), Translate);
    return 0;
}

UDP通用客户端

udp_client.hpp

#pragma once
#include "udp_socket.hpp"
class UdpClient
{
public:
    UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
    {
        assert(sock_.Socket());
    }
    ~UdpClient()
    {
        sock_.Close();
    }
    bool RecvFrom(std::string *buf)
    {
        return sock_.RecvFrom(buf);
    }
    bool SendTo(const std::string &buf)
    {
        return sock_.SendTo(buf, ip_, port_);
    }

private:
    UdpSocket sock_;
    // 服务器端的 IP 和 端口号
    std::string ip_;
    uint16_t port_;
};

实现英译汉客户端

dict_client.cpp

#include "udp_client.hpp"
#include <iostream>
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        printf("Usage ./dict_client [ip] [port]\n");
        return 1;
    }
    UdpClient client(argv[1], atoi(argv[2]));
    for (;;)
    {
        std::string word;
        std::cout << "请输入您要查的单词: ";
        std::cin >> word;
        if (!std::cin)
        {
            std::cout << "Good Bye" << std::endl;
            break;
        }
        client.SendTo(word);
        std::string result;
        client.RecvFrom(&result);
        std::cout << word << " 意思是 " << result << std::endl;
    }
    return 0;
}

4.地址转换函数

基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址,但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间进行转换;

字符串转in_addr函数:

in_addr转字符串的函数:

其实inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void* addrptr。

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

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

相关文章

day34-Animated Countdown(动画倒计时)

50 天学习 50 个项目 - HTMLCSS and JavaScript day34-Animated Countdown&#xff08;动画倒计时&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport&q…

自动驾驶感知系统-超声波雷达

超声波雷达&#xff0c;是通过发射并接收40kHz的超声波&#xff0c;根据时间差算出障碍物距离。其测距精度是1~3cm.常见的超声波雷达有两种&#xff1a;第一种是安装在汽车前后保险杠上的&#xff0c;用于测量汽车前后障碍物的驻车雷达或倒车雷达&#xff0c;称为超声波驻车辅助…

易班开放应用授权重定向,出现跨域的解决方案

问题描述 今天开发H5网站需要接入易班&#xff0c;经过易班授权然后重定向&#xff08;code: 302&#xff09;&#xff0c;使用axios发请求&#xff0c;但是前后端均配置跨域的情况下&#xff0c;不管怎么弄都是一直跨域 但是我们看network&#xff0c;network中对应请求的res…

nvm安装(win/linux)

win安装nvm Win安装nvm1、下载nvm2、直接安装nvm-setup.exe3、cmd运行查看安装情况 Linux安装nvm1、下载nvm安装包2、安装及配置环境变量3、查看安装情况 前端开发多个工程&#xff0c;node版本需要时不时的进行切换&#xff0c;如果重新下载安装nodejs会导致浪费很多无用的时间…

人群异常聚集检测告警算法 yolov5

人群异常聚集检测报警系统基于yolov5图像识别和数据分析技术&#xff0c;人群异常聚集检测告警算法通过在关键区域布设监控摄像头&#xff0c;实时监测人员的密集程度和行为动态,分析和判断人群密集程度是否超过预设阈值&#xff0c;一旦发现异常聚集&#xff0c;将自动发出信号…

Pytorch个人学习记录总结 03

目录 Transeforms的使用 常见的transforms Transeforms的使用 torchvision中的transeforms&#xff0c;主要是对图像进行变换&#xff08;预处理&#xff09;。from torchvision import transforms transeforms中常用的就是以下几种方法&#xff1a;&#xff08;Alt7可唤出…

FileNotFoundException:xxx(系统找不到指定的路径)

目录 前言 背景 解决方法 错误示例 前言 这次是有个两年前的项目吧&#xff0c;不知道为什么无法启动了。中间迭代了多个版本&#xff0c;现在另一个同事接手了&#xff0c;领导让看一下。因为时间间隔过长&#xff0c;问题处理比较费劲。其中有的是配置问题&#xff0c;比…

【Linux学习】超详细——进程(2)

三、进程状态 3.1 准备知识 进程阻塞&#xff1a;进程因为等待某种条件就绪&#xff0c;而导致的一种不推进的状态&#xff08;例如进程卡顿&#xff09;&#xff0c;因而阻塞一定是在等待某种资源。为什么阻塞&#xff1f;进程需要通过等待的方式&#xff0c;等具体的资源被别…

SpringCloudAlibaba微服务实战系列(二)Nacos配置中心

SpringCloudAlibaba Nacos配置中心 在java代码中或者在配置文件中写配置&#xff0c;是最不雅的&#xff0c;意味着每次修改配置都需要重新打包或者替换class文件。若放在远程的配置文件中&#xff0c;每次修改了配置后只需要重启一次服务即可。话不多说&#xff0c;直接干货拉…

java实现ffmpeg音频文件分割

项目中需要将视频会议中录入的音频文件通过阿里云语音识别为文件&#xff0c;但是阿里云语音识别对音频大小有限制&#xff0c;因此通过ffmpeg将大音频文件分割为几个短音频文件&#xff0c;并进行语音识别操作。 代码如下&#xff1a; package com.vion.utils; import java.i…

openGauss学习笔记-18 openGauss 简单数据管理-WHERE子句

文章目录 openGauss学习笔记-18 openGauss 简单数据管理-WHERE子句18.1 语法格式18.2 参数说明18.3 示例 openGauss学习笔记-18 openGauss 简单数据管理-WHERE子句 当我们需要根据指定条件从表中查询数据时&#xff0c;就可以在SELECT语句中添加WHERE子句&#xff0c;从而过滤…

Install Ansible on CentOS 8

环境准备&#xff1a; 1.至少俩台linux主机&#xff0c;一台是控制节点&#xff0c;一台是受控节点 2.控制节点和受控节点都需要安装Python36 3.控制节点需要安装ansible 4.控制节点需要获得受控节点的普通用户或root用户的权限&#xff0c;控制节点需要ssh客户端&#xff0c;…

24考研数据结构-——绪论

数据结构 引用文章第一章&#xff1a;绪论1.0 数据结构在学什么1.1 数据结构的基本概念1.2 数据结构的三要素1.3 算法的基本概念 引用文章 在此基础上增加自己的学习过程: 《王道》数据结构笔记整理2022 1.2数据结构三要素——逻辑结构和物理结构与数据运算之间的关系 1.3抽象…

JMeter+提取token变成全局变量

注&#xff1a;没打码&#xff0c;就代码乱写的接口&#xff0c;具体请按照你要跑的接口来输入值 一、创建线程组 二、配置HTTP请求默认值 IP地址一模一样&#xff0c;可以配置一个默认值&#xff0c;就不用每次都输入IP地址了 三、配置登陆ip 配置登陆地址&#xff0c;通过…

iOS 测试 iOS 端 Monkey 测试

说起 Monkey 测试&#xff0c;大家想到的是 monkey 测试只有安卓有&#xff0c;monkey 测试只针对安卓 app&#xff0c;今天给大家分享一下 Monkey 测试在 iOS 端也能跑&#xff01;iOS 端 app 也能使用 Monkey 测试来执行稳定性测试。 一、环境准备 1、准备 Mac 设备&#x…

物业小区管理系统登录页面以及逻辑实现

学习vue3和springboot那肯定是少不了写项目的&#xff0c;在各个项目中肯定是离不开登录和注册的事情的&#xff0c;这也是一个项目起步的需求。 接下来我们来看看我们所写的项目起步。首先搭建vue3和springboot的项目环境&#xff0c;这些搭建大家自行完成即可&#xff0c;架子…

windows 修改 RDP 远程桌面端口号

打开 PowerShell &#xff0c; 执行regedit 依次展开 PortNumber HKEY_LOCAL_MACHINE \SYSTEM \CurrentControlSet \Control \Terminal Server \WinStations \RDP-Tcp 右边找到 PortNumber &#xff0c;对应修改自己的端口号 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Co…

深度学习:tf.keras实现模型搭建、模型训练和预测

在sklearn中&#xff0c;模型都是现成的。tf.Keras是一个神经网络库,我们需要根据数据和标签值构建神经网络。神经网络可以发现特征与标签之间的复杂关系。神经网络是一个高度结构化的图&#xff0c;其中包含一个或多个隐藏层。每个隐藏层都包含一个或多个神经元。神经网络有多…

echarts3d饼图实现

一、vue中使用3d饼图 效果图&#xff1a; 二、使用步骤 1.引入库 安装echarts 在package.json文件中添加 "dependencies": {"echarts": "^5.1.2""echarts-gl": "^1.1.0",// "echarts-gl": "^2.0.8&quo…