0


【网络】套接字编程——UDP通信

作者:დ旧言~
座右铭:松树千年终是朽,槿花一日自为荣。

目标:UDP网络服务器简单模拟实现。

毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

专栏选自:网络

望小伙伴们点赞👍收藏✨加关注哟💕💕

​​

一、前言

前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!

** 二主体**

学习【网络】套接字编程——UDP通信咱们按照下面的图解:

2.1 概述

  1. 在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质的差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
  2. 以下给出了典型的UDP客户/服务器的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户端的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将接受与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应的发送给正确的客户。

2.2 Udp Server(服务端)

接下来接下来实现一批基于

  1. UDP

协议的网络程序,本节只介绍基于IPv4的socket网络编程。

2.2.1 核心功能

说明:

分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于

  1. echo

指令,该程序的核心在于 **使用

  1. socket

套接字接口,以

  1. UDP

协议的方式实现简单网络通信。**

2.2.2 核心结构

程序由server.hpp server.cc client.hpp client.cc 组成,大体框架如下:

**创建

  1. server.hpp

服务器头文件:**

  1. #pragma once
  2. #include <iostream>
  3. namespace nt_server
  4. {
  5. class UdpServer
  6. {
  7. public:
  8. // 构造
  9. UdpServer()
  10. {}
  11. // 析构
  12. ~UdpServer()
  13. {}
  14. // 初始化服务器
  15. void InitServer()
  16. {}
  17. // 启动服务器
  18. void StartServer()
  19. {}
  20. private:
  21. // 字段
  22. };
  23. }

**创建

  1. server.cc

服务器源文件:**

  1. #include <memory> // 智能指针相关头文件
  2. #include "server.hpp"
  3. using namespace std;
  4. using namespace nt_server;
  5. int main()
  6. {
  7. unique_ptr<UdpServer> usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象
  8. // 初始化服务器
  9. usvr->InitServer();
  10. // 启动服务器
  11. usvr->StartServer();
  12. return 0;
  13. }

**创建

  1. client.hpp

客户端头文件:**

  1. #pragma once
  2. #include <iostream>
  3. namespace nt_client
  4. {
  5. class UdpClient
  6. {
  7. public:
  8. // 构造
  9. UdpClient()
  10. {}
  11. // 析构
  12. ~UdpClient()
  13. {}
  14. // 初始化客户端
  15. void InitClient()
  16. {}
  17. // 启动客户端
  18. void StartClient()
  19. {}
  20. private:
  21. // 字段
  22. };
  23. }

**创建

  1. client.cc

客户端源文件:**

  1. #include <memory>
  2. #include "client.hpp"
  3. using namespace std;
  4. using namespace nt_client;
  5. int main()
  6. {
  7. unique_ptr<UdpClient> usvr(new UdpClient());
  8. // 初始化客户端
  9. usvr->InitClient();
  10. // 启动客户端
  11. usvr->StartClient();
  12. return 0;
  13. }

**创建

  1. Makefile

文件:**

  1. .PHONY:all
  2. all:server client
  3. server:server.cc
  4. g++ -o $@ $^ -std=c++11
  5. client:client.cc
  6. g++ -o $@ $^ -std=c++11
  7. .PHONY:clean
  8. clean:
  9. rm -rf server client

2.2.3 Udp Server 端代码


2.2.3.1 socket - 创建套接字

语法:

  1. #include <sys/types.h> /* See NOTES */
  2. #include <sys/socket.h>
  3. int socket(int domain, int type, int protocol);

解释说明:

  • domain:协议域(协议族)。该参数决定了 socket 的地址类型。在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 地址(32位的)与端口号(16位)的组合,AF_UNIX 决定了要用一个绝对路径名作为地址,AF_INET6(IPv6)。
  • type:指定了 socket 的类型,如 SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)等等。
  • protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
  • 返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。

功能说明:

  • socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。
  • 好了socket函数学完了,接下来在 server.hpp 的 InitServer() 函数中创建套接字,并对创建成功/失败后的结果做打印。

