本文主要是网络编程——UDP协议下的socket编程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows windows11 Ubuntu Ubuntu16.04的64位版本 VMware® Workstation 16 Pro 16.2.3 build-19376536 SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 开发板 正点原子 i.MX6ULL Linux阿尔法开发板 uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) linux内核 linux-4.15(NXP官方提供) STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
一、UDP
协议简介 1. 简介 UDP 是 User Datagram Protocol 的简称,中文名是用户数据报协议,是一种无连接、不可靠的协议,同样它也是工作在传顺层。它只是简单地实现从一端主机到另一端主机的数据传输功能,这些数据通过 IP 层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理,这就带来了很大的不方便,此外, UDP 协议更没有流量控制、拥塞控制等功能,在发送的一端, UDP 只是把上层应用的数据封装到 UDP 报文中,在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到 IP 数据报中发送出去。而在接收端,无论是否收到数据,它都不会产生一个应答发送给源主机,并且如果接收到数据发送校验错误,那么接收端就会丢弃该UDP 报文,也不会告诉源主机,这样子传输的数据是无法保障其准确性的,如果想要其准确性,那么就需要应用程序来保障了。
2. UDP
协议的特点 (1)无连接、不可靠;
(2)尽可能提供交付数据服务,出现差错直接丢弃,无反馈;
(3)面向报文,发送方的 UDP 拿到上层数据直接添加个 UDP 首部,然后进行校验后就递交给 IP 层,而接收的一方在接收到 UDP 报文后简单进行校验,然后直接去除数据递交给上层应用;
(4)速度快,因为 UDP 协议没有 TCP 协议的握手、确认、窗口、 重传、拥塞控制等机制, UDP 是一个无状态的传输协议,所以它在传递数据时非常快,即使在网络拥塞的时候 UDP 也不会降低发送的数据。
UDP 虽然有很多缺点,但也有自己的优点,所以它也有很多的应用场合,因为在如今的网络环境下,UDP 协议传输出现错误的概率是很小的,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,这也不是什么大不了的事情,所以, UDP协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。
3. TCP
与UDP
的对比 首先,我们先再次了解一下这两种协议。
TCP
是面向连接的传输协议 ,建立连接时要经过三次握手,断开连接时要经过四次握手,中间传输数据时也要回复 ACK
包确认,多种机制保证了数据能够正确到达,不会丢失或出错。
UDP
是无连接的传输协议 ,没有建立连接和断开连接的过程 ,它只是简单地把数据丢到网络中,也不需要 ACK
包确认。UDP
传输数据就好像我们邮寄包裹,邮寄前需要填好寄件人和收件人地址,之后送到快递公司即可,但包裹是否正确送达、是否损坏我们无法得知,也无法保证。UDP
协议也是如此,它只管把数据包发送到网络,然后就不管了,如果数据丢失或损坏,发送端是无法知道的,当然也不会重发。
既然如此,这点上看,TCP
应该是更加优质的传输协议嘛?其实可能不然。
如果只考虑可靠性,TCP
的确比 UDP
好。但 UDP
在结构上比 TCP
更加简洁,不会发送 ACK
的应答消息,也不会给数据包分配 seq
序号,所以 UDP
的传输效率有时会比 TCP
高出很多,编程中实现 UDP
也比 TCP
简单。
UDP
的可靠性虽然比不上TCP
,但也不会像想象中那么频繁地发生数据损毁,在更加重视传输效率而非可靠性的情况下,UDP
是一种很好的选择。比如视频通信或音频通信,就非常适合采用 UDP
协议;通信时数据必须高效传输才不会产生卡顿现象,用户体验才更加流畅,如果丢失几个数据包,视频画面可能会出现雪花,音频可能会夹带一些杂音,这些很多情况下其实都是可以忽略的。
与 UDP
相比,TCP
的生命在于流控制,这保证了数据传输的正确性。
TCP
的速度无法超越 UDP
,但在收发某些类型的数据时有可能接近 UDP
。例如,每次交换的数据量越大,TCP
的传输速率就越接近于 UDP
。
二、UDP
编程流程 1. 一些说明
UDP
不像 TCP
,无需在连接状态下交换数据,因此基于 UDP
的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen()
和 accept()
函数。UDP
中只有创建套接字的过程和数据交换的过程。
TCP
中,套接字是一对一的关系。如要向 10
个客户端提供服务,那么除了负责监听的套接字外,还需要创建 10
套接字。但在 UDP
中,不管是服务器端还是客户端都只需要 1
个套接字。就像之前邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为 UDP
套接字,只要有 1
个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1
个UDP
套接字就可以向任意主机传送数据。
创建好 TCP
套接字后,传输数据时无需再添加地址信息,因为 TCP
套接字将保持与对方套接字的连接。也就是说,TCP
中的用于数据传输的socket
描述符是知道目标IP
信息和端口的。但 UDP
套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。
所以,我们在UDP
编程中会使用到recvfrom()/sendto()
这一组函数,这两个函数说明可以看这篇笔记:《LV06-07-网络编程-socket》
2. UDP
编程步骤 使用UDP
协议编程的一般步骤如下:
(1)创建 UDP
协议的 socket
套接字,用socket()
函数,注意套接字类型选择SOCK_DGRAM
。
(2)用sendto()
函数往指定的IP
发送信息。
(3)关闭socket
套接字。
(1)创建UDP
协议的 socket
套接字,用socket()
函数,注意套接字类型选择SOCK_DGRAM
。
(2)设置socket
的属性,用setsockopt()
函数(可选)。
(3)socket
绑定包含 IP
地址信息和端口号的 struct sockaddr_in(IPv4)
结构体,用bind()
函数。
(4)循环接收消息,用recvfrom()
函数。
(5)关闭socket
套接字。
3. UDP
编程流程图 基本流程图如下:
三、UDP
循环服务器 1. 服务器模型 循环服务器程序模型如下:
1 2 3 4 5 6 7 8 socket(...); bind(...); while (1 ){ recvfrom(...); process(...); sendto(...); }
2.使用实例 【注意】
(1)socket()
函数第二个参数要换成SOCK_DGRAM
,以指明使用 UDP
协议。
(2)为方便更换服务器IP
以便于测试,这里还是使用绑定0.0.0.0
这种形式,这其实与INADDR_ANY
效果是一样的。个人感觉这样更灵活一些。
2.1server
服务器端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; if (argc != 3 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); if (port < 5000 ) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("bind" ); exit (-1 ); } printf ("Server starting....OK!\n" ); struct sockaddr_in cin ; socklen_t addrlen = sizeof (cin ); char ipv4_addr[16 ]; char buf[BUFSIZ]; char replay[BUFSIZ]; while (1 ) { bzero(buf, BUFSIZ); bzero(replay, BUFSIZ); if (recvfrom(socket_fd, buf, BUFSIZ-1 , 0 ,(struct sockaddr *)&cin , &addrlen ) < 0 ) { perror("recvfrom" ); continue ; } if (!inet_ntop (AF_INET, (void *) &cin .sin_addr, ipv4_addr, sizeof (cin ))) { perror ("inet_ntop" ); exit (-1 ); } printf ("Recived from(%s:%d), data:%s" , ipv4_addr, ntohs(cin .sin_port), buf); strcat (replay, buf); if (sendto(socket_fd, replay, strlen (replay), 0 , (struct sockaddr *)&cin , addrlen) < 0 ) { perror("sendto" ); continue ; } if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client(%s:%d) is exiting!\n" , ipv4_addr, ntohs(cin .sin_port)); } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n" ); }
2.2 client
客户端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; int portClient = -1 ; if (argc != 4 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); portClient = atoi(argv[3 ]); if (port < 5000 || portClient < 5000 || (port == portClient)) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } struct sockaddr_in sinClient ; bzero(&sinClient, sizeof (sinClient)); sinClient.sin_family = AF_INET; sinClient.sin_port = htons(portClient); if (inet_pton(AF_INET, argv[1 ], (void *)&sinClient.sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sinClient, sizeof (sinClient)) < 0 ) { perror("bind" ); exit (-1 ); } printf ("Client staring...OK!\n" ); struct sockaddr_in cin ; char ipv4_addr[16 ]; socklen_t addrlen1 = sizeof (sin ); socklen_t addrlen2 = sizeof (cin ); char buf[BUFSIZ]; char replay[BUFSIZ]; while (1 ) { bzero (buf, BUFSIZ); bzero (replay, BUFSIZ); printf (">" ); if (fgets(buf, BUFSIZ - 1 , stdin ) == NULL ) { perror("fgets" ); continue ; } if (sendto(socket_fd, buf, strlen (buf), 0 , (struct sockaddr *)&sin , addrlen1) < 0 ) { perror("sendto" ); continue ; } if (recvfrom(socket_fd, replay, BUFSIZ-1 , 0 ,(struct sockaddr *)&cin , &addrlen2 ) < 0 ) { perror("recvfrom" ); continue ; } if (!inet_ntop (AF_INET, (void *) &cin .sin_addr, ipv4_addr, sizeof (cin ))) { perror ("inet_ntop" ); exit (1 ); } printf ("server(%s:%d) replay:%s\n" , ipv4_addr, ntohs(cin .sin_port), replay); if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client is exiting!\n" ); break ; } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n" ); }
2.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make
来进行。
点击查看 Makefile 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 CC = gcc DEBUG = -g -O2 -Wall CFLAGS += $(DEBUG) TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} all : $(TARGET_LIST) %.o : %.c $(CC) $(CFLAGS) -c $< -o $@ .PHONY : all clean clean_o clean_outclean : clean_o clean_out @rm -vf $(TARGET_LIST) clean_o : @rm -vf *.o clean_out : @rm -vf *.out
2.4 测试结果 我们执行以下命令编译链接程序,生成两个可执行文件:
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server
和客户端程序client
:
1 2 gcc -g -O2 -Wall client.c -o client gcc -g -O2 -Wall server.c -o server
对于服务器端,我们执行以下命令启动服务器进程:
1 ./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口
对于客户端,我们执行以下命令启动客户端进程:
1 2 ./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003 ./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
然后再发送一些数据,我们就会看到如下现象: