目录

Linux跬步积累-网络编程套接字

【Linux跬步积累】—— 网络编程套接字

🌏博客主页:

🚩当前专栏:

💌其他专栏:

🔴

🟡

🟢

🌈座右铭: 广积粮,缓称王!


一、预备知识

理解源IP地址和目的IP地址

每台主机都有自己的IP地址,如果一个台主机想要对另外一台主机发送数据,那么该主机就需要知道对端主机的IP地址,也就是目的IP地址。但是仅仅知道目的IP地址是不够的,对端主机收到数据之后,需要对其作出响应,需要知道是谁给他发送的数据,因此我们还需要知道发送方的IP地址,也就是源IP地址。因此一个传输的数据当中应该包含源IP地址和目的IP地址,源IP地址表示是谁发送的数据,目的IP地址表示发送给谁。

在数据传输之前,会先自顶向下贯穿网络协议栈完成数据的封装,其中在网络层封装的报头中就包含了源IP地址和目的IP地址。


理解源MAC地址和目的MAC地址

大部分数据传输都是跨局域网的,数据传输过程中会经过很多路由器,最终才能到达对端主机。

源MAC地址和目的MAC地址是在数据链路层被封装在报头中的,MAC地址只在当前局域网内有效,因为每经过一个路由器,就会将链路层的报头去掉,然后重新封装一个报头,此时的源MAC地址和目的MAC地址就发生了变化。

https://i-blog.csdnimg.cn/direct/63adac4077a54abeb3b01fe3052f59e0.png

上图中主机1向主机2发送数据过程中,源MAC地址和目的MAC地址的变化过程。

时间轴源MAC地址目的MAC地址
刚开始主机1的MAC地址路由器A的MAC地址
经过路由器A后路由器A的MAC地址路由器B的MAC地址
经过路由器B后路由器B的MAC地址路由器C的MAC地址
经过路由器C后路由器C的MAC地址主机2的MAC地址

小结

数据传输的过程中是有两套地址的:

  • 源IP地址和目的IP地址,用来确定发送方的主机以及接受方的主机,这两个地址一般是不会变的;
  • 源MAC地址和目的MAC地址,这两个地址是一直变化的,因为经过路由器之后会进行解包和重新封装;

端口号

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

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够表示网络上某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;
  • 数据在传输层进行封装时,就会添加上对应的源端口号和目的端口号。

IP地址可以标识唯一的一台主机,端口号(port)可以表示一台主机上唯一的一个进程,因此IP + port = 网络上某一台主机的某一个进程。


理解源端口号和目的端口号

两台主机进行通信的目的不仅仅是将数据发送给对端主机,而是为了访问对端主机上的某个服务。

当我们知道了IP地址和MAC地址,就可以将数据发送给对端主机,但是如何再将数据发送给对端主机上的某个进程呢?那么我们还需要知道端口号,当我们知道源端口号和目的端口号之后,就可以知道是哪台主机的哪个进程发送的数据,以及发给哪台主机的哪个进程。这样就可以实现跨网络的进程间通信了。


理解“端口号”和“进程ID”

进程ID(PID)是用来标识系统内所有进程的唯一性,它属于系统级别的概念;而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性,它是属于网络级别的概念。这样设计就是为了避免将系统功能和网络功能直接进行强耦合,避免一个模块改变后影响另一个模块。

比如我们每个人都有自己的身份证号码,身份证号码就可以标识我们的唯一性了,那么为什么在学校还要有学号,在公司还要有工号呢?

因为不是每个人都是学生,也不是每个人都在同一家公司,所以需要在不同场景下在设定对应的编号来表示唯一性。

所以每一个进程都有pid,但不是每个进程都有port。


socket通信的本质

  1. 我们上网无非就两种动作:

    • 把远端数据拉取到本地(I)
    • 把本地数据发送到远端(O)
  2. 大部分的网络通信行为都是用户触发的,在计算机中,谁表示用户呢?进程!!!

  3. 我们将数据发送到目标主机,这不是目的,而是手段。真正的目的是把数据交给这个主机上的某个服务(进程)。

  4. ​ 服务端与客户端进行通信

    ==>服务端进程和客户端进程进行通信

    ==>服务端进程 = 服务端IP + 服务端port

    ​ 客户端进程 = 客户端IP + 客户端port

    ==>服务端是互联网中唯一的一个进程

    ​ 客户端是互联网中唯一的一个进程

    ==>唯一的找到彼此

  5. 所以网络通信的本质,其实就是进程间通信!两个进程间需要通信,前提条件就是要找到公共资源,这个公共资源就是网络!