代码呈现:

  1. #pragma once
  2. #include <iostream>
  3. #include <cstring>
  4. #include <cerrno>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. #include <sys/socket.h>
  8. namespace nt_server
  9. {
  10. // 错误码
  11. enum
  12. {
  13. SOCKET_ERR = 1
  14. };
  15. class UdpServer
  16. {
  17. public:
  18. // 构造
  19. UdpServer()
  20. {}
  21. // 析构
  22. ~UdpServer()
  23. {}
  24. // 初始化服务器
  25. void InitServer()
  26. {
  27. // 1.创建套接字
  28. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  29. if(sock_ == -1)//创建失败
  30. {
  31. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  32. exit(SOCKET_ERR);
  33. }
  34. // 创建成功
  35. std::cout << "Create Success Socket: " << sock_ << std::endl;
  36. }
  37. // 启动服务器
  38. void StartServer()
  39. {}
  40. private:
  41. int sock_; // 套接字
  42. };
  43. }

总结说明:

因为这里是使用 UDP 协议实现的 网络通信,参数1 domain 选择 AF_INET(基于 IPv4 标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3设置为 0,可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议。

2.2.3.2 bind - 将套接字与一个 IP 和端口号进行绑定

语法:

  1. #include <sys/types.h> /* See NOTES */
  2. #include <sys/socket.h>
  3. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

解释说明:

  • sockfd: 通过socket函数得到的文件描述符
  • addr: 需要绑定的socket地址,这个地址封装了ip和端口号的信息
  • addrlen: 第二个参数结构体占的内存大小

详细了解一下这个sockaddr_in结构体:

  1. struct sockaddr_in {
  2. short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
  3. unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
  4. struct in_addr sin_addr; // 4 字节 ,32位IP地址
  5. char sin_zero[8]; // 8 字节 ,不使用
  6. };
  7. struct in_addr {
  8. unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
  9. };

获得一个干净可用的 sockaddr_in 结构体后,可以正式绑定 IP 地址 和 端口号了。

代码呈现:

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cerrno>
  6. #include <cstdlib>
  7. #include <strings.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. namespace nt_server
  13. {
  14. // 退出码
  15. enum
  16. {
  17. SOCKET_ERR = 1,
  18. BIND_ERR
  19. };
  20. // 端口号默认值
  21. const uint16_t default_port = 8888;
  22. class UdpServer
  23. {
  24. public:
  25. // 构造
  26. UdpServer(const std::string ip, const uint16_t port = default_port)
  27. :port_(port), ip_(ip)
  28. {}
  29. // 析构
  30. ~UdpServer()
  31. {}
  32. // 初始化服务器
  33. void InitServer()
  34. {
  35. // 1.创建套接字
  36. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  37. if(sock_ == -1)
  38. {
  39. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  40. exit(SOCKET_ERR);
  41. }
  42. // 创建成功
  43. std::cout << "Create Success Socket: " << sock_ << std::endl;
  44. // 2.绑定IP地址和端口号
  45. struct sockaddr_in local;
  46. bzero(&local, sizeof(local)); // 置0
  47. // 填充字段
  48. local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  49. local.sin_port = htons(port_); // 主机序列转为网络序列
  50. local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
  51. // 绑定IP地址和端口号
  52. int n = bind(sock_, (const sockaddr*)&local, sizeof(local));
  53. if(n<0)
  54. {
  55. std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
  56. exit(BIND_ERR);
  57. }
  58. // 绑定成功
  59. std::cout << "Bind IP&&Port Success" << std::endl;
  60. }
  61. // 启动服务器
  62. void StartServer()
  63. {}
  64. private:
  65. int sock_; // 套接字
  66. uint16_t port_; // 端口号
  67. std::string ip_; // IP地址(后面需要删除)
  68. };
  69. }

总结说明:

  • 端口号会在网络里互相转发,需要把主机序列转换为网络序列,可以使用 htons 函数。
  • 需要把点分十进制的字符串,转换为无符号短整数,可以使用 inet_addr 函数,这个函数在进行转换的同时,会将主机序列转换为网络序列(因为IP地址需要在网络里面发送)。
  • 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失。
2.2.3.3 地址转换函数 - 字符串和struct in_addr互相转换

我们这里为什么要使用字符串来表示IP地址:

首先大部分用户习惯使用的IP是点分十进制的字符串,就像下面这个这样。

  1. 128.11.3.31

我们的IP地址就是下面的第3个成员:

  1. struct sockaddr_in {
  2. short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
  3. unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
  4. struct in_addr sin_addr; // 4 字节 ,32位IP地址
  5. char sin_zero[8]; // 8 字节 ,不使用
  6. };
  7. struct in_addr {
  8. unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
  9. };

点分十进制的IP地址不好输入,我们往往先用更好输入的字符串来存储IP地址,然后将字符串版的IP地址转换为struct in_addr版的IP地址(也就是点分十进制版的)。

**字符串转

  1. in_addr

结构体:**

语法:

  1. #include <arpa/inet.h>
  2. in_addr_t inet_addr(const char *cp);

说明:

  • 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型,通常用于填充sin_addr.s_addr字段。
  • ** 这个函数是更通用的函数,支持IPv4和IPv6地址的转换。**第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。第二个参数 src 是输入的字符串表示的IP地址,第三个参数 dst 是输出的二进制表示的IP地址。

使用:

  1. #include <arpa/inet.h>
  2. struct in_addr ipv4Address;
  3. const char *ipString = "192.168.1.1";
  4. inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));

