【socket编程】UDP网络通信模型 {socket创建套接字文件;填充套接字结构+bind绑定;recvfrom接收数据;sendto发送数据;ifconfig,netstat命令行工具}

在这里插入图片描述

今天我们通过以下的几个surver/client通信程序了解一下UDP网络通信模型

一、简单的服务器echo程序

以下部分内容转载自「网络编程」简单UDP网络通信程序的实现_socket udp-CSDN博客

1.1 服务端

首先明确,这个简单的UDP网络程序分客户端和服务端,所以我们要生成两个可执行程序,一个是客户端的,另一个是服务端的,服务端充当的是服务器,暂时实现的功能是客户端和服务端简单进行通信,服务端要可以收到客户端发送给服务端的信息,目前就先简单实现这样的功能

下面进行编写服务端的代码

1.1.1 创建套接字文件

先介绍创建套接字文件的函数socket

socket函数

socket函数的作用是创建套接字文件,TCP/UDP 均可使用该函数进行创建套接字,man 2 socket查看:

img

create an endpoint for communication:创建通信端点,即创建通信的一端

函数:socket
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
        int socket(int domain, int type, int protocol);
 
参数:
    第一个参数domain:套接字类型
    第二个参数type:数据的传输方式
    第三参数protocol:创建套接字的协议类别
 
返回值:
    套接字创建成功返回一个文件描述符,创建失败返回-1,错误码被设置
  • socket系统调用接口是对传输层的文件系统级别的封装,Linux下一切皆文件!
  • socket函数用于创建套接字文件描述符
  • 也就是说后续在进行网络读写时,可以用文件接口进行字节流读写
  • TCP协议的特点是面向字节流传输,所以可以使用文件接口进行网络读写
  • UDP协议的特点是面向数据报传输,所以不适用文件接口的字节流读写,UPD有自己专属的读写接口:recvfromsendto

socket函数的参数

(1)socket函数的第一个参数是domain,用于创建套接字的类型,该参数就相当于 struct sockaddr结构体的前16位,即2字节

img

该domain参数的选项已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果要选择本地通信,则选择 AF_UNIX
  • 如果要选择网络通信,则选择 AF_INET(IPv4)或者 AF_INET6(IPv6)

“inet” 是Internet Protocol(IP)的简写

img

(2)socket函数的第二个参数是type,用于创建套接字时提供的数据传输方式

该参数的选项也是已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果是基于UDP的网络通信,我们采用的就是 SOCK_DGRAM,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)
  • 如果是基于TCP的网络通信,我们采用的就是 SOCK_STREAM,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

SOCK_DGRAM对应的英文:socket datagram
SOCK_STREAM对应的英文:socket stream

img

(3)socket函数的第三个参数是protocol,用于创建套接字的协议类别。

可以指明为TCP或UDP,但该字段一般直接设置为0就可以了。
设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议

socket函数的返回值

套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码被设置

img

解释套接字创建成功返回一个文件描述符的问题

  • 当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,而这个网络文件得底层硬件实际就是“网卡”
  • 文件描述符下标0、1、2依次被标准输入、标准输出以及标准错误占用,
  • 如果程序没有打开其他文件,当套接字创建成功时,文件描述符下标为3的指针就指向了这个打开的 “网络文件”
  • 我们读取、发送数据,就从这个 “网络文件” 进行读取和发送
  • 所以操作网络就像操作文件一般,这个“网络文件”就是一个缓冲区

明确一点

  • 按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。
  • 而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码
  • 因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口

img


1.1.2 填充套接字结构+绑定端口

绑定端口的函数是bind函数

bind函数

bind函数的作用是绑定端口号,TCP/UDP 均可使用进行该函数绑定端口,man 2 bind查看:

img

bind a name to a socket:将名称绑定到套接字

函数:bind
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
         int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 
参数:
    第一个参数sockfd:文件描述符
    第二个参数addr:套接字结构的地址
    第三参数addrlen:套接字结构的大小
 
返回值:
    绑定成功返回0,绑定失败返回-1,同时错误码会被设置

下面介绍bind函数的参数

(1)bind函数的第一个参数是sockfd,是套接字文件的文件描述符

(2)bind函数的第二个参数是addr,是套接字结构, 用于保存网络相关的属性信息,比如IP地址、端口号等