我们之后要写的服务器就是应用层的代码,然后调用传输层提供的系统调用,即操作系统提供的网络系统调用。


认识TCP协议和UDP协议

TCP协议

TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP协议是面向连接的,如果两台主机想要进行通信,就必须先建立连接,当连接建立之后才能进行数据传输。TCP协议是保证可靠性的协议,当传输过程中如果发生了丢包、乱序等情况,TCP协议都有对应的解决办法。

UDP协议

UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无序建立连接的、不可靠的、面向数据报的传输层通信协议。

UDP协议进行通信时无需建立连接,两台主机如果想要进行数据传输,直接将数据发送给对端主机即可,但这意味着UDP协议是不可靠的,如果传输过程中发生了丢包、乱序等情况,UDP协议是不知道的,无法解决。

为什么UDP协议不可靠还会一直存在?

TCP协议可靠,那么也就意味着底层的实现更加复杂,接口会更加多一点,使用成本比较高;而UDP虽然不可靠,但一定更加简单,使用起来更加方便。

所以采用TCP协议还是UDP协议取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采取TCP;如果应用场景允许出现少量的丢包,那么优先使用UDP,因为足够简单。


网络字节序

计算机在存储数据时,是有大小端的概念的:

  • 大端存储:数据的 低字节 内容存储在 高地址 处,数据的 高字节 内容存储在 低地址 处。
  • 小端存储:数据的 低字节 内存存储在 低地址 处,数据的 高字节 内容存储在 高地址 处。

为了保证数据能够正常被获取,网络字节序列统一 以大端为主

为了使网络程序具有可移植性,使用样的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编程接口

socket常见API

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

sockaddr结构

socket编程,是有不同种类的,有的是专门用来进行本地通信的(Unix socket),有的是用来专门进行跨网络通信的(inet socket),有的是用来进行网络管理的(raw socket)。为了能让用户减少使用的成本,我们就需要把所有的接口进行统一,于是就使用C语言进行编写,要统一这个接口还需要统一数据类型,于是就有了 struct sockaddr

https://i-blog.csdnimg.cn/direct/4942335deebc4d179bb95a8404db37e2.png

通过 struct sockaddr 来管理 struct sockaddr_instruct sockaddr_un , struct sockaddr_in 结构体是用来跨网络通信的,而 struct sockaddr_un 结构体是用来本地通信的。

为了让网络通信和本地通信能够使用同一套函数接口,于是就出现了 sockaddr 结构体,这三个结构体都不相同,但是这三个结构体的头部字段都是16位地址类型,当我们想进行网络通信时,就传递 AF_INET ,想进行本地通信,就传递 AF_UNIX 。这样就可以通过 sockaddr 来管理网络通信和本地通信了。

三、简单的UDP网络程序

socket函数

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

功能:

  • 创建套接字的通信。

参数:

  • domain:选择协议的类型,如果是本地通信就是 AF_UNIX ;网络通信就是 AF_INET
  • type:创建套接字时的服务类型。如果是UDP,我们采用 SOCK_DGRAM ,叫做用户数据报服务;如果是TCP,我们采用 SOCK_STREAM ,叫做流式套接字,提供的是流式服务。
  • protocol:套接字的协议类别,可以指明为TCP或者UDP,一般设置为0即可。

返回值:

  • 创建成功返回一个文件描述符,所以当我们使用socket创建套接字成功时,就相当于我们打开了网卡(网络文件)。
  • 创建失败返回-1,同时错误码会被设置。

使用示例:

int sockfd = socket(AF_INET,SOCK_DGRAM,0);

bind函数

 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

功能:

  • 将文件和网络关联起来。

参数:

  • sockfd:文件描述符。
  • addr:网络相关的属性信息,类似于一个结构体,里面包含了:协议家族、IP地址、端口号。
  • addrlen:结构体addr的长度

返回值:

  • 成功返回0。
  • 失败返回-1,设置错误码。

使用示例:

int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));

bzero函数

#include <strings.h>

void bzero(void *s, size_t n);

功能:

  • 用于将一块内存空间清零,通常用于初始化内存。

参数:

  • s:指向要清零的内存区域的指针。
  • n:要清零的字节数。

返回值:空。

使用示例:

struct sockaddr_in local;
bzero(&local,sizeof(local));

inet_addr函数

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

功能:

  • 将字符串风格的点分十进制的IP地址转成4字节IP
  • 将主机序列转成网络序列

参数:

  • 目标字符串,也就是字符串类型的IP地址

返回值:

  • in_addr_t类型

使用示例:

local.sin_addr.s_addr = inet_addr(_ip.c_str());

代码初步演示

