当前位置:   article > 正文

Udp网络编程

udp网络编程

目录

一、预备知识

1.端口        

2.TCP协议和UDP协议

3.socket编程接口

①socket 常见API

②sockaddr结构

二、网络编程

1.UDP网络程序

1.1服务器

①打印

②socket​编辑

③bind

④recvfrom ​编辑

1.2客户端

①sendto

 1.3提升通信的花样性

①将字符串返还

②注册

③消息路由


一、预备知识

1.端口        

         上篇我们讲到,就是数据与数据的交互,那数据之间的交互又是用户与用户之间的交互的体现,但本质上还是用户通过进程来进行与对面用户的进程通信

        但是每个主机上的进程成千上百哪能对应准确的进程来通信?

        需要两个数据就可以准确定位。

        IP:标识主机的唯一性;

        端口号(port):标识主机内的进程的唯一性;

        IP + port = socket ->全网唯一一个进程。

        一个进程可以和多个端口号绑定,但一个端口号只能和一个进程绑定。

        传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要 发给谁"。

2.TCP协议和UDP协议

        这两个都是传输层协议。

        TCP协议:                                                  UDP协议

        ①有链接                                                     ①无连接

        ②可靠传输                                                 ②不可靠传输

        ③面向字节流                                             ③面向数据报

3.socket编程接口

①socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)

int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)

int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)

int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP, 客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

②sockaddr结构

        我们看到这些接口中都有struct sockaddr*类型的参数,这个类型到底是什么?

        网络通信本质是不同主机进程之间的通信,那使用网络通信的接口让同一主机的进程之间通信也是可以的。

        那怎么实现呢?

        靠类型的强转实现。

        特定函数在使用结构之前,会先提取判断前16位数字,如果地址类型是AF_INET,就将struct sockaddr强转为struct sockadd_in类型,如果地址类型是AF_UNIX就强转为struct sockadd_un类型。

二、网络编程

1.UDP网络程序

        当然你想配合整体代码看的话,请点击此处的gitee浏览。

1.1服务器

①打印

        为了方便打印,我们先写一个打印函数。

  1. #define DEBUG 0
  2. #define NOTICE 1
  3. #define WARNING 2
  4. #define FATAL 3
  5. const char *log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
  6. void logMessage(int level, const char *format, ...)
  7. {
  8. assert(level >= DEBUG);
  9. assert(level <= FATAL);
  10. char logInfor[1024];
  11. char *name = getenv("USER");
  12. va_list ap;
  13. va_start(ap, format);
  14. vsnprintf(logInfor, sizeof(logInfor) - 1, format, ap);
  15. va_end(ap);
  16. FILE * out = (level == FATAL) ? stderr : stdout;
  17. fprintf(out,"%s | %u | %s | %s\n",\
  18. log_level[level],\
  19. (unsigned int)time(nullptr),\
  20. name == nullptr ? "Unkown" : name,\
  21. logInfor
  22. );
  23. }

②socket

        目的是创建一个套接字。

domain:本地通信还是网络通信 

一般有这几个选项:

 type:套接字类型决定了通信的时候的报文类型,一般有流式、用户数据报类型。

一般有这几个选项:

protocol:协议类型。网络通信中设置为0。

返回值:成功一个文件描述符被返回,失败返回-1,errno被设置。

        首先来使用一下socket这个函数。

  1. int main()
  2. {
  3. int fd = socket(AF_INET, SOCK_DGRAM, 0);
  4. if (fd < 0)
  5. {
  6. logMessage(FATAL, "%s%d", strerror(errno), fd);
  7. exit;
  8. }
  9. logMessage(DEBUG, "socket create success : %d", fd);
  10. return 0;
  11. }

结果:

        还要建一个类,作为初始的框架

  1. class UdpServer
  2. {
  3. public:
  4. UdpServer()
  5. {
  6. }
  7. ~UdpServer()
  8. {
  9. }
  10. public:
  11. void init()
  12. {
  13. }
  14. void start()
  15. {
  16. }
  17. private:
  18. int sockfd;
  19. };

        这里的创建套接字要放到init初始化中。

  1. ​​​​​​ // 1.create 套接字
  2. _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  3. if (_sockfd < 0)
  4. {
  5. logMessage(FATAL, "socket: %s%d", strerror(errno), _sockfd);
  6. exit(1);
  7. }
  8. logMessage(DEBUG, "socket create success : %d", _sockfd);

