【计算机网络学习之路】UDP socket编程

news2024/11/13 16:41:49

文章目录

  • 前言
  • 一. 网络通信本质
    • 端口号
    • TCP与UDP
    • 网络字节序
  • 二. socket编程接口
    • socket()和sockaddr结构体
  • 三. 简单echo服务
  • 结束语

前言

本系列文章是计算机网络学习的笔记,欢迎大佬们阅读,纠错,分享相关知识。希望可以与你共同进步。

一. 网络通信本质

上篇博客说到,MAC地址标识网卡的全球唯一性,IP地址标识计算机在公网中的唯一性。要想进行网络通信,就必须知道目的主机的IP地址
但是这还不够,数据只是成功送到了目的主机,并没有被处理。QQ消息要发到QQ,微信消息要发到微信。数据不仅要送达目的主机,还要送达目的程序,也就是进程

所以网络通信的本质是
两个主机的两个进程基于网络的进程间通信

网络通信的过程:

  1. 先将数据通过OS,将数据发送到目标主机(TCP/IP协议),其中IP标识公网上唯一的一台主机
  2. 在本主机收到数据后,推送给上层指定的进程

那么如何标识进程呢?——端口号

端口号

首先,回答为什么不使用pid?

  1. 并不是所有的进程都需要接收发送网络数据
  2. 网络属于文件系统的一部分,同样使用pid会增加耦合度

接下来介绍端口号

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

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理
  • IP地址+端口号能标识网络上唯一一台主机的一个进程
  • 一个端口号只能被一个进程占用
  • 一个进程可以绑定多个端口号

端口号的作用

操作系统会维护一张端口号和pid对应的hash表,通过端口号可以找到对应进程pid,然后获取进程结构体,其中就有文件fd
将网络数据写入文件,进程就可以从文件中读取网络数据了,如此就将网络通信转化成文件读写

TCP与UDP

TCP和UDP都是传输层协议

TCP协议

  • 有连接
  • 可靠传输
  • 面向字节流

UDP协议

  • 无连接
  • 不可靠传输
  • 面向数据报

可靠与不可靠传输不是褒义和贬义的关系,可靠意味着需要有更多资源保证可靠,也有很多场景适合不可靠传输

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分。网络数据流同样也有大小端之分
小端是将低位数据放到低地址,高位数据放到高地址,大端反之

那么如何定义网络数据流的地址呢?

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

在这里插入图片描述

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

在这里插入图片描述

  • 记忆:h表示host主机,n表示network网络,l表示32位长整数,s表示16位短整数
  • 例如htonl表示32为长整数从主机字节序转为网络字节序
  • 如果主机是小端字节序,这些函数将参数做响应的大小端转换后返回
  • 如果主机是大端字节序,这些函数不做转换,直接返回

C语言有定义表示该主机是大端还是小端,所以只需要判断一下宏即可知道本主机是大端还是小端

二. socket编程接口

上述说到,网络通信的本质是两台主机中的两个进程通信。
在Linux学习中,进程通信有两个标准——System VPOSIX

历史
UNIX两大贡献者——贝尔实验室和BSD,在进程之间通信侧重不同,前者基于内核对进程之间的通信手段进行了改进,形成System V IPC,而后者则是基于网络形成了套接字

POSIX是IEEE制定的标准,目的是为运行在不同操作系统上的软件提供统一的接口,实现者则是不同的操作系统内核开发人员。
如今POSIX已经支持同主机的进程通信和网络通信,POSIX将会是大势所趋

参考System V 与 POSIX

本系列讲解的都是POSIX标准的接口

socket()和sockaddr结构体

socket()

//创建socket 文件描述符
int socket(int domain,int type,int protocol);

在这里插入图片描述

上述说到,OS通过端口号找到对应pid,找到对应进程,就可以找这个进程所有的文件,将网络数据写入文件,就将网络通信转换为文件读写

socket()的作用就是创建一个网络文件,返回值int就是文件描述符

  • int domain:指定通信域在这里插入图片描述
    主要使用AF_UNIX(本主机的进程通信)AF_INET(网络通信),AF_INET6(IPv6的网络通信)

  • int type:指定通信语义
    常用的是SOCK_STREAM(面向字节流——TCP),SOCK_DGRAM(面向数据报——UDP)

  • int protocol:默认为0,OS会判断是使用TCP还是UDP