in_addr 结构体转字符串:

语法:

  1. #include <arpa/inet.h>
  2. char *inet_ntoa(struct in_addr in);

说明:

  • 第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。
  • 第二个参数 src 是输入的二进制表示的IP地址。
  • 第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。

使用:

  1. #include <arpa/inet.h>
  2. struct in_addr ipv4Address;
  3. ipv4Address.s_addr = inet_addr("192.168.1.1");
  4. char ipString[INET_ADDRSTRLEN];
  5. inet_ntop(AF_INET, &(ipv4Address.s_addr), ipString, INET_ADDRSTRLEN);

**我们让IP绑定到 0.0.0.0,

  1. 0.0.0.0

表示任意IP地址:**

  1. #pragma once
  2. //.....
  3. namespace nt_server
  4. {
  5. // 退出码
  6. enum
  7. {
  8. SOCKET_ERR = 1,
  9. BIND_ERR
  10. };
  11. // 端口号默认值
  12. const uint16_t default_port = 8888;
  13. const std::string="0.0.0.0";//注意这里
  14. class UdpServer
  15. {
  16. public:
  17. // 构造
  18. UdpServer(const std::string ip=defaultip, const uint16_t port = default_port)
  19. :port_(port), ip_(ip)
  20. {}
  21. // 析构
  22. ~UdpServer()
  23. {}
  24. // 初始化服务器
  25. void InitServer()
  26. {
  27. //。。。。
  28. // 2.绑定IP地址和端口号
  29. struct sockaddr_in local;
  30. bzero(&local, sizeof(local)); // 置0
  31. // 填充字段
  32. local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  33. local.sin_port = htons(port_); // 主机序列转为网络序列
  34. local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
  35. //。。。。
  36. }
  37. // 启动服务器
  38. void StartServer()
  39. {}
  40. private:
  41. int sock_; // 套接字
  42. uint16_t port_; // 端口号
  43. std::string ip_; // IP地址
  44. };
  45. }

**

  1. server.cc

服务器源文件:**

  1. #include <memory> // 智能指针相关头文件
  2. #include "server.hpp"
  3. using namespace std;
  4. using namespace nt_server;
  5. int main()
  6. {
  7. unique_ptr<UdpServer> usvr(new UdpServer());
  8. // 初始化服务器
  9. usvr->InitServer();
  10. // 启动服务器
  11. usvr->StartServer();
  12. return 0;
  13. }
2.2.3.3 recvfrom - 从服务器的套接字里读取数据

语法:

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

解释说明:

  • buf:接收缓冲区。
  • len:接收缓冲区的大小。
  • flags:默认设置为 0,表示阻塞。
  • src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是 udp 网络通信,所以这里传入的还是 struct sockaddr_in 类型的对象地址。
  • addrlen:这里就是 struct sockaddr_in 对象的大小。
  • 返回值:成功会返回获取到数据的字节数;失败返回 -1。

使用示例:

  1. struct sockaddr_in sender;
  2. socklen_t sender_len = sizeof(sender);
  3. char buffer[1024];
  4. int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
  5. (struct sockaddr*)&sender, &sender_len);
  6. if (bytes_received < 0) {
  7. perror("recvfrom failed");
  8. // handle error
  9. }

代码呈现:**

  1. server.hpp