③bind

        给一个套接字绑定上iP地址与端口号。

  1. // 2 bind
  2. // 2.1 填入基本信息到struct sockaddr_in 中
  3. struct sockaddr_in local;
  4. // 初始化
  5. bzero(&local, sizeof(local));
  6. // 填充域 AF_INET 网络通信 AF_UNIX 本地通信
  7. local.sin_family = AF_INET;
  8. // 填充对应的端口号 htons的作用是将本地序列转换为网络序列这样才能发送给对方
  9. local.sin_port = htons(_port);
  10. // 服务器的IP地址 xx.yy.aa.ccc 每个都是0-255的数字,有四个8比特位 正好放在uint36_t中
  11. // sin_addr也是一个结构体其中的元素是s._addr是被typedef过的uint32_t
  12. // INADDR_ANY就是0,一般不关注服务器绑定哪一个IP地址,服务器会自动bind,一般所有服务器都是这样做的
  13. // inet_addr将char *转换为s_addr,还会将主机序列转换为网络序列
  14. local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
  15. // 2.2 bind网络信息
  16. if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) == -1)
  17. {
  18. logMessage(FATAL, "bind: %s%d", strerror(errno), _sockfd);
  19. exit(2);
  20. }
  21. logMessage(DEBUG, "socket success: %d", _sockfd);

       其中sockaddr_in的成员为:      

        其中包含端口号和IP地址。

        这时大致udpserver已经成型,我们来测试下。

  1. static void Usage(const string proc)
  2. {
  3. cout << "Usage:\n\t" << proc << " port [ip]" << endl;
  4. }
  1. void start()
  2. {
  3. while (1)
  4. {
  5. logMessage(NOTICE, "udpserver runing");
  6. sleep(1);
  7. }
  8. }
  1. int main(int argc, char *argv[])
  2. {
  3. if (argc != 2 && argc != 3)
  4. {
  5. Usage(argv[0]);
  6. exit(3);
  7. }
  8. uint16_t port = atoi(argv[1]);
  9. string ip;
  10. if (argc == 3)
  11. {
  12. ip = argv[2];
  13. }
  14. UdpServer svr(port, ip);
  15. svr.init();
  16. svr.start();
  17. return 0;
  18. }

         这里的端口号最好不要绑定0-1023的端口号,这些端口号是服务器自己使用的对应特定服务的。         这时我们可以通过netstat -lnup来查看当前的网络服务。

④recvfrom 

        从特定socke中读取到buf里,长度为len,默认设置flags为0,阻塞式读取,src_addr用来接收发送方的参数,addrlen为src_addr的大小。返回值为读到的字节大小。

  1. void start()
  2. {
  3. char inbuffer[1024]; // 输入进来的数据放到inbuffer中
  4. char outbuffer[1024]; // 输出的数据放outbuffer中
  5. while (1)
  6. {
  7. struct sockaddr_in peer; // 输出形参数
  8. socklen_t len = sizeof(peer); // 输入输出型参数
  9. ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
  10. if (size > 0)
  11. {
  12. // 这里将读的数据看为字符串
  13. inbuffer[size] = 0;
  14. }
  15. else if (size == -1)
  16. {
  17. logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
  18. }
  19. // 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
  20. // peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
  21. string peerip = inet_ntoa(peer.sin_addr);
  22. uint16_t peerport = ntohs(peer.sin_port);
  23. // 打印客户端IP与port 和信息
  24. logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
  25. }
  26. }

