网络套接字

目录

UDP 套接字

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

端口号

源端口号和目的端口号(port)

TCP

UDP

网络字节序

socket程序接口

UDP套接字编写

服务器编写

服务器类结构

构造函数

启动服务

主函数

客户端编写


UDP 套接字

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

源IP地址就是发送方的IP地址,而目的IP地址就是接收方的地址。

当我们想要给一台主机发送数据的时候,此时我们的IP地址就是源IP地址,而对方的主机就是目的IP地址,当对反的主机给我们返回数据的时候,此时对方的主机的IP地址就是源IP地址,而我们的主机的IP地址就是目的IP地址。

而源IP和目的IP的数据一般再IP的报头里面。

端口号

端口号是什么?

我们可以想一下,如果我们只是把数据交给对方主机就可以了吗?当然不是,因为如果是交给对方主机,那么对方主机能怎么做呢?所以其实交给对方主机并没有结束,而是需要交给对方主机的一个进程。

而端口号就是用来表示想要交给对方主机上的哪一个进程!

  • 顿口号一般用2字节来表示也就是16位

  • 端口号加IP可以表示全网唯一的一个进程

  • 而一个端口号一般只能用来表示一个进程

但是进程不是有PID吗?为什么还要用端口号呢?

如果再网络里面用进程的pid来表示端口号也不是不可以,但是这样做的话,那么就回让网络与系统耦合再一起,如果进程的pid出现问题,那么此时网络也就回出现问题,耦合度太高了,所以再网络里面采用了全新的表示进程的手段。

源端口号和目的端口号(port)

源端口号和目的端口号也是用来表示主机上的进程的。

其中源端口号是用来表示发送方主机的进程,是哪一个进程发送的,而目的端口号是用来表示想要发送给目的主机的哪一个进程。

源端口号和目的端口号的数据一般在传输层协议(TCP/UDP)中。

在网络中一般IP+port就表示的是”套接字“(socket)。

TCP

下面我们看一下传输层协议中常用的两个,其中一个就是TCP。

TCP协议现在我们还是无法很好理解的,但是我们先说一下TCP协议的特点:

  1. 传输层协议

  2. 有连接

  3. 可靠

  4. 面向字节流

后面我们回详细说明这两个协议的。

UDP

UDP协议也是我们后面回稍微介绍一下,但是我们还是主要介绍TCP协议。

UDP协议的特点:

  1. 传输层协议

  2. 无连接

  3. 不可靠

  4. 面向数据报

上面说的这两个TCP与UDP中的可靠与不可靠只是这两个协议的特定,并没有好坏之分,其中如果是可靠的话,那么一定是需要付出更多的代价的,而不可靠也是无需考虑那么多,其中也是比较快的。

网络字节序

在内存中,我们存储的时候是有大小端之分的,因为我们存储进去的顺序和读取的顺序如果不一致的话,那么就是有问题的。

而数据在网络中也同样是如此,如果发送的数据与读取的数据不一致同样是有问题的。

而在网络中规定:

发送数据需发送大端的数据,而大端的数据就是高地址存储低位的数据。

所以如果机器是小端的,那么在发送的时候是需要将小端的数据转化为大端然后发送的,那么如果是大端的数据,在发送的时候就不需要处理,但是现在我们的机器一般都是小端的。

所以我们是需要将网络转主机的,也需要主机转网络的接口:

   #include 
​
   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 表示主机,n 表示网络,l 表示 long long 也就是表示 32 位的 IP,而s 表示 short 表示短也就是16位的 port 。

socket程序接口

下面我们看一下常见的编写套接字的接口:

// 创建一个套接字
int socket(int domain, int type, int protocol);
// 将IP与端口绑定
int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
// 监听
int listen(int sockfd, int backlog);
​
// 接受请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
​
// 建立连接
int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

上卖弄就是常用的一些socket接口,而我们编写套接字的时候,也就基本使用这些接口,后面回慢慢介绍的。

下面我们就使用这些接口先简单的看一下UDP套接字的编写。

因为UDP编写比较简单,无连接,并且面向数据报,所以我们不需考虑其他问题。