服务器头文件 **

  1. namespace nt_server
  2. {
  3. // 退出码
  4. enum
  5. {
  6. SOCKET_ERR = 1,
  7. BIND_ERR
  8. };
  9. // 端口号默认值
  10. const uint16_t default_port = 8888;
  11. class UdpServer
  12. {
  13. //.....
  14. // 启动服务器
  15. void StartServer()
  16. {
  17. // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
  18. char buff[1024]; // 缓冲区
  19. while (true)
  20. {
  21. // 1. 接收消息
  22. struct sockaddr_in peer; // 客户端结构体
  23. socklen_t len = sizeof(peer); // 客户端结构体大小
  24. // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
  25. // 传入 0 表示当前是阻塞式读取
  26. ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
  27. if (n > 0)
  28. buff[n] = '\0';
  29. else
  30. continue; // 继续读取
  31. // 2.处理数据
  32. std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
  33. uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号
  34. printf("Server get message from [%s:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
  35. // 3.回响给客户端
  36. // ...
  37. }
  38. }
  39. //.....
  40. };
  41. }
2.2.3.4 sendto - 向指定套接字中发送数据

语法:

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. 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: struct sockaddr_in 对象的大小。

使用示例:

  1. struct sockaddr_in receiver;
  2. receiver.sin_family = AF_INET;
  3. receiver.sin_port = htons(12345); // Some port number
  4. inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr); // Some IP address
  5. char message[] = "Hello, World!";
  6. ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,
  7. (struct sockaddr*)&receiver, sizeof(receiver));
  8. if (bytes_sent < 0) {
  9. perror("sendto failed");
  10. // handle error
  11. }

代码呈现:**

  1. server.hpp

服务器头文件 **

  1. //。。。
  2. namespace nt_server
  3. {
  4. // 退出码
  5. enum
  6. {
  7. SOCKET_ERR = 1,
  8. BIND_ERR
  9. };
  10. // 端口号默认值
  11. const uint16_t default_port = 8888;
  12. class UdpServer
  13. {
  14. //.....
  15. // 启动服务器
  16. void StartServer()
  17. {
  18. // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
  19. char buff[1024]; // 缓冲区
  20. while (true)
  21. {
  22. // 1. 接收消息
  23. struct sockaddr_in peer; // 客户端结构体
  24. socklen_t len = sizeof(peer); // 客户端结构体大小
  25. // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
  26. // 传入 0 表示当前是阻塞式读取
  27. ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
  28. if (n > 0)
  29. buff[n] = '\0';
  30. else
  31. continue; // 继续读取
  32. // 2.处理数据
  33. std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
  34. uint16_t clientPort = ntohs(peer.sin_port); // 获取端口号
  35. printf("Server get message from [%c:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
  36. // 3.回响给客户端
  37. n = sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)&peer, sizeof(peer));
  38. if (n == -1)
  39. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  40. }
  41. }
  42. //.....
  43. };
  44. }

如何证明服务器端正在运行:

可以通过

  1. Linux

中查看网络状态的指令,因为我们这里使用的是

  1. UDP

协议,所以只需要输入下面这条指令,就可以查看有哪些程序正在运行

  1. netstat -nlup

修改 sever.cc 代码:

  1. #include <memory> // 智能指针相关头文件
  2. #include "server.hpp"
  3. using namespace std;
  4. using namespace nt_server;
  5. int main()
  6. {
  7. unique_ptr<UdpServer> usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象
  8. // 初始化服务器
  9. usvr->InitServer();
  10. // 启动服务器
  11. usvr->StartServer();
  12. return 0;
  13. }
2.2.3.5 命令行参数改装服务端

上面的代码中,我们的端口号都是在代码里面指定了的,但是我们不能每次使用的时候都去修改代码吧,我们其实通过命令行参数来指定端口号

server.hpp:

  1. //....
  2. // 退出码
  3. enum
  4. {
  5. SOCKET_ERR = 1,
  6. BIND_ERR,
  7. USAGE_ERR
  8. };
  9. //.....