这三个参数都将会标识该文件是网络文件


sockaddr结构体

OS使用sockaddr保存本主机信息。因为POSIX标准同时支持本主机进程通信和网络通信,所以用C语言模拟多态的形式实现着两种通信。

具体操作如下:

在socket常见API中

//绑定端口号
int bind(int socket,const struct sockaddr*address,socklen_t address_len);

//接收请求
int accept(int socket,struct sockaddr*address,socklen_t address_len);

//建立连接
int connect(int sockfd,const struct sockaddr*addr,socklen_t addrlen);

这三个接口的参数中,都有const struct sockaddr*

在这里插入图片描述

struct sockaddr是通用结构体,struct sockaddr_in是网络通信结构体,struct sockaddr_un是本主机进程通信结构体
只要在传参时强转成sockaddr即可

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

sockaddr_in定义如下:

在这里插入图片描述

sin_zero是填充字段
in_addr用来标识一个IPv4的IP地址,其实就是一个32位的整数

在这里插入图片描述

三. 简单echo服务

接下来,简单实现UDP网络echo服务器(接收并送回数据)和客户端
边写边讲解注意点

makefile

all:client server
client:udp_client.cc
	g++ -o $@ $^ -std=c++11
server:udp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server

先编写客户端

udp_server.hpp

#pragma once

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

namespace ns_server
{
    class UdpServer
    {
    public:
        UdpServer(){}
        void InitServer(){}//初始化服务器
        void Start(){}//启动服务器
        ~UdpServer(){}
    private:
    int _sock;//套接字
    uint16_t _port;//端口号
    std::string _ip;//IP地址
    };
}

udp_server.cc

#include "udp_server.hpp"
#include<memory>

using namespace ns_server;
using namespace std;


int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer());

    usvr->InitServer();//初始化
    usvr->Start();//启动

    return 0;
}

以上是基本框架
网络服务,服务器肯定需要端口号和IP地址,另外还需要保存套接字

  1. 创建套接字
void InitServer()
{
	_sock=socket(AF_INET,SOCK_DGRAM,0);
    if(_sock<0)
		std::cerr<<"create sock error,"<<strerror(errno)<<",errno:"<<errno<<std::endl;
}

创建套接字失败会返回-1,并设置错误码

  1. 定义struct sockaddr_in结构体

其中需要提供端口号和IP地址,我们通过构造函数获取

//构造函数获取端口号和IP地址
UdpServer(uint16_t port,std::string ip):_port(port),_ip(ip)
{}

//初始化服务器
void InitServer()
{
	_sock=socket(AF_INET,SOCK_DGRAM,0);
	if(_sock<0)
    std::cerr<<"create sock error,"<<strerror(errno)<<",errno:"<<errno<<std::endl;

    struct sockaddr_in local;
    bzero(&local,sizeof(local));//清空结构体

    local.sin_family=AF_INET;//地址类型
    local.sin_port=htons(_port);//端口号
    local.sin_addr.s_addr=inet_addr(_ip.c_str());//IP地址
}

但此时该sockaddr_in结构体仅仅是定义在栈帧上,并没有写入内核,没有和网络文件绑定

所以需要使用bind()函数

  1. 绑定端口号
//绑定端口号
int bind(int socket,struct sockaddr*address,socklen_t address_len);
  • int sokcet:要绑定的套接字(网络文件描述符
  • const struct sockaddr:相关网络信息结构体
  • socklen_t address_len:结构体大小

绑定失败返回值-1,并设置错误码

//初始化服务器
void InitServer()
{
	_sock=socket(AF_INET,SOCK_DGRAM,0);
    if(_sock<0)
    {
    	std::cerr<<"create sock error,"<<strerror(errno)<<std::endl;
        return 1;
    }

    struct sockaddr_in local;
    bzero(&local,sizeof(local));//清空结构体

    local.sin_family=AF_INET;//地址类型
    local.sin_port=htons(_port);//端口号
    local.sin_addr.s_addr=inet_addr(_ip.c_str());//IP地址

    //绑定结构体
    if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
		std::cerr<<"bind error,"<<strerror(errno)<<std::endl;
        return 2;
    }
}

