Linux网络编程之套接字 -- UDP

Posted 格式化、、

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux网络编程之套接字 -- UDP相关的知识,希望对你有一定的参考价值。

目录

🌈前言

这篇文章给大家带来套接字的学习!!!


🌸1、背景知识

🍡1.1、理解源IP地址和目的IP地址

源IP地址和目的IP地址

  • 在IP数据包头部中,有两个IP地址,分别叫做:源IP地址和目的IP地址

  • 源IP地址就是标定广域网(公网)中主机唯一性的地址(发送数据方的IP地址)

  • 目的IP地址就是“接收数据方”的IP地址(同样在广域网中具有唯一性)

我们只要有IP地址就能完成通信了吗?

  • 不是的,想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上

  • 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析

  • IP地址只能帮我们找到对方主机,我们还要考虑与对方相互交互数据

  • 比如:西游记中唐太宗要唐僧去西天拜见如来佛祖取得真经,唐僧不是去到西天就行了,他还要拜见如来佛祖取得真经,并且交付给唐太宗,光去到西天是没有用的!!!


🍢1.2、端口号

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

  • 端口号是一个2个字节16位的整数

  • 端口号用来标识一个主机上某个进程的唯一性

  • IP地址 + 端口号能够标识网络上的某一台主机的某一个唯一的进程(互联网中唯一的进程)

  • 一个端口号只能被一个进程占用(唯一性,一般指服务器),但是多个进程可以服务相同的端口号

  • 网络通信的本质其实也是进程间通信,都是二个进程在交互数据。本质上数据交互是用户与用户进行交互,用户的身份,通常是由程序(进程)体现的!

端口号和进程PID的关系

  • 端口号是标定网络上唯一的进程,进程PID是标定主机上唯一的进程

  • 实现时为了不将这二个概念强关联(强耦合),所以单独维护了一个端口号标定网络上的唯一进程,因为不是每个进程都是要进网络的!

源端口号和目的端口号

  • 传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号

  • 源端口描述的是:数据是谁发的,目的端口描述的是:要将数据发给谁!


🍧1.3、认识UDP协议

UDP(User Datagram Protocol 用户数据报协议)

特点:

  • 传输层协议

  • 无连接

  • 不可靠传输

  • 面向数据报


🍨1.4、认识TCP协议

TCP(Transmission Control Protocol 传输控制协议)

特点:

  • 传输层协议

  • 有连接

  • 可靠传输

  • 面向字节流


🍰1.5、网络字节序

前言

  • 大端:数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
  • 小端:数据的低位保存在内存的低地址中,而数据的高位, 保存在内存的高地址中
  • 内存中的多字节数据相对于内存地址有大端和小端之分

  • 磁盘文件中的多字节数据相对于文件中的地址也有大端小端之分

  • 网络数据流同样有大端小端之分

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出

  • 接收主机把从网络上接收的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存

  • 网络数据流的地址是这样规定的:先发出的数据是低地址,后发出的数据是高地址

  • TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节

  • 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定网络字节序发送/接收数据

  • 总之,如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可,发送到网络后,数据传输到对方时,还要将网络字节序转换成对方主机的存储模式

网络字节序和主机字节序的转换

#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是主机转网络字节序,参数为长整数,htons参数则是一个短整型

  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送

  • ntohl是网络转主机字节序,参数为长整型,ntohs参数是一个短整型

  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回

  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回


🌺2、套接字相关API

🍡2.1、socket常见API

下面是套接字的常见API,后面会详细解析

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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.2、struct sockaddr 结构体

上面几个API中都出现了struct sockaddr结构体指针,它是什么呢?有什么用?

  • socket API是一层抽象的网络编程接口,适用于各种底层网络协议,比如:IPv4IPv6,以及UNIX Domain Socket(本地进程间通信)

  • 不同的网络协议,它们之间的地址类型是不同的,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址

  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6

  • 我们只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容,最后确定是什么网络协议

  • 总之,sockaddr就是一个通用的类型,其他不同的类型的可以强转成它,然后通过前十六位地址来判断是什么网络通信协议,最后再强转回对应的结构体

  • 比如:我们要用IPv4进行通信,定义一个sockaddr_in,填充里面的信息,传参时,只要强转会sockaddr结构体就行了,底层会判断它是什么协议,然后重新强转回去

sockaddr 结构

sockaddr_in 结构体

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

  • sa_family:它就是我们要填充的地址类型(协议家族)

  • sin_port:它就是要我们填充的端口号

  • sin_addr:它就是需要我们填充的IP地址

in_addr 结构体

  • in_addr:用来表示一个IPv4的IP地址。其实就是一个32位的整数,直接赋值即可


