当前位置:   article > 正文

简单的UDP网络程序(后端服务器)_udp server

udp server

目录

 

准备工作

makefile

udpServer.hpp

udpServer.cc

细节1


服务端部署

创建套接字

接口认识1

socket

协议家族

绑定套接字

认识接口2

bind

sockaddr_in结构体类型

细节2

bzero

inet_addr

服务器启动(初启动)

udpServer.hpp

udpServer.cc

细节3

本地回环通信

认识指令1

netstat

细节4

代码整改

整改后代码

udpServer.hpp

udpServer.cc

认识接口3

recvfrom

参数介绍

inet_ntoa

start启动


客户端部署

认识接口4

sendto

同一台云服务器上

不同的服务器上

通信和业务逻辑解耦


全部代码

udpServer.hpp

udpServer.cc

udpClient.hpp

udpClient.cc

makefile


准备工作

这些先在Xshell上创建,后续直接使用VScode来进行编码

makefile

udpServer.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. namespace Server
  5. {
  6. class udpServer
  7. {
  8. public:
  9. udpServer()
  10. {
  11. }
  12. void initServer() //初始化
  13. {
  14. }
  15. void start() //启动
  16. {
  17. }
  18. ~udpServer() //析构
  19. {
  20. }
  21. };
  22. }

udpServer.cc

  1. #include "udpServer.hpp"
  2. #include <memory>
  3. using namespace std;
  4. using namespace Server;
  5. int main()
  6. {
  7. std::unique_ptr<udpServer> usvr(new udpServer());
  8. usvr->initServer();
  9. usvr->start();
  10. return 0;
  11. }

细节1

服务端部署

创建套接字

接口认识1

socket

参数

返回值

读写网络就像读写文件一样

初始化代码写到构造里面了,后面修改

协议家族

绑定套接字

认识接口2

bind

 这里的填充涉及到内存对齐方面,知道就好

sockaddr_in结构体类型

对类型进行了很多层的封装

细节2

从上面看到,我们可以得到一个问题:为什么库里面使用的是整型的ip地址,而我们是用string的ip地址的呢?

点分十进制和整数风格互转

bzero

往一段空间中填 0 

inet_addr

记得包含头文件

getopt

附:当我们想要做命令行解析的时候是可以用下面这个接口的,这里我们不使用(参数太少了,没必要使用)

服务器启动(初启动)

至此服务器已经可以正常启动

udpServer.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <strings.h>
  5. #include <cerrno>
  6. #include <cstring>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <arpa/inet.h>
  11. #include <netinet/in.h>
  12. namespace Server
  13. {
  14. using namespace std;
  15. static const string defaultIp = "0.0.0.0"; //TODO
  16. enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR}; //1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
  17. class udpServer
  18. {
  19. public:
  20. udpServer(const uint16_t &port, const string& ip = defaultIp): _port(port),_ip(ip),_sockfd(-1)
  21. {
  22. //注意这里是直接写在构造里面的,是写错地方了,虽然运行是没有错的,由于修改图片太麻烦,下面统一进行了修改
  23. //1.创建套接字
  24. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  25. if(_sockfd == -1)
  26. {
  27. cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
  28. exit(SOCKET_ERR); //创建套接字失败直接终止进程
  29. }
  30. //2.绑定套接字(port, ip)
  31. struct sockaddr_in local; //这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
  32. bzero(&local, sizeof(local)); //先填 0 再修正
  33. //注意这下面几个名字是拼接出来的,就是那个##拼接而来的
  34. local.sin_family = AF_INET; //这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
  35. local.sin_port = htons(_port);//你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
  36. local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1. string->unit32_t 2. htonl(); -> inet_addr
  37. int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
  38. if(n == -1)
  39. {
  40. cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
  41. exit(BIND_ERR);
  42. }
  43. //UDP Server 的预备工作完成
  44. }
  45. void initServer() // 初始化
  46. {
  47. }
  48. void start() // 启动
  49. {
  50. //服务器的本质其实就是一个死循环,
  51. for(;;)
  52. {
  53. sleep(1);
  54. }
  55. }
  56. ~udpServer() // 析构
  57. {
  58. }
  59. private:
  60. uint16_t _port;
  61. string _ip; // TODO
  62. int _sockfd;
  63. };
  64. }

