勿忘初心做自己 发表于 2024-6-8 02:43:29

网络编程套接字(二)之UDP服务器简单实现

目录

一、服务端UdpServer
1、udp_server.hpp
1、服务器的初始化
2、服务器的运行
2、udp_server.cc
二、客户端UdpClient
udp_client.cc
三、完整代码

一、服务端UdpServer

1、udp_server.hpp

首先,我们在该文件中,将服务器封装成一个类,而作为一款服务器,必须要有自己的端口号,同时网络服务器需要有对应的IP地址,文件描述符sock_:进行各种各样的数据通信,在类内进行读写操作。然后对外提供初始化和运行的接口。
1、服务器的初始化

我们最开始需要先将它进行初始化。初始化的第一步就是创建套接字,而创建套接字我们需要用到下面的函数。
socket:其作用就是创建套接字。
NAME
       socket - create an endpoint for communication

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

       int socket(int domain, int type, int protocol); 参数说明:
~ domain:域,用来表明套接字是进行网络通信还是本地通信,主要使用下面这两种 :AF_UNIX(本地通信)   AF_INET(网络通信)。
~ type:创建套接字时所需的服务类型。其中最常使用的是SOCK_STREAM和SOCK_DGRAM。如:UDP是数据报的网络通信形式,我们采用的就是SOCK_DGRAM(用户数据报服务),TCP是面向字节流式的网络通信,我们采用的就是SOCK_STREAM(叫做流式套接字,提供的是流式服务)。
~ protocol:创建套接字的协议类别。该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
~ 返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。
初始化的第二步就是绑定端口号和IP,我们需要用到bind函数
NAME
       bind - bind a name to a socket

SYNOPSIS
       #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,同时错误码会被设置。
在绑定时需要将服务器网络相关的属性信息填充到结构体struct addr_in当中,其结构如下:
https://img-blog.csdnimg.cn/direct/966a405ed46d4635b2ae5c8bb5420962.png
我们一般需要填充下面三个成员:
sin_family:填充AF_INET。
sin_port:表示服务器端口号,是一个16位的整数。需要注意主机序列和网络序列的转化。
sin_addr:表示服务器IP地址,是一个32位的整数。我们发现这个结构是一个结构体,所以一般是填充其中的成员:是一个32位的整数。
https://img-blog.csdnimg.cn/direct/2743463b9cfd47c294c5e09a7cbf6514.png
我们所看到的IP地址是点分十进制的,但是真正的IP地址是整数,所以我们在使用是需要将IP地址转换成系统能识别32位的整数,也需要注意主机序列和网络序列的转化,这些操作我们使用函数inet_addr可以一并实现。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in); 注:实际上,一款网络服务器不建议指明一个IP,也就是不要显示地绑定IP,因为一个服务器上可能会有多张网卡,所以IP可能不止一个,如果只绑定一个明确的IP,最终的数据可能用别的IP来访问端口号,这就无法访问,所以真实的服务器IP一般采用INADDR_ANY(全0,任意地址)代表任意地址bind 。
所以我们在填充IP地址时最好使用INADDR_ANY。
2、服务器的运行

首先,作为一款服务器,我们必须能够随时给用户提供服务,所以服务器是一个不能够退出的进程,需要使用死循环。然后不断接收从客户端发送过来的请求,进行处理,将结果返回给客户端。
我们一般使用recvform函数接收客户端发送过来的请求:
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

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

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

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

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); 参数说明:
sockfd:服务器绑定的套接字,buf:读取到特定缓冲区,len:缓冲区长度。
flags:读取的方式,默认为0,阻塞读取。
src_addr:收到除了消息本身,还得知道是谁发的,输入输出型参数,返回对应的消息内容是从哪一个客户端来的,len:src_addr大小。
返回值:返回-1表示失败,成功返回字节数
服务器收到消息,进行处理后,我们需要将结果发回给客户端,我们一般使用sendto函数:
NAME
       send, sendto, sendmsg - send a message on a socket

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

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); 2、udp_server.cc

服务端进行调用的代码逻辑:构建udpServer的对象,然后进行初始化,在进行启动起来;调用逻辑如下:
因为运行后服务器会自动绑定所有的IP,所以我们只需要绑定端口号即可。
#include <iostream>
#include "udp_server.hpp"
#include <string>
#include <memory>

static void usage(const std::string &proc)
{
    std::cout << "\nusage: " << proc << "port\n" << std::endl;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
      usage(argv);
      exit(0);
    }
    uint16_t port = atoi(argv);
    std::unique_ptr<UdpServer> usvr(new UdpServer(port));
    usvr->InitServer();
    usvr->start();

    return 0;
} 二、客户端UdpClient

udp_client.cc