该参数addr的类型是:struct sockaddr *,也就是如图的结构体:

img

我们要做的工作就是:定义一个 sockaddr_in 的结构体,也就是上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换

套接字结构 sockaddr_in

我们看一下 sockaddr_in 结构体的定义:

img

可以看到,sockaddr_in 有以下几个成员类型:

  • sin_family:表示协议家族,类型uint16_t。实际就是套接字类型(AF_INET, AF_UNIX等)

  • sin_port:表示端口号,类型uint16_t。(填充时需要转网络序列)

  • sin_addr.s_addr:表示IP地址,类型uint32_t。(填充时需要将字符串转4字节整形,并转为网络序列)

  • 剩下的字段不关注

端口号不能随意绑定

需要注意的是,不是所有的端口号都能成功绑定:如0~1023号端口被系统保留用于一些特定的服务和应用程序(系统端口),不允许绑定。还有一些熟知端口同样也不要进行绑定。

不建议服务器绑定特定的IP地址

首先,云服务器是不支持的绑定公网IP的;如果使用虚拟机或者独立Linux系统,那么IP地址是支持绑定的。

实际上,一款网络服务器,不建议指明绑定一个IP,上面的服务端指定绑定一个IP是错误的用法

比如你运行服务端的机器上有多个网卡,意味着你的服务端上有多个IP, 一台服务器上端口号为8080的服务只有一个。这台服务器在接收数据时,底层的多张网卡(多个IP)都有可能接收到数据,而这些数据也都是要向上递送给8080服务的。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据,而来自其他IP的数据是接收不到的。

推荐服务器绑定的IP是:INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定。绑定了该IP,只要是发送给端口号为8080的服务的数据,不管来自主机上的哪张网卡,哪个IP系统都会可以将数据自底向上全部递送给服务端。

img

网络序列与主机序列之间的转换

#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);

字符串IP和整型IP之间的转换

头文件:
        #include<sys/types.h>
        #include <arpa/inet.h>
        #include <netinet/in.h>    
函数原型:
        // 点分十进制字符串 --> 网络整型
        // 分两步:1.字符串转整型 2.主机转网络
        
        in_addr_t inet_addr(const char *cp);  //最简单
        
        int inet_aton(const char* cp,  struct in_addr* inp);  //注意第二个参数传in_addr结构的地址即&sin_addr
        
        int inet_pton(int af, const char* src, void* dst);  //第一个参数是协议家族,后两个参数和inet_aton相同
 
        // 网络整形 --> 点分十进制字符串
        // 分两步:1.网络转主机 2.in_addr结构转字符串
        char *inet_ntoa(struct in_addr in);  //函数内部将转换后的字符串保存在静态存储区,因此该函数是不可重入函数,存在线程安全问题。
 
        const char* inet_ntop(int ar, const void* src, char* dst, socklen_t size); //多线程环境下推荐使用inet_ntop,该函数需由调用者提供缓冲区,可以规避线程安全问题。参数:1.协议家族 2.整型IP地址 3.字符串缓冲区地址 4.缓冲区大小

1.1.3 从客户端接收数据

服务端要接收客户端发送的消息,接收信息的函数是recvfrom

recvfrom函数的作用是接收信息

img

receive a message from a socket:从套接字接收消息

函数: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:数据发送方的套接字结构,从哪读
    第六个参数addrlen:src_addr结构体的长度
 
返回值:
    成功返回接收到的字节数,失败返回-1,同时错误码会被设置。对等方执行有序关闭后,返回值将为0

socklen_t 是一个32位的无符号整数

img

第五个参数src_addr:输出型参数,数据发送方的套接字结构地址,recvfrom接收到数据后将对端的套接字结构存入其中。(从哪读)

第六个参数addrlen:输入输出型参数,需要传入src_addr结构体的长度,recvfrom接收到数据后将读取到的套接字结构的大小存入其中。

我们需要定义一个套接字结构struct sockaddr并置为空,将地址强转后传给src_addr,还需要定义一个结构长度socklen_t,并初始化为套接字结构的大小,将其地址传给addrlen。如果不关注数据的来源,后两个参数可以设置为nullptr


1.1.4 服务端代码

