技术: UDP 模型网络编程

探究一下 udp 模型, 主要是和TCP进行对比.

tcp模型一般说的比较多, 因为tcp稳定,可靠; 但是代价就是传输效率不高, 实时性不强.这个时候udp正好做了弥补. 本文就谈谈udp.

(所涉及的代码都是核心代码, 很多没有做出错和异常处理, 比如说sendto, recvfrom都要检查返回值, 特别recvfrom还要检查发送端是不是已知服务器, 否则忽略)

引子

本文主要总结:

  • udp的特点(对比tcp), 优缺点, 适用场景
  • basic udp c-s模型
  • 广播模型
  • 组播模型

(网络基础就不说了)

正文

udp的特点

简要说明

  1. 通信速度快, 保证实时性
  2. 不建立连接, 不维护连接, 更不用断开连接
  3. 稳定性差, 正确率无保证(可靠性差)

一句话概括: 无连接的,不可靠报文传输.

如何保证可靠性

由于无需创建连接, 所以 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 = 220x1024;
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
/*server.cpp*/
#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;

//watchout SOCK_DGRAM
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));

//processing
for(i=0;i<n;i++){
buf[i] = toupper(buf[i]);
}
//buf[n] = '\0'; //n<BUFSIZ

n = sendto(sockfd, buf, BUFSIZ, 0, (struct sockaddr*)&clie_addr, sizeof(clie_addr));
if(n==-1){
perror("send error");
}

}//end of while

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);//do not care the server side
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 setsockopt(int sockfd, int level, intoptname, 
const void *optval, socklen_t optlen);*/

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 /*must*/
#define MAXLINE 1500


//according to ur ip address, set client to broadcast ip
#define BROADCAST_IP "192.168.1.255"

int main(void)
{

int sockfd;
struct sockaddr_in serveraddr, clientaddr;
char buf[MAXLINE] = {0};

/*used for udp*/
sockfd = socket(AF_INET, SOCK_DGRAM, 0);

bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); /*server ip whatever*/
serveraddr.sin_port = htons(SERVER_PORT);


bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

/*set broadcast previlege*/
int flag = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));

/*fill out client, remember port*/
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); //must

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 /*the same as broadcast port*/

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);//client ip whatever
localaddr.sin_port = htons(CLIENT_PORT);

int ret = bind(confd, (struct sockaddr *)&localaddr, sizeof(localaddr));//must

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); /*use this*/
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; /* IP multicast address of group */
struct in_addr imr_address; /* local IP address of interface */
int imr_ifindex; /* Interface index */
};

结构体 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); /*本地任意 IP*/

group.imr_ifindex = if_nametoindex("eth0"); /* eth0 --> 编号 命令: ip ad */
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
/*server.cpp*/

#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" //watch out group ip set to client ip


int main(void)
{
int sockfd, i ;
struct sockaddr_in serveraddr, clientaddr;
char buf[MAXLINE] = "itcast\n";
char ipstr[INET_ADDRSTRLEN]; /* 16 Bytes */
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; /* IPv4 */
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 本地任意 IP INADDR_ANY = 0 */
serveraddr.sin_port = htons(SERVER_PORT);

bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

/*设置组地址*/
inet_pton(AF_INET, GROUP, &group.imr_multiaddr);
/*本地任意 IP*/
inet_pton(AF_INET, "0.0.0.0", &group.imr_address);

/* eth0 --> 编号 命令: ip ad */
group.imr_ifindex = if_nametoindex("eth0");
setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof(group));

/*构造 client 地址 IP+端口 */
bzero(&clientaddr, sizeof(clientaddr));
clientaddr.sin_family = AF_INET; /* IPv4 */
/*watch out group, not BROADCAST_IP*/
inet_pton(AF_INET, GROUP, &clientaddr.sin_addr.s_addr);
clientaddr.sin_port = htons(CLIENT_PORT);

while (1) {
//fgets(buf, sizeof(buf), stdin);
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); /*本地任意 IP*/
group.imr_ifindex = if_nametoindex("eth0"); /* eth0*/

/*设置 client 加入多播组 */
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
/*client.cpp*/

#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));//must


inet_pton(AF_INET, GROUP, &group.imr_multiaddr); /*设置组地址*/
inet_pton(AF_INET, "0.0.0.0", &group.imr_address); /*本地任意 IP*/
group.imr_ifindex = if_nametoindex("eth0"); /* eth0*/
/*设置 client 加入多播组 */
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还是有一些区别的(根据协议的特点做相应的调整), 它们适用的方向也不一样. 例如实时视频直播软件的时候可能是:

  1. 屏幕截图模块(为保证画面流畅, 至少保证有24帧)
  2. 截取帧数 (一般为保证画面, 8-12即可)
  3. 压缩图片
  4. 压缩数据包
  5. 传递数据–(多播)
  6. 解压缩
  7. 成像

其中传递数据的时候, 需要用到网络编程中的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).

文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. udp的特点
      1. 2.1.1. 简要说明
      2. 2.1.2. 如何保证可靠性
      3. 2.1.3. 与TCP对比
      4. 2.1.4. udp流量控制
    2. 2.2. c-s模型
    3. 2.3. 广播
    4. 2.4. 组播
  3. 3. 尾巴
    1. 3.1. 小结
    2. 3.2. 补充connect
|