注意,云服务器一般不允许绑定特定IP
另外,如果服务器有多个网卡,则不管哪个网卡/哪个IP地址接收到的数据,只要是该端口号的,都应该接收
所以服务器的IP一般如此设置

local.sin_addr.s_addr=INADDR_ANY;

socket INADDR_ANY就是指定地址为0.0.0.0的这个地址,这个地址不是确定的地址,而是表示“所有地址”“任意地址”
所以只要是发送给指定端口号的数据,无论是发送给本机的哪个IP地址的,都一并接收


初始化服务器到此暂告一段落
接下来是启动服务器

服务器首先是需要一直运行的,即使在凌晨,我们一样可以玩游戏,看QQ

因为是echo服务器,所以需要接收客户端发送的消息,然后再发送回去

recvfrom()

在这里插入图片描述

  • int sockfd:从哪个套接字读取数据
  • void * buf:存数据的缓冲区
  • size_t len:缓冲区大小
  • int flags:读取数据的方式(阻塞读或非阻塞读)
  • struct sockaddr* src_addr:输入输出型参数,获取客户端信息
  • socklen_t * addrlen:输入输出型参数,客户结构体大小。注意:输入src_addr的大小,返回发送方结构体大小
  • 返回值:读取数据的个数。错误返回-1并设置错误码

sendto()

在这里插入图片描述

  • int sockfd:往哪个套接字写数据
  • const void * buf:写的数据
  • size_t len:数据大小
  • int flags:写数据的方式(阻塞或非阻塞)
  • struct sockaddr* dest_addr:目的主机信息结构体
  • socklen_t * addrlen:结构体大小
  • 返回值:发送了多少数据。错误返回-1并设置错误码

Start()代码如下:

//启动服务器
void Start()
{
	char buffer[1024];
    while(true)
    {
		struct sockaddr_in client;
		socklen_t len=sizeof(client);

        //缓冲区需要预留\0的位置
        int n=recvfrom(_sock,buffer,sizeof(buffer-1),0,(struct sockaddr*)&client,&len);
        if(n>0) buffer[n]='\0';
        else continue;

        //提取客户端信息
        std::string clientIp=inet_ntoa(client.sin_addr);
        uint16_t clientPort=ntohs(client.sin_port);

        std::cout<<"["<<clientIp<<" : "<<clientPort<<"]# "<<buffer<<std::endl;
        //送回数据
        //发送回去的数据不需要携带\0
        sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&client,sizeof(client));
     }
}

接下来是udp_server.cc
我们需要在启动服务器时指明端口号,类似 ./udp_server 8080

#include "udp_server.hpp"
#include"err.hpp"
#include<memory>

using namespace ns_server;
using namespace std;

//使用手册
//   ./udp_server port
static void usage(string proc)
{
    cout<<"Usage:\n\t"<<proc<<" port\n"<<std::endl;
}

int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        usage(argv[0]);
        return -1;
    }
    //提取参数中的端口号
    uint16_t port=atoi(argv[1]);

    unique_ptr<UdpServer> usvr(new UdpServer(port));
    usvr->InitServer();
    usvr->Start();

    return 0;
}

如此,最基本的echo服务器完成。

接下来是客户端的编写
客户端简单编写一些,就不封装成类了

客户端大致流程如下:

  1. 创建套接字
  2. 提取目标服务器信息
  3. 发送消息

UDP的客户端并不需要bind,因为客户端的端口号不能指定,应该由操作系统分配。如果两个客户端自己绑定同一个端口号,那就不能同时运行了,所以为了避免这种情况,选择让操作系统分配闲置的端口号

而操作系统会在客户端首次发送数据(sendto等)时,给客户端分配IP和端口号,然后bind套接字

目标服务器是由运行程序时指定:如 ./client 127.0.0.1 8888

代码如下:

#include<iostream>
#include<string>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"err.hpp"