server.cc:

  1. #include <memory> // 智能指针相关头文件
  2. #include "server.hpp"
  3. using namespace std;
  4. using namespace nt_server;
  5. void Usage(const char* program)
  6. {
  7. cout << "Usage:" << endl;
  8. cout << "\t" << program << " ServerPort" << endl;
  9. }
  10. int main(int argc, char* argv[])
  11. {
  12. if (argc != 2)
  13. {
  14. // 错误的启动方式,提示错误信息
  15. Usage(argv[0]);
  16. return USAGE_ERR;
  17. }
  18. //命令行参数都是字符串,我们需要将其转换成对应的类型
  19. uint16_t port = stoi(argv[1]);//将字符串转换成端口号
  20. unique_ptr<UdpServer> usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象
  21. // 初始化服务器
  22. usvr->InitServer();
  23. // 启动服务器
  24. usvr->StartServer();
  25. return 0;
  26. }

2.3 Udp Client 客户端

因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。

client.hpp代码:

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cerrno>
  6. #include <cstdlib>
  7. #include <strings.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. namespace nt_client
  13. {
  14. // 退出码
  15. enum
  16. {
  17. SOCKET_ERR = 1,
  18. BIND_ERR,
  19. USAGE_ERR
  20. };
  21. class UdpClient
  22. {
  23. public:
  24. // 构造
  25. UdpClient(const std::string& ip, uint16_t port)
  26. :server_ip_(ip), server_port_(port)
  27. {}
  28. // 析构
  29. ~UdpClient()
  30. {}
  31. // 初始化客户端
  32. void InitClient()
  33. {
  34. // 1.创建套接字
  35. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  36. if(sock_ == -1)
  37. {
  38. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  39. exit(SOCKET_ERR);
  40. }
  41. std::cout << "Create Success Socket: " << sock_ << std::endl;
  42. // 2.构建服务器的 sockaddr_in 结构体信息
  43. bzero(&svr_, sizeof(svr_));
  44. svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  45. svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
  46. svr_.sin_port = htons(server_port_); // 绑定服务器端口号
  47. }
  48. // 启动客户端
  49. void StartClient()
  50. {
  51. char buff[1024];
  52. while(true)
  53. {
  54. // 1.发送消息
  55. std::string msg;
  56. std::cout << "Input Message# ";
  57. std::getline(std::cin, msg);
  58. ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
  59. if(n == -1)
  60. {
  61. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  62. continue; // 重新输入消息并发送
  63. }
  64. // 2.接收消息
  65. socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
  66. n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
  67. if(n > 0)
  68. buff[n] = '\0';
  69. else
  70. continue;
  71. // 可以再次获取IP地址与端口号
  72. std::string ip = inet_ntoa(svr_.sin_addr);
  73. uint16_t port = ntohs(svr_.sin_port);
  74. printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
  75. }
  76. }
  77. private:
  78. std::string server_ip_; // 服务器IP地址
  79. uint16_t server_port_; // 服务器端口号
  80. int sock_;
  81. struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
  82. };
  83. }

client.cc代码:

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cerrno>
  6. #include <cstdlib>
  7. #include <strings.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. namespace nt_client
  13. {
  14. // 退出码
  15. enum
  16. {
  17. SOCKET_ERR = 1,
  18. BIND_ERR,
  19. USAGE_ERR
  20. };
  21. class UdpClient
  22. {
  23. public:
  24. // 构造
  25. UdpClient(const std::string& ip, uint16_t port)
  26. :server_ip_(ip), server_port_(port)
  27. {}
  28. // 析构
  29. ~UdpClient()
  30. {}
  31. // 初始化客户端
  32. void InitClient()
  33. {
  34. // 1.创建套接字
  35. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  36. if(sock_ == -1)
  37. {
  38. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  39. exit(SOCKET_ERR);
  40. }
  41. std::cout << "Create Success Socket: " << sock_ << std::endl;
  42. // 2.构建服务器的 sockaddr_in 结构体信息
  43. bzero(&svr_, sizeof(svr_));
  44. svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  45. svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
  46. svr_.sin_port = htons(server_port_); // 绑定服务器端口号
  47. }
  48. // 启动客户端
  49. void StartClient()
  50. {
  51. char buff[1024];
  52. while(true)
  53. {
  54. // 1.发送消息
  55. std::string msg;
  56. std::cout << "Input Message# ";
  57. std::getline(std::cin, msg);
  58. ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
  59. if(n == -1)
  60. {
  61. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  62. continue; // 重新输入消息并发送
  63. }
  64. // 2.接收消息
  65. socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
  66. n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
  67. if(n > 0)
  68. buff[n] = '\0';
  69. else
  70. continue;
  71. // 可以再次获取IP地址与端口号
  72. std::string ip = inet_ntoa(svr_.sin_addr);
  73. uint16_t port = ntohs(svr_.sin_port);
  74. printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
  75. }
  76. }
  77. private:
  78. std::string server_ip_; // 服务器IP地址
  79. uint16_t server_port_; // 服务器端口号
  80. int sock_;
  81. struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
  82. };
  83. }