UdpServer.hpp

#pragma once
#include<iostream>
#include<cstdlib>
#include<strings.h>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
//echo server -> client -> server
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR,
};

const static int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(const std::string &ip,uint16_t port):_sockfd(defaultfd),_ip(ip),_port(port),_isrunning(false)
    {}
    void InitServer()
    {
        //1. 创建udp socket 套接字 —— 必须要做的 —— 可以理解成将网卡打开了
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            std::cerr<<"socket failed!\n";
            exit(SOCKET_ERROR);
        }
        std::cout<<"socket create success,sockfd:"<<_sockfd<<std::endl;
        
        //2.0 填充sockaddr_in结构
        struct sockaddr_in local;//struct sockaddr_in系统提供的数据类型。local是变量,在用户栈上开辟空间。int a = 100;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//port要经过网络传输给对面,也就是说需要先到网络,_port:主机序列->主机序列需要转换成网络序列
        //a. 字符串类型的点分十进制IP地址转成4字节IP
        //b. 主机序列,转成网络序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//"192.168.3.1"->字符串风格的点分十进制的IP地址-> 4字节IP
        
        //2.1 bind sockfd和网络信息(IP(?) + Port)
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            std::cerr<<"bind failed!\n";
            exit(BIND_ERROR);
        }
        std::cout<<"socket bind success\n";
    }
    void Start()
    {
        //一直运行,知道管理者不想运行了,服务器都是死循环
        _isrunning = true;
        while(true)
        {
            sleep(1);
            std::cout<<"server is running...\n";
        }
        _isrunning = false;
    }
    ~UdpServer()
    {}
private:
    int _sockfd;//文件描述符
    std::string _ip;//暂时这样写,这个地方不是必须的
    uint16_t _port;//服务器所用的端口号
    bool _isrunning;//是否启动
};

Main.cc

#include<iostream>
#include<memory>
#include"UdpServer.hpp"

void Usage(std::string proc)
{
    std::cout<<"Usage:\n\t"<<proc<<"local_ip local_port\n"<<std::endl;
}

//udpserver ip port
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port);//C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

运行演示

https://i-blog.csdnimg.cn/direct/bafe245fdba44da7b0ef02471898107d.gif


重要知识

本地环回地址

我们上面运行代码时,127.0.0.1叫做本地环回,使用这个ip地址之后,客户端和服务端都可以在一台机器上。

本地环回的作用就是可以实现本地通信,常用于进行代码测试。

netstat指令

当我们想查看网络当中启动的服务时,可以使用 netstat 指令

  • -a:all,显示所有终端下的网络服务
  • -u:udp,显示udp协议的网络服务
  • -n:number,将能显示成数字的显示成数字
  • -p:process,进程

recvfrom函数

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

ssize_t recvfrom(int sockfd, void *buf, size_t len,int flags,struct sockaddr *src_addr, socklen_t *addrlen);

功能:读取其他主机发送过来的数据。

参数:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置。输出型参数的缓冲区。
  • len:期望读取数据的字节数。
  • flags:读取方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。一般是创建一个struct sockaddr _in peer;将peer传送过去。
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回是代表实际读取到的src_addr结构体的长度。这是一个输入输出型参数。

返回值:

  • 读取成功返回实际读取到的字节数。
  • 读取失败返回-1,同时错误码会被设置。

使用示例:

char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);//必须初始化成为sizeof(peer)
int n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);

sendto函数

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr,socklen_t addrlen);

功能:

  • UDP客户端发送数据

参数:

  • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:dest_addr结构体的长度。

返回值:

  • 写入成功返回实际写入的字节数。
  • 写入失败返回-1,同时错误码会被设置。

使用示例:

sendto(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,len);

本地服务器通信代码演示

UdpServer.hpp

#pragma once
#include<iostream>
#include<cstdlib>
#include<strings.h>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
//echo server -> client -> server
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR,
};