🍀3、UDP网络编程

🍡3.1、API

使用UDP网络通信所用到的接口比TCP少,因为它是不用进行连接的

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.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);

// 读取网络数据到指定的缓冲区当中 -- 并且将对方的struct sockaddr信息填充到src_addr当中
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

// 将缓冲区的内容通过网络发送到对方主机(根据端口号和IP地址发送)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                        const struct sockaddr *dest_addr, socklen_t addrlen);

// 指定填充确定IP地址, 转化字符串风格IP("xx.zz.yy.sss"),并且自动进行主机字节序转换网络字节序
in_addr_t inet_addr(const char *cp));


int socket(int domain, int type, int protocol);

  • 作用:它是用来创建套接字文件的,创建成功返回一个文件描述符,失败返回-1,并且设置errno

  • domain:它需要我们填充协议家族(地址类型)来指定网络协议

  • type:它需要我们填充通信类型,UDP协议是面向数据报的,我们填充SOCK_DGRAM即可,如果是TCP协议,那么填充SOCK_STREAM(其他详细查看man手册)

  • protocol:套接口所用的协议,一般为0,不指定


int bind(int socket, const struct sockaddr *address, socklen_t address_len);

  • 作用:绑定网络信息(地址类型、端口号和IP地址)到内核中,成功返回0,错误返回-1,并且设置errno

  • address:sockaddr_in或sockaddr_un的地址,并且需要强转为sockaddr*

  • len:sockaddr_in或sockaddr_un结构体所占内存空间的大小


🍢3.2、简单的UDP网络程序

实现小写英文转大写服务器 – 使用UDP协议

#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

class UdpServer

public:
    UdpServer(uint16_t port, string ip = "")
        : port_(port), ip_(ip), sockfd_(-1)
    
        assert(port > 1024);
    

    ~UdpServer()
    
        if (sockfd_ > 2)
        
            close(sockfd_);
        
    

public:
    void Init()
    
        // 1、创建socket套接字 -- 创建通信端点并返回文件描述符fd
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        
            cout << "socket error: " << strerror(errno) << endl;
            exit(EXIT_FAILURE);
        
        // 填充网络基本信息到struct sockaddr_in结构体中 -- 地址类型、port、ip address
        struct sockaddr_in Server;
        socklen_t len = sizeof(Server);
        memset(&Server, 0, len);

        // 协议家族 -> 填套接字的通信域 -> AF_INET
        Server.sin_family = AF_INET;

        // 端口号 -> 要发给对方(服务器) -> 一定会到网络中 -> 主机大小端序列转成网络序列(htons转换)
        Server.sin_port = htons(port_);

        // 服务器IP地址,格式: "xx.yy.zz.vvv"(字符串风格"点分十进制") -> 去掉点就是4字节IP -> uint32_t IP
        // inet_addr:指定填充确定IP地址, 转化字符串风格IP,并且自动进行主机序列转网络序列
        // 不关心绑定到哪一个IP地址,表示不确定地址,或“所有地址”、“任意地址” -- 即:OS可以随意分配本机ip地址(本机可能存在多个网卡)
        Server.sin_addr.s_addr = (ip_.empty() ? htonl(INADDR_ANY) : (inet_addr(ip_.c_str())));

        // 2、绑定网络信息到内核中
        if (bind(sockfd_, ((const struct sockaddr *)&Server), len) < 0)
        
            cout << "bind error: " << strerror(errno) << endl;
            exit(EXIT_FAILURE);
        
    

    void Start()
    
#define BUFFER 1024
        char buffer[BUFFER];
        char outbuffer[BUFFER];
        struct sockaddr Client;
        socklen_t len = sizeof(Client);
        while (true)
        
            memset(buffer, 0, BUFFER);
            // 接收客户端发送的消息
            ssize_t index = recvfrom(sockfd_, (void *)buffer, BUFFER - 1, 0, (struct sockaddr *)&Client, &len);
            if (index < 0)
            
                cout << "recvfrom error: " << strerror(errno) << endl;
                exit(EXIT_FAILURE);
            
            else
            
                buffer[index] = '\\0';
                cout << "Client echo# " << buffer << endl;
            

            // 小写英文转大写 -- 服务
            memset(outbuffer, 0, BUFFER);
            for (int i = 0; i < strlen(buffer); ++i)
            
                if (buffer[i] >= 'a' && buffer[i] <= 'z')
                
                    outbuffer[i] = buffer[i] - 32;
                
                else
                
                    outbuffer[i] = buffer[i];
                
            
            outbuffer[strlen(buffer)] = '\\0';

            // 答复Client -- 发送消息给客户端
            ssize_t st = sendto(sockfd_, outbuffer, BUFFER - 1, 0, (const struct sockaddr *)&Client, len);
            if (st < 0)
            
                cout << "sendto error: " << strerror(errno) << endl;
                exit(EXIT_FAILURE);
            
        
    