udpServer.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
 
// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};
 
const static string defaultIp = "0.0.0.0";
const static int gnum = 1024;
 
class udpServer
{
public:
    udpServer(const uint16_t &port, const string &ip = defaultIp)
        : _port(port), _ip(ip)
    {}
 
    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字文件
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        cout << "socket success: " << _sockfd << endl;
        // 2.绑定端口
        // 2.1 填充套接字结构sockaddr_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local));  // 把 sockaddr_in结构体全部初始化为0
        local.sin_family = AF_INET;    // 未来通信采用的是网络通信
        local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
        // 绑定IP方法1:INADDR_ANY
        // local.sin_addr.s_addr = INADDR_ANY;//服务器的真实写法
        // 绑定IP方法2:把外部的构造函数传参去掉,使用我们自己定义的string defaultIp = "0.0.0.0";
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
        // 2.2 绑定
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
        if (n == -1)
        {
            cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
            exit(BIND_ERR);
        }
        // UDP server 预备工作完成
    }
 
    // 启动服务器
    void start()
    {
        // 服务器的本质就是一个死循环
        char buffer[gnum];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 接收成功
            {
                buffer[s] = 0;
                // 发消息对方的IP
                string clientip = inet_ntoa(peer.sin_addr); // 直接传sin_addr结构体,整数IP 转 字符串IP(点分十进制IP)
                // 发消息对方的端口号
                uint16_t clientport = ntohs(peer.sin_port); // ntohs:网络字节序转主机字节序
                // 发送的消息
                string message = buffer;
 
                // 打印
                cout << clientip << "[" << clientport << "]" << "# " << message << endl;
            }
        }
    }
 
    ~udpServer()
    {}
 
private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _sockfd;    // 文件描述符
};

udpServer.cc

#include "udpServer.hpp"
#include <memory>
 
// 使用手册
// ./udpServer port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " local_port\n\n";
}
 
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Uage(argv[0]);
        exit(UAGE_ERR);
    }
 
    uint16_t port = atoi(argv[1]); // string to int
    //不需要传IP了
    std::unique_ptr<udpServer> usvr(new udpServer(port));
    usvr->initServer(); // 初始化服务器
    usvr->start();      // 启动服务器
    
    return 0;
}

1.1.5 几个网络相关的命令行工具

ifconfig 命令

ifconfig:interface configuration接口配置,显示和配置网络接口的信息

第一个IP:inet 10.0.4.14,这个IP是内网IP
第二个IP: inet 127.0.0.1,这个IP是本地环回,用于本地测试
注:“inet” 是Internet Protocol(IP)的简写

img

什么是本地环回??

img

  • 所谓本地环回是指client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中
  • 通常用于本地网络服务器测试,通过本地环回测试的程序后期仍无法通信大概率是网络问题而非编码问题。
  • 本地环回地址通常是127.0.0.1

netstat 命令

netstat是一个用于显示网络连接、路由表和网络接口信息的命令行工具

netstat:network statistics网络统计

常用选项:

  • -a:all (显示所有连接和监听端口)
  • -t:tcp (仅显示TCP连接)
  • -u:udp (仅显示UDP连接)
  • -n:numeric (以数字形式显示IP地址和端口号)
  • -p:program (显示与连接关联的进程信息)
  • -l:listen(显示所有的监听端口)
  • -r:route (显示路由表信息)
  • -s:statistics (显示网络统计信息)

netstat -nuap 查看本机所有的udp连接

Foreign Address:(外部地址)是指与本地计算机建立网络连接的远程计算机的IP地址和端口号,也就是客户端连服务器

0.0.0.0:* 表示任意IP地址、任意的端口号的程序都可以访问当前进程

img


1.2 客户端

1.2.1 关于客户端的绑定问题

明确一点

  • 客户端是需要服务端的IP和端口号的,没有这些客户端就连不上服务端
  • 也就是说服务端的 IP 和端口号是不能轻易改变的,否则用户端不知道就会连不上服务端
  • 所以现在我们写的需要手动传入服务端的IP和端口号

img