客户端调用方式:./udpClient server_ip server_port,客户端想连接服务器,必须得知道服务器的IP(公网IP)以及端口号。
客户端的实现方式:创建套接字,发送请求给服务器,接收服务器返回的结果。
需要注意的是:在服务端bind的时候,最重要的不是绑定IP,而是绑定端口号,服务器需要显示地绑定端口号是为了客户端未来能够明确地找到服务器是对应的服务端进程,不能随意改变。
而客户端虽然也需要端口号,但是不重要,自己启动后有端口号就可以了,不需要显示地绑定是哪一个。因为如果不同公司的程序员在写不同软件客户端时,绑定了同一个端口号的话,就会发生冲突,所以我们在客户端不需要程序员自己去绑定端口号,而是由系统随机分配。
三、完整代码

udp_server.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <string>

class UdpServer
{
public:
    UdpServer(const uint16_t &port, std::string ip = "")
      : ip_(ip), port_(port), sock_(-1)
    {
    }

    void InitServer()
    {
      // 1.创建套接字
      sock_ = socket(AF_INET, SOCK_DGRAM, 0);
      if (sock_ < 0)
      {
            std::cerr << "创建套接字失败!" << errno << strerror(errno) << std::endl;
            exit(0);
      }

      // 2.进行绑定:绑定ip和端口号
      // 2.1 服务器的套接字填充结构
      struct sockaddr_in local_server;
      bzero(&local_server, sizeof(local_server));
      local_server.sin_family = AF_INET;
      local_server.sin_port = htons(port_);                                             // 端口号需要从主机转网络
      local_server.sin_addr.s_addr = ip_.empty() ? INADDR_ANY : inet_addr(ip_.c_str()); // ip地址需要主机转网络,并且要转化成4字节形式
      int len = sizeof(local_server);
      if (bind(sock_, (struct sockaddr *)&local_server, len))
      {
            std::cout << "绑定失败!" << errno << strerror(errno) << std::endl;
            exit(1);
      }
      std::cout << "服务器初始化完成!" << std::endl;
    }

    void start()
    {
      std::cout << "服务器运行成功!" << std::endl;
      char buffer;
      char result;
      std::string server_echo;
      // 服务器不能停下来,除非挂掉——死循环
      for (;;)
      {
            struct sockaddr_in src_client; // 服务器收到的客户端来源
            socklen_t len = sizeof(src_client);
            ssize_t s = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&src_client, &len);
            if (s < 0)
            {
                std::cout << "接收消息失败!" << std::endl;
            }
            buffer = 0;
            //std::string src_client_ip = inet_ntoa(src_client.sin_addr);
            //uint16_t src_client_port = ntohs(src_client.sin_port);
            //std::cout << "[" << src_client_ip << "," << src_client_port << "]: " << buffer << std::endl;


             // 客户端发过来的信息是指令,服务器将结果返回给客户端
            FILE *fp = popen(buffer, "r");
            if (fp == nullptr)
            {
                std::cout << "popen失败!" << std::endl;
                continue;
            }
            while (fgets(result, sizeof(result), fp) != nullptr)
            {
                server_echo += result;
            }

            // 服务器发回消息
            sendto(sock_, server_echo.c_str(), server_echo.size(), 0, (struct sockaddr *)&src_client, len);
            //sendto(sock_, buffer, sizeof(buffer), 0, (struct sockaddr *)&src_client, len);
      }
    }

private:
    std::string ip_; // 服务器ip地址
    uint16_t port_;// 服务器端口号
    int sock_;       // 套接字
}; udp_server.cc
#include <iostream>
#include "udp_server.hpp"
#include <string>
#include <memory>

static void usage(const std::string &proc)
{
    std::cout << "\nusage: " << proc << "port\n" << std::endl;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
      usage(argv);
      exit(0);
    }
    uint16_t port = atoi(argv);
    std::unique_ptr<UdpServer> usvr(new UdpServer(port));
    usvr->InitServer();
    usvr->start();

    return 0;
} udp_client.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <unistd.h>
#include <string>
#include <stdio.h>
#include <cstring>

static void usage(const std::string &proc)
{
    std::cout << "\nusage: "
            << proc << "port ip\n"
            << std::endl;
}

// ./udpclient ip port带上服务器的ip和端口号
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      usage(argv);
      exit(0);
    }
    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
      std::cerr << "创建套接字失败!" << errno << strerror(errno) << std::endl;
      exit(1);
    }
    // 作为客户端,其不需要进行绑定,由os自动进行绑定
    std::string message;
    // 给哪个服务器发送消息,填充服务器信息
    struct sockaddr_in send_server;
    bzero(&send_server, sizeof(send_server));
    send_server.sin_family = AF_INET;
    send_server.sin_port = htons(atoi(argv));
    send_server.sin_addr.s_addr = inet_addr(argv);
    while (true)
    {
      // 2.从键盘获取信息
      std::cout << "请输入#";
      std::getline(std::cin, message);
      if (message == "quit")
            break;
      // 3.发送消息
      sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&send_server, sizeof(send_server));

      // 4.接收服务器的消息
      char buffer;
      struct sockaddr_in revc_server;
      bzero(&revc_server, sizeof(revc_server));
      socklen_t len = sizeof(revc_server);
      ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&revc_server, &len);
      std::cout << "服务器说:" << buffer << std::endl;
    }
    close(sock);
    return 0;
}
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 网络编程套接字(二)之UDP服务器简单实现