const static int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(const std::string &ip,uint16_t port):_sockfd(defaultfd),_ip(ip),_port(port),_isrunning(false)
    {}
    void InitServer()
    {
        //1. 创建udp socket 套接字 —— 必须要做的 —— 可以理解成将网卡打开了
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            std::cerr<<"socket failed!\n";
            exit(SOCKET_ERROR);
        }
        std::cout<<"socket create success,sockfd:"<<_sockfd<<std::endl;
        
        //2.0 填充sockaddr_in结构
        struct sockaddr_in local;//struct sockaddr_in系统提供的数据类型。local是变量,在用户栈上开辟空间。int a = 100;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//port要经过网络传输给对面,也就是说需要先到网络,_port:主机序列->主机序列需要转换成网络序列
        //a. 字符串类型的点分十进制IP地址转成4字节IP
        //b. 主机序列,转成网络序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//"192.168.3.1"->字符串风格的点分十进制的IP地址-> 4字节IP
        
        //2.1 bind sockfd和网络信息(IP(?) + Port)
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            std::cerr<<"bind failed!\n";
            exit(BIND_ERROR);
        }
        std::cout<<"socket bind success\n";
    }
    void Start()
    {
        //一直运行,知道管理者不想运行了,服务器都是死循环
        _isrunning = true;
        while(true)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);//必须初始化成为sizeof(peer)
            //1. 我们要让server先收数据
            int n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            {
                buffer[n] = 0;
                std::cout<<"get message:"<<buffer<<std::endl;
                //2. 我们要将server收到的数据,发回给对方
                sendto(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,len);
            }
        }
        _isrunning = false;
    }
    ~UdpServer()
    {}
private:
    int _sockfd;//文件描述符
    std::string _ip;//暂时这样写,这个地方不是必须的
    uint16_t _port;//服务器所用的端口号
    bool _isrunning;//是否启动
};

UdpClient.cc

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

void Usage(std::string proc)
{
    std::cout<<"Usage:\n\t"<<proc<<" local_ip local_port\n";
}

int main(int argc,char* argv[])
{
    if(argc < 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    //1. 创建socket
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        std::cerr<<"socket error"<<std::endl;
    }
    //client要不要bind?要。client也要有自己的ip和port,但是不需要显式的bind
    //a. 如何bind?udp client首次发送数据的时候,os会自己自动随机的给client进行bind —— 为什么? 防止client port冲突。 要bind,必然要喝port关联起来
    //b. 什么时候bind呢?首次发送数据的时候

    //构建目标主机的socket信息
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    std::string message;
    //2. 直接通信即可
    while(true)
    {
        std::cout<<"Place Enter# ";
        std::getline(std::cin,message);
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));

        // 不一定向一台UDP服务器发消息,可能想多台服务器发消息,这时它收到的报文可能来自不同的服务器的应答
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buffer[1024];
        ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout<<"server echo# "<<buffer<<std::endl;
        }
    }
}

Main.cc

#include<iostream>
#include<memory>
#include"UdpServer.hpp"

void Usage(std::string proc)
{
    std::cout<<"Usage:\n\t"<<proc<<" local_ip local_port\n"<<std::endl;
}

//udpserver ip port
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip,port);//C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

运行演示

https://i-blog.csdnimg.cn/direct/3e4ad5e0e60b40aaa40d122fde31b422.gif


重要知识点

进行UDP通信的主要流程
  1. 创建UDP socket套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
  1. 填充sockaddr_in结构
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());
  • port需要经过网络传输给对面,所以需要将主机序列转化成网络序列:使用htons;
  • 我们输入的ip地址是点分十进制的,需要将点分十进制的ip地址,转化成4字节ip:使用inet_addr;
  1. 发送数据
//构建目标主机的socket信息
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());

std::string message;

std::cout<<"Place Enter# ";
std::getline(std::cin,message);
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
  1. 接收数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer);//必须初始化成为sizeof(peer)
char buffer[1024];
ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
  • 接收数据时:在服务端中,接收的主机信息需要我们使用之前创建一个peer,用来存储发送方的相关信息;在客户端中,我们也需要创建一个peer来接受不同主机发送的数据。
  • 发送数据时:在服务端中,发送的主机信息只需要我们使用之前创建的peer,因为peer中就存放了发送方的相关信息;而客户端则不需要。因为在服务端,会给很多不同的客户端主机发送数据,而客户端就是对指定的服务端主机发送数据。所以客户端使用sendto的时候直接填写服务端的socket信息就好。
  • 总结:接收数据需要创建临时的peer来存储发送方的IP地址和端口号等信息;发送时直接指明即可。

INADDR_ANY

上面的代码是在本地上进行的,那如果我们绑定自己的云服务器ip地址,那是不是就可以进行跨网络通信了呢?

https://i-blog.csdnimg.cn/direct/e762cbecb7a94d84a5286d64df1db557.png

会显示绑定错误!

这是因为服务器上的IP地址是厂商提供的,不一定是真正的公网IP,即使可以绑定也不建议这样做,因为有些服务端不止一个IP地址,可能有多个,如果你绑定了其中一个,那么我们之后就只能通过这个IP地址来访问服务端,内部其他的IP地址就访问不了了。

例如下图,如果我们绑定了111.222.333.444这个IP地址,那么我们就无法通过100.200.300.400这个IP地址来进行访问了。