关于客户端的绑定问题

  • 首先由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要绑定各自的IP地址和端口号。
  • 只不过服务端需要指定端口号进行显式地绑定,而客户端不需要显式地绑定端口号。
  • 服务器的IP+端口号需要保证唯一性、固定性和公开性,因此显示绑定端口号就是要将服务器的端口号固定下来。不管服务器重新启动多少次,端口号都不会改变。
  • 而客户端的IP+端口号只要保证唯一性就可以了,端口号不需要进行固定。如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,如果被其他程序抢占该客户端就无法联网了。
  • 总之,客户端不需要显示的绑定端口号,在首次发送数据的时候,操作系统会为该客户端进程分配空闲的端口号进行绑定,也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要客户端的端口号没有被耗尽,客户端就永远可以启动

1.2.2 向服务端发送数据

客户端要发送消息给服务端,发送消息的函数是sendto

sendto函数的作用是发送消息

img

send a message on a socket:在套接字上发送消息

函数: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);
 
参数:
    第一个参数sockfd:文件描述符,从哪个套接字去发送消息
    第二个参数buf:待写入数据的存放位置
    第三参数len:写入数据的长度
    第四个参数flags:写入方式,0代表阻塞式写入
    第五个参数dest_addr:数据接收方的套接字结构,发给谁
    第六个参数addrlen:dest_addr结构体的长度
 
返回值:
    成功返回写入的字节数,失败返回-1,同时错误码会被设置

第五个参数dest_addr:输入型参数,数据接收方的套接字结构的地址,需要提前填充。(发给谁)

第六个参数addrlen:输入型参数,dest_addr结构体的长度。

我们要做的工作就是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体地址传给第五个参数dest_addr,需要强制类型转换

在使用UDP通信的过程中,为什么发送数据时没有将主机序列转为网络序列,接收数据时没有将网络序列转为主机序列呢?
答:因为 recvfrom 和 sendto 是系统调用,这两个函数在函数内部已经帮我们做了,即主机序列转为网络序列和网络序列转为主机序列的工作


1.2.3 客户端代码

udpClient.hpp

#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <string>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;
 
class udpClient
{
public:
    udpClient(const string &serverip, const uint16_t serverport)
        : _serverip(serverip), _serverport(serverport), _sockfd(-1),  _quit(false)
    {}
 
    // 初始化客户端
    void initClient()
    {
        // 创建套接字文件
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(2);
        }
        cout << "socket success: " << _sockfd << endl;
    }
 
    // 启动客户端
    void run()
    {
        // 填充数据接收方的套接字结构(服务端)
        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());// 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
 
        string message;
        while ((!_quit))
        {
           cout << "Please Enter# ";
           cin >> message;
           sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
        }
        
    }
 
    ~udpClient()
    {}
 
private:
    uint16_t _serverport; // 端口号
    string _serverip;     // ip地址
    int _sockfd;    // 文件描述符
    bool _quit;
};

udpClient.cc

#include "udpClient.hpp"
#include <memory>
 
// 使用手册
// ./udpClient ip port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " server_ip server_port\n\n";
}
 
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Uage(argv[0]);
        exit(1);
    }
 
    // 客户端需要服务端的 IP 和 port
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]); // string to int
    std::unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));
    ucli->initClient(); // 初始化服务器
    ucli->run();        // 启动服务器
 
    return 0;
}

二、简单的远程控制程序

2.1 popen、pclose函数

原理:client向server发送shell命令,再由server调用popen执行命令,最后server将命令的执行结果返回给客户端。

popen函数
popen函数是用于创建一个子进程执行命令,并打开一个管道与该进程进行通信。该函数的原型如下:

#include <stdio.h>

FILE *popen(const char *command, const char *type);

其中,command参数是一个以null结尾的字符串,包含shell命令来执行。type参数是一个"r"或"w"的字符串,用于指定管道的方向。

popen函数返回一个文件指针(FILE*),这个指针指向由command命令生成的进程的标准输入或标准输出(重定向到管道文件)。调用popen函数会创建一个新的进程,并且返回一个文件指针,可以对其进行读或写操作。

pclose函数

pclose函数是用于关闭由popen函数打开的管道并等待子进程结束。该函数的原型如下:

#include <stdio.h>

int pclose(FILE *stream);

其中,stream参数是由popen函数返回的文件指针。pclose函数会等待子进程结束,并返回子进程的终止状态。

