Linux--Socket 编程 UDP(简单的回显服务器和客户端代码)

avatar
作者
筋斗云
阅读量:0

目录

0.上篇文章

1.V1 版本 - echo server

1.1认识接口

1.2实现 V1 版本 - echo server(细节)

1.3添加的日志系统(代码)

 1.4 解析网络地址

 1.5 禁止拷贝逻辑(基类)

1.6 服务端逻辑 (代码)

1.7客户端逻辑(代码)

1.8 用例测试 


0.上篇文章

Linux--Socket编程预备-CSDN博客

1.V1 版本 - echo server


1.1认识接口

1.创建socket

        它允许不同计算机或同一计算机上的不同进程之间进行数据交换。Socket编程基于客户端-服务器模型,其中服务器监听来自客户端的连接请求,并在连接建立后交换数据。

  1. socket():创建一个新的socket。
    • 原型:int socket(int domain, int type, int protocol);
    • 参数:
      • domain:指定socket使用的协议族,如AF_INET(IPv4)或AF_INET6(IPv6)。
      • type:指定socket的类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
      • 此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;
        后面我们再详细讨论 TCP 的一些细节问题.
        • 传输层协议
        • 有连接
        • 可靠传输(可靠性高)
        • 面向字节流
      • 此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后
        面再详细讨论.
        • 传输层协议
        • 无连接
        • 不可靠传输(但操作简单)
        • 面向数据报
      • protocol:通常设置为0,表示选择给定domain和type的默认协议。

        AF_UNIX表示本地协议族,是进行本地通信的;AF_INET表示网络协议族,是进行网络通信的。

返回值概述

  • 成功时socket()函数成功执行时,会返回一个非负整数,这个整数被称为socket的文件描述符。文件描述符是一个非负整数,用于标识打开的文件、管道、socket等I/O资源。在后续的网络通信中,会使用这个文件描述符来引用和操作这个socket。
  • 失败时:如果socket()函数执行失败,它会返回-1,并设置全局变量errno以指示错误的具体原因。errno是一个由系统定义的全局变量,用于在函数调用失败时提供错误代码。

2.bind

        络服务的 bind(绑定):在网络编程中,bind 是一个系统调用,用于将套接字(socket)与一个特定的 IP 地址和端口号关联起来。这是服务器程序启动时的常见步骤,它告诉操作系统这个套接字将监听来自特定 IP 地址和端口号的连接请求。

参数说明:

1. sockfd

  • 类型int
  • 描述:这是由socket函数返回的套接字文件描述符(socket file descriptor)。它代表了创建的套接字,是后续网络通信操作的基础。

2. addr

  • 类型const struct sockaddr *
  • 描述:这是一个指向sockaddr结构体的指针,但实际上更常用的是其派生结构体如sockaddr_in(用于IPv4)或sockaddr_in6(用于IPv6)。这个结构体包含了要绑定的IP地址和端口号信息。
    • 对于sockaddr_in结构体,它至少包含以下成员:
      • sin_family:地址族,对于IPv4,此值应为AF_INET
      • sin_port:端口号,在网络字节序中。
      • sin_addr:IP地址,也是以网络字节序表示。
    • 需要注意的是,由于bind函数要求的是sockaddr类型的指针,因此在使用sockaddr_insockaddr_in6时,需要将其地址强制转换为sockaddr*类型。

3. addrlen

  • 类型socklen_t
  • 描述:这个参数指定了addr参数所指向的地址结构体的长度(以字节为单位)。由于sockaddr是一个通用结构体,其大小可能因不同的地址族而异(尽管sockaddr本身的大小是固定的,但使用其派生结构体时,实际长度会更大),因此需要通过这个参数来明确告诉bind函数应该如何处理addr参数。

3.recvfrom收消息

        这个函数允许程序从指定的套接字(socket)接收数据,并且能够获取发送数据的源地址信息。

参数说明

  • sockfd:要接收数据的套接字文件描述符。
  • buf:指向数据缓冲区的指针,用于存储接收到的数据。
  • len:缓冲区 buf 的长度,即最大可接收的数据量。
  • flags:调用标志,通常设置为 0,但在某些情况下可以指定特殊的行为(如 MSG_DONTWAIT)。
  • src_addrsrc_addr 是一个指向 sockaddr 结构(或其特定类型,如 sockaddr_in 用于 IPv4)的指针。当 recvfrom 函数被调用时,如果成功接收到数据,发送方的地址信息(包括 IP 地址和端口号)将被填充到这个结构中。这样,接收方就可以知道数据是从哪里来的,并可能基于此信息进行后续操作,比如回复发送方。
  • addrlenaddrlen 是一个指向 socklen_t 类型变量的指针。在调用 recvfrom 之前,这个变量应该被设置为 src_addr 指向的缓冲区的大小(即 sockaddr_in 或其他 sockaddr 派生类型的大小)。recvfrom 函数在成功执行后,会通过这个指针返回实际存储在 src_addr 中的地址结构的大小。这允许调用者知道有多少字节的 src_addr 被实际使用,尽管在大多数情况下,对于特定的 sockaddr 类型(如 sockaddr_in),这个大小是固定的。

返回值

  • 成功时,recvfrom 返回接收到的字节数。
  • 如果连接被对方正常关闭,返回 0。
  • 如果发生错误,返回 -1,并设置 errno 以指示错误的原因。

4.sento发消息

        用于在网络编程中发送数据,特别是与UDP(用户数据报协议)通信时。

参数说明:

  • sockfd:套接字文件描述符,表示要发送数据的套接字。
  • buf:指向要发送数据的缓冲区的指针。
  • len:要发送数据的字节数。
  • flags:发送数据的标志位,通常设置为0。
  • dest_addr:指向目的地址结构体的指针,包括目的IP地址和端口号等信息。
  • addrlen:目的地址结构体的长度。

特点:

  1. 无连接发送sendto支持无连接的发送方式,发送方不需要与接收方建立连接即可发送数据。这使得UDP协议在实时通信、视频流传输等场景中具有优势。
  2. 灵活性sendto允许发送方指定目标地址和端口号,这使得它可以向不同的接收方发送数据,而不需要为每个接收方建立单独的连接。
  3. 可靠性:虽然UDP协议本身不提供可靠性保证(如TCP的确认和重传机制),但sendto函数可以通过分片发送数据的方式,在网络环境不稳定或拥塞的情况下,尽量保证数据的完整性和准确性。

返回值:

  • 成功时sendto函数成功执行时,其返回值是成功发送的数据的字节数。这意味着如果应用程序请求发送的数据量为N字节,并且这N字节全部被成功发送,那么sendto将返回N。
  • 失败时:如果发送操作失败,sendto函数将返回-1。此时,可以通过检查全局变量errno来获取具体的错误原因。

1.2实现 V1 版本 - echo server(细节)

        该服务器就是创建好对应的套接字,和网络信息进行绑定,服务器不断地接收和发送数据。因为我们对应的服务器和客户端都在一台机器上,127.0.0.1 是一个特殊的IP地址,被称为回环地址,当你在计算机上访问 127.0.0.1 时,实际上是在与本机上的某个服务或应用程序进行通信,而不是通过网络与其他计算机通信。

        当然如果你是使用的云服务器,你当然可以使用你服务器的ip。但云服务器上,服务端不能直接(也强烈不建议)bind自己的公网ip!因为该ip是虚拟出来的,该机器上也没有这个ip。下面这个才是你机器上的ip(内网ip)

        但是bind内网ip的话,就不会往服务器上收消息了,外部无法直接访问内网。在云服务器上,ip地址一般设置为0,那如何理解呢?这可以让服务器bind任意ip。

        一般现在的服务器只有一张网卡,绑定了一个公网ip。但如果你的服务器上有很多的ip地址,一个ip1,一个ip2,(如内网ip,回环ip)而我们上层的端口号为:8888。如果你的服务器bind ip1和8888,未来你的服务器收到各种报文都是发给8888的。有ip1的也有ip2的,但服务器只会接收ip1的报文,但如果服务器bind的ip为0,就意味着不管发送的ip是谁,只要是发给端口号为:8888的,服务器都会接收!!!(在做本地测试的时候,你也可以使用公网ip向你的服务器发消息,能不能成功,就看你的云服务器是怎么设定的了)

        因此,我们在server端就不需要ip了,bind端口号为8888就行了  


1.3添加的日志系统(代码)

LockGuard.hpp

#pragma once  #include <pthread.h>  class LockGuard { public:     LockGuard(pthread_mutex_t *mutex):_mutex(mutex)     {         pthread_mutex_lock(_mutex);     }     ~LockGuard()     {         pthread_mutex_unlock(_mutex);     } private:     pthread_mutex_t *_mutex; };

Log.hpp

#pragma once #include <iostream> #include <sys/types.h> #include <unistd.h> #include <ctime> #include <cstdarg> #include <fstream> #include <cstring> #include <pthread.h> #include "LockGuard.hpp"  namespace log_ns {      enum     {         DEBUG = 1,         INFO,         WARNING,         ERROR,         FATAL     };      std::string LevelToString(int level)     {         switch (level)         {         case DEBUG:             return "DEBUG";         case INFO:             return "INFO";         case WARNING:             return "WARNING";         case ERROR:             return "ERROR";         case FATAL:             return "FATAL";         default:             return "UNKNOWN";         }     }      std::string GetCurrTime()     {         time_t now = time(nullptr);         struct tm *curr_time = localtime(&now);         char buffer[128];         snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",                  curr_time->tm_year + 1900,                  curr_time->tm_mon + 1,                  curr_time->tm_mday,                  curr_time->tm_hour,                  curr_time->tm_min,                  curr_time->tm_sec);         return buffer;     }      class logmessage     {     public:         std::string _level;         pid_t _id;         std::string _filename;         int _filenumber;         std::string _curr_time;         std::string _message_info;     };  #define SCREEN_TYPE 1 #define FILE_TYPE 2      const std::string glogfile = "./log.txt";     pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;      // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );     class Log     {     public:         Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)         {         }         void Enable(int type)         {             _type = type;         }         void FlushLogToScreen(const logmessage &lg)         {             printf("[%s][%d][%s][%d][%s] %s",                    lg._level.c_str(),                    lg._id,                    lg._filename.c_str(),                    lg._filenumber,                    lg._curr_time.c_str(),                    lg._message_info.c_str());         }         void FlushLogToFile(const logmessage &lg)         {             std::ofstream out(_logfile, std::ios::app);             if (!out.is_open())                 return;             char logtxt[2048];             snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",                      lg._level.c_str(),                      lg._id,                      lg._filename.c_str(),                      lg._filenumber,                      lg._curr_time.c_str(),                      lg._message_info.c_str());             out.write(logtxt, strlen(logtxt));             out.close();         }         void FlushLog(const logmessage &lg)         {             // 加过滤逻辑 --- TODO              LockGuard lockguard(&glock);             switch (_type)             {             case SCREEN_TYPE:                 FlushLogToScreen(lg);                 break;             case FILE_TYPE:                 FlushLogToFile(lg);                 break;             }         }         void logMessage(std::string filename, int filenumber, int level, const char *format, ...)         {             logmessage lg;              lg._level = LevelToString(level);             lg._id = getpid();             lg._filename = filename;             lg._filenumber = filenumber;             lg._curr_time = GetCurrTime();              va_list ap;             va_start(ap, format);             char log_info[1024];             vsnprintf(log_info, sizeof(log_info), format, ap);             va_end(ap);             lg._message_info = log_info;              // 打印出来日志             FlushLog(lg);         }         ~Log()         {         }      private:         int _type;         std::string _logfile;     };      Log lg;  #define LOG(Level, Format, ...)                                        \     do                                                                 \     {                                                                  \         lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \     } while (0) #define EnableScreen()          \     do                          \     {                           \         lg.Enable(SCREEN_TYPE); \     } while (0) #define EnableFILE()          \     do                        \     {                         \         lg.Enable(FILE_TYPE); \     } while (0) };


 1.4 解析网络地址

此功能实现较为简单,请看注释:

#pragma once  #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> //网络地址 class InetAddr { private:     void ToHost(const struct sockaddr_in &addr)//网络序列转主机序列     {         _port = ntohs(addr.sin_port);//这个端口号是随机bind的         _ip = inet_ntoa(addr.sin_addr);//四字节地址转字符串     }  public:     InetAddr(const struct sockaddr_in &addr):_addr(addr)     {         ToHost(addr);     }     std::string Ip()     {         return _ip;     }     uint16_t Port()     {         return _port;     }     ~InetAddr()     {     }  private:     std::string _ip;     uint16_t _port;     struct sockaddr_in _addr;//保存一下网络序列 };


 1.5 禁止拷贝逻辑(基类)

 禁止对象之间的拷贝,而非智能指针的,因为有很多网络相关的,这样可以避免出错:

nocopy.hpp

#pragma once //禁止拷贝 class nocopy { public:     nocopy(){}     ~nocopy(){}     nocopy(const nocopy&) = delete;     const nocopy& operator=(const nocopy&) = delete; };


1.6 服务端逻辑 (代码)

UdpServer.hpp:

以下是 UdpServer 类的主要功能和组成部分:

  1. 构造函数:接收一个可选的本地端口号参数,默认为 8888。构造函数初始化了文件描述符 _sockfd 为 -1(表示无效的文件描述符),设置了本地端口号 _localport,并将服务器运行状态 _isrunning 设置为 false

  2. InitServer 方法:用于初始化服务器。这个方法首先创建一个UDP套接字,如果创建失败,则记录一条致命错误日志并退出程序。然后,它将套接字绑定到一个本地地址和端口上。如果绑定失败,同样记录一条致命错误日志并退出程序。

  3. Start 方法:用于启动服务器。这个方法将 _isrunning 设置为 true,然后进入一个循环,不断接收来自客户端的数据,处理数据,并向客户端发送响应。如果接收数据失败,它会打印一条错误消息。

  4. 析构函数:在 UdpServer 类的实例被销毁时调用。如果文件描述符 _sockfd 是有效的(即大于 -1),则关闭该套接字。

整个类使用了日志记录功能来记录关键事件,如套接字创建和绑定成功或失败。这有助于调试和监控服务器的运行状态。

        因为实现比较简单,结合逻辑和注释,理解起来较为容易:

#pragma once  #include <iostream> #include <unistd.h> #include <string> #include <cstring> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>  #include "nocopy.hpp" #include "Log.hpp" #include "InetAddr.hpp"  using namespace log_ns;  static const int gsockfd = -1; static const uint16_t glocalport = 8888;  enum {     SOCKET_ERROR = 1,     BIND_ERROR//绑定失败 };  // UdpServer user("192.1.1.1", 8899); // 一般服务器主要是用来进行网络数据读取和写入的。IO的 // 服务器IO逻辑 和 业务逻辑 解耦 class UdpServer : public nocopy//继承,禁止拷贝构造,禁止赋值,                               {                              //禁止对象之间的拷贝,而非智能指针的 public:                        //因为有很多网络相关的,这样可以避免出错     UdpServer(uint16_t localport = glocalport)         : _sockfd(gsockfd),//初始化为-1           _localport(localport),           _isrunning(false)     {     }     void InitServer()//初始化服务器     {         // 1. 创建socket文件         //AF_INET表示的是网络套接(IPv4),SOCK_DGRAM表示为UDP协议          _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//系统调用,创建套接字         if (_sockfd < 0)         {             LOG(FATAL, "socket error\n");//提示创建失败,致命错误,日志             exit(SOCKET_ERROR);         }         // 把此次套接字所对应的文件fd也写进日志         LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 把此次套接字所对应的文件fd也写进日志           // 2. bind         struct sockaddr_in local;//用于表示Internet地址,特别是IPv4地址和端口号。         memset(&local, 0, sizeof(local));//先清空再使用         local.sin_family = AF_INET;//表示你的套接字将使用IPv4协议。         local.sin_port = htons(_localport);//端口号,htons主机序列转为网络序列         // local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时         local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定          int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));//填充进入bind         if (n < 0)         {             LOG(FATAL, "bind error\n");             exit(BIND_ERROR);         }         LOG(DEBUG, "socket bind success\n");       }     void Start()//启动服务器     {         _isrunning = true;         char inbuffer[1024];         while (_isrunning)         {             struct sockaddr_in peer;             socklen_t len = sizeof(peer);             //读取数据            // (struct sockaddr *)&peer,接收客户端的套接字(知道客户端是谁)             ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);             if (n > 0)             {                 InetAddr addr(peer);                 inbuffer[n] = 0;                 //客户端发的消息                 std::cout << "[" << addr.Ip() << ":" << addr.Port() << "]# " << inbuffer << std::endl;                 std::string echo_string = "[udp_server echo] #";                 echo_string += inbuffer;//返回给客户端的内容                 sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr *)&peer, len);             }             else             {                 std::cout << "recvfrom ,  error"  << std::endl;             }         }     }     ~UdpServer()     {         if(_sockfd > gsockfd) ::close(_sockfd);     }  private:     int _sockfd;//文件描述符     //socket()函数成功执行时,会返回一个非负整数,这个整数被称为socket的文件描述符。     uint16_t _localport;//端口号     // std::string _localip; //不需要了     bool _isrunning;//服务器是否在运行 };