udpServer.cc

  1. #include "udpServer.hpp"
  2. #include <memory>
  3. using namespace std;
  4. using namespace Server;
  5. static void Usage(string proc)
  6. {
  7. cout << "Usage:\n\t" << proc << " local_ip local_port\n\n"; //命令提示符
  8. }
  9. int main(int argc, char *argv[])
  10. {
  11. if(argc != 3) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
  12. {
  13. Usage(argv[0]);
  14. exit(USAGE_ERR);
  15. }
  16. uint16_t port = atoi(argv[2]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
  17. string ip = argv[1];
  18. std::unique_ptr<udpServer> usvr(new udpServer(port, ip));
  19. usvr->initServer();
  20. usvr->start();
  21. return 0;
  22. }

认识接口2

细节3

刚开始测试的地址是直接使用这一个

本地回环通信

127.0.0.1

认识指令1

netstat

查看网络的接口 netstat

细节4

假如绑定其他的公网IP地址需要注意

代码整改

所以一个服务器真实情况下是要接受任意ip发过来的通信,因此我们修改ip的,不需要传ip号了

整改后代码

udpServer.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <strings.h>
  5. #include <cerrno>
  6. #include <cstring>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <arpa/inet.h>
  11. #include <netinet/in.h>
  12. namespace Server
  13. {
  14. using namespace std;
  15. static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值
  16. enum
  17. {
  18. USAGE_ERR = 1,
  19. SOCKET_ERR,
  20. BIND_ERR
  21. }; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
  22. class udpServer
  23. {
  24. public:
  25. udpServer(const uint16_t &port, const string &ip = defaultIp) : _port(port), _ip(ip), _sockfd(-1)
  26. {}
  27. void initServer() // 初始化
  28. {
  29. // 1.创建套接字
  30. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  31. if (_sockfd == -1)
  32. {
  33. cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
  34. exit(SOCKET_ERR); // 创建套接字失败直接终止进程
  35. }
  36. // 2.绑定套接字(port, ip)
  37. struct sockaddr_in local; // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
  38. bzero(&local, sizeof(local)); // 先填 0 再修正
  39. // 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
  40. local.sin_family = AF_INET; // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
  41. local.sin_port = htons(_port); // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
  42. local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t 2. htonl(); -> inet_addr
  43. // local.sin_addr.s_addr = htonl(INADDR_ANY); //可以主机转网络,不够也可以不处理,直接赋值也行
  44. int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
  45. if (n == -1)
  46. {
  47. cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
  48. exit(BIND_ERR);
  49. }
  50. // UDP Server 的预备工作完成
  51. }
  52. void start() // 启动
  53. {
  54. // 服务器的本质其实就是一个死循环,
  55. for (;;)
  56. {
  57. sleep(1);
  58. }
  59. }
  60. ~udpServer() // 析构
  61. {
  62. }
  63. private:
  64. uint16_t _port;
  65. // 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
  66. string _ip;
  67. int _sockfd;
  68. };
  69. }

udpServer.cc

  1. #include "udpServer.hpp"
  2. #include <memory>
  3. using namespace std;
  4. using namespace Server;
  5. static void Usage(string proc)
  6. {
  7. cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
  8. }
  9. int main(int argc, char *argv[])
  10. {
  11. if(argc != 2) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
  12. {
  13. Usage(argv[0]);
  14. exit(USAGE_ERR);
  15. }
  16. uint16_t port = atoi(argv[1]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
  17. //string ip = argv[1];
  18. std::unique_ptr<udpServer> usvr(new udpServer(port));
  19. usvr->initServer();
  20. usvr->start();
  21. return 0;
  22. }

        注意这里是服务端是不需要IP,可以接受任意IP地址发来的请求,但是客户端是需要的,这一点后面再细谈,所以并不会造成淘宝的请求跑到京东去了

        这里为什么使用8080的因为服务器可以有很多端口号,当服务器收到了大量的数据,并不是全部都是由一个端口号来进行处理的,也可能是8081之类的端口号,这时候的端口号是没有意义的,后序会详谈,其实不同的端口号是有指定的绑定的不能任意绑定,这是因为只有我自己使用

        且无论是UDP还是TCP都是采取这样的形式,接受任意IP的数据,通过端口号来确定谁是谁处理

认识接口3

recvfrom

读取数据

参数介绍

socket_t

inet_ntoa

        将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)

 

start启动

  1. void start() // 启动
  2. {
  3. // 服务器的本质其实就是一个死循环
  4. char buffer[gnum]; // 定义一个数组来充当缓冲区
  5. for (;;)
  6. {
  7. // 读取数据
  8. struct sockaddr_in peer;
  9. socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
  10. ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
  11. // 关系两件事情
  12. // 1.数据是什么? 2. 谁发的?
  13. if (s > 0)
  14. {
  15. buffer[s] = 0;
  16. // 因为是从网络上读取的,所以一定要转,可以使用接口
  17. string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
  18. uint16_t clientport = ntohs(peer.sin_port);
  19. string message = buffer;
  20. cout << clientip << "[" << clientport << "]# " << message << endl;
  21. }
  22. }
  23. }

至此服务器端基本完成,停下来处理客户端

客户端部署

认识接口4

sendto

sendto告诉客户端要发给谁

0~1023在云服务器上已经被绑定了 

同一台云服务器上

不同的服务器上

现在无法跨主机发送消息,权限问题,后续解决

sz:下载到本地

rz:上传到服务器

chmod:修改权限

注意一台新的云服务器

至于如何打开端口后续文章介绍 -- 未完持续

通信和业务逻辑解耦

        我们可以添加function来对业务逻辑进行解耦操作,融入下面代码

        function对server通信和业务逻辑解耦!

全部代码

udpServer.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <strings.h>
  5. #include <cerrno>
  6. #include <functional>
  7. #include <cstring>
  8. #include <unistd.h>
  9. #include <sys/types.h>
  10. #include <sys/socket.h>
  11. #include <arpa/inet.h>
  12. #include <netinet/in.h>
  13. namespace Server
  14. {
  15. using namespace std;
  16. static const string defaultIp = "0.0.0.0"; // 直接使用这个缺省值
  17. static const int gnum = 1024;
  18. enum
  19. {
  20. USAGE_ERR = 1,
  21. SOCKET_ERR,
  22. BIND_ERR
  23. }; // 1.命令行输入错误 2.创建套接字错误 3.绑定端口号错误
  24. typedef function<void(string, uint16_t, string)> func_t;
  25. class udpServer
  26. {
  27. public:
  28. udpServer(const func_t &cd, const uint16_t &port, const string &ip = defaultIp)
  29. : _callback(cd), _port(port), _ip(ip), _sockfd(-1)
  30. {
  31. }
  32. void initServer() // 初始化
  33. {
  34. // 1.创建套接字
  35. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  36. if (_sockfd == -1)
  37. {
  38. cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
  39. exit(SOCKET_ERR); // 创建套接字失败直接终止进程
  40. }
  41. cout << "udpServer success: "
  42. << " : " << _sockfd << endl;
  43. // 2.绑定套接字(port, ip)
  44. // 未来服务器要明确的port, 不能随意改变 -- 变了别人就找不到了
  45. struct sockaddr_in local; // 这里是定义了一个变量,在栈上,而且是用户层,还没有bind之前都是没有产生联系
  46. bzero(&local, sizeof(local)); // 先填 0 再修正
  47. // 注意这下面几个名字是拼接出来的,就是那个##拼接而来的
  48. local.sin_family = AF_INET; // 这里设置与套接字的AF_INET设置意义是不一样的,socket是创建一个网络通信的套接字,在这里是填充一个sockaddr_in的结构体用来网络通信
  49. local.sin_port = htons(_port); // 你如果给别人发信息,你的port和ip要不要发送给对方? 答案是要的
  50. local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->unit32_t 2. htonl(); -> inet_addr
  51. // local.sin_addr.s_addr = htonl(INADDR_ANY); //可以主机转网络,不够也可以不处理,直接赋值也行
  52. int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
  53. if (n == -1)
  54. {
  55. cerr << "bin error: " << errno << " : " << strerror(errno) << endl;
  56. exit(BIND_ERR);
  57. }
  58. // UDP Server 的预备工作完成
  59. }
  60. void start() // 启动
  61. {
  62. // 服务器的本质其实就是一个死循环
  63. char buffer[gnum]; // 定义一个数组来充当缓冲区
  64. for (;;)
  65. {
  66. // 读取数据
  67. struct sockaddr_in peer;
  68. socklen_t len = sizeof(peer); // 必须设置成这个结构体的大小,当作为输入时,告诉recvfrom的长度的多少
  69. ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
  70. // 关系两件事情
  71. // 1.数据是什么? 2. 谁发的?
  72. if (s > 0)
  73. {
  74. buffer[s] = 0;
  75. // 因为是从网络上读取的,所以一定要转,可以使用接口
  76. // inet_ntoa 将一个网络字节序的IP地址(也就是结构体in_addr类型变量)转化为点分十进制的IP地址(字符串)
  77. string clientip = inet_ntoa(peer.sin_addr); // 1.网络序列 2.整数 -> 点分十进制的ip
  78. uint16_t clientport = ntohs(peer.sin_port);
  79. string message = buffer;
  80. cout << clientip << "[" << clientport << "]# " << message << endl;
  81. //我们只把数据读上来就完了吗? 我们要对数据进行处理 -- 所以我们用回调函数的方式来解决
  82. _callback(clientip, clientport, message);
  83. }
  84. }
  85. }
  86. ~udpServer() // 析构
  87. {
  88. }
  89. private:
  90. uint16_t _port;
  91. // 实际上,一款网络服务器,不建议指明一个IP,因为一个服务器可以能有多个ip,万一用户使用其他的ip地址来访问该端口号(这里是8080,就收不到了),这也是我们为什么使用0.0.0.0的IP缺省值
  92. string _ip;
  93. int _sockfd;
  94. func_t _callback; // 回调函数,用以处理数据
  95. };
  96. }

udpServer.cc

  1. #include "udpServer.hpp"
  2. #include <memory>
  3. using namespace std;
  4. using namespace Server;
  5. static void Usage(string proc)
  6. {
  7. cout << "Usage:\n\t" << proc << " local_port\n\n"; //命令提示符
  8. }
  9. void handlerMessage(string clientip, uint16_t clientport, string message)
  10. {
  11. //这里就可以对message进行特定的业务处理,而不关心message怎么来的 --- 这就是server通信和业务逻辑解耦!
  12. }
  13. int main(int argc, char *argv[])
  14. {
  15. if(argc != 2) //这里我们只想要传递两个参数,所以当argc不是3的时候就直接报错退出就行了,注意文件名运行的那个指令也会算进去所以argc +1
  16. {
  17. Usage(argv[0]);
  18. exit(USAGE_ERR);
  19. }
  20. uint16_t port = atoi(argv[1]); //使用atoi强转,因为argv里面放置的都是字符串,类型需要转换
  21. //string ip = argv[1];
  22. std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage, port));
  23. usvr->initServer();
  24. usvr->start();
  25. return 0;
  26. }

