本文主要是网络编程——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)
点击查看本文参考资料
点击查看相关文件下载
一、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
然后再发送一些数据,我们就会看到如下现象: