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

文章目录

  • 前言
  • 一. 网络通信本质
    • 端口号
    • 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编程的内容到此就结束了,感谢看到此处。
欢迎大家纠错和补充
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

安卓:Android Studio4.0~2023中正确的打开Android Device Monitor

Android Studio4.0~2023 中如何正确的打开Android Device Monitor(亲测有效) 前些天买了新电脑&#xff0c;安装了新版本的Android Studio4.0想试一试&#xff0c;结果就出现了一些问题。 问题引出&#xff1a; Android Device Monitor在工具栏中找不到&#xff0c;后来上网查…

基于nbiot的矿车追踪定位系统(论文+源码)

1.系统设计 鉴于智能物联网的大趋势&#xff0c;本次基于窄带物联网的矿车追踪定位系统应具备以下功能&#xff1a; &#xff08;1&#xff09;实现实时定位&#xff0c;真正实现矿车随时随地定位; &#xff08;2&#xff09;定位精度高&#xff0c;采用该系统可以实现矿车在…

dump备份命令

dump备份文件系统&#xff0c;或者目录 文件系统有等级划分&#xff0c;0为全部备份&#xff0c;1.针对上一次有变动的文件进行备份&#xff0c;以此类崔 目录备份&#xff1a;只有一个等级0&#xff0c; 针对文件系统类型有要求ext2&#xff0c;ext3&#xff0c;如果是其他…

数据分析基础之《jupyter notebook工具》

一、安装库 1、linux库 yum install python3-devel 2、python库 pip3 install -U matplotlib pip3 install -U numpy pip3 install -U pandas pip3 install -U TA-Lib pip3 install -U tables pip3 install -U notebook 3、如果TA-Lib安装不上&#xff0c;先手动安装依赖库 …

Servlet---上传文件

文章目录 上传文件的方法上传文件的示例前端代码示例后端代码示例 上传文件的方法 上传文件的示例 前端代码示例 <body><form action"upload" method"post" enctype"multipart/form-data"><input type"file" name&qu…

transformer学习资料

一、NLP 自然语言处理 NLP 是机器学习在语言学领域的研究&#xff0c;专注于理解与人类语言相关的一切。NLP 的目标不仅是要理解每个单独的单词含义&#xff0c;而且也要理解这些单词与之相关联的上下文之间的意思。 常见的NLP 任务列表&#xff1a; 对整句的分类&#xff1…

【C/PTA】函数专项练习(一)

本文结合PTA专项练习带领读者掌握函数&#xff0c;刷题为主注释为辅&#xff0c;在代码中理解思路&#xff0c;其它不做过多叙述。 目录 6-1 输出星期名6-2 三整数最大值6-3 数据排序6-4 多项式求值 6-1 输出星期名 请编写函数&#xff0c;根据星期数输出对应的星期名。 函数原…

xss漏洞挖掘

xss漏洞挖掘 以xss-challenge第二关为例 输入123 查看网页源代码 发现value值原样返回 手动挖掘 此处发现尖括号和双引号闭合完整&#xff0c;因此可以直接使用<script>alert(/xss/)</script>测试 发现提交过后标签内容被双引号闭合进去了 因此此处需要将标签…

CAS还能这样理解??

1.CAS全称 全称&#xff1a;compare and swap&#xff0c;比较并交换。 虽然翻译过来是[比较并交换]&#xff0c;但它是一个原子性的操作&#xff0c;对应到CPU指令为cmpxchg。 2.通俗理解CAS CAS 有三个操作数:当前值A、内存值V、要修改的新值B。假设 当前值A 跟 内存值V 相…

高并发分布式架构的演进之路

目录 单体架构 应用数据分离架构 应用数据集群架构 读写分离 / 主从分离架构 引⼊缓存⸺冷热分离架构 垂直分库 业务拆分-微服务 单体架构 一个系统初期&#xff0c;我们需要利⽤我们精⼲的技术团队&#xff0c;快速将业务系统投⼊市场进⾏检验&#xff0c;并且可以迅速…

前端uniapp生成海报绘制canvas画布并且保存到相册【实战/带源码/最新】

目录 插件市场效果如下图注意使用my-share.vue插件文件如下图片hch-posterutilsindex.js draw-demo.vuehch-poster.vue 最后 插件市场 插件市场 效果如下图 注意 主要&#xff1a;使用my-share.vue和绘制canvas的hch-poster.vue这两个使用 使用my-share.vue <template&…

通过css设置元素隐藏和显示

背景&#xff1a;鼠标悬浮时显示删除&#xff0c;放开后显示组件名 解决&#xff1a;通过display:none和display:block显示和隐藏元素&#xff1b; 使用 div p选择器选择当前div的下一个紧跟的p元素 <div v-if"!preview" class"name">{{propertyDa…

java每日一记 —— 浅浅的说下在Lombok中遇到的一个坑

今天说下Lombok 1.先谈谈Lombok吧2.来聊聊为什么有些公司不喜欢用Lombok3.聊点废话 本篇代码在jdk11中测试通过&#xff0c;Lombok版本使用的是1.18.20 昨天写注解时提到了一下Lombok&#xff0c;然而最近写毕业论文又因为Lombok踩过几次坑&#xff0c;所以今天来聊聊 1.先谈谈…

windows10关闭自动更新

windows10关闭自动更新 下载策略组改配置组配置 下载策略组 我自己的电脑里没有这个文件gpedit.msc所以要下载 pushd "%~dp0"dir /b C:\Windows\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >List.txtdir /b C:\Windows…

[uni-app]记录APP端跳转页面自动滚动到底部的bug

文章目录 bug描述原因分析: 处理方案 bug描述 1.点击的A页面, 跳转到了B页面, 第一次页面正常显示 2.从B页面返回A页面 3.A页面不进行任何操作,再次点击A页面进入B页面 4.B页面自动滚动到底部. 原因 看一段A页面代码 let that thisthis.defaultScrollTop uni.getStorageSy…

OpenAI内斗剧情反转!微软力保ChatGPT之父回归?

美东时间11月17日下午&#xff0c;全球最热门的聊天机器人ChatGPT开发商OpenAI宣布了一项重磅管理层调整&#xff0c;Sam Altman将辞去CEO一职&#xff0c;并离开公司董事会。 Altman被踢出的消息除了让业界担心会影响该公司未来发展外&#xff0c;OpenAI另一位共同创办人暨总裁…

Linux学习第43天:Linux 多点电容触摸屏实验:难忘记第一次牵你手的温存

Linux版本号4.1.15 芯片I.MX6ULL 大叔学Linux 品人间百味 思文短情长 人都是性情中人&#xff0c;如果把学习当做自己的女朋友&#xff0c;对她细致入微、掏心掏肺、有耐心有恒心&#xff0c;终会修成正果。 而我们本节需要学习的电…

idea导入javaweb变成灰色

解决办法&#xff1a; 如果这时候src是蓝色&#xff0c;其余都是灰色文件夹&#xff0c;这时候要先把src文件夹变成灰色&#xff0c;否则之后会报错 src文件变成灰色方法&#xff0c;右键src,选择make direcory as 选择unmark 如果src不是蓝色&#xff0c;就是灰色&#xff0…

《C++PrimePlus》第8章 函数探幽

8.1 内联函数 使用内联函数 #include <iostream> using namespace std;inline double square(double x) { return x * x; }int main(){double a;a square(5.0);cout << "a " << a << endl;return 0; } 8.2 引用变量 将引用用作函数参数&…