2.4 简易公共聊天室

server.hpp代码:

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <functional>//注意这个
  5. #include <cstring>
  6. #include <cerrno>
  7. #include <cstdlib>
  8. #include <strings.h>
  9. #include <sys/types.h>
  10. #include <sys/socket.h>
  11. #include <netinet/in.h>
  12. #include <arpa/inet.h>
  13. #include<unordered_map>
  14. namespace nt_server
  15. {
  16. // 退出码
  17. enum
  18. {
  19. SOCKET_ERR = 1,
  20. BIND_ERR,
  21. USAGE_ERR
  22. };
  23. // 端口号默认值
  24. const uint16_t default_port = 8888;
  25. using func_t = std::function<std::string(std::string)>;
  26. // 可以简单的理解为func_t是一个参数为string,返回值同样为string的函数的类型
  27. class UdpServer
  28. {
  29. public:
  30. // 构造
  31. UdpServer(const func_t& func, uint16_t port = default_port)//注意这里的func_t
  32. :port_(port)
  33. ,serverHandle_(func)
  34. //注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string,返回值同样为string的函数的类型
  35. {}
  36. // 析构
  37. ~UdpServer()
  38. {}
  39. // 初始化服务器
  40. void InitServer()
  41. {
  42. // 1.创建套接字
  43. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  44. if(sock_ == -1)
  45. {
  46. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  47. exit(SOCKET_ERR);
  48. }
  49. // 创建成功
  50. std::cout << "Create Success Socket: " << sock_ << std::endl;
  51. // 2.绑定IP地址和端口号
  52. struct sockaddr_in local;
  53. bzero(&local, sizeof(local)); // 置0
  54. // 填充字段
  55. local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  56. local.sin_port = htons(port_); // 主机序列转为网络序列
  57. // local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
  58. local.sin_addr.s_addr = INADDR_ANY; // 绑定任何可用IP地址
  59. // 绑定IP地址和端口号
  60. if(bind(sock_, (const sockaddr*)&local, sizeof(local)))
  61. {
  62. std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
  63. exit(BIND_ERR);
  64. }
  65. // 绑定成功
  66. std::cout << "Bind IP&&Port Success" << std::endl;
  67. }
  68. //检测是不是新用户
  69. void CheckUser(const struct sockaddr_in & client,const std::string clientIp_,uint16_t clientPort_)
  70. {
  71. auto iter= online_user_.find(clientIp_);
  72. if(iter==online_user_.end())
  73. {
  74. online_user_.insert({clientIp_,client});
  75. std::cout<<"["<<clientIp_<<":"<<clientPort_<<"] add to oniline user."<<std::endl;
  76. }
  77. }
  78. //广播给所有人
  79. void Broadcast(const std::string& respond,const std::string clientIp_,uint16_t clientPort_)
  80. {
  81. for(const auto&usr :online_user_)
  82. {
  83. std::string message="[";
  84. message+=clientIp_;
  85. message+=" : ";
  86. message+=std::to_string(clientPort_);
  87. message+="]#";
  88. message+=respond;
  89. int z = sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)&usr.second, sizeof(usr.second));
  90. if(z == -1)
  91. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  92. }
  93. }
  94. // 启动服务器
  95. void StartServer()
  96. {
  97. // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
  98. char buff[1024]; // 缓冲区
  99. while(true)
  100. {
  101. // 1. 接收消息
  102. struct sockaddr_in client; // 客户端结构体
  103. socklen_t len = sizeof(client); // 客户端结构体大小
  104. // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
  105. // 传入 0 表示当前是阻塞式读取
  106. ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&client, &len);
  107. if(n > 0)
  108. buff[n] = '\0';
  109. else
  110. continue; // 继续读取
  111. // 2.处理数据
  112. std::string clientIp = inet_ntoa(client.sin_addr); // 获取用户的IP地址
  113. uint16_t clientPort = ntohs(client.sin_port); // 获取端口号
  114. //2.1.判断是不是新用户,如果是就加入,如果不是就什么也不做
  115. CheckUser(client,clientIp,clientPort);
  116. //2.2 对数据进行业务处理,并获取业务处理后的结果
  117. std::string respond = serverHandle_(buff);
  118. //特别注意这里,业务处理的代码已经放到了这个serverHandle_这个函数了,
  119. // 3.回响给所有在线客户端
  120. Broadcast(respond,clientIp,clientPort);
  121. }
  122. }
  123. private:
  124. int sock_; // 套接字
  125. uint16_t port_; // 端口号
  126. func_t serverHandle_; // 业务处理函数(回调函数)
  127. std::unordered_map<std::string,struct sockaddr_in> online_user_;//在线用户列表
  128. };
  129. }