UdpServerMain.cc:

        服务端启动程序:

#include "UdpServer.hpp"  #include <memory>  // ./udp_server local-port // ./udp_server  8888 int main(int argc, char *argv[]) {     if(argc != 2)     {         std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;         exit(0);     }     uint16_t port = std::stoi(argv[1]);      EnableScreen();       std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port); //C++14的标准     usvr->InitServer();     usvr->Start();     return 0; }


1.7客户端逻辑(代码)

UdpClientMain.cc:

  1. 参数检查:程序首先检查用户是否提供了正确的参数数量(即服务器的IP地址和端口号)。如果参数数量不正确,程序将打印出正确的使用方式并退出。

  2. 解析参数:程序从命令行参数中解析出服务器的IP地址和端口号,并将它们存储在相应的变量中。

  3. 创建套接字:程序调用socket函数创建一个UDP套接字。如果套接字创建失败,程序将打印出错误信息并退出。

  4. 填充服务器信息:程序创建一个sockaddr_in结构体来存储服务器的地址信息,包括IP地址和端口号。这些信息将用于向服务器发送数据。

  5. 主循环:程序进入一个无限循环,等待用户输入。用户输入的字符串将被发送到服务器。

    • 发送数据:程序使用sendto函数将用户输入的字符串发送到服务器。如果发送失败,程序将打印出错误信息并退出循环。

    • 接收数据:发送数据后,程序使用recvfrom函数等待并接收来自服务器的响应。如果接收到数据,程序将打印出服务器的响应。如果接收失败,程序将打印出错误信息并退出循环。

  6. 关闭套接字:当程序退出循环时(通常是因为用户输入了某些特定的命令或发生了错误),它将关闭套接字并退出。

#include <iostream> #include <string> #include <cstring> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h>  // 客户端在未来一定要知道服务器的IP地址和端口号 // ./udp_client server-ip server-port // ./udp_client 127.0.0.1 8888 int main(int argc, char *argv[])//获取服务端的ip和端口号 {     if(argc != 3)//参数个数不对就报错用法不对     {         std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;         exit(0);     }      std::string serverip = argv[1];     uint16_t serverport = std::stoi(argv[2]);      int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//和服务端一样创建     if(sockfd < 0)     {         std::cerr << "create socket error" << std::endl;         exit(1);     }     //服务器端口号是固定的,但是客户端不是,因为客服是随机的     // client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢?     // client 需要 bind它自己的IP和端口, 但是client 不需要 “显示指明” bind它自己的IP和端口,      // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口,     //避免端口冲突      //填充服务器的相关信息     struct sockaddr_in server;//服务器的套接字信息     memset(&server, 0, sizeof(server));     server.sin_family = AF_INET;     server.sin_port = htons(serverport);//要把主机序列转为网络序列     // inet_addr把字符串形式的ip转为4字节     server.sin_addr.s_addr = inet_addr(serverip.c_str());         while(1)     {         std::string line;         std::cout << "Please Enter# ";         std::getline(std::cin, line);          // std::cout << "line message is@ " << line << std::endl;         //向服务端发消息 ,并且自动的将客户端的ip和端口号进行绑定         int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr*)&server, sizeof(server)); // 你要发送消息,你得知道你要发给谁啊!                //接收来自服务器的信息         if(n > 0)         {             //一个客户端可能会访问多个服务器,但今天我们只是测试             //一个客服端一台服务器的情况,所以我们不用考虑是谁发的,因为只能是该服务器             //下面的收端当占位符用就行了             struct sockaddr_in temp;             socklen_t len = sizeof(temp);             char buffer[1024];             int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);             if(m > 0)             {                 //收到服务器的字符串                 buffer[m] = 0;                 std::cout << buffer << std::endl;             }             else             {                 std::cout << "recvfrom error" << std::endl;                 break;             }         }         else         {             std::cout << "sendto error" << std::endl;             break;         }     }      ::close(sockfd);     return 0; }

1.8 用例测试 

如果你的云服务器支持通过公网ip访问,当然是可以通过网络跨机器进行交互的。那么就可以通过公网ip找到你的主机,通过端口号找到你对应的进程了。

 

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!