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

news/2024/5/18 15:13:53 标签: 计算机网络, 学习, udp, 网络, c++, 信息与通信, c语言

文章目录

  • 前言
  • 一. 网络通信本质
    • 端口号
    • 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.niftyadmin.cn/n/5197846.html

相关文章

外汇天眼:每周都能赢奖金?

最近&#xff0c;有不少外汇天眼的用户询问天眼客服&#xff0c;每周举办的外汇天眼模拟比赛是真的能拿到奖金吗&#xff1f;答案是&#xff1a;是的&#xff01;表现优秀者可瓜分350美金&#xff0c;如果周周参加&#xff0c;周周获得名次&#xff0c;那这个奖金也是能叠加获得…

[C语言 数据结构] 栈

1.什么是栈&#xff1f; 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则。 压…

【3D 图像分割】基于 Pytorch 的 3D 图像分割11(预测结果评估)

如果要对预测结果,与标记结果进行评估,判断下模型的预测能力怎么样,该怎么办呢?在论文里面经常采用dice coeff这个值作为横向比较评判的标准。 而在实际的使用中,你们都会采用什么其他的评估方法,评价指标呢?本文采用更加直观的召回率和平均案例的假阳性率作为评价模型…

一行代码搞定GPT4.0禁止升级开通

GPT4.0官方停止开通&#xff1f;看我一行代码就搞定他&#xff0c;又可以愉快的充值升级了 首先打开你的chatgpt的界面 正常点击这个升级是没有用的 这个界面中windows用户按键盘的F12打开开发者工具 mac电脑点菜单栏的开发–页面检查 然后输入这一串命令并回车 等待两…

2024年csdn最新最全的Postman接口测试: postman定义公共函数

postman定义公共函数 在postman中&#xff0c;如下面的代码&#xff1a; 1、返回元素是否与预期值一致 var assertEqual(name,actual,expected)>{tests[${name}&#xff1a;实际结果&#xff1a; ${actual} &#xff0c; 期望结果&#xff1a;${expected}]actualexpected…

深度学习(小土堆)

self代表当前类的实例&#xff0c;并用于访问实例的属性和方法,主要方便后面访问属性或者方法。 启动事件文件夹 进行上一步需要在pycharm中设置当打开Terminal终端时&#xff0c;自动进入虚拟环境 防止与别人冲突可以修改端口号 将图像的数据类型转为numpy trans…

Linux内核分析(十九)--内存管理之Linux中的内存管理机制汇总

目录 一、引言 二、虚拟内存 ------>2.1、linux中的分段与分页 ------>2.2、Linux的内存分配与管理 ------>2.3、vm_area_struct ------>2.4、两部分的页表分配 三、物理内存 ------>3.1、伙伴系统 ------>3.2、slab分配器 ------>3.3、内核态内…

Javaweb之Vue生命周期的详细解析

2.4 生命周期 vue的生命周期&#xff1a;指的是vue对象从创建到销毁的过程。vue的生命周期包含8个阶段&#xff1a;每触发一个生命周期事件&#xff0c;会自动执行一个生命周期方法&#xff0c;这些生命周期方法也被称为钩子方法。其完整的生命周期如下图所示&#xff1a; 状…