UDP套接字编写

在套接字里面实际上分为几种套接字:

  1. 原始套接字

  2. 域间套接字

  3. 网络套接字

一般我们使用最多的也就是这三种,但是实际上,我们最多就是会写两种,网络套接字和域间套接字,实际上我们最多还是网络套接字,所以下面我们主要以网络套接字为主,虽然套接字分为三种,但是我们使用网络编程的接口却只有一个,因为在设计的时候为了方便,同时因为这三种套接字如果设计不同的接口,那么差别也仅仅是函数参数有一点差别,实际上并无太大的差别,所以就设计为一种接口,而由于是三种套接字,所以函数参数的差别库函数的设计就是使用类似于C++的继承的方法。

我们可以拿一个 bind 函数看,这个函数的第二个参数是一个 struct sockaddr 的指针,但是实际上我们真实的参数并不是struct sockaddr 这歌类型,而是这歌类型类似于void一样,因为当时网络出来的时候,还没有void这歌功能。

一般我们在网络中使用的这歌参数是:

struct sockaddr_in;// 网络套接字
struct sockaddr_un;// 域间套接字

由于我们经常使用网络套接字,我们下面看一下这个结构体里面有一些什么:

struct sockaddr_in
    {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;         /* Port number.  */
    struct in_addr sin_addr;        /* Internet address.  */
​
    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
               __SOCKADDR_COMMON_SIZE -
               sizeof (in_port_t) -
               sizeof (struct in_addr)];
    };

就是这样,里面有一些字段,其中有 sin_port 这个对象,里面就是存储的是要 bind 的端口号,还有上面的一个宏

#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

实际是这个样子,其实他使用了宏里面的##拼接的语法,拼接后就是一个 sa_family_t,这个里面存储的是一个协议家族,也就是创建 sockfd 的时候使用的。

其中协议家族里面有一个 AF_INET 和 AF_UNIX 表示网络通信和域间通信,而这两个参数也就是 socket 函数里面的第一个参数,表示我们想要创建的套接字使用哪种协议家族。

还有一个ip在哪呢?就在这个结构体里面 struct in_addr sin_addr

我们可以看到该结构体中还有一个用来存储ip的结构体,我们卡伊看一下这个结构体里面是什么:

typedef uint32_t in_addr_t;
​
struct in_addr
  {
    in_addr_t s_addr;
  };

这个存储ip的结构体里卖弄也就只有一个字段, in_addr_t 类型的字段,其中这个类型实际上就是一个32位的数字,用来表示ip。

现在背景知识基本已经介绍的差不多了,协议的协议再后面会详细的说,现在我们先看一下网络的代码如何编写。

我们准备如何写这个网络的代码?

  1. 我们打算写一个CS模型,也就是客户端服务器模型。

  2. 服务器接受客户端发送的消息,然后打印出来,最后将这个消息继续返回给客户端。

  3. 我们认为发送的数据就是字符串。

下面我们打算将服务器封装一下,然后我们客户端就以面向过程的方式写:

服务器编写

服务器类结构

服务器我们打算封装一下,所以我们需要将它抽象出来。

既然是一个网络的服务器,那么一定是需要几个成员变量的,用来存储自己的 IP,以及绑定的端口号。

那么还需要什么成员变量吗?通信的时候,我们需要向哪一个套接字里面发送,所以我们还需要保存一下创建好的套接字,所以我们还需要一个套接字的成员变量。

那么我们需要什么方法呢?

首先就是创建一个服务器,由于我们使用C++写,所以初始化的工作交给构造函数完成,所以我们再创建的时候只需要定义这个对象即可,那么还需要什么方法呢?

我们还需要一个启动该函数的方法,也就是让服务器开始接受网络里面发送的数据,也就是让服务器一致进行读取数据从创建的socket里面。

上面的方法就是我们暴露给外面的,但是我们再写启动函数的时候,一定需要一些其他的方法,例如再进行读取的时候,我们可以将读取封装为一个函数,在将数据写回去的时候,我们还需要一个send的方法。

