探究一下 udp 模型, 主要是和TCP进行对比.
tcp模型一般说的比较多, 因为tcp稳定,可靠; 但是代价就是传输效率不高, 实时性不强.这个时候udp正好做了弥补. 本文就谈谈udp.
(所涉及的代码都是核心代码, 很多没有做出错和异常处理, 比如说sendto, recvfrom都要检查返回值, 特别recvfrom还要检查发送端是不是已知服务器, 否则忽略)
引子 本文主要总结:
udp的特点(对比tcp), 优缺点, 适用场景
basic udp c-s模型
广播模型
组播模型
(网络基础就不说了)
正文 udp的特点 简要说明
通信速度快, 保证实时性
不建立连接, 不维护连接, 更不用断开连接
稳定性差, 正确率无保证(可靠性差)
一句话概括: 无连接的,不可靠报文传输.
如何保证可靠性 由于无需创建连接, 所以 UDP 开销较小, 数据传输速度快, 实时性较强.多用于对实时性要求较高的通信场合, 如视频会议, 电话会议等. 但随之也伴随着数据传输不可靠, 传输数据的正确率, 传输顺序和流量都得不到控制和保证. 所以, 通常情况下, 使用 UDP 协议进行数据传输, 为保证数据的正确性, 我们需要 在应用层添加辅助校验协议
来弥补 UDP 的不足, 以达到数据可靠传输的目的.
与TCP对比 TCP: (TCP协议, 在IP网络不稳定的基础上, 做相关的弥补)
数据稳定(有回传应答机制, 可以保证丢包后能重传)
速率稳定(建立连接后, 基本上所有数据都是走的同一条连接线路)
流量稳定(滑动窗口, 拥塞控制以及相关算法)
传输效率低. 发送前一定要建立连接, 之后还要处理断开连接; 每次传递过去还需要回执之火才发送下一次数据(可能是多个接收对应一个回执, 稍稍弥补了一下).
(适合大文件, 重要文件传输; 因为丢包可以重传)
UDP
需要自己在应用层, 添加辅助校验协议来弥补 UDP 的不足. (可能接收方缓冲区满了, 对方还在继续发(接收方只能照单收), 套接字的缓冲区被填满后, 新到达的数据报就会被丢弃, 导致大量丢包) (也就是说, 需要在应用层自定义网络协议, 来弥补UDP的丢包; 但是应用层自定义协议的开发量的是非常大的, 技术要求高)
传输效率高, 实时性强, 传输速度快.
(适合视频会议, 视频电话, 广播, 聊天客户端)
udp流量控制 由于它没有 TCP 滑动窗口的机制, 通常采用如下两种方法解决:
服务器应用层设计流量控制, 控制发送数据速度
借助 setsockopt
函数改变接收缓冲区大小(注意设置的是 SO_RCVBUF
)
1 2 3 4 #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;int n = 220 x1024;setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof (n));
但是设置缓冲区大小一般是在bind之后, 真正开始通信之前; 特别是TCP时, 一定要在listen之前设置.
补充: 缓冲区大小, 不是越大越好, 经验表明 220x1024
时, 缓冲区的丢包率最低.
(网络环境不好, 属于硬件或者环境问题, 单单依靠软件, 如改变缓冲区的大小, 解决不了)
c-s模型 由于 UDP 不需要维护连接, 程序逻辑简单了很多, 但是 UDP 协议是不可靠的, 保证通讯可靠性的机制需要在应用层实现. 基本代码如下:
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 #include <string.h> #include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main (void ) { struct sockaddr_in serv_addr , clie_addr ; socklen_t clie_addr_len; int sockfd; char buf[BUFSIZ] = {0 }; char str[INET_ADDRSTRLEN]; int i, n; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(SERV_PORT); bind(sockfd, (struct sockaddr *)&serv_addr, sizeof (serv_addr)); printf ("bind ok, new waitting for recv\n" ); while (1 ){ clie_addr_len = sizeof (clie_addr); n = recvfrom(sockfd, buf, BUFSIZ, 0 , (struct sockaddr *)&clie_addr, &clie_addr_len); if (n == -1 ) { perror("recvfrom error" ); } printf ("received from %s at PORT %d\n" , inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof (str)), ntohs(clie_addr.sin_port)); for (i=0 ;i<n;i++){ buf[i] = toupper (buf[i]); } n = sendto(sockfd, buf, BUFSIZ, 0 , (struct sockaddr*)&clie_addr, sizeof (clie_addr)); if (n==-1 ){ perror("send error" ); } } close(sockfd); return 0 ; }
bind之后直接开始传输数据, 不需要再进行listen等操作.
client的代码更简单, 不需要connect了:
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 #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main (int argc, char *argv[]) { struct sockaddr_in servaddr ; int sockfd, n; char buf[BUFSIZ] = {0 }; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, "127.0.0.1" , &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); while (fgets(buf, BUFSIZ, stdin ) != NULL ) { n = sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&servaddr, sizeof (servaddr)); if (n==-1 ){ perror("send error" ); } n = recvfrom(sockfd, buf, BUFSIZ, 0 , NULL , 0 ); if (n==-1 ){ perror("send error" ); } buf[n]='\0' ; write(STDOUT_FILENO, buf, n); } return 0 ; }
具体代码可以参考: https://github.com/WizardMerlin/network_life/tree/master/code/udp_c_s
模型总结如下图:
广播 广播(broadcast)一般用在局域网之内,需要的基础大概是:
xxx.xxx.xxx.255 最后一个地址 255
表示广播
一般是通过交换机进行转发(局域网内使用交换机足够了)server---交换机(局域网)---局域网内的client(多个)
简单说: 交换机发现数据包中的ip地址是255, 表明是一个广播, 就把该数据包发送给当前网络环境中的所有机器.(也就是说代码中你发送数据给 255
就足够了, 其他的你别管)
socket默认是不允许广播的, 需要设置socket参数(开放广播权限, 即 SO_BROADCAST
): (注意是在bind之后设置)
1 2 3 4 5 int flag = 1 ;setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof (flag));
具体代码参考如下:
server发送给广播地址BROADCAST_IP(根据自己的网段指定), 填充client_ip的时候要手动指定端口号, 不能依靠隐式绑定了, 因为这个端口号也是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 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define CLIENT_PORT 9000 #define MAXLINE 1500 #define BROADCAST_IP "192.168.1.255" int main (void ) { int sockfd; struct sockaddr_in serveraddr , clientaddr ; char buf[MAXLINE] = {0 }; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serveraddr, sizeof (serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERVER_PORT); bind(sockfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)); int flag = 1 ; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof (flag)); bzero(&clientaddr, sizeof (clientaddr)); clientaddr.sin_family = AF_INET; inet_pton(AF_INET, BROADCAST_IP, &clientaddr.sin_addr.s_addr); clientaddr.sin_port = htons(CLIENT_PORT); int i = 0 ; while (1 ) { sprintf (buf, "send %d message\n" , i++); sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&clientaddr, sizeof (clientaddr)); sleep(1 ); } close(sockfd); return 0 ; }
client代码:
client绑定IP的时候, 绑定”0.0.0.0”即可(不能只接受本机广播吧)
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 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define SERVER_PORT 8000 #define MAXLINE 4096 #define CLIENT_PORT 9000 int main (int argc, char *argv[]) { struct sockaddr_in localaddr ; int confd; ssize_t len; char buf[MAXLINE] = {0 }; confd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton(AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons(CLIENT_PORT); int ret = bind(confd, (struct sockaddr *)&localaddr, sizeof (localaddr)); if (ret == 0 ) { printf ("bind ok..\n" ); } while (1 ) { len = recvfrom(confd, buf, sizeof (buf), 0 , NULL , 0 ); write(STDOUT_FILENO, buf, len); } close(confd); return 0 ; }
具体代码请参考我的github库:https://github.com/WizardMerlin/network_life/tree/master/code/udp_broadcast
组播 这个又叫多播(multicast), 也就是说, 发送随便你发, 但是我客户端接收不接受就有限制了, 只有在相应的组播组里, 才能接收.(简单举例: 不进相关的群, 就收不到群主的红包)
组播组
224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址), 地址 224.0.0.0 保留不做分配, 其它地址供路由协议使用
224.0.1.0~224.0.1.255 是公用组播地址, 可以用于 Internet; 欲使用需申请
224.0.2.0~238.255.255.255 为用户可用的组播地址(临时组地址), 全网范围内有效;
239.0.0.0~239.255.255.255 为本地管理组播地址, 仅在特定的本地范围内有效
可以使用 ip address
或者 ip ad
查看一下网卡支持多播与否. (可以使用命令打开或者关闭网卡 sudo ifconfig eth0 up/down)
1 2 3 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 link/ether 00:16:3e:30:e0:f9 brd ff:ff:ff:ff:ff:ff inet 172.17.216.110/20 brd 172.17.223.255 scope global eth0
注意网卡编号, 代码中, 则通过 if_nametoindex
命令可以根据网卡名, 获取网卡序号:
1 2 3 4 5 6 #include <net/if.h> unsigned if_nametoindex (const char *ifname) ; char *if_indextoname (unsigned ifindex, char *ifname) ;struct if_nameindex *if_nameindex (void ) ;void if_freenameindex (struct if_nameindex *ptr) ;
编程中还涉及到要填充一个组播地址结构体, /usr/include/linux/in.h
下定义的结构体 ip_mreqn
:
1 2 3 4 5 struct ip_mreqn { struct in_addr imr_multiaddr ; struct in_addr imr_address ; int imr_ifindex; };
结构体 imr_address group
在设置server 发送组播权限
时需要:
1 setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof (group));
补充:
构造imr_address 如果传入 0.0.0.0
即INADDR_ANY, 那么会在发送的时候自动分配一个本地地址.
server代码中clientaddr的地址设置为group ip即可, 例如 #define GROUP "239.0.0.2"
核心代码(构造):
1 2 3 4 5 inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof (group));
并且注意server代码中发送给client的端口也要和client代码保持一致, 即指定相同的port.
详细代码如下: 服务端:
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 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 6666 #define CLIENT_PORT 9000 #define MAXLINE 1500 #define GROUP "239.0.0.2" int main (void ) { int sockfd, i ; struct sockaddr_in serveraddr , clientaddr ; char buf[MAXLINE] = "itcast\n" ; char ipstr[INET_ADDRSTRLEN]; socklen_t clientlen; ssize_t len; struct ip_mreqn group ; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&serveraddr, sizeof (serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERVER_PORT); bind(sockfd, (struct sockaddr *)&serveraddr, sizeof (serveraddr)); inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof (group)); bzero(&clientaddr, sizeof (clientaddr)); clientaddr.sin_family = AF_INET; inet_pton(AF_INET, GROUP, &clientaddr.sin_addr.s_addr); clientaddr.sin_port = htons(CLIENT_PORT); while (1 ) { sendto(sockfd, buf, strlen (buf), 0 , (struct sockaddr *)&clientaddr, sizeof (clientaddr)); sleep(1 ); } close(sockfd); return 0 ; }
客户端
这边当然也要指定相关的组, 加入相关的组播组才可以接收到相关的数据. (没有加入group组播组的客户端, 虽然server还是会借助交换机发送, 但是client不接收)
1 setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group));
其中group即 struct ip_mreqn group;
核心代码:
1 2 3 4 5 6 7 8 struct ip_mreqn group ;inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group));
详细代码如下:
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 #include <netinet/in.h> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include <net/if.h> #define SERVER_PORT 6666 #define MAXLINE 4096 #define CLIENT_PORT 9000 #define GROUP "239.0.0.2" int main (int argc, char *argv[]) { struct sockaddr_in serveraddr , localaddr ; int confd; ssize_t len; char buf[MAXLINE]; struct ip_mreqn group ; confd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton(AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons(CLIENT_PORT); bind(confd, (struct sockaddr *)&localaddr, sizeof (localaddr)); inet_pton(AF_INET, GROUP, &group.imr_multiaddr); inet_pton(AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt(confd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group)); while (1 ) { len = recvfrom(confd, buf, sizeof (buf), 0 , NULL , 0 ); write(STDOUT_FILENO, buf, len); } close(confd); return 0 ; }
代码地址: https://github.com/WizardMerlin/network_life/tree/master/code/udp_multicast
尾巴 小结 全文把udp的核心内容大致讲了讲, 也说了开发中的注意事项. 详细的可以参考一下 《unp 卷1》.
在做网络的应用的时候, 和udp还是有一些区别的(根据协议的特点做相应的调整), 它们适用的方向也不一样. 例如实时视频直播软件的时候可能是:
屏幕截图模块(为保证画面流畅, 至少保证有24帧)
截取帧数 (一般为保证画面, 8-12即可)
压缩图片
压缩数据包
传递数据–(多播)
解压缩
成像
其中传递数据的时候, 需要用到网络编程中的udp模型(加入错误和异常处理).
补充connect 自然而然的认为udp模型下就应该没有connect啥事儿, 因为压根儿就不建立连接. 这样想就错了, 因为在TCP模型下connect有发送ack请求建立连接的作用, 但是除此之外, 它还有一个作用 指明目的地址/端口
.
一般来说, UDP客户端在建立了socket后会直接用sendto()函数发送数据, 需要在sendto()函数的参数里指明目的地址/端口. 如果一个UDP客户端在建立了插口后首先用connect()函数指明了目的地址/端口, 然后也可以用send函数发送数据, 因为此时send函数已经知道对方地址/端口, 用getsockname()也可以得到这个对端socket addr信息, 即UDP的connect只是告诉内核保存了对端的IP和端口号.
从内核上看: connect绑定对端ip端口之后, 内核以后就将该套接字的数据发给这个对端地址, 从这个对端地址收到的数据也会发送给用户态客户程序.
由此, UDP的套接字分为 已连接的套接字
和 未连接的套接字
, 默认的是未连接的套接字,上面的例程采用的是未连接的套接字.
区别如下:
socket()——>sendto()或recvfrom(), 默认未连接的套接字
socket()——>connect()——>send或recv() 已连接套接字
UDP客户端在建立了插口后会直接用sendto()函数发送数据, 还隐含了一个操作: 在发送数据之前, UDP会首先为该socket选择一个独立的UDP端口(在1024—5000之间), 将该插口置为已绑定状态. 当然你也可以自己bind, 强制使用某端口.
但是一旦使用了connect, 也就是说强制绑定了发送和接收对象, 那么只能完成一对一的传输了. 特别是服务端使用了connect函数绑定了对端之后, 在不能接收其他的客户端请求了. UDP已连接的套接字只能实现一对一的传输, 如果要从多个地方接受数据和发送数据, 则只能使用未连接的套接字.
因此, UDP客户端多用已连接的套接字(可以使用connect, 当然也可以不用), 服务端用未连接的套接字(服务器更没必要用connect).