Linux网络--UDP套接字


文章目录

  • 预备知识
  • socket套接字
  • UDP网络编程

一、预备知识

        1.源IP地址和目的IP地址

IP地址标识计算机在网络中的唯一性。

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
源IP地址 网络通信的发起者。
目的IP地址 网络通信的接受者。

        2.端口号

端口号:可以用来标识进程的唯一性。

网络通信的目的是让两台计算机上的两个进程在进行通信。

因为两台计算机之间进行数据的发送时,发送到计算机中的是进行网络通信的手段并不是目的,而真正进行网络通信的是两台计算机上面的某个应用之间的通信。

端口号(port)是传输层协议的内容.
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
(全网唯一进程=IP地址+端口号)
一个端口号只能被一个进程占用。

 3.认识TCP/UDP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)和UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

 TCP协议

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

UDP协议

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

4.网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

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

#include <arpa/inet.h>

//主机序列转网络序列
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_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地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

二、socket套接字

1.socket 常见API

#include <sys/types.h>
#include <sys/socket.h>

// 创建 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);

2.sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.
套接字的种类常见的可以分为三种:
  • 域间套接字:存在于本地间通信。
  • 网络套接字:跨主机进行网络通信同时也支持本地间通信。
  • 原始套接字:可以跨越传输层(TCP/UDP)访问底层数据。

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

sockaddr_in结构

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

 sockaddr结构in_addr结构

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

将sockaddr看成是基类,将sockaddr_in和sockaddr_un看成是派生类。

地址转换函数
基于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。

inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码

 

运行结果如下: 

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

三、UDP网络编程

1.服务端实现

class UdpServer
{
public:
    

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;
    int _sock;
};

 首先构造服务器时需要传入端口号和ip地址

一般来说端口号是指定的,一旦指定之后不能更改这里为8080,ip地址为服务器的地址。

UdpServer(const uint16_t &port,const std::string& ip=""):_port(port),_ip(ip),_sock(-1)
    {}

 通过调用ipconfig指令我们可以看到此时服务器的ip信息。

 为了能够使服务器接受所有的网络请求,将服务器的ip地址绑定为INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。

 初始化服务器

    void initServer()
    {
        //1.创建套接字
        _sock=socket(AF_INET,SOCK_DGRAM,0);

        if(_sock<0){
            std::cerr<<"socket err: "<<errno<<" : "<<strerror(errno)<<std::endl;
            exit(2);
        }

        //bind绑定
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
        {
            std::cerr<<"bind error: "<<errno<<" : "<<strerror(errno)<<std::endl;
            exit(2);
        }
    }

启动服务器

 void Start()
    {
        char buffer[SIZE];
        while(true){
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            socklen_t len=sizeof(peer);

            ssize_t s=recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&(peer),&len);//阻塞读取
            if(s>0){
                buffer[s]=0;
                
                std::string cli_ip=inet_ntoa(peer.sin_addr);
                uint16_t cli_port=ntohs(peer.sin_port);

                std::cout<<cli_ip<<"["<<cli_port<<"]#"<<buffer<<std::endl;
            }

            sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
        }
    }

利用recvfrom读取网络中的数据。

 udp_server.cc

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

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

int main(int argc,char*argv[])
{
    if(argc!=2){//命令行参数
        usage(argv[0]);
        exit(1);
    }

    uint16_t port=atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));//利用智能指针创建服务器对象
    svr->initServer();
    svr->Start();
    return 0;
}
此时启动服务器,利用netstat -nuap来查看进程

 2.客户端实现

class UdpClient
{
private:
        uint16_t _port;
        std::string _ip;
        int sock;
};

客户端可以有无数个但是服务端只有一个,所以对于客户端来说只需要知道服务端的ip和端口号。

初始客户端

void initClient()
    {
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);

        if (_sock < 0)
        {
            std::cerr << "socket err: " << errno << " : " << strerror(errno) << std::endl;
            exit(2);
        }

        std::cout << "socket success: " << std::endl;
        // 客户端不需要显示的绑定OS会随机分配
    }

对于客户端的初始化不需要创建绑定ip和端口号,因为没有用户会对客户端进行网络请求,所以OS会随机分配
客户端启动

 void run()
    {
        //填充服务器的ip
        struct sockaddr_in server;
        memset(&server, 0, sizeof server);
        server.sin_family = AF_INET;
        server.sin_port = htons(_port);
        server.sin_addr.s_addr = inet_addr(_ip.c_str());

        char buffer[1024];
        std::string message;
        while (true)
        {
            std::cout << "请输入你的信息# ";
            std::getline(std::cin, message);

            //将数据发送到网络中
            sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);

            struct sockaddr_in temp;
            socklen_t len = sizeof(temp);
            ssize_t s = recvfrom(_sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server echo# " << buffer << std::endl;
            }
        }
    }

利用sendto向网络中发送数据

 udp_client.cc

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

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

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

    std::string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    std::unique_ptr<UdpClient> ucli(new UdpClient(serverport,serverip));
    ucli->initClient();
    ucli->run();
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
UDP是一种无连接的传输协议,它不保证数据包的可靠性和顺序性。而ICMP是一种网络协议,它主要用于网络故障排查、错误报告和路由器之间的通信。在使用UDP协议时,我们可以使用原始套接字来发送和接收ICMP消息。 在Linux系统中,我们可以使用socket()函数来创建一个原始套接字,指定协议族为AF_INET(IPv4)或AF_INET6(IPv6),协议类型为SOCK_RAW,协议号为IPPROTO_ICMP。然后我们可以使用sendto()函数来发送UDP数据包,并使用recvfrom()函数来接收ICMP消息。 以下是一个简单的示例代码: ``` #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #define BUFSIZE 1024 int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in addr; char buffer[BUFSIZE]; // 创建原始套接字 sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd < 0) { perror("socket"); exit(EXIT_FAILURE); } // 设置地址结构体 memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 发送UDP数据包 strcpy(buffer, "Hello, UDP!"); if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("sendto"); exit(EXIT_FAILURE); } // 接收ICMP消息 if (recvfrom(sockfd, buffer, BUFSIZE, 0, NULL, NULL) < 0) { perror("recvfrom"); exit(EXIT_FAILURE); } struct iphdr *iph = (struct iphdr *)buffer; struct icmphdr *icmph = (struct icmphdr *)(buffer + (iph->ihl * 4)); printf("Received ICMP packet from %s\n", inet_ntoa(addr.sin_addr)); printf("Type: %d\n", icmph->type); printf("Code: %d\n", icmph->code); close(sockfd); return 0; } ``` 在上面的示例代码中,我们创建了一个原始套接字,并指定协议为ICMP。然后我们使用sendto()函数发送了一个UDP数据包,目标地址为127.0.0.1。接着,我们使用recvfrom()函数接收ICMP消息,并解析出其中的类型和代码。 需要注意的是,使用原始套接字需要具有管理员权限。此外,使用原始套接字需要特殊的权限,因此需要小心使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

‘(尐儍苽-℡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值