上面的这几个私有的方法事对于服务器读取的方法,那么在创建的时候,我们在构造函数里面完成的,我们也可以将构造函数里面的哪些使用到的都封装一下,比如在构造的时候需要创建套接字,还需要bind,所以我们也可以封装围殴两个方法。

但是这些返回发不需要暴露给外面,所以我们就可以设计为私有的方法。

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "log.hpp"
​
const int SIZE = 1024;
​
class UdpServer
{
public:
    // 构造函数将网络中的资源准备好
    UdpServer(uint16_t port, std::string ip = "")
        : _ip(ip), _port(port),_sockfd(-1)
    {
        // 1. 需要创建一个 socket 套接字
        _sockfd = Socket();
        // 2. bind
        // 为什么需要 bind 我们前面说的,在网络通信的时候,我们需要将IP与port绑定
        Bind();
    }
​
    void start()
    {
    }
​
private:
    void Recv()
    {
    }
​
    void Send()
    {
    }
​
    int Socket()
    {
    }
​
    void Bind()
    {}
​
private:
    std::string _ip; // 服务器的IP地址
    uint16_t _port;  // 需要绑定的端口号
    int _sockfd;
    static Log log;
};
​
Log UdpServer::log;

服务器里面需要的函数基本就是这些,但是这些函数的参数目前都是空,我们一边写一边说函数里面需要的参数事哪些,然后我们可以设计参数。

这里还有一个 Log 的对象,这个对象就是一个日志的对象,没有什么特别的用处。

下面我们介绍一下构造函数如何写,首先构造函数中的ip和端口一定是需要传进的,所以我们需要这两个参数,然后就是套接字,套接字实际上就是一个文件描述符,我们一会可以看一下,所以既然是文件描述符,那么我们就可以先初始化为-1。

然后再初始化列表走完后,我们就可以开始再函数体内调用其他的函数,我们可以调用一个函数创建套接字,还可以将套接字bind。

上面这个头文件里面还有网络里面常用到的头文件,都已经包进去了。

构造函数

下面既然需要先构造这个对象,那么我们先写构造方法,但是构造方法的结构以及写好了,我们只需将里面的两个函数完成即可:

创建套接字

再创建套接字的时候,我们需要调用一个系统调用:

NAME
       socket - create an endpoint for communication
​
SYNOPSIS
       #include           /* See NOTES */
       #include 
​
       int socket(int domain, int type, int protocol);
  • 该函数的功能就是创建一个套接字

  • 第一个参数是一个套接字的协议域,表示想要使用哪种协议。由于协议家族可以分为原始、域间、网络,我们经常使用的是网络和域间。

    	   Name                Purpose                          Man page
           AF_UNIX, AF_LOCAL   Local communication              unix(7)
           AF_INET             IPv4 Internet protocols          ip(7)
           AF_INET6            IPv6 Internet protocols          ipv6(7)
           AF_IPX              IPX - Novell protocols
           AF_NETLINK          Kernel user interface device     netlink(7)
           AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
           AF_AX25             Amateur radio AX.25 protocol
           AF_ATMPVC           Access to raw ATM PVCs
           AF_APPLETALK        Appletalk                        ddp(7)
           AF_PACKET           Low level packet interface       packet(7)

    这就是常使用的协议家族,其中第一个(AF_UNIX,AF_LOCAL)表示的是域间套接字,第二个AF_INET表示网络套接字,但是这个是IPv4的,第三个是IPv6的,下面就不介绍了。

  • 第二个参数是套接字的类型,其中类型有TCP/UDP就是最常用的,UDP是面向数据报的,TCP是面字节流的,而我们今天使用UDP的,后面在使用TCP。

    这个也是一个宏,我们可以看一下:

    	   SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data
                           transmission mechanism may be supported.
    
           SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
    
           SOCK_SEQPACKET  Provides  a sequenced, reliable, two-way connection-based data transmission path for data‐
                           grams of fixed maximum length; a consumer is required to read an entire packet  with  each
                           input system call.
    
           SOCK_RAW        Provides raw network protocol access.
    
           SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.
    
           SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

    其中第一个SOCK_STREAM表示的就是字节流,也就是表示使用TCP,还有第二个就是SOCK_DGRAM表示使用UDP。

  • 最后一个参数表示通信协议,我们设置为0即可

  • 返回值,socket函数的返回值是一个文件描述符,所以如果返回小于0的数字,表示失败了,既然是文件描述符,那么说明可以使用我们之前学习的文件操作的接口,但是如果使用UDP协议的话,UDP是面向数据报的,所以不适合使用文件接口,但是如果是TCP的话,那么是面向字节流的,所以可以使用文件的接口。