using namespace std;

static void usage(string proc)
{
    cout<<"Usage\n\t"<<proc<<" serverIp serverPort"<<endl;
}

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    //提取服务器信息
    string serverIp=argv[1];
    uint16_t serverPort=atoi(argv[2]);

    //创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock<0)
    {
        cerr<<"create sock error,"<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
    std::cout << "create socket success: " << sock << std::endl;

    //客户端不需要自己bind
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_addr.s_addr=inet_addr(serverIp.c_str());
    server.sin_port=htons(serverPort);

    while(true)
    {
        cout<<"please enter your message# ";
        string message;
        getline(cin,message);

        sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));

        char buffer[1024];//接收返回的数据
        struct sockaddr_in tmp;//发送方
        memset(&tmp,0,sizeof(tmp));
        socklen_t len=sizeof(tmp);

        int n=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&tmp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }

    }
    return 0;
}

在这里插入图片描述

结束语

UDP socket编程的内容到此就结束了,感谢看到此处。
欢迎大家纠错和补充
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

【UE】线框材质

一、方式1 新建一个材质&#xff0c;混合模式设置为“已遮罩”&#xff0c;勾选“双面” 勾选“线框” 然后可以随便给一个自发光颜色&#xff0c;这样最基本的线框材质就完成了 二、方式2 新建一个材质&#xff0c;混合模式设置为“已遮罩”&#xff0c;勾选“双面”&#x…

yolo系列模型训练数据集全流程制作方法(附数据增强代码)

yolo系列的模型在目标检测领域里面受众非常广&#xff0c;也十分流行&#xff0c;但是在使用yolo进行目标检测训练的时候&#xff0c;往往要将VOC格式的数据集转化为yolo专属的数据集&#xff0c;而yolo的训练数据集制作方法呢&#xff0c;最常见的也是有两种&#xff0c;下面我…

AC修炼计划(AtCoder Beginner Contest 328)

传送门&#xff1a; Toyota Programming Contest 2023#7&#xff08;AtCoder Beginner Contest 328&#xff09; - AtCoder 本章对于自己的提升&#xff1a;dfs的运用&#xff0c;带权并查集&#xff0c;以及状压dp。 A&#xff0c;B&#xff0c;C题比较简单&#xff0c;直接…

Adult数据集预处理

因为adult数据集没有列名&#xff0c;先设置列名 df pd.read_csv(adult.csv, header None, names [age, workclass, fnlwgt, education, education-num, marital-status, occupation, relationship, race,sex, capital-gain, capital-loss, hours-per-week, native-countr…

【Qt开发流程之】布局管理

介绍 一个界面呈现&#xff0c;如果要让用户有更好的观感&#xff0c;布局必不可少。 【Qt之布局】QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout介绍及使用 链接: https://blog.csdn.net/MrHHHHHH/article/details/133915208 qt布局类图&#xff1a; Qt布局是Qt图形…

Jmeter监听器

Jmeter监听器 一、监听器介绍二、监听器的类型三、监听器详解3.1 察看结果树3.2 Summary Report3.3 聚合报告3.4 后端监听器3.5 Aggregate Graph3.6 Comparison Assertion Visualizer&#xff08;比较断言可视化器&#xff09;3.7 JSR223 Listener3.8 Response Time Graph3.9 S…

一份WhatsApp矩阵账号营销模式全解,有你不知道的玩法吗?

将WhatsApp营销践行到底&#xff0c;是傲途针对海外Social营销一直在做的事。在WhatsApp全球营销范围越来越广泛、营销模式越来越深入的当下&#xff0c;我们也在实践中积累了一套比较系统而全面的差异化矩阵营销模式&#xff0c;帮助大中小不同类型企业获得了有价值的结果。 …

Linux CentOS7配置网络参数

CentOS6及以前版本中主要使用ifconfig工具&#xff0c;查看、配置网络参数。后来对推荐使用ip命令查看配置网络参数。而centos7中&#xff0c;不再赞成使用ifconfig工具&#xff0c;取而代之的是nmcli工具&#xff0c;服务管理也是以systemctl工具取代了service,这些之前版本的…