popen和pclose函数通常用于在一个进程中执行外部命令,并与该命令进行输入输出的交互。这对于一些需要执行外部命令的操作非常有用,比如执行shell命令并获取输出结果。


2.2 程序代码

对上一个程序的代码做一些小小的改动即可:

udpServer::Start

void Start()
    {
        char buffer[1024]; // 网络输入缓冲区
        std::string mesg;  // 回复给客户端的命令执行结果

        for (;;)
        {
            sockaddr_in client;             // 输出型参数
            socklen_t len = sizeof(client); // 输入输出型参数:输入:client缓冲器的大小,输出:实际读到的client的大小
            memset(&client, 0, sizeof(client));
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
            if (s > 0)
            {
                buffer[s] = 0;

                // 过滤非法请求
                if (strcasestr(buffer, "rm") != nullptr)
                {
                    mesg = "坏人... ";
                    mesg += buffer;
                    sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);
                    continue;
                }

                // 打印控制命令
                std::string client_ip = inet_ntoa(client.sin_addr); // 注意inet_ntoa的参数是sin_addr结构
                uint16_t client_port = ntohs(client.sin_port);
                printf("[%s:%d]# %s\n", client_ip.c_str(), client_port, buffer);

                // 执行控制命令
                FILE *fp = popen(buffer, "r");
                if (fp == nullptr)
                {
                    LogMessage(ERROR, "(%d)%s", errno, strerror(errno));
                    mesg = "error: (";
                    mesg += std::to_string(errno);
                    mesg += ")";
                    mesg += strerror(errno);
                    sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);
                    continue;
                }

                // 收集执行结果
                char ret[128]; // 临时缓冲区
                mesg.clear();
                while (fgets(ret, sizeof(ret), fp) != nullptr)
                {
                    mesg += ret;
                }
                // 一定要使用pclose关闭popen返回的文件流
                pclose(fp);
            }
            // 返回控制命令的执行结果
            sendto(_sockfd, mesg.c_str(), mesg.size(), 0, (sockaddr *)&client, len);
        }
    }

udpClient::Run

void Run()
    { 
        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());

        char buffer[1024];

        while (true)
        {
            //1.获取命令
            printf("Please Enter$ ");
            fgets(buffer, sizeof(buffer), stdin);
            buffer[strlen(buffer) - 1] = '\0'; // 删除换行符
            if (strcmp(buffer, "quit") == 0)
            {
                break;
            }
            //2.将命令发送给服务端
            // 当client首次给server发送数据时,OS会自动给client绑定IP地址和端口号
            ssize_t s = sendto(_sockfd, buffer, strlen(buffer), 0, (sockaddr *)&server, sizeof(server));
            if (s == -1)
            {
                LogMessage(ERROR, "(%d)%s", errno, strerror(errno));
                continue;
            }
            //3.接收服务端返回的执行结果
            sockaddr_in temp;
            memset(&temp, 0, sizeof(temp));
            socklen_t len = sizeof(temp);
            s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (sockaddr*)&temp, &len);
            if(s>0)
            {
                buffer[s] = 0;
                printf("server:\n%s\n", buffer);
            }
            
        }
    }

三、简单的网络聊天室程序

3.1 工作原理

  1. client启动时会自动向server发送一条login消息,此时server根据来源记录用户信息,用户上线;
  2. client将消息发送给server,server再将消息转发给所有的在线用户。
  3. 直到client退出自动向server发送一条quit消息,此时server根据来源删除对应的用户记录,用户下线。
  4. client的读写功能分离,创建两个子线程分别完成读写操作,使得消息的发送和接收功能可以并发执行。

注意:无论是多线程读还是写,使用的_sockfd都是同一个套接字文件描述符。也就是说,UDP协议是全双工的可以同时进行收发而不受干扰。这是因为底层有两个缓冲区,一个是读缓冲区,一个是写缓冲区。


3.2 程序代码

udp_server.hpp

#ifndef __UDP_SEVER_HPP__
#define __UDP_SEVER_HPP__
#include ...

class UdpServer
{
    int _sockfd;
    uint16_t _port;
    std::string _ip;
    std::unordered_map<std::string, sockaddr_in> _users; //在线用户列表

public:
    UdpServer(const std::string &ip, const uint16_t port)
        : _ip(ip),
          _port(port),
          _sockfd(-1)
    {
    }