那么看一下这个函数如何编写:

  1. 我们先要确定第一个参数写什么,第一个参数表示协议域,协议域就是有常用的三个:原始套接字、域间套接字、网络套接字,其中我们想要使用网络套接字,所以我们第一个参数需要填写网络套接字,AF_INET。

  2. 第二个参数表示套接字的类型,套接字的类型其中常用的有流式套接字,还有数据报,由于UDP式面向数据报的,所以我们可以填SOCK_DGRAM,表示面向数据报。

  3. 第三个参数就是通信协议,我们直接写0即可。

  4. 返回值就是一个文件描述符,我们需要对这个文件描述符进行判断,如果小于0表示创建失败,报错即可,如果创建成功,然后将该文件描述符返回即可。

   int Socket()
    {
        // socket 的第一个参数是协议家族,表示需要使用哪一个协议
        // 第二个参数是表示想要通信的使用什么:SOCK_DGRAM 表示使用UDP通信,SOCK_STREAM 表示使用TCP通信
        // 最后一个参数表示协议,一般为0
        // 返回值如果是-1,表示创建失败,而成功的话,那么就返回一个文件描述符
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            log(FATAL, "错误码: %d 错误原因: 创建socket文件描述符失败", errno);
            exit(errno);
        }
        log(INFO, "创建套接字成功 sockfd: %d", sockfd);
        return sockfd;
    }

bind 函数:

NAME
       bind - bind a name to a socket

SYNOPSIS
       #include           /* See NOTES */
       #include 

       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
  • 这个函数就是将套接字和IP以及端口绑定

  • 第一个参数就是创建好的套接字

  • 第二个参数就是上面的结构体,struct sockaddr_in 的类型。

  • 第三个参数就是一个长度,表示的是 struct sockaddr_in 的大小

  • 返回值如果小于0,那么就失败了

在构造函数中,我们创建好socket后,我们还需要将IP和端口bind在一起,为什么?

因为我们如果不将这个服务与端口绑定在一起,那么我们怎么直到当数据发送到这一个主机上的时候是发送给哪一个进程的,还有发送给这个IP的数据如何给固定的端口?

所以我们一定是需要将IP和端口bind在一起的还有将端口号也bing在一起。

我们把bind也封装一下:

  1. 这里我们调用了系统调用bind函数,我们将这个函数封装为一个接口供我们自己调用。

  2. 我们将端口号和IP传进去,还有套接字也传入,然后供bind函数使用。

  3. 首先这个函数里面我们需要调用bind函数,bind函数里面需要struct sockaddr_in 的对象,因为我们需要将这个对象里面设置进我们需要的参数(IP 、协议家族、端口号),所以我们需要提前将该对象清空。

  4. 然后我们将该对象里面的内容设置好。

  5. 第一个设置的就是一个协议家族,那么设置什么协议家族呢?我们前面使用的是网络套接字,所以我们就使用前面使用的网络套接字。

  6. 第二个还需要设置端口,但是我们前面说了主机和网网络里面大小端是不一样的,所以我们需要主机转网络,所以我们要使用一个函数来完成主机转网络。

  7. 当然除了端口需要主机转网络,IP也需要主机转网络,但是IP是点分十进制的,所以在主机转网络之前,我们还需要将点分十进制转成unit32_t类型的,然后在进行主机转网络。

  8. 等转好后,我们就可以进行bind,将参数传进去就可以了。