udpClient.hpp

  1. #pragma once
  2. #include <iostream>
  3. #include <string>
  4. #include <strings.h>
  5. #include <cerrno>
  6. #include <cstring>
  7. #include <unistd.h>
  8. #include <sys/types.h>
  9. #include <sys/socket.h>
  10. #include <arpa/inet.h>
  11. #include <netinet/in.h>
  12. namespace Client
  13. {
  14. using namespace std;
  15. class udpClient
  16. {
  17. public:
  18. udpClient(const string &serverip, const uint16_t &serverport)
  19. : _serverip(serverip), _serverprot(serverport), _sockfd(-1), _quit(false)
  20. {
  21. }
  22. void initClient()
  23. {
  24. // 1. 创建socket
  25. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  26. if (_sockfd == -1)
  27. {
  28. cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
  29. exit(1); // 创建套接字失败直接终止进程
  30. }
  31. cout << "socket success: " << " : " << _sockfd << endl; //成功打印出来
  32. // 2. client要不要bind(必须要!)因为bind就是和系统、网络产生联系. client要不要显示的bind -> 需不需要程序员bind? 名字不重要,重要的是唯一性的,和服务端是不一样的
  33. // 就像宿舍号是几不重要,有就行了。一个端口号只能被一个客户端绑定,就像是服务端是明星,客户端是民众,民众名字不重要
  34. // 服务端是具体的一家公司,比如抖音是字节的,就像一个手机有很多app比如抖音,快手这样的客户端,不能让它们固定bind端口号,万一其他公司也用了用一个端口就冲突其他不来了
  35. // 写服务器的是一家公司,写client是无数家公司 -- 有OS自动形成端口进行bind!不需要自己操作,包括ip地址也不需要,OS自己会处理,当然也可以自己写
  36. // 那么OS在什么时候,如何bind
  37. }
  38. void run()
  39. {
  40. struct sockaddr_in server;
  41. memset(&server, 0, sizeof(server));
  42. server.sin_family = AF_INET;
  43. server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  44. server.sin_port = htons(_serverprot);
  45. string messages;
  46. while (!_quit)
  47. {
  48. cout << "Please Enter# ";
  49. cin >> messages;
  50. sendto(_sockfd, messages.c_str(), sizeof(messages), 0, (struct sockaddr *)&server, sizeof(server));
  51. }
  52. }
  53. ~udpClient()
  54. {
  55. }
  56. private:
  57. int _sockfd;
  58. string _serverip;
  59. uint16_t _serverprot;
  60. bool _quit;
  61. };
  62. }

udpClient.cc

  1. #include "udpClient.hpp"
  2. #include <memory>
  3. using namespace Client;
  4. static void Usage(string proc)
  5. {
  6. cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n"; //命令提示符
  7. }
  8. // ./udpClient server_ip server_port 第一个是运行方式 要知道要发送的服务端的ip地址 和 端口号
  9. int main(int argc, char *argv[])
  10. {
  11. if(argc != 3)
  12. {
  13. Usage(argv[0]);
  14. exit(1);
  15. }
  16. string serverip = argv[1];
  17. uint16_t serverport = atoi(argv[2]);
  18. unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));
  19. ucli->initClient();
  20. ucli->run();
  21. return 0;
  22. }

makefile

  1. cc=g++
  2. .PHONY:all
  3. all:udpClient udpServer
  4. udpClient:udpClient.cc
  5. $(cc) -o $@ $^ -std=c++11
  6. udpServer:udpServer.cc
  7. $(cc) -o $@ $^ -std=c++11
  8. .PHONY:clean
  9. clean:
  10. rm -f udpClient udpServer

至于如何打开端口后续文章介绍 -- 简单的UDP网络程序·续写

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/179943
推荐阅读
相关标签
  

闽ICP备14008679号