server.cc代码:

  1. #include <string>
  2. #include <vector>
  3. #include <memory> // 智能指针相关头文件
  4. #include <cstdio>
  5. #include "server.hpp"
  6. using namespace std;
  7. using namespace nt_server;
  8. //业务处理函数
  9. std::string ExecCommand(const std::string& request)
  10. {
  11. return request;
  12. }
  13. void Usage(const char* program)
  14. {
  15. cout << "Usage:" << endl;
  16. cout << "\t" << program << " ServerPort" << endl;
  17. }
  18. int main(int argc, char* argv[])
  19. {
  20. if (argc != 2)
  21. {
  22. // 错误的启动方式,提示错误信息
  23. Usage(argv[0]);
  24. return USAGE_ERR;
  25. }
  26. //命令行参数都是字符串,我们需要将其转换成对应的类型
  27. uint16_t port = stoi(argv[1]);//将字符串转换成端口号
  28. unique_ptr<UdpServer> usvr(new UdpServer(ExecCommand,port));
  29. // 初始化服务器
  30. usvr->InitServer();
  31. // 启动服务器
  32. usvr->StartServer();
  33. return 0;
  34. }

client.hpp代码:

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cerrno>
  6. #include <cstdlib>
  7. #include <strings.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <pthread.h>
  12. #include <unistd.h>
  13. #include <arpa/inet.h>
  14. #include<functional>
  15. namespace nt_client
  16. {
  17. // 退出码
  18. enum
  19. {
  20. SOCKET_ERR = 1,
  21. BIND_ERR,
  22. USAGE_ERR
  23. };
  24. class UdpClient
  25. {
  26. public:
  27. // 构造
  28. UdpClient(const std::string &ip, uint16_t port)
  29. : server_ip_(ip), server_port_(port)
  30. {
  31. }
  32. // 析构
  33. ~UdpClient()
  34. {
  35. }
  36. static void *send_message(void *argc)//传进来的是this指针
  37. {
  38. UdpClient*_this =(UdpClient*)argc;//强制转换为类指针
  39. char buff[1024];
  40. while (1)
  41. {
  42. // 1.发送消息
  43. std::string msg;
  44. std::cout << "Input Message# ";
  45. std::getline(std::cin, msg);
  46. ssize_t n = sendto(_this->sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)&_this->svr_, sizeof(_this->svr_));
  47. if (n == -1)
  48. {
  49. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  50. continue; // 重新输入消息并发送
  51. }
  52. }
  53. return (void*)0;
  54. }
  55. static void *recv_message(void *argc)//传进来的是this指针
  56. {
  57. UdpClient*_this =(UdpClient*)argc;
  58. char buff[1024];
  59. while (1)
  60. {
  61. // 2.接收消息
  62. socklen_t len = sizeof(_this->svr_); // 创建一个变量,因为接下来的参数需要传左值
  63. int n = recvfrom(_this->sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&_this->svr_, &len);
  64. if (n > 0)
  65. buff[n] = '\0';
  66. else
  67. continue;
  68. // 可以再次获取IP地址与端口号
  69. std::string ip = inet_ntoa(_this->svr_.sin_addr);
  70. uint16_t port = ntohs(_this->svr_.sin_port);
  71. printf("Client get message from [%s:%d]# %s\n", ip.c_str(), port, buff);
  72. }
  73. return (void*)0;
  74. }
  75. // 初始化客户端
  76. void InitClient()
  77. {
  78. // 1.创建套接字
  79. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  80. if (sock_ == -1)
  81. {
  82. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  83. exit(SOCKET_ERR);
  84. }
  85. std::cout << "Create Success Socket: " << sock_ << std::endl;
  86. // 2.构建服务器的 sockaddr_in 结构体信息
  87. bzero(&svr_, sizeof(svr_));
  88. svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  89. svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
  90. svr_.sin_port = htons(server_port_); // 绑定服务器端口号
  91. }
  92. // 启动客户端
  93. void StartClient()
  94. {
  95. pthread_t recv, sender;
  96. //只需将类内的线程函数变化为static函数,但是static函数就不能直接访问到我们类内的私有数据,
  97. //不要忘了这个线程函数的void*参数,我们可以把this指针作为参数传给它,
  98. //然后就能通过这个this指针来访问到我们的类内的私有数据
  99. pthread_create(&recv, nullptr, recv_message, (void*)this);
  100. pthread_create(&sender, nullptr, send_message, (void*)this);
  101. pthread_join(recv,nullptr);
  102. pthread_join(sender,nullptr);
  103. }
  104. private:
  105. std::string server_ip_; // 服务器IP地址
  106. uint16_t server_port_; // 服务器端口号
  107. int sock_;//套接字描述符
  108. struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
  109. };
  110. }