void Bind(int sockfd, string ip, uint16_t port)
    {
        // bind 的话,需要的参数是一个 socket 返回的文件描述符,这个我们在 bind 之前已经通过 socket 已经获得了
        // 第二个参数是一个struct sockaddr 类型的指针,这个指针里面传入你想要发送对端主机的IP和端口
        // 第三个参数就是第二个参数的的大小
        struct sockaddr_in local;
        // 对 local 进行初始化
        bzero(&local, sizeof local);
        // 将 ip 与 port 填进去
        // 协议家族
        local.sin_family = AF_INET;
        // local.sin_port = _port; ?? 这样可以吗? 不可以为什么?因为我们前面说了,主机和网络是有区别的,主机上的大小端是不同的,所以我们需要主机转网络
        local.sin_port = htons(_port);
        // 当然既然端口号需要主机转网络,那么IP也需要主机转网络
        // 但是我们传入的 ip 是点分十进制的,而点分十进制并不是给计算机看的,所以我们还需要将点分十进制转为4字节的uint32_t 类型
        // 同时我们也需要将转好的IP在进行主机转网络
        // 但是其实我们并不需要做这些,其实有一个函数就可以帮我们做好
    	// 下面我们的IP我们使用INADDR_ANY,后面说为什么
        local.sin_addr.s_addr = _ip.size() == 0 ? INADDR_ANY : inet_addr(_ip.c_str());
        int r = bind(sockfd, (struct sockaddr *)&local, sizeof(local));
        if (r < 0)
        {
            log(FATAL, "bind 失败! 错误码: %d", errno);
            exit(errno);
        }
        log(INFO, "bind 成功");
    }

这里我们想要介绍一下初始化 struct sockaddr_in 类型的对象里面的IP,我们这里还有前面构造函数选择了默认,也就是可以不传IP,为什么?

因为我们其实使用的是云服务器,云服务器不允许bind固定的IP,所以这里就只能选择任意IP,向该主机任意IP发送的数据,都会被接收,所以这里就不使用固定的IP。

启动服务

启动服务就是一个函数,那么如何启动服务?其实前面我们已经介绍过了,我们只需要让该程序一直读取网络中的数据即可,然后我们为了方便,所以我们就认为来的数据是字符串,然后我们将该字符串打印出来,然后我们在将该字符串返回即可:

  1. 我们既然是读取数据,那么我们一档需要一个缓冲区,也就是 buffer

  2. 然后我们后面不断的读取数据即可,我们为了方便写读取数据,我们将读取数据封装成一个函数,我们将该函数里面需要的参数传入即可。

  3. 因为是UDP面向数据报,所以我们不好使用文件的接口,也就是字节流读取,我们可以使用一个 recvfrom 的函数(这个函数一会介绍)。

  4. 我们可以将该函数封装为一个接口供我们调用,这个接口里面有一些参数,我们一会介绍为什么需要这样传参。

  5. 然后等读取到数据后,我们将数据打印出来,然后我们继续将数据返回回去即可。

  6. 既然是返回,那么我们也一定需要一个将数据发送回去的接口,所以我们可以调用一个系统调用,sendto函数,这个函数我们也打算封装起来。

   void start()
    {
        // SIZE 是一个const int 类型的变量
        char buffer[SIZE];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof peer;
            Recv(_sockfd, buffer, SIZE - 1, (struct sockaddr *)&peer, &len);
            // 读取结束,此时数据以及到 buffer 里面
            // 还有就是 peer 对象里面此时里面也保存了对端主机的IP和端口
            uint32_t ip = ntohl(peer.sin_addr.s_addr);
            uint16_t port = ntohs(peer.sin_port);
            cout << "[" << ip << ":" << port << "] " << buffer << endl;
            // 将读取到的数据回显回去
            Send(_sockfd, buffer, (struct sockaddr*)&peer, len);
        }
    }

上面就是让该服务启动的一个函数,里面有一个 struct sockaddr_in 的对象,为什么需要这个对象?

我们可以想一下,当一台计算机给一台计算机发送数据的时候,需要告诉自己是谁吗?当然需要,为什么?如果不告诉别人自己是谁的时候,那么当别人处理完你发送过来的数据之后,那么别人怎么将处理的结果返回给你呢?