https://i-blog.csdnimg.cn/direct/9cc24b240dec4298b5b8b8ef223b385b.png

所以我们一般将服务端的IP地址绑定 INADDR_ANY ,这个就是一个宏,对应的值为0。

简单的UDP回声程序

UdpServer.hpp

#pragma once
#include<iostream>
#include<cstdlib>
#include<cerrno>
#include<cstring>
#include<cstdio>
#include<strings.h>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include<arpa/inet.h>
#include"InetAddr.hpp"
//echo server -> client -> server
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR
};

const static int defaultfd = -1;

class UdpServer
{
public:
    UdpServer(uint16_t port):_sockfd(defaultfd),_port(port),_isrunning(false)
    {}
    void InitServer()
    {
        //1. 创建udp socket 套接字 —— 必须要做的 —— 可以理解成将网卡打开了
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            std::cerr<<"socket failed!\n";
            exit(SOCKET_ERROR);
        }
        std::cout<<"socket create success,sockfd:"<<_sockfd<<std::endl;
        
        //2.0 填充sockaddr_in结构
        struct sockaddr_in local;//struct sockaddr_in系统提供的数据类型。local是变量,在用户栈上开辟空间。int a = 100;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//port要经过网络传输给对面,也就是说需要先到网络,_port:主机序列->主机序列需要转换成网络序列
        //a. 字符串类型的点分十进制IP地址转成4字节IP
        //b. 主机序列,转成网络序列
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());//"192.168.3.1"->字符串风格的点分十进制的IP地址-> 4字节IP
        local.sin_addr.s_addr = INADDR_ANY;

        //2.1 bind sockfd和网络信息(IP(?) + Port)
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            std::cerr<<"bind failed!\n";
            exit(BIND_ERROR);
        }
        std::cout<<"socket bind success\n";
    }
    void Start()
    {
        //一直运行,服务器都是死循环
        _isrunning = true;
        while(true)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);//必须初始化成为sizeof(peer)
            //1. 我们要让server先收数据
            ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(n > 0)
            {
                buffer[n] = 0;
                InetAddr addr(peer);
                printf("get message from:[%s:%d]:%s\n",addr.IP().c_str(),addr.Port(),buffer);
                //2. 我们要将server收到的数据,发回给对方
                sendto(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,len);
            }
        }
        _isrunning = false;
    }
    ~UdpServer()
    {}
private:
    int _sockfd;//文件描述符
    //std::string _ip;//暂时这样写,这个地方不是必须的
    uint16_t _port;//服务器所用的端口号
    bool _isrunning;//是否启动
};

Main.cc

#include<iostream>
#include<memory>
#include"UdpServer.hpp"

void Usage(std::string proc)
{
    std::cout<<"Usage:\n\t"<<proc<<" local_port\n"<<std::endl;
}

//./udpserver port
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);//C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

UdpClient.cc

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

void Usage(std::string proc)
{
    std::cout<<"Usage:\n\t"<<proc<<" local_ip local_port\n";
}

int main(int argc,char* argv[])
{
    if(argc < 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    
    //1. 创建socket
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        std::cerr<<"socket error"<<std::endl;
    }
    //client要不要bind?要。client也要有自己的ip和port,但是不需要显式的bind
    //a. 如何bind?udp client首次发送数据的时候,os会自己自动随机的给client进行bind —— 为什么? 防止client port冲突。 要bind,必然要和port关联起来
    //b. 什么时候bind呢?首次发送数据的时候

    //构建目标主机的socket信息
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    std::string message;
    //2. 直接通信即可
    while(true)
    {
        std::cout<<"Place Enter# ";
        std::getline(std::cin,message);
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));

        // 不一定向一台UDP服务器发消息,可能想多台服务器发消息,这时它收到的报文可能来自不同的服务器的应答
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buffer[1024];
        ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout<<"server echo# "<<buffer<<std::endl;
        }
    }
    return 0;
}

运行演示

https://i-blog.csdnimg.cn/direct/350c87a61ea8472782472af497b00867.gif

重要知识点

  • 服务端中IP地址不要绑定具体的某一个,使用0即可。所以在服务端,IP地址不是必须的,只需要知道端口号即可,IP地址统一绑定0。
  • 客户端中不需要显式的绑定ip和port,避免客户端port冲突。在首次发送数据时,OS会自动随机的bind。因为当我们自己绑定一个确定的port之后,可能会和其他的客户端port冲突,比如京东客户端绑定的是8100,如果淘宝客户端显式的绑定8100,那么你打开京东的时候就打开不了淘宝。所以为了避免这种情况,OS会帮我们绑定一个未使用的port。