private:
    uint16_t port_;
    string ip_;
    int sockfd_;
;

int main(int argc, char *argv[])

    if (!(argc >= 2 && argc <= 3))
    
        cout << "Usage: ./udpserver port [ip]" << endl;
        exit(3);
    
    uint16_t port = atoi(argv[1]);
    string ip = "";
    if (argc == 3)
    
        ip = argv[2];
    
    UdpServer us(port, ip);
    us.Init();
    us.Start();
    return 0;

该程序在填充网络信息中的IP地址时用了INADDR_ANY,是为什么呢?

百度百科 – INADDR_ANY

  • INADDR_ANY就是指定地址为0.0.0.0的IP地址,这个地址事实上表示不确定地址,或所有地址、任意地址

  • 多网卡的状况下,这个就表示全部网卡ip地址的意思

  • 如果使用云服务器进行测试,必须使用它,因为云服务器不是给用户真正的提供公网IP,而是内网IP

  • 服务器在绑定ip地址时,如果为INADDR_ANY,那么服务器端的IP地址能够随意配置,这样使得该服务器端程序能够运行在任意计算机上,可以使任意计算机做为服务器,便于程序移植

摘取至 – INADDR_ANY的确切含义


简单客户端代码

#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cerrno>
#include <string>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

class UdpClient

public:
    UdpClient(uint16_t port, string ip = "")
        : port_(port), ip_(ip), sockfd_(-1)
    
    

    ~UdpClient()
    
        if (sockfd_ > 2)
        
            close(sockfd_);
        
    

public:
    void Init()
    
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        
            cout << "socket error: " << strerror(errno) << endl;
            exit(EXIT_FAILURE);
        

        // client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
        // 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
        // 如果我非要自己bind呢?可以!严重不推荐!
        // 所有的客户端软件 <-> 服务器 通信的时候,必须得有 client[ip:port] <-> server[ip:port]
        // 为什么呢??client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法启动了
        // 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!
    

    void Start()
    
#define BUFFER 1024
        string outbuffer;
        char inbuffer[BUFFER];
        while (true)
        
            memset(inbuffer, 0, BUFFER);
            // 发送请求给服务器 -- 需要对方网络套接字信息
            struct sockaddr_in si;
            socklen_t len = sizeof(si);
            si.sin_family = AF_INET;
            si.sin_port = htons(port_);
            si.sin_addr.s_addr = inet_addr(ip_.c_str());

            cout << "Client: Please enter the sending information# ";
            fflush(stdout);
            getline(cin, outbuffer);

            ssize_t st = sendto(sockfd_, outbuffer.c_str(), BUFFER - 1, 0, (const struct sockaddr *)&si, len);
            if (st < 0)
            
                cout << "sendto error: " << strerror(errno) << endl;
                exit(EXIT_FAILURE);
            

            //---------------------------------------------------------------------------------------------

            // 获取Server回应消息
            int rf = recvfrom(sockfd_, inbuffer, BUFFER - 1, 0, nullptr, 0);
            if (rf >= 0)
            
                inbuffer[rf] = '\\0';
                cout << "Server echo# " << inbuffer << endl;
            
            else
            
                cout << "recvfrom error: " << strerror(errno) << endl;
                exit(EXIT_FAILURE);
            
        
    

private:
    uint16_t port_;
    string ip_;
    int sockfd_;
;

// ./ 端口号 ip地址
int main(int argc, char *argv[])

    if (argc != 3)
    
        cout << "Usage: ./udpserver port [ip]" << endl;
        exitLinux之网络管理(8)网络监控工具

linux中有很多查看网络、进程通信状态的查看工具,而网络之间建立通信是通过soket套接字进行的,所谓套接字,就是相当于插座,而一台主机上会产生多种套接字,就是相当于插头向插座插入的过程,也就是主机中进程通信。而在网络中建立通信,就是双方各种插入对方。当然这些只是粗率的比喻,实际上,soket是用来将tcp/udp等协议发送的数据包进行封装:也就是相当于食品包装,把数据包加上port端口号、进程号等然后发送给对方,然后对方根据数据的包装袋来放回特定需要的数据。这样网络之间各种进程直接通信之间互不干扰。就像送快递和收快递的一样,谁的快递,谁来收,路线就是到你家。

 

linux中提供的工具:

netstat  命令

man 文档帮助的说明:

netstat - Print network connections, routing tables, interface statistics, masquerade connections, and multicast memberships

打印网络连接、路由表、接口统计、伪装连接、多播连接等信息。