所以在UDP发送过来的数据后,除了本来的数据,那么一定需要发送方主机的IP和端口,为了方便对方处理完数据后将处理结果返回。

那么我们如何接受这个IP和端口呢?

我们前面在服务器bind的时候,我们是不是使用了一个 struct sockaddr_in 类型的对象,而我们就使用这个类型来接收,所以我们需要一个这样的对象传进去来接收对端主机的IP和端口。

我们还需要将这个类型的大小也传进去,可以看一下上面的代码里面的注释。

下面我们看一下这个 Recv 函数如何实现:

recvfrom:

NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include 
       #include 

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
  • 该函数作用就是从一个套接字里面读取数据

  • 第一个参数就是从哪一个套接字里面读取

  • 第二个参数就是表示读取到哪一个缓冲区中

  • 第三个参数表示缓冲区的大小

  • 第四个参数表示读取的方式,其中默认设置为0,表示阻塞读取

  • 第五个我参数就是对端主机的IP和端口,使用 struct sockaddr_in 类型的对象接收

  • 第六个参数表示第五个参数的大小,其中第五第六个参数都是输出形参数,第六个参数同时还是一个输入型参数,但是一般输入输出的大小一样。

下面函数就是对上面这个系统调用的封装:

  void Recv(int sockfd, char* buffer, size_t size, struct sockaddr *peer, socklen_t *len)
    {
        // recvfrom 是从 socket 里面读取数据
        // 第一个参数就是 sockfd,也就是 socket 获得的返回值
        // 第二个数就是想要读取到哪一个缓冲区
        // 第三个参数就是缓冲区的大小
        // 第四个参数表示读取方式,0 表示阻塞读取
        // 第五个参数是一个输出型参数,用来存储谁发送的这个数据,里面有IP和port
        // 第六个参数是一个长度,同时是一个输入输出形参数,输入表示传入的 perr 的大小,输出表示perr返回后的大小,但是一般的时候 len 都是相同的
        int r = recvfrom(sockfd, buffer, size, 0, peer, len);
        if(r < 0)
        {
            log(FATAL, "读取失败!");
            exit(errno);
        }
        else if(r == 0)
        {
            log(INFO, "对端关闭");
        }
        buffer[r] = 0;// 将读取到的数据以字符串的方式处理
    }

当我们接收数据结束后,我们将数据打印出来,然后我们将读取到的数据返回即可,所以我们需要一个发送的函数,我们将发送的函数也封装一下:

sendto:

NAME
       send, sendto, sendmsg - send a message on a socket

SYNOPSIS
       #include 
       #include 

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
  • 该函数是一个发送数据的函数,可以将数据发送到指定IP的主机。

  • 第一个参数就是想要将数据发送到哪一个文件描述符中

  • 第二个参数表示想要发送的数据

  • 第三个参数表示该数据的大小

  • 第四个参数表示发送的方式,默认为0,表示阻塞发送

  • 第五个参数表示想要发送给哪一个主机的哪一个端口,其中和bind是一样的。

  • 第六个参数表示第五个参数的大小

下面就是对上面这个系统调用的封装:

 void Send(int sockfd, char* buffer, struct sockaddr* dest_addr, socklen_t len)
    {
        // sendto 是一个发送数据的函数
        // 第一个参数就是向哪一个套接字里面发送
        // 第二个参数表示想要发送的数据
        // 第三个参数表示数据的大小
        // 第四个参数表示发送的方式 0 表示阻塞
        // 第五个参数 struct sockaddr 里面有发送的IP和端口
        // 第六个参数就表示第五个参数的大小
        size_t s = sendto(sockfd, buffer, strlen(buffer), 0, dest_addr, len);
        if(s < 0)
        {
            log(FATAL, "发送数据失败!");
            exit(errno);
        }
    }

发送的时候不是需要直到对方的IP和端口吗?

那么我们在给对方发送数据的时候,我们直到对方的IP和端口吗?知道!

我们怎么知道的?还记得我们前面recvfrom函数中的 struct sockaddr 类型的指针,这就是一个输出型参数,其中会将发送方主机的IP和端口放入,所以我们是知道对方的IP和端口的,所以我们可以直接使用上面获得的IP和端口。