1.2客户端

        以上写的都是服务端,现在完善一下客户端。

        上面提到过作为服务器不用bind特定的IP地址和port端口号,但是作为客户端必须知道服务器的IP地址和port端口号。

  1. static void Usage(const string proc)
  2. {
  3. cout << "Usage:\n\t"
  4. << "server IP ,server port" << endl;
  5. }
  6. int main(int argc, char *argv[])
  7. {
  8. if (argc != 3)
  9. {
  10. Usage(argv[0]);
  11. exit(1);
  12. }
  13. // 1. 获取服务端
  14. string serverip = argv[1];
  15. uint16_t serverport = atoi(argv[2]);
  16. // 2. 创建客户端
  17. // 2.1 创建socket
  18. int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  19. assert(sockfd > 0);
  20. // 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
  21. // 2.3 填写对应服务器信息
  22. struct sockaddr_in server;
  23. bzero(&server, sizeof(server));
  24. // 都需要转换为网络序列
  25. server.sin_family = AF_INET;
  26. server.sin_addr.s_addr = inet_addr(serverip.c_str());
  27. server.sin_port = htons(serverport);
  28. ...

①sendto

        既然到了客户端,服务端来接收,客户端就要发送。

        通过指定的套接字,从缓冲区buf中读取len的长度的内容,默认flags为0阻塞式,dest_addr为目的地。

  1. // 3. 发送消息
  2. string output;
  3. while (1)
  4. {
  5. cout << "Please entry | ";
  6. getline(cin, output);
  7. // 发送
  8. sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
  9. }

        写了这么多,我们来测试下。

        这里的IP地址127.0.0.1,是本地环回,就指的是本主机。

        当然这是本地之间的测试,你如果想远程测试可以,将udpclient发给他,然后将自己的IP地址告诉对方,自己打开udpserver并确定端口号,对方使用udpclient通过IP地址和端口号就可以远程通信了。当然记得使用g++进行编译时要加-static,库变成静态连接,这样对面没有对应的库也没有关系。具体的可以看我这篇博客

        那要是没有两台linux呢?我们可以用Visual Studio做出windows版本的客户端。

代码:

  1. #pragma warning(disable:4996)// 使warning去掉
  2. #pragma comment(lib,"ws2_32.lib")// 所需要包含的连接库
  3. #include <iostream>
  4. #include <cstdio>
  5. #include <cassert>
  6. #include <string>
  7. #include <WinSock2.h>
  8. using namespace std;
  9. int serverport = 8888;
  10. string serverip ="121.4.139.131"; // 此处要填自己主机的IP地址
  11. int main()
  12. {
  13. // 用作初始化套接字
  14. WSADATA data;
  15. // 初始套接字
  16. WSAStartup(MAKEWORD(2, 2), &data);
  17. (void)data;
  18. SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  19. assert(sockfd > 0);
  20. struct sockaddr_in server;
  21. memset(&server, 0 ,sizeof(server));
  22. server.sin_family = AF_INET;
  23. server.sin_addr.s_addr = inet_addr(serverip.c_str());
  24. server.sin_port = htons(serverport);
  25. string output;
  26. while (1)
  27. {
  28. cout << "Please entry | ";
  29. getline(cin, output);
  30. sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr*)&server, sizeof(server));
  31. }
  32. closesocket(sockfd);
  33. WSACleanup();
  34. return 0;
  35. }

结果:


         注意:有些云服务器没有开放对应的端口,是不能使用这种Udp方法通信的。我们要在自己云服务器的官网找到你的云服务器,在你的实例中点击防火墙,然后点击添加规则,我们这次测试为Udp所以点击Udp,再将你自定义的端口输入,点击确定,这样你就可以和远处的人通信了。虽然这次实验只是单方面的通信。


 1.3提升通信的花样性

①将字符串返还

        我们服务端只接受信息太单一了,我们将发送过来的字符串,转换成大写转换回去。这些代码分别写在上一步server打印信息和client发送信息之后。

代码:

  1. UdpServer.cc:start():
  2. // 转换字符串小写-大写
  3. for (int i = 0; i < strlen(inbuffer); i++)
  4. {
  5. if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
  6. {
  7. outbuffer[i] = toupper(inbuffer[i]);
  8. }
  9. else
  10. {
  11. outbuffer[i] = inbuffer[i];
  12. }
  13. }
  14. sendto(_sockfd, outbuffer, strlen(outbuffer), 0, (struct sockaddr *)&peer, len);
  15. memset(outbuffer,0,sizeof(outbuffer));
  1. UdpClient.cc:main():
  2. // 接收
  3. char buffer[1024];
  4. struct sockaddr temp;
  5. socklen_t len = sizeof(temp);
  6. ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
  7. if (s > 0)
  8. {
  9. buffer[s] = 0;
  10. cout << "Server output| " << buffer << endl;
  11. }

结果:               

②注册

        使用unordered_map来简单存储用户信息,来区分新老用户。

        客户端没有任何变化,服务端新增一个成员变量,将转换字符注释掉,向客户端发送字符也注释掉。注意服务端的代码都是写在类中的。

代码:

  1. void start()
  2. {
  3. char inbuffer[1024]; // 输入进来的数据放到inbuffer中
  4. char outbuffer[1024]; // 输出的数据放outbuffer中
  5. int i = 1;
  6. while (1)
  7. {
  8. struct sockaddr_in peer; // 输出形参数
  9. socklen_t len = sizeof(peer); // 输入输出型参数
  10. ssize_t size = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
  11. if (size > 0)
  12. {
  13. // 这里将读的数据看为字符串
  14. inbuffer[size] = 0;
  15. }
  16. else if (size == -1)
  17. {
  18. logMessage(WARNING, "recevfrom : %s %d", strerror(errno), _sockfd);
  19. }
  20. // 拿到发送方的IP地址 peer.sin_addr的类型是四字节uint36_t 要转换为string
  21. // peer.sin_port是从网络中接收到的是网络序列,ntohs目的是将网络序列转换为本地序列
  22. string peerip = inet_ntoa(peer.sin_addr);
  23. uint16_t peerport = ntohs(peer.sin_port);
  24. // 打印客户端IP与port 和信息
  25. logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
  26. checkOnlineUser(i,peerip, peerport, peer);
  27. }
  28. }
  29. bool checkOnlineUser(int &i,string &ip, uint16_t port, struct sockaddr_in &peer)
  30. {
  31. string userInfor = ip;
  32. userInfor += " ";
  33. userInfor += to_string(port);
  34. auto iter = user.find(userInfor);
  35. if (iter == user.end())
  36. {
  37. // 没找到
  38. user.insert({userInfor, peer});
  39. i = 1;
  40. if (i == 1)
  41. {
  42. cout << "新用户登录" << endl;
  43. }
  44. }
  45. else
  46. {
  47. // 找到了
  48. if (i == 1)
  49. {
  50. i = 0;
  51. cout << "老用户登录" << endl;
  52. }
  53. }
  54. }
  55. private:
  56. unordered_map<string, struct sockaddr_in> user;