选项介绍:

netstat  [option]...

          -r   : 显示路由表

  -t   : 已经建立的tcp协议相关

  -u  :  已经建立的udp协议相关     

          -w  raw  sokect (未包装处理的)

          -l   : 处于监听状态

  -a  : 所有状态

  -n  : 及数字显示IP和端口

  -e  : 扩展方式详细更多状态信息

  -p  : 显示相关进程及PID

常用组合使用:

 查明网络连接:

netstat  -tan    显示所有tcp相关的连接状态

netstat  -uan    显示所有udp相关的连接状态

netstat  -tnl     显示所有监听状态的tcp连接

netstat  -unl     显示所有监听状态的udp连接

 显示路由表:

netstat  -rn      不做反向解析显示内核路由表

 显示接口统计数据:

 netstat  -i       #显示所有网络接口信息状态

 netstat  -I=FACE_NAME  #显示指定设备信息状态

        例子:netstat  -I=eth0  #显示eth0设备数据连接状态信息

 

 

 

ss命令

ss - another utility to investigate sockets

这是另一种显示套接字信息的工具,netstat命令通过遍历proc 来获取socket信息,以及是老旧的命令了,ss使用netlink与内核tcp_diga模块通信获取socket信息,更加准确。

格式及选项介绍:

ss[option]  [FILTER]

option:

   -t  tcp 协议相关

   -u  udp协议相关

   -w : 裸套接字符相关

   -x  unix sock系统内核相关

   -l  listen监听状态的连接

   -a  : 所有连接

   -n  : 数字格式

   -p  : 相关的程序及PID

   -e  : 扩展的信息

   -m  : 内存用量

   -o  : 计时器信息

 

FILTER :  =  [ state TCP-STATE ]  [ EXPRESSION ]

TCP的标准状态:

       LISTEN :监听

       ESTABLISHED:已建立的连接

       FIN_WAIT_1

       FIN_WAIT_2

       SYN_SENT

   SYN_RECV

   CLOSED关闭

EXPRESSION

dport =#目标端口

sport =              #源端口

例子:(  dport  =  :ssh  or  sport  = :ssh  )

     

 

常用组合:

ss  -tan#所有tcp连接状态

ss  -tanl     #所有监听的tcp状态

ss  -tanlp    #所有监听tcp的并显示进程PID

ss  -uan     #所有udp状态

 

额外用法:

ssh  -A  QUERY#查看对应类型或协议的状态,QUERY参数为要指定的类型

QUERY

all,  inet,  tcp,  udp,  raw,  unix,  packet,  netlink,  unix_dgram,  unix_stream,

unix_seqpacket,  packet_raw,  packet_dgram.

 

TCP-STATE中可用的标示符参数:

  所有TCP标准状态参数

   established,  syn-sent,   syn-recv,  fin-wait-1,  fin-wait-2,  time-wait,  closed,

   close-wait,   last-ack,  listen,  closing.

  复杂状态参数:

      all              所有的状态

      connection       所有连接的(除了监听和关闭的)状态

      synchronized      所有同步的,除了syn-sent所有连接的状态

      bucket           所有维护scokectsyn-recv的状态

      big              所有和bucket相反的状态

 

案例展示:

显示本地打开的所有端口  ss  -l

[[email protected] www]# ss -l | tail -n 10

技术分享

解析:udp是没有状态的,所以打开的连接会显示两个协议所有处于监听状态连接

显示每个进程具体打开的 socket    

[[email protected] www]# ss -pl | tail -n 10

显示所有tcp socket             

[[email protected] www]# ss -t -a

技术分享 

显示所有UDP Socekt

[[email protected] www]# ss -u -a

技术分享

显示所有已经建立的SMTP连接

[[email protected] www]# ss -o state established  ‘( dport = :smtp or sport = :smtp )‘

显示所有已经建立的HTTP连接

[[email protected] www]# ss -o state established  ‘( dport = :http or sport = :http )‘

找出所有连接X服务器的进程

[[email protected] www]# ss -x src /tmp/.X11-unix/*

列出当前各协议的各套接字个数

[[email protected] www]# ss -s


本文出自 “孟天霸-IT的垃圾回收站” 博客,请务必保留此出处http://mengzhaofu.blog.51cto.com/10085198/1851651

以上是关于Linux网络编程之套接字 -- UDP的主要内容,如果未能解决你的问题,请参考以下文章

网络骇客入门之UDP编程

Linux之socket套接字编程20160704

Linux之网络管理网络监控工具

Java网络编程之UDP和TCP套接字

网络编程之基于UDP协议套接字

Linux篇第十八篇——网络套接字编程(预备知识+UDP套接字的编写)