    void InitServer()
    {
        //同第一个echo程序...
    }

    void Start()
    {
        char buffer[1024];
        char username[64];
        char msg[1024];

        for (;;)
        {
			// 接收消息
            sockaddr_in client;             // 输出型参数
            socklen_t len = sizeof(client); // 输入输出型参数:输入:client缓冲器的大小,输出:实际读到的client的大小
            memset(&client, 0, sizeof(client));
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
            if (s > 0)
            {
                buffer[s] = 0;

                std::string client_ip = inet_ntoa(client.sin_addr); // 注意inet_ntoa的参数是sin_addr结构
                uint16_t client_port = ntohs(client.sin_port);
                snprintf(username, sizeof(username), "%s:%d", client_ip.c_str(), client_port);
                snprintf(msg, sizeof(msg), "[%s]# %s", username, buffer);
                // 添加在线用户
                if (_users.find(username) == _users.end())
                {
                    LogMessage(NORMAL, "%s log in!", username);
                    _users.insert({username, client});
                }
                printf("%s\n", msg);

            }

            // 群发消息
            for (auto &user : _users)
            {
                LogMessage(NORMAL, "push message: %s", user.first.c_str());
                sendto(_sockfd, msg, sizeof(msg), 0, (sockaddr *)&user.second, sizeof(user.second));
            }
            // 用户退出,删除在线用户
            if (strcmp(buffer, "quit") == 0)
            {
                LogMessage(NORMAL, "%s log out!", username);
                _users.erase(username);
            }
        }
    }

    ~UdpServer()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }
};

#endif

udp_client.cc

#include ...

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

    std::string serverip = "43.143.194.141";
    uint16_t serverport = 8080;
    //创建客户端对象
    UdpClient udpcli(serverip, serverport);
    udpcli.InitClient();
    //创建两个子线程分别完成读写操作,使得消息的发送和接收功能可以并发执行。
    std::thread t1(&UdpClient::SendMsg, &udpcli);
    std::thread t2(&UdpClient::RecvMsg, &udpcli);
    t1.join();
    t2.join();
    return 0;
}

udp_client.hpp

#pragma once

#include ...

class UdpClient
{
    int _sockfd;
    std::string _serverip;
    uint16_t _serverport;
    bool _quit;

public:
    UdpClient(const std::string &ip, const uint16_t port)
        : _serverip(ip),
          _serverport(port),
          _sockfd(-1),
          _quit(false)
    {
    }

    void InitClient()
    {
        //同第一个echo程序...
    }

    void SendMsg()
    {
        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());
        char buffer[1024] = "login";
        bool first = true; //client启动时会自动向server发送一条login消息

        while (!_quit)
        {
            if (!first)
            {
                printf("Please Enter$ ");
                fgets(buffer, sizeof(buffer), stdin);
                buffer[strlen(buffer) - 1] = '\0'; // 删除换行符
            }
            first = false;
            if (strcmp(buffer, "quit") == 0)
            {
                _quit = true;
            }
            if (strlen(buffer) < 1)
                continue;
            // 当client首次给server发送数据时,OS会自动给client绑定IP地址和端口号
            ssize_t s = sendto(_sockfd, buffer, strlen(buffer), 0, (sockaddr *)&server, sizeof(server));
            if (s == -1)
            {
                LogMessage(ERROR, "(%d)%s", errno, strerror(errno));
                continue;
            }
        }
    }

    void RecvMsg()
    {
        char buffer[1024];

        while (!_quit)
        {
            // sockaddr_in temp;
            // memset(&temp, 0, sizeof(temp));
            // socklen_t len = sizeof(temp);
            // ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&temp, &len);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
            if (s > 0)
            {
                buffer[s] = 0;
                fprintf(stderr, "%s\n"); //将接收的的消息打印到标准错误
            }
        }
    }

    ~UdpClient()
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }
};

为了分屏进行输入和输出,可以将标准错误(2号文件)重定向到其他终端进行显示

在这里插入图片描述

提示:可以在/dev/pts目录下查看其他打开的终端(字符设备文件)

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

芥末虾

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

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

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

打赏作者

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

抵扣说明:

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

余额充值