③消息路由

        将输出与命名管道结合起来。

        先在当前路径建立命名管道,修改代码,服务端中将客户端发回的消息使用管道保存起来。代码:

  1. void start()
  2. {
  3. ......
  4. logMessage(NOTICE, "[%s %d]# %s", peerip.c_str(), peerport, inbuffer);
  5. checkOnlineUser(i, peerip, peerport, peer);
  6. messageRoute(inbuffer);
  7. }
  8. void messageRoute(string message)
  9. {
  10. for (auto &ch : user)
  11. {
  12. sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(ch.second),sizeof(ch.second));
  13. }
  14. }

结果:

         我们呢,还可以使用线程来改造下。让主线程不断在发消息,另一个线程去接收发回来的消息。 

代码:

  1. struct sockaddr_in server;
  2. static void Usage(const string proc)
  3. {
  4. cout << "Usage:\n\t"
  5. << "server IP ,server port" << endl;
  6. }
  7. void *fuc(void *argc)
  8. {
  9. while (1)
  10. {
  11. int sockfd = *(int *)argc;
  12. // 接收
  13. char buffer[1024];
  14. memset(buffer, 0, sizeof(buffer));
  15. struct sockaddr temp;
  16. socklen_t len = sizeof(temp);
  17. ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&server, &len);
  18. if (s > 0)
  19. {
  20. buffer[s] = 0;
  21. cout << "Server output| " << buffer << endl;
  22. }
  23. }
  24. }
  25. int main(int argc, char *argv[])
  26. {
  27. if (argc != 3)
  28. {
  29. Usage(argv[0]);
  30. exit(1);
  31. }
  32. // 1. 获取服务端
  33. string serverip = argv[1];
  34. uint16_t serverport = atoi(argv[2]);
  35. // 2. 创建客户端
  36. // 2.1 创建socket
  37. int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  38. assert(sockfd > 0);
  39. // 2.2 client 需不需要bind 可以不用bind OS会自动帮我们bind 不推荐自己bind
  40. // 2.3 填写对应信息
  41. bzero(&server, sizeof(server));
  42. // 都需要转换为网络序列
  43. server.sin_family = AF_INET;
  44. server.sin_addr.s_addr = inet_addr(serverip.c_str());
  45. server.sin_port = htons(serverport);
  46. pthread_t t;
  47. pthread_create(&t, nullptr, fuc, (void *)&sockfd);
  48. // 3. 发送消息
  49. string output;
  50. while (1)
  51. {
  52. cerr << "Please entry | ";
  53. getline(cin, output);
  54. // 发送
  55. sendto(sockfd, output.c_str(), output.size(), 0, (const struct sockaddr *)&server, sizeof(server));
  56. }
  57. close(sockfd);
  58. return 0;
  59. }

         运行过程与上文的结果一样,这里就不演示了。

        这里的fifo好像没什么用啊?为什么要管道呢?

        我们启动服务器,在客户端输入消息,服务器再将消息返回,使用fifo来展示返回的消息。一个人这样使用其实没有fifo用处,但如果是多个人使用,无论谁向客户端输入消息,服务器都会返还所有它接受的消息到fifo中,意味着我们可以通过fifo来查看别人发的消息,这样不就可以双端聊天了嘛。至于fifo中不显示谁发送的消息,我们将server中的sendto所使用的buffer中填写从recevfrom中获取到的IP地址与端口,就可以辨识是谁发送的消息。

        到这里我们Udp网络程序的编写暂时告一段落基本的要求我们都已经实现,Udp的全部代码我已上传gitee,有兴趣的可以看一下。  

        限于篇幅,Tcp网络编程就移至下一节去讲,感谢观看,如有错误请指出,我们下次再见。      

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

闽ICP备14008679号