client.cc保持不变

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <cstring>
  5. #include <cerrno>
  6. #include <cstdlib>
  7. #include <strings.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <netinet/in.h>
  11. #include <arpa/inet.h>
  12. namespace nt_client
  13. {
  14. // 退出码
  15. enum
  16. {
  17. SOCKET_ERR = 1,
  18. BIND_ERR,
  19. USAGE_ERR
  20. };
  21. class UdpClient
  22. {
  23. public:
  24. // 构造
  25. UdpClient(const std::string& ip, uint16_t port)
  26. :server_ip_(ip), server_port_(port)
  27. {}
  28. // 析构
  29. ~UdpClient()
  30. {}
  31. // 初始化客户端
  32. void InitClient()
  33. {
  34. // 1.创建套接字
  35. sock_ = socket(AF_INET, SOCK_DGRAM, 0);
  36. if(sock_ == -1)
  37. {
  38. std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
  39. exit(SOCKET_ERR);
  40. }
  41. std::cout << "Create Success Socket: " << sock_ << std::endl;
  42. // 2.构建服务器的 sockaddr_in 结构体信息
  43. bzero(&svr_, sizeof(svr_));
  44. svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
  45. svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
  46. svr_.sin_port = htons(server_port_); // 绑定服务器端口号
  47. }
  48. // 启动客户端
  49. void StartClient()
  50. {
  51. char buff[1024];
  52. while(true)
  53. {
  54. // 1.发送消息
  55. std::string msg;
  56. std::cout << "Input Message# ";
  57. std::getline(std::cin, msg);
  58. ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
  59. if(n == -1)
  60. {
  61. std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
  62. continue; // 重新输入消息并发送
  63. }
  64. // 2.接收消息
  65. socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
  66. n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
  67. if(n > 0)
  68. buff[n] = '\0';
  69. else
  70. continue;
  71. // 可以再次获取IP地址与端口号
  72. std::string ip = inet_ntoa(svr_.sin_addr);
  73. uint16_t port = ntohs(svr_.sin_port);
  74. printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
  75. }
  76. }
  77. private:
  78. std::string server_ip_; // 服务器IP地址
  79. uint16_t server_port_; // 服务器端口号
  80. int sock_;
  81. struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
  82. };
  83. }

makefile代码:

  1. .PHONY:all
  2. all:server client
  3. server:server.cc
  4. g++ -o $@ $^ -std=c++11 -lpthread
  5. client:client.cc
  6. g++ -o $@ $^ -std=c++11 -lpthread
  7. .PHONY:clean
  8. clean:
  9. rm -rf server client

三、结束语** **

  1. 今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​

标签: 服务器 php 运维

本文转载自: https://blog.csdn.net/AAlykk/article/details/140567878
版权归原作者 დ旧言~ 所有, 如有侵权,请联系我们删除。

“【网络】套接字编程——UDP通信”的评论:

还没有评论