HarmonyOS ArkTS语言,运行Hello World(二)

一、认识DevEco Studio界面 进入IDE后&#xff0c;我们首先了解一下基础的界面。整个IDE的界面大致上可以分为四个部分&#xff0c;分别是代码编辑区、通知栏、工程目录区以及预览区。 代码编辑区 1、中间的是代码编辑区&#xff0c;你可以在这里修改你的代码&#xff0c;以…

clickhouse 业务日志告警

一、需求 对入库到clickhouse的业务日志进行告警&#xff0c;达阀值后发送企业微信告警。 方法一、 fluent-bit–>clickhouse(http)<–shell脚本,每隔一分钟获取分析结果 --> 把结果保存到/dev/shm/目录下 <-- node_exporter读取指标入库到prometheus<-- rules…

未来之路:互联网技术驱动汽车行业的创新浪潮

在互联网迅猛发展的今天&#xff0c;它的触角已延伸至各行各业&#xff0c;其中最引人注目的莫过于汽车行业。随着互联网技术的融合&#xff0c;汽车正变得越来越智能&#xff0c;预示着一场关于出行方式的革命。 首先&#xff0c;自动驾驶技术的发展正日益成熟。依托先进的传感…

物联网AI MicroPython学习之语法 PWM脉宽调制模块

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; PWM 介绍 模块功能: PWM脉宽调制驱动模块 接口说明 PWM - 构建PWM对象 函数原型&#xff1a;PWM(ch, freq, duty)参数说明&#xff1a; 参数类型必选参数&#xff1f;说明chobjectYPin对象例如&#xf…

pytest-rerunfailures插件之测试用例失败重跑

环境前提&#xff1a; 只有同时满足一下先决条件才能使用pytest-rerunfailures ①python的版本不能过低&#xff1b; ②pytest 5.0或更高版本&#xff1b; 背景&#xff1a; 平时在做接口测试的时候&#xff0c;经常会遇到网络抖动或者环境问题导致测试用例运行失败&#x…

Python的简单web框架flask快速实现详解

文章目录 简介web框架的重要组成部分快速上手flaskflask的第一个应用 flask中的路由不同的http方法静态文件使用模板 总结关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战…

UDS诊断入门

UDS定义的是诊断服务&#xff0c;属于应用层的内容&#xff0c;实现诊断通信的底层总线技术有很多&#xff0c;比如CAN&#xff0c;LIN&#xff0c;Ethernet&#xff0c;Flexray等&#xff0c;由于法规强制的OBD接口是CAN总线的&#xff0c;所以绝大多数场景中诊断都是基于CAN实…

kafka原理看这一篇就够了

为何使用消息队列 异步。接口方式实现多个系统协作&#xff0c;如图A系统作为用户请求接收方&#xff0c;需要调用多个系统的接口&#xff0c;这些接口还有可能是在A系统里同步调用&#xff0c;所以最后的接口耗时是多个系统接口耗时的总和&#xff1b;mq方式则可以异步发送消…

netty整合websocket(完美教程)

websocket的介绍&#xff1a; WebSocket是一种在网络通信中的协议&#xff0c;它是独立于HTTP协议的。该协议基于TCP/IP协议&#xff0c;可以提供双向通讯并保有状态。这意味着客户端和服务器可以进行实时响应&#xff0c;并且这种响应是双向的。WebSocket协议端口通常是80&am…

Spring源码-5.aop代理

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

Python实现WOA智能鲸鱼优化算法优化随机森林分类模型(RandomForestClassifier算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 鲸鱼优化算法 (whale optimization algorithm,WOA)是 2016 年由澳大利亚格里菲斯大学的Mirjalili 等提…

nginx学习(4)Nginx 配置高可用集群(主从配置)

Nginx 配置高可用集群 Nginx的高可用集群是指由两台或多台Nginx服务器组成的集群系统&#xff0c;通过负载均衡和故障转移等技术&#xff0c;实现高可用性和可伸缩性的目标。在这种集群系统中&#xff0c;每个Nginx服务器都配置为主节点&#xff08;master&#xff09;或从节点…