• 简单的UDP网络程序


    目录

     

    准备工作

    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
    3. #include
    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
    3. using namespace std;
    4. using namespace Server;
    5. int main()
    6. {
    7. std::unique_ptr 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
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    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
    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 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
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    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
    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 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
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. #include
    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
    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 usvr(new udpServer(handlerMessage, port));
    23. usvr->initServer();
    24. usvr->start();
    25. return 0;
    26. }

    udpClient.hpp

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    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
    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 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网络程序·续写

  • 相关阅读:
    微服务系列之Api文档 swagger整合
    java毕业设计车辆监管系统mybatis+源码+调试部署+系统+数据库+lw
    java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统
    服务端技术方案模板参考
    Vue-router路由
    布隆过滤器Moudule安装
    数据库新开账号,并授予了相应表的查询权限。访问时,其他PC端远程被拒绝
    中介者模式:简化对象间的通信
    [附源码]计算机毕业设计JAVAjsp学术文献分享网站
    什么是产品经理&&为什么说做好一款产品需要情怀?
  • 原文地址:https://blog.csdn.net/weixin_67595436/article/details/130894394