我们就可以直接将数据发送给对方即可。

主函数

其实主函数要做的事情很简单,只需要创建一个 UdpServer的对象,然后调用里面的 start 函数,那么此时该程序就开始从网络里面读取数据了。

#include "udp_server.hpp"
#include 

void Usage(char* proc)
{
    cout << "\nUse: " << proc << " ip proc\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    unique_ptr sev(new UdpServer(atoi(argv[1])));
    sev->start();
    return 0;
}

此时主函数在命令行参数里面需要传入一个端口号,这个端口号只要不是前面一千基本都是不能使用的,因为很多服务都使用了前面一千的端口号,比如ssh就使用了22。

还有就是如果使用云服务器,那么记得一定要开放端口号。

客户端编写

既然我们编写了服务器,所以我们的客户端就简单多了,因为客户端基本和服务器是相同的代码:

但是客户端是由一点和服务器是不同的:

  1. 客户端需要套接字吗?需要,所以客户端也需要创建套接字进行网络通信。

  2. 客户端需要bind吗?需要!但是不需要我们自己显示的bind为什么?因为如果我们显示的bind了客户端,那么就是固定的bind了一个端口号,所以如果在我们使用这个客户端之前,以及有其他的服务占用了该端口号,那么这个服务就会启动失败,所以我们客户端并不需要显示的bind端口号,如果我们不显示的bind端口号那么怎么办呢?

  3. 如果我们不显示的bind端口号,那么操作系统会给我们默认的选择端口号bind,但是什么时候bind呢?

  4. 在我们第一次给服务器发送数据的时候,操作系统给我们bind!

下面我们就看一下客户端编写:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void Send(int sockfd, std::string message, struct sockaddr* peer, socklen_t len)
{
    ssize_t s = sendto(sockfd, message.c_str(), message.size(), 0, peer, len);
    if(s < 0)
    {
        std::cout << "main Send error" << std::endl;
    }
}

void Recv(int sockfd, std::string &message)
{
    char buffer[1024];
    struct sockaddr_in tmp;
    socklen_t len = sizeof tmp;
    ssize_t s = recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&tmp, &len);
    if (s > 0)
    {
        buffer[s] = 0;
        message += buffer;
    }
}

void Usage(char *proc)
{
    std::cout << "\nUse " << proc << " ip proc\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    // 1. 客户端第一步需要创建 sockfd
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    struct  sockaddr_in peer;
    socklen_t len = sizeof peer;
    peer.sin_family = AF_INET;
    peer.sin_port = htons(atoi(argv[2]));
    peer.sin_addr.s_addr = inet_addr(argv[1]);
    // 2. 客户端需要 bind 吗?
    // 不需要! 为什么?
    // 因为如果是 bind 的话,那么就会将特定的端口与服务bind,但是如果这样的话,那么如果有一个服务先将该服务的端口号抢先bind呢?
    // 所以不能 bind 固定的端口号
    // 那么客户端需要bind吗?当然需要,只是我们不需要显示的 bind 端口号,而是在客户端第一次给服务器发送数据的时候,回自动bind
    // 下面直接通信
    while (true)
    {
        std::cout << "请输入> ";
        std::string message;
        std::getline(std::cin, message);

        Send(sockfd, message, (struct sockaddr*)&peer, len);
        std::string tmp;
        Recv(sockfd, tmp);
        std::cout << tmp << std::endl;
    }
    return 0;
}

客户端这里就不仔细解释了,下面稍微介绍一下:

  • 我们客户端也是需要创建套接字,所以第一步也是创建套接字

  • 但是我们不需要bind,所以创建好后可以直接通信

  • 我们也可以将 recvfrom 和 sendto 函数封装

  • 然后我们进行发送数据,发送数据后我们在接收数据

  • 这就是客户端,写完服务器代码后,客户端就简单多了。

想要源代码的在下面:

[套接字编写]  https://gitee.com/naxxkuku/linux/tree/master/udp 

你可能感兴趣的:(网络)